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.
154 lines
6.6 KiB
Rust
154 lines
6.6 KiB
Rust
//! Dragging sketch geometry in the viewport: live frames re-solve the sketch only, letting go
|
|
//! brings the model up to date in the drag's one undo step, and the grab stays on the geometry.
|
|
|
|
use serde_json::{Value, json};
|
|
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}");
|
|
}
|
|
}
|
|
|
|
fn screen(h: &mut Harness, x: f64, y: f64) -> egui::Pos2 {
|
|
let [sx, sy] = h.at(&json!({"sketch": [x, y]})).expect("on screen");
|
|
egui::pos2(sx as f32, sy as f32)
|
|
}
|
|
|
|
fn point(h: &Harness, id: &str) -> Vec2 {
|
|
let sid = h.app.session.active_sketch.expect("sketch mode");
|
|
let st = h.app.session.world_state();
|
|
let sk = &st.sketch(sid).expect("the sketch").sketch;
|
|
sk.resolve_point(id).and_then(|i| sk.point(i)).unwrap_or_else(|| panic!("no point {id}"))
|
|
}
|
|
|
|
/// Press at `path[0]` and move through the rest, one frame per event.
|
|
fn press_and_move(h: &mut Harness, path: &[egui::Pos2]) {
|
|
let Some(&a) = path.first() else { return };
|
|
h.app.synthetic.push(egui::Event::PointerMoved(a));
|
|
h.app.synthetic.push(egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
|
h.app.synthetic.extend(path.iter().skip(1).map(|&p| egui::Event::PointerMoved(p)));
|
|
play(h);
|
|
}
|
|
|
|
fn release(h: &mut Harness, at: egui::Pos2) {
|
|
h.app.synthetic.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
|
play(h);
|
|
}
|
|
|
|
fn play(h: &mut Harness) {
|
|
while !h.app.synthetic.is_empty() {
|
|
h.frame();
|
|
}
|
|
h.frames(2);
|
|
}
|
|
|
|
/// Points from `a` to `b` in `n` equal moves (without `a`).
|
|
fn steps(a: egui::Pos2, b: egui::Pos2, n: usize) -> Vec<egui::Pos2> {
|
|
(1..=n).map(|k| a + (b - a) * (k as f32 / n as f32)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn a_viewport_drag_solves_the_sketch_live_and_rebuilds_the_model_on_release() {
|
|
let mut h = Harness::new();
|
|
run(&mut h, "sketch.create", json!({"plane": "XY"}));
|
|
run(&mut h, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [25, 20]}));
|
|
run(&mut h, "sketch.dimension", json!({"entities": ["l1"], "value": 20}));
|
|
run(&mut h, "sketch.dimension", json!({"entities": ["l2"], "value": 15}));
|
|
run(&mut h, "sketch.finish", json!({}));
|
|
run(&mut h, "solid.extrude", json!({"distance": 5}));
|
|
let sketch = h.app.session.doc.features.first().map(|f| f.id).expect("the sketch");
|
|
h.app.edit_sketch(sketch);
|
|
h.settle();
|
|
let undo = h.app.session.undo.len();
|
|
let a = screen(&mut h, 5.0, 5.0);
|
|
let b = screen(&mut h, -3.0, -6.0);
|
|
let mut path = vec![a];
|
|
path.extend(steps(a, b, 8));
|
|
press_and_move(&mut h, &path);
|
|
assert!(h.app.viewport.point_drag.is_some(), "the drag is under way");
|
|
assert!(h.app.session.model.is_stale(), "the frames updated the sketch only");
|
|
assert!(point(&h, "l1.start").dist(Vec2::new(-3.0, -6.0)) < 0.1, "{:?}", point(&h, "l1.start"));
|
|
release(&mut h, b);
|
|
assert!(h.app.viewport.point_drag.is_none());
|
|
assert!(!h.app.session.model.is_stale(), "letting go evaluated the model");
|
|
assert_eq!(h.app.session.undo.len(), undo + 1, "one undo step for the drag");
|
|
assert!(point(&h, "l1.start").dist(Vec2::new(-3.0, -6.0)) < 0.1);
|
|
run(&mut h, "edit.undo", json!({}));
|
|
assert!(point(&h, "l1.start").dist(Vec2::new(5.0, 5.0)) < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn a_dragged_line_that_stops_short_stays_under_the_cursor_when_it_can_follow_again() {
|
|
let mut h = Harness::new();
|
|
run(&mut h, "sketch.create", json!({"plane": "XY"}));
|
|
// Two 10 mm links from the origin hold the start of a horizontal 10 mm line: the line can
|
|
// slide right until its start is 20 mm from the origin (3 mm).
|
|
let (x, y) = (15.0, 75f64.sqrt());
|
|
run(&mut h, "sketch.line", json!({"points": [[0, 0], [10, 0], [x, y], [x + 10.0, y]]}));
|
|
for l in ["l1", "l2", "l3"] {
|
|
run(&mut h, "sketch.dimension", json!({"entities": [l], "value": 10}));
|
|
}
|
|
run(&mut h, "sketch.constraint.horizontal_vertical", json!({"line": "l3"}));
|
|
let grab = screen(&mut h, x + 5.0, y);
|
|
let far = screen(&mut h, x + 11.0, y);
|
|
let back = screen(&mut h, x + 7.0, y);
|
|
let mut path = vec![grab];
|
|
path.extend(steps(grab, far, 8));
|
|
path.extend(steps(far, back, 4));
|
|
press_and_move(&mut h, &path);
|
|
release(&mut h, back);
|
|
// The cursor went 6 mm right, the line only 3; back at 2 mm right it is under it again.
|
|
let start = point(&h, "l3.start");
|
|
assert!(start.dist(Vec2::new(x + 2.0, y)) < 0.1, "{start:?}");
|
|
assert!(!h.app.session.model.is_stale());
|
|
}
|