mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 03:51:08 +00:00
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:
+73
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user