diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index d06c1451..bfbe6967 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -1355,7 +1355,29 @@ impl SheetBody { } (None, Some(p)) => { let q = apply_point(&inv, p); - Vec2::new(q.x, q.y).dot(n) < l0 + let q2 = Vec2::new(q.x, q.y); + // A point in the panel's slab must be on material: inside the outline and + // outside every cut-out (a side face of either counts as material). + let near = |poly: &[Vec2]| { + let m = poly.len(); + (0..m).any(|i| match (poly.get(i), poly.get((i + 1) % m)) { + (Some(&a), Some(&b)) => { + let d = b - a; + let t = if d.len2() > 1e-18 { ((q2 - a).dot(d) / d.len2()).clamp(0.0, 1.0) } else { 0.0 }; + (a + d * t).dist(q2) < 1e-3 + } + _ => false, + }) + }; + let in_slab = q.z > -1e-4 && q.z < self.t + 1e-4; + let in_hole = self.holes.iter().any(|h| h.contains(q2) && !near(&h.outer.ccw().polyline(1e-3))); + if in_slab && ((!point_in(&pts, q2) && !near(&pts)) || in_hole) { + return Err(DocError::Invalid(format!( + "the fixed point ({:.3}, {:.3}, {:.3}) is not on the sheet: pick a point on the face of the side that stays", + p.x, p.y, p.z + ))); + } + q2.dot(n) < l0 } (None, None) => side_area(n) <= side_area(n * -1.0), }; diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index 0fdef519..f136535c 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -1568,6 +1568,25 @@ fn outline_blank(s: &mut Session, outline: Value) { run(s, "sheet.flange", json!({"sketch": "Blank"})); } +/// `fixed` inside a cut-out touches no material: the fold must refuse it or say which side it kept (#263). +#[test] +fn fold_fixed_point_off_the_sheet_is_not_silently_ignored() { + let mut s = Session::default(); + outline_blank(&mut s, json!([[0, 0], [100, 0], [100, 60], [0, 60]])); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "2", "name": "Window"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 15], "p1": [40, 45]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Window", "distance": "4", "direction": "negative", "operation": "cut"})); + match s.execute("sheet.fold", &json!({"points": [[80, 0, 0.8], [80, 60, 0.8]], "fixed": [25, 30, 0.8]})) { + Err(e) => assert!(e.to_string().contains("fixed"), "{e}"), + Ok(r) => { + let warned = + ["warning", "warnings"].iter().any(|k| r.get(*k).is_some_and(|w| !w.is_null() && w != &json!([]) && w.to_string().contains("fixed"))); + assert!(warned, "fixed [25, 30] is inside the window, yet the fold succeeded silently: {r}"); + } + } +} + /// A tab whose fold line sits exactly at its root: the bend zone reaches the blank outline beside /// the tab. Every edge is straight, so the error must not blame a slanted or curved edge (#262). #[test]