Persist typed sketch dimensions and bounded ordered configuration rules in format 8. Use exact occurrence paths and source identity for editing, movement, appearance, and mass. Preserve sheet material regions through cutting, folding, and exact DXF export; retain nested cavities and material islands. Validation: cargo xtask ci passed, including 368 engine tests, 178 UI tests, 68 UI scenarios, 136 kernel tests, and the 10-crate wasm check. Actual preview workflows and legacy format-7 origin-corner reopening were inspected.
168 lines
9.6 KiB
Rust
168 lines
9.6 KiB
Rust
use crate::Session;
|
|
use serde_json::{Value, json};
|
|
use solvecraft_sketch::ConstraintKind;
|
|
|
|
fn run(s: &mut Session, command: &str, params: Value) -> Value {
|
|
s.execute(command, ¶ms).unwrap_or_else(|error| panic!("{command} {params}: {error}"))
|
|
}
|
|
fn session() -> Session {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane":"XY"}));
|
|
s
|
|
}
|
|
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
|
|
s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.clone()
|
|
}
|
|
fn draw(s: &mut Session, command: &str, params: Value, dimensions: Value) -> Value {
|
|
run(s, "sketch.draw.dimensioned", json!({"command":command,"params":params,"dimensions":dimensions}))
|
|
}
|
|
fn close(a: f64, b: f64) {
|
|
assert!((a - b).abs() < 1e-6, "{a} != {b}");
|
|
}
|
|
fn measure(s: &Session, kind: fn(&ConstraintKind) -> bool) -> f64 {
|
|
let sk = sketch(s);
|
|
let c = sk.constraints.iter().find(|c| kind(&c.kind)).unwrap();
|
|
solvecraft_sketch::measure_dimension(&sk, &c.kind).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn typed_line_angle_survives_quadrants_move_reopen_and_one_undo() {
|
|
let mut s = session();
|
|
run(&mut s, "parameters.add", json!({"name":"heading","expression":"-45 deg","unit":"deg"}));
|
|
let undo = s.undo.len();
|
|
let out = draw(&mut s, "sketch.line", json!({"points":[[0,0],[10,0]],"infer":true}), json!({"length":"10 mm","angle":"heading"}));
|
|
assert_eq!(s.undo.len(), undo + 1);
|
|
assert!(!sketch(&s).constraints.iter().any(|c| matches!(c.kind, ConstraintKind::Horizontal { .. } | ConstraintKind::Vertical { .. })));
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::LineAngle { .. })), -45f64.to_radians());
|
|
for degrees in [135.0_f64, 270.0, -135.0, 0.0, 90.0] {
|
|
let result = run(&mut s, "parameters.change", json!({"name":"heading","expression":format!("{degrees} deg")}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::LineAngle { .. })), degrees.to_radians());
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Length { .. })), 10.0);
|
|
}
|
|
let encoded = serde_json::to_value(&*s.doc).unwrap();
|
|
let doc: solvecraft_doc::Document = serde_json::from_value(encoded).unwrap();
|
|
let mut reopened = Session { doc: std::sync::Arc::new(doc), active_sketch: s.active_sketch, ..Session::default() };
|
|
reopened.refresh();
|
|
close(measure(&reopened, |k| matches!(k, ConstraintKind::LineAngle { .. })), 90f64.to_radians());
|
|
run(&mut reopened, "sketch.move", json!({"entities":out["curves"],"translate":[4,3],"angle":20}));
|
|
close(measure(&reopened, |k| matches!(k, ConstraintKind::LineAngle { .. })), 90f64.to_radians());
|
|
let mut atomic = session();
|
|
draw(&mut atomic, "sketch.line", json!({"points":[[0,0],[5,5]]}), json!({"length":10,"angle":45}));
|
|
run(&mut atomic, "edit.undo", json!({}));
|
|
assert!(sketch(&atomic).curves.is_empty());
|
|
assert!(atomic.doc.params.iter().all(|p| !p.model));
|
|
}
|
|
|
|
#[test]
|
|
fn signed_arc_sweep_retains_radius_and_logical_endpoint_identity() {
|
|
let mut s = session();
|
|
run(&mut s, "parameters.add", json!({"name":"turn","expression":"-90 deg","unit":"deg"}));
|
|
let out = draw(&mut s, "sketch.arc.center_point", json!({"center":[0,0],"start":[10,0],"end":[0,-10]}), json!({"radius":10,"sweep":"turn"}));
|
|
let id = out["curves"][0].as_str().unwrap().to_string();
|
|
let original = sketch(&s).resolve_point(&format!("{id}.start")).unwrap();
|
|
for degrees in [-90.0_f64, -270.0, 90.0, 270.0, -45.0] {
|
|
let result = run(&mut s, "parameters.change", json!({"name":"turn","expression":format!("{degrees} deg")}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
let sk = sketch(&s);
|
|
assert_eq!(sk.resolve_point(&format!("{id}.start")), Some(original));
|
|
assert_eq!(sk.curves[sk.curve_index(&id).unwrap()].reversed, degrees < 0.0);
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::ArcSweep { .. })), degrees.to_radians());
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Radius { .. })), 10.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn slots_and_polygon_keep_permanent_typed_dimensions() {
|
|
for command in ["sketch.slot.center_to_center", "sketch.slot.overall"] {
|
|
let mut s = session();
|
|
let out = draw(&mut s, command, json!({"p0":[2,3],"p1":[22,3],"width":4,"connect":false}), json!({"length":20,"width":4}));
|
|
let length = out["dimensions"]["length"]["param"].as_str().unwrap();
|
|
let result = run(&mut s, "parameters.change", json!({"name":length,"expression":30}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Length { .. } | ConstraintKind::SlotOverallLength { .. })), 30.0);
|
|
let width = out["dimensions"]["width"]["param"].as_str().unwrap();
|
|
let result = run(&mut s, "parameters.change", json!({"name":width,"expression":6}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Diameter { .. })), 6.0);
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Length { .. } | ConstraintKind::SlotOverallLength { .. })), 30.0);
|
|
}
|
|
for command in ["sketch.polygon.inscribed", "sketch.polygon.circumscribed"] {
|
|
let mut s = session();
|
|
let out = draw(&mut s, command, json!({"center":[4,5],"radius":10,"sides":6,"connect":false}), json!({"radius":10}));
|
|
let support = out["support_circle"].as_str().unwrap();
|
|
assert!(sketch(&s).curves[sketch(&s).curve_index(support).unwrap()].construction);
|
|
let radius = out["dimensions"]["radius"]["param"].as_str().unwrap();
|
|
let result = run(&mut s, "parameters.change", json!({"name":radius,"expression":15}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
close(measure(&s, |k| matches!(k, ConstraintKind::Radius { .. })), 15.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_dimensioned_shapes_are_atomic_including_late_constraint_failure() {
|
|
let mut s = session();
|
|
let before = serde_json::to_value(&*s.doc).unwrap();
|
|
let history = s.undo.len();
|
|
for params in [
|
|
json!({"command":"edit.undo","params":{},"dimensions":{}}),
|
|
json!({"command":"sketch.line","params":{"points":[[0,0],[10,0]]},"dimensions":{"length":"missing+1"}}),
|
|
json!({"command":"sketch.line","params":{"points":[[0,0],[10,0]]},"dimensions":{"length":-1}}),
|
|
json!({"command":"sketch.line","params":{"points":[[0,0],[10,0]]},"dimensions":{"length":"1".repeat(4097)}}),
|
|
json!({"command":"sketch.arc.center_point","params":{"center":[0,0],"start":[10,0]},"dimensions":{"sweep":360}}),
|
|
json!({"command":"sketch.polygon.inscribed","params":{"center":[0,0],"radius":10,"sides":3.5},"dimensions":{"radius":10}}),
|
|
json!({"command":"sketch.line","params":{"points":[[0,0],[10,0]]},"dimensions":{"length":20,"angle":0}}),
|
|
] {
|
|
// The last case connects both endpoints to fixed existing points and fails only after creation.
|
|
if params["dimensions"]["length"] == 20 {
|
|
run(&mut s, "sketch.point", json!({"point":[10,0]}));
|
|
run(&mut s, "sketch.constraint.fix", json!({"entity":"p1","fixed":true}));
|
|
}
|
|
let snapshot = serde_json::to_value(&*s.doc).unwrap();
|
|
let undo = s.undo.len();
|
|
// Preserve a live redo branch, which scratch-only dimension failure must not consume.
|
|
run(&mut s, "sketch.point", json!({"point":[100,100]}));
|
|
run(&mut s, "edit.undo", json!({}));
|
|
s.selection = vec![crate::Sel::SketchPoint { id: "origin".into() }];
|
|
let redo = s.redo.len();
|
|
let selection = s.selection.clone();
|
|
let pending = s.pending_moves.clone();
|
|
let active = (s.active_sketch, s.active_component, s.active_occurrence_path.clone());
|
|
assert!(s.execute("sketch.draw.dimensioned", ¶ms).is_err(), "{params}");
|
|
assert_eq!(serde_json::to_value(&*s.doc).unwrap(), snapshot);
|
|
assert_eq!(s.undo.len(), undo);
|
|
assert_eq!(s.redo.len(), redo);
|
|
assert_eq!(s.selection, selection);
|
|
assert_eq!(s.pending_moves, pending);
|
|
assert_eq!((s.active_sketch, s.active_component, s.active_occurrence_path.clone()), active);
|
|
}
|
|
assert_eq!(before["features"].as_array().unwrap().len(), 1);
|
|
assert!(s.undo.len() >= history);
|
|
}
|
|
|
|
#[test]
|
|
fn dimensioned_authoring_uses_selected_occurrence_and_keeps_local_geometry() {
|
|
let mut s = Session::default();
|
|
let component = run(&mut s, "component.create", json!({"name":"Shared"}))["component"].as_u64().unwrap();
|
|
let second = run(&mut s, "occurrence.copy", json!({"component":component,"translate":[20,0,0]}))["occurrence"].as_u64().unwrap();
|
|
run(&mut s, "component.activate", json!({"component":component,"occurrence":second,"path":[second]}));
|
|
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2,"corner":[20,0,0],"operation":"new"}));
|
|
s.auto_project = false;
|
|
run(&mut s, "sketch.create", json!({"plane":{"face":[21,1,2]}}));
|
|
let out = draw(&mut s, "sketch.line", json!({"points":[[0,0],[5,5]]}), json!({"length":5,"angle":45}));
|
|
let model = s.model.state();
|
|
let local = model.sketch(s.active_sketch.unwrap()).unwrap();
|
|
close(local.plane.origin.x, 0.0);
|
|
close(local.plane.origin.z, 2.0);
|
|
let world = s.world_state();
|
|
let displayed = world.sketch(s.active_sketch.unwrap()).unwrap();
|
|
close(displayed.plane.origin.x, 20.0);
|
|
close(displayed.plane.origin.z, 2.0);
|
|
assert_eq!(s.active_occurrence_path, Some(vec![second]));
|
|
let sk = sketch(&s);
|
|
let line = sk.curve_index(out["curves"][0].as_str().unwrap()).unwrap();
|
|
let solvecraft_sketch::CurveKind::Line { a, b } = sk.curves[line].kind else { panic!("line") };
|
|
close(sk.points[a].pos.x, 0.0);
|
|
close(sk.points[b].pos.x, 5.0 / std::f64::consts::SQRT_2);
|
|
}
|