* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
291 lines
13 KiB
Rust
291 lines
13 KiB
Rust
//! Live drags (`sketch.drag` with `live: true`): each frame solves the sketch once, without a
|
|
//! rank analysis, and evaluates nothing after it; the gesture's end evaluates the model once.
|
|
|
|
use serde_json::{Value, json};
|
|
use solidcraft_geom::Vec3;
|
|
|
|
use super::solve_counts;
|
|
use crate::Session;
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
match s.execute(id, &p) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{id} {p}: {e}"),
|
|
}
|
|
}
|
|
|
|
/// A dimensioned 20 x 15 rectangle from (5, 5) extruded 5 mm, its sketch being edited again;
|
|
/// returns the session and the extrude's id.
|
|
fn extruded() -> (Session, u64) {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [25, 20]}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 20}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "value": 15}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 5}));
|
|
let extrude = s.doc.features.last().map(|f| f.id).expect("the extrude");
|
|
run(&mut s, "sketch.edit", json!({"sketch": "Sketch1"}));
|
|
(s, extrude)
|
|
}
|
|
|
|
/// Counters from here on.
|
|
fn reset(s: &mut Session) {
|
|
s.evaluations = 0;
|
|
solve_counts::take();
|
|
}
|
|
|
|
/// Frames moving the rectangle's corner (5, 5) (and so the whole rectangle) left by 1 mm each.
|
|
fn frames(s: &mut Session, n: usize, live: bool) -> Vec<Value> {
|
|
(1..=n)
|
|
.map(|k| {
|
|
let (from, to) = ([6.0 - k as f64, 5.0], [5.0 - k as f64, 5.0]);
|
|
run(s, "sketch.drag", json!({"entity": "l1.start", "from": from, "to": to, "live": live}))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn body_contains(s: &Session, p: Vec3) -> bool {
|
|
s.model.state().bodies.first().expect("a body").body.contains(p)
|
|
}
|
|
|
|
fn pt(s: &Session, id: &str) -> solidcraft_geom::Vec2 {
|
|
let sk = s.active_sketch.and_then(|i| s.doc.sketch(i).ok()).expect("sketch mode");
|
|
sk.resolve_point(id).and_then(|i| sk.point(i)).expect("the point")
|
|
}
|
|
|
|
fn corner(s: &Session) -> solidcraft_geom::Vec2 {
|
|
let id = s.active_sketch.expect("sketch mode");
|
|
let st = s.world_state();
|
|
let sk = &st.sketch(id).expect("the shown sketch").sketch;
|
|
sk.resolve_point("l1.start").and_then(|i| sk.point(i)).expect("the corner")
|
|
}
|
|
|
|
#[test]
|
|
fn a_live_frame_solves_once_and_leaves_the_features_after_the_sketch_stale() {
|
|
let (mut s, extrude) = extruded();
|
|
let body = s.model.state().bodies.first().expect("a body").body.clone();
|
|
reset(&mut s);
|
|
let r = frames(&mut s, 1, true).remove(0);
|
|
assert_eq!(s.evaluations, 0, "a live frame evaluates no feature");
|
|
assert_eq!(solve_counts::take(), (1, 0), "one held solve, no rank analysis");
|
|
assert_eq!(r["reached"], json!(1.0));
|
|
assert_eq!(r["sketch"]["solved"], json!(true));
|
|
assert_eq!(r["sketch"]["dof"], json!(2), "the degrees of freedom from before the drag");
|
|
// The sketch shows its new place; the extrude keeps its last result.
|
|
assert!(corner(&s).dist(solidcraft_geom::Vec2::new(4.0, 5.0)) < 1e-9);
|
|
let result = s.model.result(extrude).expect("the extrude's result");
|
|
assert!(result.stale);
|
|
assert!(s.model.state().bodies.first().expect("a body").body.same(&body), "the body is not rebuilt");
|
|
}
|
|
|
|
#[test]
|
|
fn the_drag_end_evaluates_once_to_the_scripted_drags_result() {
|
|
let (mut live, extrude) = extruded();
|
|
let (mut scripted, _) = extruded();
|
|
frames(&mut live, 3, true);
|
|
reset(&mut live);
|
|
let end = run(&mut live, "sketch.drag_end", json!({}));
|
|
assert_eq!(live.evaluations, 1, "one full evaluation at the end");
|
|
assert_eq!(solve_counts::take(), (0, 0));
|
|
assert_eq!(end["sketch"], json!({"solved": true, "dof": 2}));
|
|
assert!(!live.model.is_stale());
|
|
frames(&mut scripted, 3, false);
|
|
assert_eq!(*live.doc, *scripted.doc, "the same design as dragging without `live`");
|
|
let (a, b) = (live.model.state(), scripted.model.state());
|
|
let (sa, sb) = (&a.sketches[0], &b.sketches[0]);
|
|
assert_eq!(sa.sketch, sb.sketch);
|
|
assert_eq!((sa.report.dof, &sa.report.point_determined), (sb.report.dof, &sb.report.point_determined));
|
|
assert!(body_contains(&live, Vec3::new(2.5, 6.0, 2.5)) && !body_contains(&live, Vec3::new(22.5, 6.0, 2.5)), "the body followed");
|
|
assert!(live.model.result(extrude).is_some_and(|r| !r.stale && r.error.is_none()));
|
|
// One undo step for the gesture is the UI's business; each frame is still undoable.
|
|
run(&mut live, "edit.undo", json!({}));
|
|
assert!(corner(&live).dist(solidcraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn an_abandoned_live_drag_is_evaluated_before_the_next_command() {
|
|
let (mut s, extrude) = extruded();
|
|
frames(&mut s, 2, true);
|
|
assert!(s.model.is_stale());
|
|
reset(&mut s);
|
|
// A command that changes nothing still sees (and leaves) a fully evaluated model.
|
|
run(&mut s, "sketch.solve", json!({}));
|
|
assert_eq!(s.evaluations, 1);
|
|
assert!(!s.model.is_stale());
|
|
assert!(s.model.result(extrude).is_some_and(|r| !r.stale));
|
|
assert!(body_contains(&s, Vec3::new(3.5, 6.0, 2.5)));
|
|
// Leaving the sketch does too.
|
|
frames(&mut s, 1, true);
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
assert!(!s.model.is_stale());
|
|
}
|
|
|
|
#[test]
|
|
fn a_scripted_drag_still_evaluates_the_whole_model() {
|
|
let (mut s, extrude) = extruded();
|
|
reset(&mut s);
|
|
let r = frames(&mut s, 1, false).remove(0);
|
|
assert_eq!(s.evaluations, 1);
|
|
assert_eq!(solve_counts::take(), (2, 1), "the held solve, then the sketch's own solve with its analysis");
|
|
assert_eq!(r["sketch"], json!({"solved": true, "dof": 2}));
|
|
assert!(!s.model.is_stale());
|
|
assert!(s.model.result(extrude).is_some_and(|r| !r.stale));
|
|
assert!(body_contains(&s, Vec3::new(4.5, 6.0, 2.5)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_drag_reports_how_far_the_geometry_followed() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
run(&mut s, "sketch.line", json!({"points": [[10, 10], [40, 10]]}));
|
|
run(&mut s, "sketch.line", json!({"points": [[10, 30], [40, 30]]}));
|
|
run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start"]}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 30}));
|
|
for live in [false, true] {
|
|
// A line of fixed length with a fixed end cannot slide: the sketch's own solve only
|
|
// turns it a little toward the cursor, and the grab follows its free end.
|
|
let before = pt(&s, "l1.end");
|
|
let r = run(&mut s, "sketch.drag", json!({"entity": "l1", "from": [25, 10], "to": [25, 20], "live": live}));
|
|
assert_eq!(r["reached"], json!(0.0), "{r}");
|
|
let moved = pt(&s, "l1.end") - before;
|
|
let at = crate::params::vec2(&r["at"]).expect("`at`");
|
|
assert!(at.dist(solidcraft_geom::Vec2::new(25.0, 10.0) + moved) < 1e-9 && at.y < 20.0, "{r}");
|
|
let to = 40 + 10 * usize::from(live);
|
|
let r = run(&mut s, "sketch.drag", json!({"entity": "l2", "from": [25, to - 10], "to": [25, to]}));
|
|
assert_eq!((&r["reached"], &r["at"]), (&json!(1.0), &json!([25.0, to as f64])), "{r}");
|
|
}
|
|
// A point that cannot move at all.
|
|
let r = run(&mut s, "sketch.drag", json!({"entity": "l1.start", "from": [10, 10], "to": [0, 0], "live": true}));
|
|
assert_eq!(r["reached"], json!(0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_live_drag_of_a_lone_sketch_shows_it_without_evaluating() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [25, 20]}));
|
|
reset(&mut s);
|
|
frames(&mut s, 2, true);
|
|
assert_eq!(s.evaluations, 0);
|
|
assert_eq!(solve_counts::take(), (2, 0));
|
|
// The free rectangle's corner moves alone; its profile follows.
|
|
assert!(corner(&s).dist(solidcraft_geom::Vec2::new(3.0, 5.0)) < 1e-9);
|
|
let st = s.model.state();
|
|
let profile = st.sketches[0].profiles.first().expect("a profile");
|
|
assert!((profile.area - 22.0 * 15.0).abs() < 1e-9, "{}", profile.area);
|
|
run(&mut s, "sketch.drag_end", json!({}));
|
|
assert_eq!(s.evaluations, 1);
|
|
}
|
|
|
|
/// Frames of a UI drag of `entity` grabbed at `grab`, the cursor moving by `moves` in turn: each
|
|
/// frame's `from` is the previous frame's `at`, as the viewport does.
|
|
fn ui_frames(s: &mut Session, entity: &str, grab: [f64; 2], moves: &[[f64; 2]]) {
|
|
let (mut from, mut cursor) = (grab, grab);
|
|
for m in moves {
|
|
cursor = [cursor[0] + m[0], cursor[1] + m[1]];
|
|
let r = run(s, "sketch.drag", json!({"entity": entity, "from": from, "to": cursor, "live": true}));
|
|
let at = crate::params::vec2(&r["at"]).expect("`at`");
|
|
from = [at.x, at.y];
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_grab_follows_the_points_that_move() {
|
|
// A line with a fixed end, dragged by its middle: only the free end moves, and it keeps the
|
|
// cursor's offset frame after frame.
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
run(&mut s, "sketch.line", json!({"points": [[10, 10], [30, 10]]}));
|
|
run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start"]}));
|
|
ui_frames(&mut s, "l1", [20.0, 10.0], &[[0.0, 1.0]; 6]);
|
|
assert!(pt(&s, "l1.end").dist(solidcraft_geom::Vec2::new(30.0, 16.0)) < 1e-9, "{:?}", pt(&s, "l1.end"));
|
|
assert!(pt(&s, "l1.start").dist(solidcraft_geom::Vec2::new(10.0, 10.0)) < 1e-12);
|
|
}
|
|
|
|
#[test]
|
|
fn a_query_during_a_live_drag_does_not_evaluate_the_model() {
|
|
let (mut s, _) = extruded();
|
|
frames(&mut s, 1, true);
|
|
reset(&mut s);
|
|
for (id, p) in
|
|
[("parameters.complete", json!({"prefix": "d"})), ("parameters.list", json!({})), ("expr.evaluate", json!({"expression": "d1 * 2"}))]
|
|
{
|
|
run(&mut s, id, p);
|
|
}
|
|
assert_eq!(s.evaluations, 0);
|
|
assert!(s.model.is_stale(), "the drag goes on");
|
|
frames(&mut s, 1, true);
|
|
assert_eq!(s.evaluations, 0);
|
|
}
|
|
|
|
fn sketch_of(s: &Session) -> solidcraft_sketch::Sketch {
|
|
s.active_sketch.and_then(|i| s.doc.sketch(i).ok()).cloned().expect("sketch mode")
|
|
}
|
|
|
|
#[test]
|
|
fn a_live_frame_solves_the_dimensions_current_values() {
|
|
let (mut s, _) = extruded();
|
|
// The bottom side's parameter now says 25 while the sketch still holds 20 mm.
|
|
let sk = sketch_of(&s);
|
|
let l1 = sk.curve_index("l1").expect("l1");
|
|
let param = sk
|
|
.constraints
|
|
.iter()
|
|
.find(|c| matches!(c.kind, solidcraft_sketch::ConstraintKind::Length { l, .. } if l == l1))
|
|
.and_then(|c| c.param.clone())
|
|
.expect("l1's dimension");
|
|
if let Some(p) = s.doc_mut().params.iter_mut().find(|p| p.name == param) {
|
|
p.expr = "25".into();
|
|
}
|
|
let r = frames(&mut s, 1, true).remove(0);
|
|
assert_eq!(r["sketch"]["solved"], json!(true));
|
|
let (a, b) = (pt(&s, "l1.start"), pt(&s, "l1.end"));
|
|
assert!((a.dist(b) - 25.0).abs() < 1e-6, "l1 is {} mm", a.dist(b));
|
|
}
|
|
|
|
#[test]
|
|
fn a_live_frame_swaps_only_the_dragged_sketch() {
|
|
let (mut s, _) = extruded();
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "sketch.create", json!({"plane": "XZ"}));
|
|
run(&mut s, "sketch.circle.center", json!({"center": [50, 50], "radius": 4}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "sketch.edit", json!({"sketch": "Sketch1"}));
|
|
let id = s.active_sketch.expect("sketch mode");
|
|
let other = |s: &Session| s.model.state().sketches.iter().find(|x| x.feature != id).cloned().expect("the other sketch");
|
|
let before = other(&s);
|
|
frames(&mut s, 1, true);
|
|
assert!(std::sync::Arc::ptr_eq(&before, &other(&s)), "the other sketch is shared, not copied");
|
|
// Later frames change the state they own in place.
|
|
let state = std::sync::Arc::as_ptr(&s.model.state());
|
|
frames(&mut s, 1, true);
|
|
assert_eq!(state, std::sync::Arc::as_ptr(&s.model.state()));
|
|
}
|
|
|
|
#[test]
|
|
fn letting_go_on_a_snap_or_undoing_evaluates_once() {
|
|
let (mut s, _) = extruded();
|
|
run(&mut s, "sketch.point", json!({"point": [-10, 5], "connect": false}));
|
|
let point = sketch_of(&s).points.last().map(|p| p.id.clone()).expect("the point");
|
|
frames(&mut s, 3, true);
|
|
reset(&mut s);
|
|
let snap = json!({"command": "sketch.constraint.coincident", "params": {"a": "l1.start", "b": point}});
|
|
let r = run(&mut s, "sketch.drag_end", json!({ "constraint": snap }));
|
|
assert_eq!(s.evaluations, 1, "{r}");
|
|
assert!(r["constraint"].get("error").is_none(), "{r}");
|
|
assert!(!s.model.is_stale());
|
|
// Undo after an abandoned drag.
|
|
frames(&mut s, 1, true);
|
|
reset(&mut s);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.evaluations, 1);
|
|
assert!(!s.model.is_stale());
|
|
// A snap that over-constrains the sketch is not kept; the drag still ends.
|
|
frames(&mut s, 1, true);
|
|
let bad = json!({"command": "sketch.constraint.coincident", "params": {"a": "l1.start", "b": "l1.end"}});
|
|
let r = run(&mut s, "sketch.drag_end", json!({ "constraint": bad }));
|
|
assert!(r["constraint"]["error"].is_string(), "{r}");
|
|
assert!(!s.model.is_stale());
|
|
}
|