829 lines
42 KiB
Rust
829 lines
42 KiB
Rust
use serde_json::{Value, json};
|
|
use solvecraft_geom::Vec2;
|
|
use solvecraft_sketch::CurveKind;
|
|
|
|
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}"),
|
|
}
|
|
}
|
|
|
|
fn ids(v: &Value) -> Vec<String> {
|
|
v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
|
|
}
|
|
|
|
fn sketch(s: &Session) -> solvecraft_sketch::Sketch {
|
|
let id = s.active_sketch.unwrap();
|
|
s.model.state().sketch(id).unwrap().sketch.clone()
|
|
}
|
|
|
|
fn new_sketch() -> Session {
|
|
let mut s = Session::default();
|
|
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
|
s
|
|
}
|
|
|
|
fn profiles(s: &Session) -> Vec<f64> {
|
|
let id = s.active_sketch.unwrap();
|
|
let mut a: Vec<f64> = s.model.state().sketch(id).unwrap().profiles.iter().map(|p| p.area).collect();
|
|
a.sort_by(|x, y| x.total_cmp(y));
|
|
a
|
|
}
|
|
|
|
fn close(a: f64, b: f64) -> bool {
|
|
(a - b).abs() < 1e-6
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_freeform_break_preserves_exact_geometry_and_undo() {
|
|
for (command, params) in [
|
|
("sketch.spline.control_point", json!({"points": [[0,0], [0,12], [20,12], [20,0]], "connect": false})),
|
|
("sketch.spline.control_point_5", json!({"points": [[0,0], [0,12], [5,20], [12,20], [20,12], [20,0]], "connect": false})),
|
|
("sketch.spline.fit_point", json!({"points": [[0,0], [6,12], [14,8], [20,0]], "connect": false})),
|
|
("sketch.conic", json!({"start": [0,0], "end": [20,0], "apex": [10,20], "rho": 0.65, "connect": false})),
|
|
] {
|
|
let mut s = new_sketch();
|
|
let id = ids(&run(&mut s, command, params)["curves"])[0].clone();
|
|
let before = sketch(&s);
|
|
let exact = before.exact_segs(before.curve_index(&id).unwrap()).unwrap();
|
|
let a = exact.first().unwrap().point_at(0.25);
|
|
let b = exact.last().unwrap().point_at(0.75);
|
|
let out = run(&mut s, "sketch.break", json!({"curve": id, "points": [b, a, a]}));
|
|
assert!(ids(&out["curves"]).len() >= 3, "{command}: {out}");
|
|
let after = sketch(&s);
|
|
let segs: Vec<_> = after.curves.iter().enumerate().flat_map(|(i, _)| after.exact_segs(i).unwrap_or_default()).collect();
|
|
for old in &exact {
|
|
for k in 0..=40 {
|
|
let q = old.point_at(k as f64 / 40.0);
|
|
let d = segs.iter().map(|seg| seg.point_at(seg.closest_param(q)).dist(q)).fold(f64::INFINITY, f64::min);
|
|
assert!(d < 1e-6, "{command}: geometry moved {d} at {q:?}");
|
|
}
|
|
}
|
|
let encoded = serde_json::to_value(&after).unwrap();
|
|
let restored: solvecraft_sketch::Sketch = serde_json::from_value(encoded).unwrap();
|
|
assert_eq!(restored.curves.len(), after.curves.len());
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert!(sketch(&s).curve_index(&id).is_some());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_ellipse_break_keeps_closed_profile() {
|
|
let mut s = new_sketch();
|
|
let id = ids(&run(&mut s, "sketch.ellipse", json!({"center": [0,0], "major": [10,0], "minor": [0,4]}))["curves"])[0].clone();
|
|
let area = profiles(&s)[0];
|
|
let out = run(&mut s, "sketch.break", json!({"curve": id, "points": [[10,0], [0,4], [10,0]]}));
|
|
assert_eq!(ids(&out["curves"]).len(), 4, "{out}");
|
|
let after = sketch(&s);
|
|
assert!(after.curves.iter().all(|c| matches!(c.kind, CurveKind::Conic { .. })));
|
|
assert_eq!(profiles(&s).len(), 1);
|
|
assert!((profiles(&s)[0] - area).abs() < 1e-6);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let original = sketch(&s);
|
|
let q = original.exact_segs(original.curve_index(&id).unwrap()).unwrap()[0].point_at(0.4);
|
|
let out = run(&mut s, "sketch.break", json!({"curve": id, "points": [q]}));
|
|
assert_eq!(ids(&out["curves"]).len(), 5, "a single cut inside a quadrant: {out}");
|
|
assert_eq!(profiles(&s).len(), 1);
|
|
assert!((profiles(&s)[0] - area).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_break_rejects_bad_points_atomically_and_merges_duplicates() {
|
|
for freeform in [false, true] {
|
|
let mut s = new_sketch();
|
|
let out = if freeform {
|
|
run(&mut s, "sketch.spline.control_point", json!({"points": [[0,0], [0,10], [20,10], [20,0]], "connect": false}))
|
|
} else {
|
|
run(&mut s, "sketch.line", json!({"points": [[0,0], [20,0]], "connect": false}))
|
|
};
|
|
let id = ids(&out["curves"])[0].clone();
|
|
let before = serde_json::to_value(sketch(&s)).unwrap();
|
|
for points in [json!([]), json!("wrong"), json!([[8, 4], ["bad", 0]]), json!([[0, 0]]), json!([[1e20, 0]]), json!(vec![[10, 5]; 1001])] {
|
|
assert!(s.execute("sketch.break", &json!({"curve": id, "points": points})).is_err());
|
|
assert_eq!(serde_json::to_value(sketch(&s)).unwrap(), before);
|
|
}
|
|
let out = run(&mut s, "sketch.break", json!({"curve": id, "points": [[10,7], [10,7]]}));
|
|
assert_eq!(ids(&out["curves"]).len(), 2, "{out}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analytic_trim_and_break_use_freeform_cutters() {
|
|
let mut s = new_sketch();
|
|
let line = ids(&run(&mut s, "sketch.line", json!({"points": [[-5,5], [25,5]], "connect": false}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.spline.control_point", json!({"points": [[0,0], [0,10], [20,10], [20,0]], "connect": false}));
|
|
let out = run(&mut s, "sketch.break", json!({"curve": line, "at": [10,5]}));
|
|
assert_eq!(ids(&out["curves"]).len(), 3, "{out}");
|
|
let sk = sketch(&s);
|
|
for id in ids(&out["curves"]) {
|
|
let shape = sk.shape(sk.curve_index(&id).unwrap()).unwrap();
|
|
assert!(matches!(shape, solvecraft_sketch::Shape::Line { a, b } if (a.y - 5.0).abs() < 1e-6 && (b.y - 5.0).abs() < 1e-6));
|
|
}
|
|
|
|
let mut s = new_sketch();
|
|
let circle = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0,0], "radius": 5}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.ellipse", json!({"center": [0,0], "major": [8,0], "minor": [0,3]}));
|
|
run(&mut s, "sketch.trim", json!({"curve": circle, "at": [5,0]}));
|
|
let sk = sketch(&s);
|
|
let ci = sk.curve_index(&circle).unwrap();
|
|
assert!(matches!(sk.curves[ci].kind, CurveKind::Arc { .. }));
|
|
assert!(sk.shape(ci).unwrap().dist(Vec2::new(-5.0, 0.0)) < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn trim_extend_break() {
|
|
let mut s = new_sketch();
|
|
// A cross: horizontal line crossed by a vertical one.
|
|
let h = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
|
let v = ids(&run(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 5]]}))["curves"])[0].clone();
|
|
// Trim the right half of the horizontal line.
|
|
run(&mut s, "sketch.trim", json!({"curve": h, "at": [15, 0]}));
|
|
let sk = sketch(&s);
|
|
let sh = sk.shape(sk.curve_index(&h).unwrap()).unwrap();
|
|
assert!(
|
|
matches!(sh, solvecraft_sketch::Shape::Line { a, b } if a.dist(Vec2::new(0.0, 0.0)) < 1e-9 && b.dist(Vec2::new(10.0, 0.0)) < 1e-9),
|
|
"{sh:?}"
|
|
);
|
|
// Trim the lower half of the vertical line: the original id survives on the upper piece.
|
|
run(&mut s, "sketch.trim", json!({"curve": v, "at": [10, -3]}));
|
|
let sk = sketch(&s);
|
|
assert_eq!(sk.curves.len(), 2);
|
|
// Extend: a short line reaching toward the vertical one.
|
|
let e = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 3], [4, 3]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.extend", json!({"curve": e, "at": [4, 3]}));
|
|
let sk = sketch(&s);
|
|
let end = sk.resolve_point(&format!("{e}.end")).and_then(|i| sk.point(i)).unwrap();
|
|
assert!(end.dist(Vec2::new(10.0, 3.0)) < 1e-7, "{end:?}");
|
|
// Break the horizontal line where... nothing crosses it inside: error.
|
|
assert!(s.execute("sketch.break", &json!({"curve": h, "at": [5, 0]})).is_err());
|
|
// Break the vertical at the extended line: two pieces.
|
|
let r = run(&mut s, "sketch.break", json!({"curve": v, "at": [10, 1]}));
|
|
assert_eq!(ids(&r["curves"]).len(), 2, "{r}");
|
|
// A lone curve trims away entirely.
|
|
let lone = ids(&run(&mut s, "sketch.line", json!({"points": [[50, 50], [60, 50]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.trim", json!({"curve": lone, "at": [55, 50]}));
|
|
assert!(sketch(&s).curve_index(&lone).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn trim_circle_makes_an_arc_and_a_profile() {
|
|
let mut s = new_sketch();
|
|
let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.line", json!({"points": [[-20, 0], [20, 0]]}));
|
|
run(&mut s, "sketch.trim", json!({"curve": c, "at": [0, -10]}));
|
|
let sk = sketch(&s);
|
|
let ci = sk.curve_index(&c).unwrap();
|
|
assert!(matches!(sk.curves[ci].kind, CurveKind::Arc { .. }));
|
|
// Upper half disc closes against the line.
|
|
let a = profiles(&s);
|
|
assert_eq!(a.len(), 1, "{a:?}");
|
|
assert!(close(a[0], std::f64::consts::PI * 50.0), "{a:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn exact_extend_ellipse_preserves_support_target_and_history() {
|
|
for (y, expected_x) in [(3.0, 12.0), (5.0, 20.0)] {
|
|
let mut s = new_sketch();
|
|
let ellipse = ids(&run(&mut s, "sketch.ellipse", json!({"center":[20,0],"major":[30,0],"minor_radius":5}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.constraint.fix", json!({"entity":ellipse}));
|
|
let line = ids(&run(&mut s, "sketch.line", json!({"points":[[0,y],[5,y]],"connect":false,"infer":false}))["curves"])[0].clone();
|
|
let before = s.doc.to_json();
|
|
let target = sketch(&s).exact_segs(0).unwrap();
|
|
run(&mut s, "sketch.extend", json!({"curve":line,"at":[5,y]}));
|
|
let sk = sketch(&s);
|
|
let end = sk.point(sk.resolve_point(&format!("{line}.end")).unwrap()).unwrap();
|
|
assert!(end.dist(Vec2::new(expected_x, y)) < 1e-7, "exact ellipse contact {end:?}");
|
|
assert_eq!(sk.exact_segs(0).unwrap(), target);
|
|
let persisted = s.doc.to_json();
|
|
let reopened = solvecraft_doc::format::read(&persisted).unwrap();
|
|
assert_eq!(reopened.to_json(), persisted);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc.to_json(), before);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
assert_eq!(s.doc.to_json(), persisted);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_extend_arc_rejects_interior_hit_atomically() {
|
|
let mut s = new_sketch();
|
|
let arc = ids(&run(&mut s, "sketch.arc.center_point", json!({"center":[0,0],"start":[10,0],"end":[0,10],"connect":false}))["curves"])[0].clone();
|
|
let x = 50.0_f64.sqrt();
|
|
let cutter = ids(&run(&mut s, "sketch.line", json!({"points":[[x,5],[x,9]],"connect":false,"infer":false}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.constraint.fix", json!({"entity":cutter}));
|
|
let before = s.doc.to_json();
|
|
let history = s.undo.len();
|
|
assert!(s.execute("sketch.extend", &json!({"curve":arc,"at":[10,0]})).is_err());
|
|
assert_eq!(s.doc.to_json(), before);
|
|
assert_eq!(s.undo.len(), history);
|
|
}
|
|
|
|
#[test]
|
|
fn exact_extend_refuses_ambiguous_stop_without_history_change() {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.line", json!({"points":[[0,0],[5,0]],"connect":false,"infer":false}));
|
|
run(&mut s, "sketch.line", json!({"points":[[10,-5],[10,5]],"connect":false,"infer":false}));
|
|
run(&mut s, "sketch.line", json!({"points":[[8,-2],[12,2]],"connect":false,"infer":false}));
|
|
let before = s.doc.to_json();
|
|
let history = s.undo.len();
|
|
let error = s.execute("sketch.extend", &json!({"curve":"l1","at":[5,0]})).unwrap_err();
|
|
assert!(error.to_string().contains("multiple curves"), "{error}");
|
|
assert_eq!(s.doc.to_json(), before);
|
|
assert_eq!(s.undo.len(), history);
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_and_chamfers() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
|
|
let l = ids(&r["curves"]);
|
|
// Pin the rectangle so a dimension change only moves the fillet.
|
|
run(&mut s, "sketch.constraint.fix", json!({"entity": l[2]}));
|
|
run(&mut s, "sketch.constraint.fix", json!({"entity": l[3]}));
|
|
let f = run(&mut s, "sketch.fillet", json!({"a": l[0], "b": l[1], "radius": 5}));
|
|
assert!(f["param"].is_string());
|
|
let a = profiles(&s);
|
|
assert!(close(a[0], 1200.0 - (25.0 - std::f64::consts::PI * 25.0 / 4.0)), "{a:?}");
|
|
// The fillet radius is a dimension: change it.
|
|
let pname = f["param"].as_str().unwrap().to_string();
|
|
run(&mut s, "parameters.change", json!({"name": pname, "expression": "8 mm"}));
|
|
let a = profiles(&s);
|
|
assert!(close(a[0], 1200.0 - (64.0 - std::f64::consts::PI * 64.0 / 4.0)), "{a:?}");
|
|
run(&mut s, "sketch.chamfer.equal_distance", json!({"a": l[2], "b": l[3], "distance": 4}));
|
|
let a2 = profiles(&s)[0];
|
|
assert!(close(a[0] - a2, 8.0), "{a2}");
|
|
run(&mut s, "sketch.chamfer.two_distance", json!({"a": l[1], "b": l[2], "distance": 2, "distance2": 6}));
|
|
let a3 = profiles(&s)[0];
|
|
assert!(close(a2 - a3, 6.0), "{a3}");
|
|
// Distance-angle at the last corner (origin, 90 degrees): 45 deg gives an isosceles cut.
|
|
run(&mut s, "sketch.chamfer.distance_angle", json!({"a": l[3], "b": l[0], "distance": 3, "angle": 45}));
|
|
let a4 = profiles(&s)[0];
|
|
assert!(close(a3 - a4, 4.5), "{a4}");
|
|
assert!(s.execute("sketch.fillet", &json!({"a": l[0], "b": l[2], "radius": 1})).is_err(), "no shared corner");
|
|
}
|
|
|
|
#[test]
|
|
fn offset_chain_mirror_patterns_move_scale() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
|
|
let l = ids(&r["curves"]);
|
|
// Outward offset by 2 (toward a point outside).
|
|
let o = run(&mut s, "sketch.offset", json!({"curves": [l[0]], "distance": 2, "side": [20, -10]}));
|
|
assert_eq!(ids(&o["curves"]).len(), 4, "{o}");
|
|
let a = profiles(&s);
|
|
assert!(close(a[1], 44.0 * 34.0 - 1200.0) || close(a.iter().sum::<f64>(), 44.0 * 34.0), "{a:?}");
|
|
// Circle offset inward.
|
|
let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [100, 0], "radius": 10}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.offset", json!({"curves": [c], "distance": 3, "side": [100, 0]}));
|
|
// Mirror a circle about a vertical line.
|
|
let m = ids(&run(&mut s, "sketch.line", json!({"points": [[120, -50], [120, 50]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.mirror", json!({"entities": [c], "line": m}));
|
|
let mc = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
assert!(sk.center(sk.curve_index(&mc).unwrap()).unwrap().dist(Vec2::new(140.0, 0.0)) < 1e-7);
|
|
// Circular pattern of the mirrored circle: 4 around (120, 0).
|
|
let r = run(&mut s, "sketch.pattern.circular", json!({"entities": [mc], "center": [120, 0], "count": 4}));
|
|
assert_eq!(ids(&r["curves"]).len(), 3);
|
|
let sk = sketch(&s);
|
|
let c2 = sk.center(sk.curve_index(&ids(&r["curves"])[0]).unwrap()).unwrap();
|
|
assert!(c2.dist(Vec2::new(120.0, 20.0)) < 1e-7, "{c2:?}");
|
|
// Rectangular pattern 3 x 2.
|
|
let small = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 100], "radius": 1}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.pattern.rectangular", json!({"entities": [small], "count": 3, "spacing": 5, "count2": 2, "spacing2": 4}));
|
|
assert_eq!(ids(&r["curves"]).len(), 5);
|
|
// Move and scale.
|
|
run(&mut s, "sketch.move", json!({"entities": [small], "translate": [0, 10]}));
|
|
run(&mut s, "sketch.scale", json!({"entities": [small], "base": [0, 110], "factor": 3}));
|
|
let sk = sketch(&s);
|
|
let ci = sk.curve_index(&small).unwrap();
|
|
assert!(sk.center(ci).unwrap().dist(Vec2::new(0.0, 110.0)) < 1e-7);
|
|
assert!(close(sk.radius(ci).unwrap(), 3.0));
|
|
let r = run(&mut s, "sketch.move", json!({"entities": [small], "angle": 90, "center": [0, 0], "copy": true}));
|
|
let cp = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
assert!(sk.center(sk.curve_index(&cp).unwrap()).unwrap().dist(Vec2::new(-110.0, 0.0)) < 1e-7);
|
|
}
|
|
|
|
#[test]
|
|
fn new_creation_tools() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.line.midpoint", json!({"mid": [0, 0], "end": [10, 5]}));
|
|
let l = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let start = sk.resolve_point(&format!("{l}.start")).and_then(|i| sk.point(i)).unwrap();
|
|
assert!(start.dist(Vec2::new(-10.0, -5.0)) < 1e-9);
|
|
// Tangent arc off the end of a horizontal line.
|
|
let h = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 20], [10, 20]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.arc.tangent", json!({"start": format!("{h}.end"), "end": [20, 30]}));
|
|
let a = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let ai = sk.curve_index(&a).unwrap();
|
|
assert!(sk.center(ai).unwrap().dist(Vec2::new(10.0, 30.0)) < 1e-7, "{:?}", sk.center(ai));
|
|
assert!(close(sk.radius(ai).unwrap(), 10.0));
|
|
// 2-tangent circle in the corner of two lines.
|
|
let x = ids(&run(&mut s, "sketch.line", json!({"points": [[100, 0], [150, 0]]}))["curves"])[0].clone();
|
|
let y = ids(&run(&mut s, "sketch.line", json!({"points": [[100, 0], [100, 50]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.circle.two_tangent", json!({"curves": [x, y], "radius": 5, "near": [106, 104 - 100]}));
|
|
let c = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let ci = sk.curve_index(&c).unwrap();
|
|
assert!(sk.center(ci).unwrap().dist(Vec2::new(105.0, 5.0)) < 1e-6, "{:?}", sk.center(ci));
|
|
// 3-tangent circle inside a triangle: the incircle of the 3-4-5 triangle (r = 1).
|
|
let t = run(&mut s, "sketch.line", json!({"points": [[200, 0], [203, 0], [200, 4]], "closed": true}));
|
|
let tl = ids(&t["curves"]);
|
|
let r = run(&mut s, "sketch.circle.three_tangent", json!({"curves": tl, "near": [201, 1.2]}));
|
|
let c = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let ci = sk.curve_index(&c).unwrap();
|
|
assert!(close(sk.radius(ci).unwrap(), 1.0), "{:?}", sk.radius(ci));
|
|
assert!(sk.center(ci).unwrap().dist(Vec2::new(201.0, 1.0)) < 1e-6);
|
|
// Slots.
|
|
let before = profiles(&s).len();
|
|
run(&mut s, "sketch.slot.center_point", json!({"center": [0, -100], "end": [10, -100], "width": 4}));
|
|
run(&mut s, "sketch.slot.arc_three_point", json!({"start": [100, -100], "through": [110, -90], "end": [120, -100], "width": 2}));
|
|
run(&mut s, "sketch.slot.arc_center", json!({"center": [200, -100], "start": [210, -100], "end": [200, -90], "width": 2}));
|
|
let a = profiles(&s);
|
|
assert_eq!(a.len(), before + 3, "{a:?}");
|
|
let pi = std::f64::consts::PI;
|
|
assert!(a.iter().any(|x| close(*x, 20.0 * 4.0 + pi * 4.0)), "{a:?}");
|
|
// Half-circle arc slot: centre arc length pi*10, width 2, plus a full cap circle.
|
|
assert!(a.iter().any(|x| close(*x, pi * 10.0 * 2.0 + pi)), "{a:?}");
|
|
assert!(a.iter().any(|x| close(*x, pi * 5.0 * 2.0 + pi)), "{a:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn ellipse_splines_conic() {
|
|
let pi = std::f64::consts::PI;
|
|
let mut s = new_sketch();
|
|
let e = run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor": [0, 4]}));
|
|
let eid = ids(&e["curves"])[0].clone();
|
|
let a = profiles(&s);
|
|
assert!((a[0] - pi * 40.0).abs() / (pi * 40.0) < 2e-3, "{a:?}");
|
|
// A point constrained onto the ellipse lands on it.
|
|
run(&mut s, "sketch.point", json!({"point": [3, 5], "id": "q"}));
|
|
run(&mut s, "sketch.constraint.coincident", json!({"a": "q", "b": eid}));
|
|
let sk = sketch(&s);
|
|
let q = sk.point(sk.point_index("q").unwrap()).unwrap();
|
|
let exact = sk.exact_segs(sk.curve_index(&eid).unwrap()).unwrap();
|
|
let distance = exact.iter().map(|seg| q.dist(seg.point_at(seg.closest_param(q)))).fold(f64::INFINITY, f64::min);
|
|
assert!(distance < 1e-6, "on the exact ellipse: {q:?}");
|
|
// Fit spline closed by a line: one profile.
|
|
let sp = run(&mut s, "sketch.spline.fit_point", json!({"points": [[100, 0], [110, 8], [120, 3], [130, 0]]}));
|
|
let sid = ids(&sp["curves"])[0].clone();
|
|
run(&mut s, "sketch.line", json!({"points": [format!("{sid}.end"), format!("{sid}.start")]}));
|
|
assert_eq!(profiles(&s).len(), 2);
|
|
// Moving a fit point reshapes the spline (it is a sketch point).
|
|
let sk = sketch(&s);
|
|
let si = sk.curve_index(&sid).unwrap();
|
|
let pts = sk.curves[si].kind.point_ids();
|
|
let pid = sk.points[pts[1]].id.clone();
|
|
run(&mut s, "sketch.move_point", json!({"point": pid, "to": [110, 12]}));
|
|
let sk = sketch(&s);
|
|
assert!(sk.polyline(si).iter().any(|q| q.dist(Vec2::new(110.0, 12.0)) < 1e-9));
|
|
// Control point splines (degree 3 and 5) and a conic.
|
|
run(&mut s, "sketch.spline.control_point", json!({"points": [[0, 50], [0, 60], [10, 60], [10, 50]]}));
|
|
run(&mut s, "sketch.spline.control_point_5", json!({"points": [[20, 50], [20, 60], [25, 65], [30, 60], [35, 62], [40, 50]]}));
|
|
let w = std::f64::consts::FRAC_1_SQRT_2;
|
|
let k = run(&mut s, "sketch.conic", json!({"start": [210, 0], "end": [200, 10], "apex": [210, 10], "rho": w / (1.0 + w)}));
|
|
let kid = ids(&k["curves"])[0].clone();
|
|
// A line tangent to the conic at its end: the tangent arc tool continues it.
|
|
let r = run(&mut s, "sketch.arc.tangent", json!({"start": format!("{kid}.end"), "end": [190, 0]}));
|
|
let aid = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
// The conic is a quarter circle about (200, 0) leaving (200, 10) heading -x: the tangent
|
|
// arc from there to (190, 0) continues the same circle.
|
|
let ai = sk.curve_index(&aid).unwrap();
|
|
assert!(sk.center(ai).unwrap().dist(Vec2::new(200.0, 0.0)) < 1e-6, "{:?}", sk.center(ai));
|
|
assert!(s.execute("sketch.spline.fit_point", &json!({"points": [[0, 0]]})).is_err());
|
|
assert!(s.execute("sketch.conic", &json!({"start": [0, 0], "end": [1, 0], "apex": [0, 1], "rho": 1.5})).is_err());
|
|
// Mirror and pattern copy free-form curves too.
|
|
let m = ids(&run(&mut s, "sketch.line", json!({"points": [[-50, -50], [-50, 50]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.mirror", json!({"entities": [eid, sid], "line": m}));
|
|
assert_eq!(ids(&r["curves"]).len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn text_makes_profiles_that_extrude_and_edit() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.text", json!({"text": "SO", "at": [0, 0], "height": 10}));
|
|
let link = r["link"].as_str().unwrap().to_string();
|
|
let a = profiles(&s);
|
|
// S (one region) + O (ring = outer with a hole, plus the counter as its own region).
|
|
assert!(a.len() >= 2, "{a:?}");
|
|
let si = run(&mut s, "sketch.inspect", json!({}));
|
|
assert_eq!(si["dof"], 0);
|
|
// Edit the text: the outlines change, the link stays.
|
|
let r2 = run(&mut s, "sketch.edit_text", json!({"link": link, "text": "I", "height": 20}));
|
|
assert_eq!(r2["link"], json!(link));
|
|
let a = profiles(&s);
|
|
assert_eq!(a.len(), 1, "{a:?}");
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 2}));
|
|
let m = run(&mut s, "inspect.measure", json!({}));
|
|
let v = m["total"]["volume_mm3"].as_f64().unwrap();
|
|
// An "I" 20 mm tall is a bar a couple of mm wide.
|
|
assert!(v > 40.0 && v < 200.0, "{v}");
|
|
assert!(s.execute("sketch.text", &json!({"text": "", "at": [0, 0]})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn blend_curve_joins_tangentially() {
|
|
let mut s = new_sketch();
|
|
let a = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}))["curves"])[0].clone();
|
|
let b = ids(&run(&mut s, "sketch.line", json!({"points": [[20, 10], [20, 20]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.blend_curve", json!({"a": format!("{a}.end"), "b": format!("{b}.start")}));
|
|
assert_eq!(r["sketch"]["solved"], true, "{r}");
|
|
let sp = ids(&r["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let pts: Vec<Vec2> = sk.curves[sk.curve_index(&sp).unwrap()].kind.point_ids().iter().map(|i| sk.point(*i).unwrap()).collect();
|
|
// Leaves along +x, arrives going +y (control polygon end legs).
|
|
let d0 = (pts[1] - pts[0]).normalized().unwrap();
|
|
let d1 = (pts[3] - pts[2]).normalized().unwrap();
|
|
assert!(d0.y.abs() < 1e-6 && d0.x > 0.0, "{d0:?}");
|
|
assert!(d1.x.abs() < 1e-6 && d1.y > 0.0, "{d1:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn centerline_is_left_out_of_profiles() {
|
|
let mut s = new_sketch();
|
|
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
|
|
let l = ids(&r["curves"]);
|
|
let m = ids(&run(&mut s, "sketch.line", json!({"points": [[5, -5], [5, 15]]}))["curves"])[0].clone();
|
|
assert_eq!(profiles(&s).len(), 2);
|
|
run(&mut s, "sketch.centerline", json!({"curves": [m]}));
|
|
assert_eq!(profiles(&s).len(), 1);
|
|
assert!(s.execute("sketch.centerline", &json!({"curves": ["nope"]})).is_err());
|
|
let _ = l;
|
|
}
|
|
|
|
#[test]
|
|
fn free_form_profiles_extrude_exactly() {
|
|
let pi = std::f64::consts::PI;
|
|
let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
|
|
// Ellipse a = 10, b = 4: π a b h.
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}));
|
|
let a = profiles(&s)[0];
|
|
assert!((a - pi * 40.0).abs() < 1e-9, "{a}");
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 5}));
|
|
let v = vol(&mut s);
|
|
assert!((v - pi * 40.0 * 5.0).abs() / (pi * 200.0) < 2e-4, "{v}");
|
|
let m = run(&mut s, "inspect.measure", json!({}));
|
|
assert_eq!(m["bodies"][0]["faces"], 3, "an exact ellipse: top, bottom and one side ({m})");
|
|
// A region closed by a fit spline and a line.
|
|
let mut s = new_sketch();
|
|
let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 8], [20, 0]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]}));
|
|
let a = profiles(&s)[0];
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 2}));
|
|
let v = vol(&mut s);
|
|
assert!((v - a * 2.0).abs() / (a * 2.0) < 2e-4, "{v} vs {}", a * 2.0);
|
|
let m = run(&mut s, "inspect.measure", json!({}));
|
|
assert!(m["bodies"][0]["faces"].as_u64().unwrap() <= 5, "{m}");
|
|
}
|
|
|
|
#[test]
|
|
fn degree_five_and_cut_free_form_profiles_extrude_exactly() {
|
|
let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
|
|
// A degree-5 control spline closed by a line: one B-spline side face.
|
|
let mut s = new_sketch();
|
|
let sp =
|
|
ids(&run(&mut s, "sketch.spline.control_point_5", json!({"points": [[0, 0], [4, 10], [10, 12], [16, 12], [22, 10], [26, 0]]}))["curves"])[0]
|
|
.clone();
|
|
run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]}));
|
|
let a = profiles(&s)[0];
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 3}));
|
|
let v = vol(&mut s);
|
|
assert!((v - a * 3.0).abs() / (a * 3.0) < 2e-4, "{v} vs {}", a * 3.0);
|
|
let m = run(&mut s, "inspect.measure", json!({}));
|
|
assert_eq!(m["bodies"][0]["faces"], 4, "{m}");
|
|
// A fit spline arch cut by a line: both sides extrude with exact curved faces.
|
|
let mut s = new_sketch();
|
|
let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [6, 9], [14, 9], [20, 0]]}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]}));
|
|
run(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 20]]}));
|
|
let areas = profiles(&s);
|
|
assert_eq!(areas.len(), 2, "{areas:?}");
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"distance": 2, "profiles": [0]}));
|
|
let v = vol(&mut s);
|
|
assert!((v - areas[0] * 2.0).abs() / (areas[0] * 2.0) < 2e-4, "{v} vs {}", areas[0] * 2.0);
|
|
let m = run(&mut s, "inspect.measure", json!({}));
|
|
assert!(m["bodies"][0]["faces"].as_u64().unwrap() <= 7, "no polyline facets: {m}");
|
|
}
|
|
|
|
#[test]
|
|
fn trim_and_break_free_form_curves_exactly() {
|
|
let pi = std::f64::consts::PI;
|
|
let mut s = new_sketch();
|
|
// An ellipse cut by a line through its centre: trimming the lower half leaves the upper
|
|
// half, which closes with the line into half the ellipse.
|
|
let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone();
|
|
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-15, 0], [15, 0]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.trim", json!({"curve": e, "at": [0, -4]}));
|
|
assert!(!r["result"]["pieces"].as_array().unwrap().is_empty(), "{r}");
|
|
let a = profiles(&s);
|
|
assert_eq!(a.len(), 1, "{a:?}");
|
|
assert!((a[0] - pi * 20.0).abs() < 1e-6, "{a:?}");
|
|
let _ = l;
|
|
// A fit spline broken where a line crosses it: two pieces that still trace the curve.
|
|
let mut s = new_sketch();
|
|
let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 10], [20, 0], [30, 5]]}))["curves"])[0].clone();
|
|
let before = sketch(&s).polyline(sketch(&s).curve_index(&sp).unwrap());
|
|
run(&mut s, "sketch.line", json!({"points": [[15, -10], [15, 20]]}));
|
|
let r = run(&mut s, "sketch.break", json!({"curve": sp, "at": [15, 5]}));
|
|
let pieces = ids(&r["curves"]);
|
|
assert_eq!(pieces.len(), 4, "three Béziers, the middle one split: {r}");
|
|
let sk = sketch(&s);
|
|
// Every original sample lies on one of the pieces.
|
|
let polys: Vec<Vec<Vec2>> = pieces.iter().map(|id| sk.polyline(sk.curve_index(id).unwrap())).collect();
|
|
for q in before.iter().step_by(5) {
|
|
let d = polys
|
|
.iter()
|
|
.flat_map(|p| {
|
|
p.windows(2).map(|w| {
|
|
let (a, b) = (w[0], w[1]);
|
|
let t = ((*q - a).dot(b - a) / (b - a).len2()).clamp(0.0, 1.0);
|
|
q.dist(a + (b - a) * t)
|
|
})
|
|
})
|
|
.fold(f64::INFINITY, f64::min);
|
|
assert!(d < 0.05, "{q:?} {d}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ellipse_radii_dimensions() {
|
|
let mut s = new_sketch();
|
|
let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.dimension", json!({"entities": [e], "type": "major", "value": 12}));
|
|
run(&mut s, "sketch.dimension", json!({"entities": [e], "value": 5}));
|
|
let a = profiles(&s)[0];
|
|
assert!((a - std::f64::consts::PI * 60.0).abs() < 1e-6, "{a}");
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_a_line_and_an_arc() {
|
|
let mut s = new_sketch();
|
|
// A line into a quarter arc meeting at a sharp corner.
|
|
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
|
let a =
|
|
ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone();
|
|
let _ = a;
|
|
let f = run(&mut s, "sketch.fillet", json!({"point": format!("{l}.end"), "radius": 2}));
|
|
let arc = ids(&f["curves"])[0].clone();
|
|
let sk = sketch(&s);
|
|
let ai = sk.curve_index(&arc).unwrap();
|
|
assert!((sk.radius(ai).unwrap() - 2.0).abs() < 1e-7);
|
|
// The fillet touches the line (its centre 2 below it) and the arc's circle (outside it: 10 + 2).
|
|
let c = sk.center(ai).unwrap();
|
|
assert!((c.y - 2.0).abs() < 1e-6 || (c.y + 2.0).abs() < 1e-6, "{c:?}");
|
|
let d = c.dist(Vec2::new(30.0, 0.0));
|
|
assert!((d - 12.0).abs() < 1e-6, "{d} {c:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn offset_free_form_curves() {
|
|
let mut s = new_sketch();
|
|
let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 8], [20, 0], [30, 6]]}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.offset", json!({"curves": [sp], "distance": 2, "side": [10, 20]}));
|
|
let made = ids(&r["curves"]);
|
|
assert_eq!(made.len(), 1, "{r}");
|
|
let sk = sketch(&s);
|
|
let orig = sk.polyline(sk.curve_index(&sp).unwrap());
|
|
let off = sk.polyline(sk.curve_index(&made[0]).unwrap());
|
|
let dist = |q: Vec2| {
|
|
orig.windows(2)
|
|
.map(|w| {
|
|
let (a, b) = (w[0], w[1]);
|
|
let t = ((q - a).dot(b - a) / (b - a).len2()).clamp(0.0, 1.0);
|
|
q.dist(a + (b - a) * t)
|
|
})
|
|
.fold(f64::INFINITY, f64::min)
|
|
};
|
|
for q in off.iter().step_by(4) {
|
|
assert!((dist(*q) - 2.0).abs() < 0.08, "{q:?} {}", dist(*q));
|
|
}
|
|
// An ellipse grows by the offset all round.
|
|
let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [100, 0], "major": [110, 0], "minor_radius": 5}))["curves"])[0].clone();
|
|
let r = run(&mut s, "sketch.offset", json!({"curves": [e], "distance": 1, "side": [100, 20]}));
|
|
assert_eq!(ids(&r["curves"]).len(), 1, "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn certified_offset_follows_dimension_drag_reopen_break_and_undo() {
|
|
let mut s = new_sketch();
|
|
let ellipse = ids(&run(&mut s, "sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5,"connect":false}))["curves"])[0].clone();
|
|
run(&mut s, "sketch.dimension", json!({"entities":[ellipse],"type":"major","value":10}));
|
|
let before = s.doc.to_json();
|
|
let out = run(&mut s, "sketch.offset", json!({"curves":[ellipse],"distance":-1,"chain":false}));
|
|
let output = out["curves"][0].as_str().unwrap().to_string();
|
|
assert_eq!(sketch(&s).freeform_offsets.len(), 1);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc.to_json(), before);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
let result = run(&mut s, "parameters.change", json!({"name":"d1","expression":12}));
|
|
assert_eq!(result["errors"], json!([]));
|
|
let verify = |s: &Session| {
|
|
let sk = sketch(s);
|
|
let source = sk.curve_index(&ellipse).unwrap();
|
|
let (c, m) = match sk.curves[source].kind {
|
|
CurveKind::Ellipse { c, m, .. } => (sk.point(c).unwrap(), sk.point(m).unwrap()),
|
|
_ => panic!("ellipse"),
|
|
};
|
|
let derived = sk.exact_segs(sk.curve_index(&output).unwrap()).unwrap();
|
|
let want = m + (m - c).normalized().unwrap();
|
|
assert!(derived[0].point_at(0.0).dist(want) < 1e-7, "updated offset {want:?}");
|
|
};
|
|
verify(&s);
|
|
run(&mut s, "sketch.drag", json!({"entity":ellipse,"from":[0,0],"to":[4,3]}));
|
|
verify(&s);
|
|
let mut reopened = Session {
|
|
doc: std::sync::Arc::new(solvecraft_doc::format::read(&s.doc.to_json()).unwrap()),
|
|
active_sketch: s.active_sketch,
|
|
..Session::default()
|
|
};
|
|
reopened.refresh();
|
|
verify(&reopened);
|
|
let attached = reopened.doc.to_json();
|
|
run(&mut reopened, "sketch.offset.break", json!({"curve":output}));
|
|
let sk = sketch(&reopened);
|
|
assert!(sk.freeform_offsets.is_empty());
|
|
assert!(sk.curves[sk.curve_index(&output).unwrap()].kind.point_ids().iter().all(|i| !sk.points[*i].fixed));
|
|
run(&mut reopened, "edit.undo", json!({}));
|
|
assert_eq!(reopened.doc.to_json(), attached);
|
|
}
|
|
|
|
#[test]
|
|
fn certified_offset_delete_output_undo_reopen_and_refuse_dependent_source() {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5}));
|
|
let out = run(&mut s, "sketch.offset", json!({"curves":["e1"],"distance":-1,"chain":false}));
|
|
let id = out["curves"][0].as_str().unwrap().to_string();
|
|
let attached = s.doc.to_json();
|
|
let history = s.undo.len();
|
|
assert!(s.execute("sketch.delete", &json!({"entities":["e1"]})).is_err());
|
|
assert_eq!(s.doc.to_json(), attached);
|
|
assert_eq!(s.undo.len(), history);
|
|
run(&mut s, "sketch.delete", json!({"entities":[id]}));
|
|
let sk = sketch(&s);
|
|
assert!(sk.freeform_offsets.is_empty());
|
|
assert!(sk.curve_index(&id).is_none());
|
|
assert!(sk.curve_index("e1").is_some());
|
|
let deleted = s.doc.to_json();
|
|
let reopened = solvecraft_doc::format::read(&deleted).unwrap();
|
|
assert_eq!(reopened.to_json(), deleted);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc.to_json(), attached);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
assert_eq!(s.doc.to_json(), deleted);
|
|
// The central removal path also handles a deleted fit node, which removes its curve.
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let sk = sketch(&s);
|
|
let node = sk.points[sk.curves[sk.curve_index(&id).unwrap()].kind.point_ids()[3]].id.clone();
|
|
run(&mut s, "sketch.delete", json!({"entities":[node]}));
|
|
assert!(sketch(&s).freeform_offsets.is_empty());
|
|
assert!(sketch(&s).curve_index(&id).is_none());
|
|
assert_eq!(sketch(&s).points.len(), 3, "all private derived nodes must be removed");
|
|
}
|
|
|
|
#[test]
|
|
fn periodic_closed_offset_chain_break_drag_reopen_and_material_volume() {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5}));
|
|
run(&mut s, "sketch.dimension", json!({"entities":["e1"],"type":"major","value":10}));
|
|
let source = sketch(&s).points.clone();
|
|
let a = run(&mut s, "sketch.offset", json!({"curves":["e1"],"distance":-1,"chain":false}))["curves"][0].as_str().unwrap().to_string();
|
|
let before_second = s.doc.to_json();
|
|
let b = run(&mut s, "sketch.offset", json!({"curves":[a],"distance":-0.5,"chain":false}))["curves"][0].as_str().unwrap().to_string();
|
|
assert_eq!(&sketch(&s).points[..source.len()], source.as_slice());
|
|
let check = |session: &Session| {
|
|
let sk = sketch(session);
|
|
assert_eq!(sk.freeform_offsets.iter().map(|r| r.certificate).collect::<Vec<_>>(), [2, 2]);
|
|
assert_eq!(solvecraft_sketch::find_profiles(&sk).len(), 3);
|
|
for id in [&a, &b] {
|
|
assert!(matches!(sk.curves[sk.curve_index(id).unwrap()].kind, CurveKind::Spline { periodic: true, .. }));
|
|
}
|
|
};
|
|
check(&s);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc.to_json(), before_second);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
check(&s);
|
|
run(&mut s, "parameters.change", json!({"name":"d1","expression":12}));
|
|
check(&s);
|
|
let mut reopened =
|
|
Session { doc: solvecraft_doc::format::read(&s.doc.to_json()).unwrap().into(), active_sketch: s.active_sketch, ..Default::default() };
|
|
reopened.refresh();
|
|
check(&reopened);
|
|
let attached = reopened.doc.to_json();
|
|
let exact_before = sketch(&reopened).exact_segs(sketch(&reopened).curve_index(&b).unwrap()).unwrap();
|
|
run(&mut reopened, "sketch.offset.break", json!({"curve":b}));
|
|
let sk = sketch(&reopened);
|
|
assert_eq!(sk.exact_segs(sk.curve_index(&b).unwrap()).unwrap(), exact_before);
|
|
let points = sk.curves[sk.curve_index(&b).unwrap()].kind.point_ids();
|
|
let first = sk.points[points[0]].id.clone();
|
|
let from = sk.points[points[0]].pos;
|
|
run(&mut reopened, "sketch.drag", json!({"entity":first,"from":from,"to":[from.x+0.2,from.y+0.1]}));
|
|
let moved = sketch(&reopened);
|
|
moved.validate_periodic_splines().unwrap();
|
|
let indices = moved.curves[moved.curve_index(&b).unwrap()].kind.point_ids();
|
|
assert_eq!(moved.point(indices[0]), moved.point(*indices.last().unwrap()));
|
|
assert!(moved.point(indices[0]).unwrap().dist(from) > 0.1);
|
|
assert_eq!(solvecraft_doc::format::read(&reopened.doc.to_json()).unwrap().to_json(), reopened.doc.to_json());
|
|
run(&mut reopened, "edit.undo", json!({}));
|
|
run(&mut reopened, "edit.undo", json!({}));
|
|
assert_eq!(reopened.doc.to_json(), attached);
|
|
// An independent ring area multiplied by height probes actual profile material.
|
|
let regions = solvecraft_sketch::find_profiles(&sketch(&s));
|
|
let selected = regions.iter().enumerate().min_by(|(_, x), (_, y)| x.area.total_cmp(&y.area)).unwrap();
|
|
let n = 4096;
|
|
let h = std::f64::consts::TAU / n as f64;
|
|
let perimeter = (0..=n)
|
|
.map(|i| {
|
|
let t = i as f64 * h;
|
|
(144.0 * t.sin().powi(2) + 25.0 * t.cos().powi(2)).sqrt()
|
|
* if i == 0 || i == n {
|
|
1.0
|
|
} else if i % 2 == 0 {
|
|
2.0
|
|
} else {
|
|
4.0
|
|
}
|
|
})
|
|
.sum::<f64>()
|
|
* h
|
|
/ 3.0;
|
|
let analytic_ring = 0.5 * perimeter + 1.25 * std::f64::consts::PI;
|
|
assert!((selected.1.area - analytic_ring).abs() < 0.03);
|
|
let expected = analytic_ring * 3.0;
|
|
let sketch_id = s.active_sketch.unwrap();
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"sketch":sketch_id,"profiles":[selected.0],"distance":3,"operation":"new"}));
|
|
let state = s.world_state();
|
|
let volume = solvecraft_kernel::measure(&state.bodies.last().unwrap().body).unwrap().volume;
|
|
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
|
|
}
|
|
|
|
#[test]
|
|
fn periodic_fit_creator_keeps_legacy_and_refuses_hostile_intent_atomically() {
|
|
let mut s = new_sketch();
|
|
let p = json!({"points":[[0,0],[4,0],[3,2],[-1,1]],"periodic":true});
|
|
let id = run(&mut s, "sketch.spline.fit_point", p.clone())["curves"][0].as_str().unwrap().to_string();
|
|
let sk = sketch(&s);
|
|
let i = sk.curve_index(&id).unwrap();
|
|
assert!(matches!(sk.curves[i].kind, CurveKind::Spline { periodic: true, .. }));
|
|
sk.validate_periodic_splines().unwrap();
|
|
assert_eq!(solvecraft_sketch::find_profiles(&sk).len(), 1);
|
|
let before = s.doc.to_json();
|
|
let history = s.undo.len();
|
|
for (cmd, p) in [
|
|
("sketch.spline.control_point", p),
|
|
("sketch.spline.fit_point", json!({"points":[[0,0],[1,0],[0,0],[1,0]],"periodic":true})),
|
|
("sketch.spline.fit_point", json!({"points":[[0,0],[1,0],[1,1]],"periodic":"yes"})),
|
|
] {
|
|
assert!(s.execute(cmd, &p).is_err());
|
|
assert_eq!(s.doc.to_json(), before);
|
|
assert_eq!(s.undo.len(), history);
|
|
}
|
|
let indices = sk.curves[i].kind.point_ids();
|
|
let point = sk.points[indices[1]].id.clone();
|
|
let target = sk.points[indices[0]].pos;
|
|
let redo = s.redo.len();
|
|
assert!(s.execute("sketch.drag", &json!({"entity":point,"from":sk.points[indices[1]].pos,"to":target})).is_err());
|
|
assert_eq!(s.doc.to_json(), before);
|
|
assert_eq!(s.undo.len(), history);
|
|
assert_eq!(s.redo.len(), redo);
|
|
let legacy =
|
|
run(&mut s, "sketch.spline.fit_point", json!({"points":[[0,0],[4,0],[3,2],[-1,1],[0,0]]}))["curves"][0].as_str().unwrap().to_string();
|
|
assert!(matches!(sketch(&s).curves[sketch(&s).curve_index(&legacy).unwrap()].kind, CurveKind::Spline { periodic: false, .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn analytic_offset_partial_delete_releases_survivors_preserves_source_and_undo() {
|
|
let mut s = new_sketch();
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[20,30]}));
|
|
let out = run(&mut s, "sketch.offset", json!({"curves":["l1"],"distance":-1}));
|
|
let outputs = ids(&out["curves"]);
|
|
assert_eq!(outputs.len(), 4);
|
|
let before = s.doc.to_json();
|
|
run(&mut s, "sketch.delete", json!({"entities":[outputs[0]]}));
|
|
let sk = sketch(&s);
|
|
assert!(sk.offsets.is_empty());
|
|
for id in outputs.iter().skip(1) {
|
|
let curve = &sk.curves[sk.curve_index(id).unwrap()];
|
|
assert!(curve.kind.point_ids().iter().all(|i| !sk.points[*i].fixed));
|
|
}
|
|
assert!(sk.curve_index("l1").is_some());
|
|
let saved = s.doc.to_json();
|
|
assert_eq!(solvecraft_doc::format::read(&saved).unwrap().to_json(), saved);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc.to_json(), before);
|
|
}
|