fix(sketch): move and copy editable text as a whole source (#715)
Dragging a text outline now moves the whole editable text, and Move/Copy shifts or rotates it as one piece (a copy stays independently editable). Content, font, height and outline identities are kept; a drag is one undo step and Esc restores the start.
This commit is contained in:
1 parent
2c4d197bf4
commit
11084a5963
14 files changed
+469
-9
No files matched your search
@@ -247,6 +247,7 @@ pub enum Kind {
|
||||
Part(crate::dialogs_parts::Pt),
|
||||
/// Edit Text: a sketch text's content and placement (`sketch_text`).
|
||||
Text(crate::sketch_text::Tx),
|
||||
TextMove(crate::sketch_text_move::TextMove),
|
||||
}
|
||||
|
||||
/// The rest of the Hole dialog: placement, extents, tap type and drill point.
|
||||
@@ -503,7 +504,8 @@ impl Dialog {
|
||||
.or_else(|| crate::dialogs_appearance::start(app, id))
|
||||
.or_else(|| crate::dialogs_motion::start(app, id))
|
||||
.or_else(|| crate::dialogs_parts::start(app, id))
|
||||
.or_else(|| crate::sketch_text::start(app, id))?;
|
||||
.or_else(|| crate::sketch_text::start(app, id))
|
||||
.or_else(|| crate::sketch_text_move::start(app, id))?;
|
||||
Dialog::new(kind, inputs)
|
||||
}
|
||||
};
|
||||
@@ -933,6 +935,7 @@ fn title(k: &Kind) -> &'static str {
|
||||
Kind::Motion(k) => k.title(),
|
||||
Kind::Part(_) => "INSERT PART",
|
||||
Kind::Text(_) => "EDIT TEXT",
|
||||
Kind::TextMove(_) => "MOVE/COPY TEXT",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1354,6 +1357,11 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
let _language = crate::i18n::scope(&app.preferences.language);
|
||||
if let Kind::TextMove(k) = &d.kind
|
||||
&& !crate::sketch_text_move::valid(app, k)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto_operation(app, &mut d);
|
||||
update_measure(app, &mut d);
|
||||
update_fill_cells(app, &mut d);
|
||||
@@ -2061,6 +2069,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
|
||||
Kind::Part(k) => enter |= crate::dialogs_parts::rows(app, ui, k, &d.inputs),
|
||||
Kind::Text(k) => enter |= crate::sketch_text::rows(ui, k),
|
||||
Kind::TextMove(k) => enter |= crate::sketch_text_move::rows(ui, k),
|
||||
Kind::ConfirmDelete { with, fail, .. } => {
|
||||
if !with.is_empty() {
|
||||
row_label(ui, "Also deletes");
|
||||
@@ -2171,7 +2180,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
crate::dialogs_motion::closed(app, &mut d, applied);
|
||||
}
|
||||
// The edited text's outlines are new curves: the old selection would name stale ids.
|
||||
if applied && matches!(d.kind, Kind::Text(_)) {
|
||||
if applied && matches!(d.kind, Kind::Text(_) | Kind::TextMove(_)) {
|
||||
let _ = app.session.execute("select.clear", &json!({}));
|
||||
}
|
||||
if keep && !cancel {
|
||||
@@ -3067,6 +3076,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
Kind::Motion(k) => return crate::dialogs_motion::commands(app, k, &d.inputs),
|
||||
Kind::Part(k) => return crate::dialogs_parts::commands(app, k, &d.inputs),
|
||||
Kind::Text(k) => return crate::sketch_text::commands(app, k),
|
||||
Kind::TextMove(k) => return crate::sketch_text_move::commands(app, k),
|
||||
};
|
||||
let mut params = params;
|
||||
if let Value::Object(m) = &mut params {
|
||||
|
||||
@@ -80,6 +80,7 @@ fn put_view(app: &mut SolveApp, v: DocView) {
|
||||
/// Put `slot` into the app and return what was there.
|
||||
fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot {
|
||||
// Whatever was going on in the design leaves with it.
|
||||
crate::sketch_text_move::cancel_drag(app);
|
||||
crate::dialogs::cancel(app);
|
||||
crate::dim_view::cancel();
|
||||
app.tool = None;
|
||||
@@ -196,6 +197,7 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
app.docs.closing = None;
|
||||
if n <= 1 {
|
||||
// The last design: a blank one takes its place.
|
||||
crate::sketch_text_move::cancel_drag(app);
|
||||
let rev = app.session.revision;
|
||||
crate::dialogs::cancel(app);
|
||||
app.tool = None;
|
||||
|
||||
@@ -601,6 +601,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
cancel_capture(app);
|
||||
// Pointer release may have happened during review; never finalize it against
|
||||
// the report's last cursor position when CAD interaction resumes.
|
||||
if app.viewport.text_drag.is_some() {
|
||||
let _ = app.session.execute("sketch.drag_end", &serde_json::json!({}));
|
||||
crate::sketch_text_move::end_drag(app);
|
||||
}
|
||||
app.viewport.point_drag = None;
|
||||
app.viewport.boxsel = None;
|
||||
app.viewport.axis_drag = None;
|
||||
|
||||
@@ -103,6 +103,7 @@ mod sketch_edit_tests;
|
||||
mod sketch_families;
|
||||
pub mod sketch_palette;
|
||||
pub(crate) mod sketch_text;
|
||||
mod sketch_text_move;
|
||||
pub mod sketch_tools;
|
||||
pub mod stand_up;
|
||||
pub mod theme;
|
||||
@@ -526,7 +527,8 @@ impl SolveApp {
|
||||
return;
|
||||
}
|
||||
// Another command ends Edit Text (its dialog is not a feature edit to keep).
|
||||
if sketch_text::editing(self) {
|
||||
sketch_text_move::cancel_drag(self);
|
||||
if sketch_text::editing(self) || self.dialog.as_ref().is_some_and(|d| matches!(d.kind, dialogs::Kind::TextMove(_))) {
|
||||
dialogs::cancel(self);
|
||||
}
|
||||
// A component moved without Capture Position: ask first (Capture, Revert or Cancel).
|
||||
@@ -712,7 +714,8 @@ impl SolveApp {
|
||||
/// Finish the active sketch: stop editing, restore the history marker Edit Sketch moved and
|
||||
/// return to the view from before the sketch.
|
||||
pub fn finish_sketch(&mut self) {
|
||||
if sketch_text::editing(self) {
|
||||
sketch_text_move::cancel_drag(self);
|
||||
if sketch_text::editing(self) || self.dialog.as_ref().is_some_and(|d| matches!(d.kind, dialogs::Kind::TextMove(_))) {
|
||||
dialogs::cancel(self);
|
||||
}
|
||||
dim_view::cancel();
|
||||
@@ -737,6 +740,7 @@ impl SolveApp {
|
||||
v.extend(d.items());
|
||||
}
|
||||
v.extend(sketch_text::highlighted(self));
|
||||
v.extend(sketch_text_move::highlighted(self));
|
||||
v
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,52 @@ use solidcraft_engine::geom::Vec2;
|
||||
|
||||
use crate::scenario::Harness;
|
||||
|
||||
#[test]
|
||||
fn text_move_drag_is_one_editable_unit_and_escape_restores_the_gesture() {
|
||||
for cancel in [false, true] {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
run(&mut h, "sketch.create", json!({"plane":"XY"}));
|
||||
run(&mut h, "sketch.text", json!({"text":"II","at":[0,0],"height":10}));
|
||||
run(&mut h, "sketch.line", json!({"points":[[15,0],[15,10]],"connect":false}));
|
||||
h.call("ui.view", json!({"view":"top"}));
|
||||
h.app.cam.distance = 35.0;
|
||||
h.frames(3);
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
let sk = h.app.session.doc.sketch(1).unwrap();
|
||||
let line = sk.curves.last().unwrap().id.clone();
|
||||
let neighbor = point(&h, &format!("{line}.start"));
|
||||
let polyline = sk.polyline(0);
|
||||
let at = (polyline[0] + polyline[1]) * 0.5;
|
||||
let a = screen(&mut h, at.x, at.y);
|
||||
let b = screen(&mut h, at.x + 3.0, at.y + 2.0);
|
||||
let mut path = vec![a];
|
||||
path.extend(steps(a, b, 6));
|
||||
press_and_move(&mut h, &path);
|
||||
assert!(h.app.viewport.text_drag.is_some());
|
||||
assert_ne!(h.app.session.doc.to_json(), before);
|
||||
if cancel {
|
||||
h.step(&json!({"key":"Escape"})).unwrap();
|
||||
}
|
||||
release(&mut h, b);
|
||||
assert!(h.app.viewport.text_drag.is_none());
|
||||
assert_eq!(h.app.session.active_sketch, Some(1));
|
||||
assert!(point(&h, &format!("{line}.start")).dist(neighbor) < 1e-9);
|
||||
if cancel {
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
} else {
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||
let sk = h.app.session.doc.sketch(1).unwrap();
|
||||
let solidcraft_engine::sketch::LinkSource::Text { at, .. } = &sk.links[0].source else { panic!() };
|
||||
assert!(at.dist(Vec2::new(3.0, 2.0)) < 0.02, "{at:?}");
|
||||
run(&mut h, "edit.undo", json!({}));
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run(h: &mut Harness, id: &str, p: Value) {
|
||||
if let Err(e) = h.app.run(id, p.clone()) {
|
||||
panic!("{id} {p}: {e}");
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Move/Copy changes the text source, never independent generated outlines.
|
||||
use crate::{SolveApp, dialogs::Kind, selection::SelInput};
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{Sel, doc::expr::Kind as ValueKind, sketch::LinkKind};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextMove {
|
||||
sketch: u64,
|
||||
link: String,
|
||||
values: [String; 3],
|
||||
copy: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
if id != "sketch.move" {
|
||||
return None;
|
||||
}
|
||||
let entity = crate::sketch_text::selected_entity(app)?;
|
||||
let sketch = app.session.active_sketch?;
|
||||
let sk = app.session.doc.sketch(sketch).ok()?;
|
||||
let link = sk.curves.iter().find(|c| c.id == entity)?.link.clone()?;
|
||||
let (_, unit) = crate::prefs::unit_scale(&app.session.doc.units);
|
||||
Some((Kind::TextMove(TextMove { sketch, link, values: [format!("0 {unit}"), format!("0 {unit}"), "0 deg".into()], copy: false }), vec![]))
|
||||
}
|
||||
|
||||
pub(crate) fn valid(app: &SolveApp, k: &TextMove) -> bool {
|
||||
app.session.active_sketch == Some(k.sketch)
|
||||
&& app.session.doc.sketch(k.sketch).ok().and_then(|sk| sk.link(&k.link)).is_some_and(|l| l.kind == LinkKind::Text)
|
||||
}
|
||||
|
||||
pub(crate) fn rows(ui: &mut egui::Ui, k: &mut TextMove) -> bool {
|
||||
let mut enter = false;
|
||||
for (label, value) in ["Distance X", "Distance Y", "Angle"].iter().zip(&mut k.values) {
|
||||
crate::dialogs::row_label(ui, label);
|
||||
enter |= crate::dialogs::field(ui, value);
|
||||
ui.end_row();
|
||||
}
|
||||
crate::dialogs::row_label(ui, "Create Copy");
|
||||
let response = ui.checkbox(&mut k.copy, "");
|
||||
crate::scenario::publish_handle("dialog:Create Copy", response.rect.center());
|
||||
ui.end_row();
|
||||
enter
|
||||
}
|
||||
|
||||
pub(crate) fn commands(app: &SolveApp, k: &TextMove) -> Result<Vec<(String, Value)>, String> {
|
||||
if !valid(app, k) {
|
||||
return Err("The text is no longer in the active sketch".into());
|
||||
}
|
||||
let doc = &app.session.doc;
|
||||
let mut values = [0.0; 3];
|
||||
for (i, (input, value)) in k.values.iter().zip(&mut values).enumerate() {
|
||||
let kind = if i == 2 { ValueKind::Angle } else { ValueKind::Length };
|
||||
*value = doc.eval(&doc.with_design_unit(input.trim(), kind), kind).map_err(|e| e.to_string())?;
|
||||
if !value.is_finite() {
|
||||
return Err("Move values must be finite".into());
|
||||
}
|
||||
}
|
||||
let sk = doc.sketch(k.sketch).map_err(|e| e.to_string())?;
|
||||
let curve = sk.curves.iter().find(|c| c.link.as_deref() == Some(&k.link)).ok_or("The text has no outlines")?;
|
||||
let Some(solidcraft_engine::sketch::LinkSource::Text { at, .. }) = sk.link(&k.link).map(|l| &l.source) else {
|
||||
return Err("The source is no longer text".into());
|
||||
};
|
||||
Ok(vec![(
|
||||
"sketch.move".into(),
|
||||
json!({"entities":[curve.id],"translate":[values[0],values[1]],
|
||||
"angle":values[2].to_degrees(),"center":[at.x,at.y],"copy":k.copy}),
|
||||
)])
|
||||
}
|
||||
|
||||
pub(crate) fn highlighted(app: &SolveApp) -> Vec<Sel> {
|
||||
let link = match app.dialog.as_ref().map(|d| &d.kind) {
|
||||
Some(Kind::TextMove(k)) => Some(k.link.as_str()),
|
||||
_ => app.viewport.text_drag.as_ref().map(|(_, link)| link.as_str()),
|
||||
};
|
||||
let Some(link) = link else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(sk) = app.session.active_sketch.and_then(|sid| app.session.doc.sketch(sid).ok()) else {
|
||||
return vec![];
|
||||
};
|
||||
sk.curves.iter().filter(|c| c.link.as_deref() == Some(link)).map(|c| Sel::SketchCurve { id: c.id.clone() }).collect()
|
||||
}
|
||||
|
||||
/// Esc restores the document and both history stacks to the start of this gesture.
|
||||
pub(crate) fn cancel_drag(app: &mut SolveApp) -> bool {
|
||||
let Some((checkpoint, _)) = app.viewport.text_drag.take() else {
|
||||
return false;
|
||||
};
|
||||
let _ = app.session.execute("edit.restore_checkpoint", &json!({"checkpoint":checkpoint}));
|
||||
app.viewport.point_drag = None;
|
||||
crate::drag_snap::clear();
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn end_drag(app: &mut SolveApp) {
|
||||
if let Some((checkpoint, _)) = app.viewport.text_drag.take() {
|
||||
let _ = app.session.execute("edit.restore_checkpoint", &json!({"checkpoint":checkpoint,"forget":true}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scenario::Harness;
|
||||
use solidcraft_engine::sketch::LinkSource;
|
||||
|
||||
fn setup() -> (Harness, String) {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
let result = h.app.run("sketch.text", json!({"text":"II","at":[3,4],"height":10})).unwrap();
|
||||
h.app.session.selection = vec![Sel::SketchCurve { id: result["curves"][0].as_str().unwrap().into() }];
|
||||
h.app.start("sketch.move");
|
||||
h.frames(3);
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(Kind::TextMove(_))));
|
||||
(h, result["link"].as_str().unwrap().into())
|
||||
}
|
||||
|
||||
fn replace(h: &mut Harness, field: &str, value: &str) {
|
||||
h.step(&json!({"click":{"handle":field}})).unwrap();
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text":value})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_move_dialog_uses_units_and_one_undo_and_preserves_cancel() {
|
||||
let (mut h, link) = setup();
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
replace(&mut h, "dialog:Distance X", "1 in");
|
||||
replace(&mut h, "dialog:Distance Y", "-2 mm");
|
||||
replace(&mut h, "dialog:Angle", "90 deg");
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
h.step(&json!({"click":{"handle":"dialog:OK"}})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.dialog.is_none());
|
||||
let sk = h.app.session.doc.sketch(1).unwrap();
|
||||
let LinkSource::Text { at, angle, text, .. } = &sk.link(&link).unwrap().source else { panic!() };
|
||||
assert!(at.dist(solidcraft_engine::geom::Vec2::new(28.4, 2.0)) < 1e-8);
|
||||
assert!((*angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
|
||||
assert_eq!(text, "II");
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
let curve = h.app.session.doc.sketch(1).unwrap().curves[0].id.clone();
|
||||
h.app.session.selection = vec![Sel::SketchCurve { id: curve }];
|
||||
h.app.start("sketch.move");
|
||||
h.frames(3);
|
||||
replace(&mut h, "dialog:Distance X", "17");
|
||||
h.step(&json!({"widget":"Cancel"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_move_dialog_copies_an_editable_source() {
|
||||
let (mut h, link) = setup();
|
||||
let original = h.app.session.doc.sketch(1).unwrap().link(&link).unwrap().source.clone();
|
||||
replace(&mut h, "dialog:Distance X", "20");
|
||||
h.step(&json!({"click":{"handle":"dialog:Create Copy"}})).unwrap();
|
||||
h.step(&json!({"click":{"handle":"dialog:OK"}})).unwrap();
|
||||
h.frames(2);
|
||||
let sk = h.app.session.doc.sketch(1).unwrap();
|
||||
assert_eq!(sk.links.len(), 2);
|
||||
assert_eq!(sk.link(&link).unwrap().source, original);
|
||||
let new_link = sk.links.iter().find(|l| l.id != link).unwrap().id.clone();
|
||||
h.app.run("sketch.edit_text", json!({"link":new_link,"text":"O"})).unwrap();
|
||||
assert_eq!(h.app.session.doc.sketch(1).unwrap().link(&link).unwrap().source, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_move_cancels_before_switching_documents() {
|
||||
let (mut h, link) = setup();
|
||||
crate::dialogs::cancel(&mut h.app);
|
||||
let before = h.app.session.doc.to_json();
|
||||
let checkpoint = h.app.session.execute("edit.checkpoint", &json!({})).unwrap()["checkpoint"].as_u64().unwrap();
|
||||
let curve = h.app.session.doc.sketch(1).unwrap().curves[0].id.clone();
|
||||
h.app.session.execute("sketch.drag", &json!({"entity":curve,"from":[0,0],"to":[2,3],"live":true})).unwrap();
|
||||
h.app.viewport.text_drag = Some((checkpoint, link));
|
||||
h.app.viewport.point_drag = Some((curve, 0, None));
|
||||
crate::documents::open_tab(&mut h.app, solidcraft_engine::Session::default()).unwrap();
|
||||
assert!(h.app.viewport.text_drag.is_none());
|
||||
assert!(h.app.viewport.point_drag.is_none());
|
||||
crate::documents::switch(&mut h.app, 0);
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
}
|
||||
}
|
||||
@@ -1187,6 +1187,9 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
match k {
|
||||
Key::Escape => {
|
||||
app.esc_handled = true;
|
||||
if crate::sketch_text_move::cancel_drag(app) {
|
||||
continue;
|
||||
}
|
||||
if crate::select_other::is_open(app) {
|
||||
crate::select_other::close(app);
|
||||
} else if app.tool.is_some() {
|
||||
|
||||
@@ -97,6 +97,8 @@ pub struct ViewportState {
|
||||
/// A sketch point or curve being dragged (its id, the undo depth when the drag began: the
|
||||
/// whole drag is one undo step, and for a curve where it was last under the cursor).
|
||||
pub point_drag: Option<(String, usize, Option<Vec2>)>,
|
||||
/// Checkpoint and source for a cancellable whole-text drag.
|
||||
pub(crate) text_drag: Option<(u64, String)>,
|
||||
}
|
||||
|
||||
/// Something under the cursor.
|
||||
@@ -2202,6 +2204,28 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some((id, _, _)) = &app.viewport.point_drag {
|
||||
let link = app.session.active_sketch.and_then(|sid| app.session.doc.sketch(sid).ok()).and_then(|sk| {
|
||||
let link = sk
|
||||
.curve_index(id)
|
||||
.and_then(|i| sk.curves.get(i))
|
||||
.and_then(|c| c.link.as_deref())
|
||||
.or_else(|| sk.resolve_point(id).and_then(|i| sk.points.get(i)).and_then(|p| p.link.as_deref()))?;
|
||||
sk.link(link).filter(|l| l.kind == solidcraft_engine::sketch::LinkKind::Text).map(|l| l.id.clone())
|
||||
});
|
||||
if let Some(link) = link
|
||||
&& let Some(q) = on_plane(app, a)
|
||||
{
|
||||
if let Some((_, _, grab)) = &mut app.viewport.point_drag {
|
||||
*grab = Some(q);
|
||||
}
|
||||
if let Ok(result) = app.session.execute("edit.checkpoint", &json!({}))
|
||||
&& let Some(checkpoint) = result["checkpoint"].as_u64()
|
||||
{
|
||||
app.viewport.text_drag = Some((checkpoint, link));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((id, depth, grab)) = app.viewport.point_drag.clone()
|
||||
&& resp.dragged_by(egui::PointerButton::Primary)
|
||||
@@ -2252,6 +2276,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
crate::drag_snap::clear();
|
||||
app.viewport.point_drag = None;
|
||||
crate::sketch_text_move::end_drag(app);
|
||||
crate::dim_view::drag_end();
|
||||
crate::sketch3d::drag_end();
|
||||
}
|
||||
@@ -2264,6 +2289,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
app.viewport.point_drag = None;
|
||||
crate::drag_snap::clear();
|
||||
let _ = app.session.execute("sketch.drag_end", &json!({}));
|
||||
crate::sketch_text_move::end_drag(app);
|
||||
}
|
||||
// Box selection: a primary drag on the model when no navigation mode is on.
|
||||
if app.tool.is_none()
|
||||
|
||||
Reference in new issue
Block a user