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.
188 lines
8.2 KiB
Rust
188 lines
8.2 KiB
Rust
//! 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);
|
|
}
|
|
}
|