An edge flange picked inside a notch is built on the notch's edge (#5306)

A notch cut into a sheet's base is a cut-out, so the base outline that
edge_at and the edge names read has no edge along it: a flange picked on
the notch's bottom edge took the name of the nearest outline edge (the
plate's far edge) and was built on the stretch of it the notch left. A
pick nearer a notch's edge than any outline edge now names no edge, and
the flange runs the base's outline round that notch first, keeping as a
cut-out only what it takes of the flanges, then trims and bends as on any
edge of the outline. The edge of a hole inside the base is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-26 05:55:43 -04:00
parent 5a9bf47420
commit 9a93f9d215
2 changed files with 113 additions and 1 deletions
+73 -1
View File
@@ -2297,6 +2297,67 @@ impl SheetBody {
best
}
/// The cut-out whose edge across the base is the sheet's edge nearest the world point `p`,
/// nearer than any edge of a panel's outline (`edge_at`, which doesn't see cut-outs): the
/// edge of a notch.
fn base_cut_edge_at(&self, p: Vec3) -> Option<usize> {
let inv = mat_inverse(&self.world(None))?;
let q = apply_point(&inv, p);
let (q2, dz) = (Vec2::new(q.x, q.y), q.z.abs().min((q.z - self.t).abs()));
let outline = self.base.outer.ccw().polyline(1e-3);
let panel_edge = self.edge_at(p).map_or(f64::INFINITY, |e| e.4);
let mut best: Option<(usize, f64)> = None;
for (k, cut) in self.holes.iter().enumerate() {
let pts = cut.outer.polyline(1e-3);
let m = pts.len();
for i in 0..m {
let (Some(&a), Some(&b)) = (pts.get(i), pts.get((i + 1) % m)) else { continue };
let d = b - a;
let mid = (a + b) * 0.5;
// Only where it runs across the base's material is it an edge of the sheet.
if d.len2() < 1e-18 || !outline.contains(mid) || self.holes.iter().enumerate().any(|(j, h)| j != k && h.contains(mid)) {
continue;
}
let t = ((q2 - a).dot(d) / d.len2()).clamp(0.0, 1.0);
let dist = ((a + d * t) - q2).len().hypot(dz);
if dist < panel_edge - 1e-9 && best.is_none_or(|x| dist < x.1) {
best = Some((k, dist));
}
}
}
best.map(|x| x.0)
}
/// Run the base's outline round its cut-out `k`, a notch, so that the notch's edges are the
/// base's own. What the cut-out takes of the flanges stays a cut-out; the rest of it is in
/// the base's outline now, and a flange laid out in the flat where the notch was keeps its
/// material. A cut-out that leaves a hole in the base or splits it has no edge a flange can
/// stand on yet.
fn open_base_to(&mut self, k: usize) -> Result<()> {
let cut = planar_region(self.holes.get(k).ok_or_else(|| DocError::Invalid("cut-out".into()))?)?;
let mut work = FlatWork::new();
let mut left = material_boolean(&[planar_region(&self.base)?], std::slice::from_ref(&cut), kernel::BoolOp::Cut, &mut work)?;
let open = match (left.pop(), left.is_empty()) {
(Some(open), true) if open.holes.len() == self.base.holes.len() => open,
_ => {
return Err(DocError::Invalid(
"not supported yet: a flange on the edge of a hole in the sheet, or of a cut-out that splits it".into(),
));
}
};
let mut pieces = Vec::with_capacity(self.flanges.len() * 2);
for f in &self.flanges {
pieces.push(planar_region(&self.zone(f))?);
if f.leg > 1e-9 {
pieces.push(planar_region(&self.panel(f))?);
}
}
let rest = if pieces.is_empty() { Vec::new() } else { material_boolean(&[cut], &pieces, kernel::BoolOp::Intersect, &mut work)? };
self.base = open;
self.holes.splice(k..=k, rest);
Ok(())
}
/// What `cuts` (the sheet's cut-outs) leave of a panel's edge a→b: the stretch nearest `pick`
/// (a world point), as distances from `a` along the edge; of two as near, the first. `None`
/// where they have taken the edge whole.
@@ -2610,6 +2671,13 @@ impl SheetBody {
}
sizes.push((setback, leg));
}
// A pick on an edge of a notch (a cut-out that opens the base's outline) is on an edge of
// the base: its outline runs round the notch first (#5306).
for e in edges {
if let Some(k) = self.base_cut_edge_at(e.pick) {
self.open_base_to(k)?;
}
}
// Find every edge before anything moves: of an edge that cut-outs have taken part of, the
// stretch they leave nearest the pick, and at which of its ends a cut-out is.
let cuts = self.cut_outs();
@@ -4019,8 +4087,12 @@ impl SheetBody {
}
}
/// The persistent name of the sheet edge nearest a world point.
/// The persistent name of the sheet edge nearest a world point. An edge a notch made (see
/// `base_cut_edge_at`) has none: no line of the base sketch runs along it.
pub fn edge_name(&self, pick: Vec3) -> Option<String> {
if self.base_cut_edge_at(pick).is_some() {
return None;
}
let (panel, a, b, top, _) = self.edge_at(pick)?;
self.edge_name_of(panel, a, b, top)
}
+40
View File
@@ -1640,6 +1640,46 @@ fn plate_with_a_cut(s: &mut Session, cut: [[f64; 2]; 2]) {
run(s, "solid.extrude", json!({"sketch": "Cut", "through_all": true, "direction": "symmetric", "operation": "cut"}));
}
/// A flange picked on the edge inside a notch is on that edge (#5306): the notch's bottom (y 30,
/// x 80 … 120) bends 90° up, inside (setback 3), its outer face on the edge (y 30 … 32), the base
/// set back under it; the plate's outer edge beside the notch keeps no flange. An edge of a hole
/// (a cut-out inside the plate) is refused, not taken for the outline's nearest edge.
#[test]
fn an_edge_flange_picked_inside_a_notch_is_on_the_notchs_edge() {
let mut s = Session::default();
plate_with_a_cut(&mut s, [[80.0, -10.0], [120.0, 30.0]]);
run(&mut s, "sheet.flange", json!({"edges": [[100, 30, 2]], "height": 40, "angle": 90, "position": "inside"}));
let f = flat(&mut s);
let bends = f["bends"].as_array().cloned().unwrap_or_default();
let span = |b: &Value| {
let (x0, x1) = (b["start"][0].as_f64().unwrap_or(f64::NAN), b["end"][0].as_f64().unwrap_or(f64::NAN));
(x0.min(x1), x0.max(x1), b["start"][1].as_f64().unwrap_or(f64::NAN))
};
assert_eq!(bends.len(), 1, "{f}");
let (lo, hi, y) = span(&bends[0]);
assert!((lo - 80.0).abs() < 1e-9 && (hi - 120.0).abs() < 1e-9 && y > 30.0, "{f}");
assert_eq!(f["outline"].as_array().map(Vec::len), Some(1), "{f}");
for (p, inside) in [
([100.0, 31.0, 20.0], true),
([100.0, 30.5, 0.5], false),
([100.0, 34.0, 1.0], true),
([40.0, 1.0, 20.0], false),
([160.0, 1.0, 20.0], false),
([40.0, 0.2, 0.2], true),
] {
assert_eq!(contains(&s, p), inside, "{p:?}");
}
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(body.validity().is_empty() && body.shell_count() == 1);
drop(state);
let mut s = Session::default();
plate_with_a_cut(&mut s, [[80.0, 30.0], [120.0, 60.0]]);
let error = s.execute("sheet.flange", &json!({"edges": [[100, 30, 2]], "height": 40, "angle": 90})).unwrap_err().to_string();
assert!(error.contains("not supported yet: a flange on the edge of a hole"), "{error}");
}
#[test]
fn edge_flanges_on_either_side_of_a_notch_span_the_side_they_were_picked_on() {
// A notch 40 wide and 30 deep in the middle of the bottom edge leaves x 0 … 80 and 120 … 200 of