use serde_json::json; use solvecraft_engine::Sel; use solvecraft_ui_egui::scenario::Harness; fn setup() -> (Harness, u64) { let mut h = Harness::new(); let source = h.app.run("sketch.create", json!({"plane":"XY","offset":10})).unwrap()["sketch"].as_u64().unwrap(); h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false})).unwrap(); h.app.run("sketch.point", json!({"point":[20,20],"id":"p"})).unwrap(); h.app.run("sketch.finish", json!({})).unwrap(); h.app.run("sketch.create", json!({"plane":"XY"})).unwrap(); h.app.run("sketch.line", json!({"points":[[40,40],[50,40]],"infer":false})).unwrap(); h.app.run("sketch.point", json!({"point":[45,50],"id":"p"})).unwrap(); h.call("ui.set", json!({"shownSketches":[source]})); h.call("ui.view", json!({"view":"top"})); (h, source) } #[test] fn clicked_external_curve_and_point_project_with_source_identity_and_one_undo() { let (mut h, source) = setup(); h.step(&json!({"click":{"world":[20,10,10]}})).unwrap(); h.step(&json!({"click":{"world":[20,20,10]},"shift":true})).unwrap(); assert_eq!( h.app.session.selection, vec![Sel::ExternalSketchCurve { sketch: source, id: "l1".into() }, Sel::ExternalSketchPoint { sketch: source, id: "p".into() }] ); let before = h.app.session.doc.to_json(); let undo = h.app.session.undo.len(); h.step(&json!({"key":"P"})).unwrap(); assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{:?}", h.app.status); assert!(h.app.session.selection.is_empty()); assert!(h.app.tool.is_none()); assert_eq!(h.app.session.undo.len(), undo + 1); let state = h.app.session.world_state(); let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch; assert_eq!(sk.curves.len(), 2); assert!(sk.curves.iter().any(|curve| curve.link.is_some())); assert!(sk.points.iter().any(|point| point.link.is_some() && (point.pos.x - 20.).abs() < 1e-6 && (point.pos.y - 20.).abs() < 1e-6)); let local = sk.point(sk.resolve_point("p").unwrap()).unwrap(); assert!((local.x - 45.).abs() < 1e-6 && (local.y - 50.).abs() < 1e-6); let after = h.app.session.doc.to_json(); h.app.run("edit.undo", json!({})).unwrap(); assert_eq!(h.app.session.doc.to_json(), before); h.app.run("edit.redo", json!({})).unwrap(); assert_eq!(h.app.session.doc.to_json(), after); } #[test] fn bad_reference_preserves_selection_and_document_atomically() { let (mut h, source) = setup(); h.app.session.selection = vec![Sel::ExternalSketchCurve { sketch: source, id: "l1".into() }, Sel::ExternalSketchPoint { sketch: source, id: "missing".into() }]; let selection = h.app.session.selection.clone(); let before = h.app.session.doc.to_json(); let undo = h.app.session.undo.len(); h.step(&json!({"start":"sketch.project"})).unwrap(); assert!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); assert_eq!(h.app.session.doc.to_json(), before); assert_eq!(h.app.session.undo.len(), undo); assert_eq!(h.app.session.selection, selection); } #[test] fn external_point_drag_and_delete_cannot_edit_same_named_local_point() { let (mut h, source) = setup(); let before = h.app.session.doc.to_json(); let undo = h.app.session.undo.len(); h.step(&json!({"drag":[{"world":[20,20,10]},{"world":[25,25,10]}]})).unwrap(); assert_eq!(h.app.session.doc.to_json(), before); assert_eq!(h.app.session.undo.len(), undo); h.app.session.selection = vec![Sel::ExternalSketchPoint { sketch: source, id: "p".into() }, Sel::ExternalSketchCurve { sketch: source, id: "l1".into() }]; let error = h.app.run("selection.delete", json!({"dry_run":true})).unwrap_err(); assert!(error.contains("nothing to delete"), "{error}"); assert_eq!(h.app.session.doc.to_json(), before); } #[test] fn axis_and_plane_preselection_projects_as_one_command() { let (mut h, _) = setup(); h.app.session.selection = vec![Sel::Axis { name: "X".into() }, Sel::Plane { name: "YZ".into() }]; let undo = h.app.session.undo.len(); h.step(&json!({"start":"sketch.project"})).unwrap(); assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{:?}", h.app.status); assert_eq!(h.app.session.undo.len(), undo + 1); let state = h.app.session.world_state(); let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch; assert_eq!(sk.curves.iter().filter(|c| c.link.is_some()).count(), 2); assert!(h.app.session.selection.is_empty()); } #[test] fn mixed_local_and_external_selection_keeps_ordinary_project_picking() { let (mut h, source) = setup(); h.app.session.selection = vec![Sel::SketchCurve { id: "l1".into() }, Sel::ExternalSketchCurve { sketch: source, id: "l1".into() }]; let before = h.app.session.doc.to_json(); let selection = h.app.session.selection.clone(); let undo = h.app.session.undo.len(); h.step(&json!({"key":"P"})).unwrap(); assert_eq!(h.app.tool.as_ref().map(|tool| tool.cmd), Some("sketch.project")); assert_eq!(h.app.session.selection, selection); assert_eq!(h.app.session.doc.to_json(), before); assert_eq!(h.app.session.undo.len(), undo); h.step(&json!({"click":{"world":[20,10,10]}})).unwrap(); assert_eq!(h.app.session.undo.len(), undo + 1); let state = h.app.session.world_state(); assert!(state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch.curves.iter().any(|curve| curve.link.is_some())); } #[test] fn oversized_preselection_does_not_silently_project_a_subset() { let (mut h, _) = setup(); h.app.session.selection = vec![Sel::Axis { name: "X".into() }; 1001]; let before = h.app.session.doc.to_json(); let undo = h.app.session.undo.len(); h.step(&json!({"start":"sketch.project"})).unwrap(); assert_eq!(h.app.tool.as_ref().map(|tool| tool.cmd), Some("sketch.project")); assert_eq!(h.app.session.doc.to_json(), before); assert_eq!(h.app.session.undo.len(), undo); assert_eq!(h.app.session.selection.len(), 1001); }