Sheet metal: rules as expressions (Thickness, K-factor, bend radius, reliefs, gaps; may use parameters), sheet bodies kept as flat patterns and sewn folded from exact faces (new kernel::sew), Flange (base, edge with height to the outer face, angle, inside/outside/middle, several edges at once with corners closed to the rule gap, contour from an open chain), Hem, Unfold/Refold with cut-outs that follow the bends, Convert to Sheet Metal, flat pattern with bend lines and bend table, DXF export; recipes: sm_flange, sm_hem, unfold/refold, rule edits (oracle 67-76 all pass)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
df64a373b0
commit
e5560e8035
14 files changed
+2473
-1
No files matched your search
@@ -187,6 +187,9 @@ impl FeatureKind {
|
||||
dir(direction);
|
||||
}
|
||||
FeatureKind::Thread { face, .. } => pt(face),
|
||||
FeatureKind::SheetFlange { edges, .. } | FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::SheetConvert { face, .. } => pt(face),
|
||||
FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetUnfold { .. } => {}
|
||||
FeatureKind::Coil { base, axis, .. } => {
|
||||
pt(base);
|
||||
dir(axis);
|
||||
|
||||
@@ -406,6 +406,70 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
targets: Vec<String>,
|
||||
},
|
||||
/// Sheet metal base flange: a sheet from a closed profile (thickness from the rule).
|
||||
SheetBase {
|
||||
sketch: u64,
|
||||
profiles: ProfileSel,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
rule: Option<String>,
|
||||
/// Thickness grows against the sketch normal.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
},
|
||||
/// Sheet metal contour flange: a sheet bent along an open chain of sketch lines.
|
||||
SheetContour {
|
||||
sketch: u64,
|
||||
curves: Vec<String>,
|
||||
distance: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
rule: Option<String>,
|
||||
/// Material on the other side of the chain.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
/// Width the other way along the sketch normal.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
reverse: bool,
|
||||
},
|
||||
/// Edge flanges on sheet edges (points on the top or bottom edges).
|
||||
SheetFlange {
|
||||
edges: Vec<Vec3>,
|
||||
height: String,
|
||||
angle: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
radius: Option<String>,
|
||||
/// inside | outside | middle.
|
||||
#[serde(default = "inside_str")]
|
||||
position: String,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Flat hems on sheet edges.
|
||||
SheetHem {
|
||||
edges: Vec<Vec3>,
|
||||
length: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
gap: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
},
|
||||
/// Unfold a sheet body flat (all bends), or refold it.
|
||||
SheetUnfold {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
refold: bool,
|
||||
},
|
||||
/// Convert a plate body (constant thickness from the face at `face`) to sheet metal.
|
||||
SheetConvert {
|
||||
body: String,
|
||||
face: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
rule: Option<String>,
|
||||
},
|
||||
/// Remove bodies from the model (from here on in the timeline).
|
||||
Remove {
|
||||
bodies: Vec<String>,
|
||||
@@ -575,6 +639,19 @@ fn z_axis() -> Vec3 {
|
||||
}
|
||||
|
||||
impl FeatureKind {
|
||||
/// A sheet metal feature (its result depends on the sheet metal rules).
|
||||
pub fn is_sheet(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
FeatureKind::SheetBase { .. }
|
||||
| FeatureKind::SheetContour { .. }
|
||||
| FeatureKind::SheetFlange { .. }
|
||||
| FeatureKind::SheetHem { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Fusion-like type name (timeline tooltip, `inspect`).
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -606,6 +683,13 @@ impl FeatureKind {
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
|
||||
FeatureKind::Pipe { .. } => "PipeFeature",
|
||||
FeatureKind::Emboss { .. } => "EmbossFeature",
|
||||
FeatureKind::SheetBase { .. } => "BaseFlangeFeature",
|
||||
FeatureKind::SheetContour { .. } => "ContourFlangeFeature",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlangeFeature",
|
||||
FeatureKind::SheetHem { .. } => "HemFeature",
|
||||
FeatureKind::SheetUnfold { refold: false, .. } => "UnfoldFeature",
|
||||
FeatureKind::SheetUnfold { .. } => "RefoldFeature",
|
||||
FeatureKind::SheetConvert { .. } => "ConvertToSheetMetalFeature",
|
||||
FeatureKind::Coil { .. } => "CoilFeature",
|
||||
FeatureKind::Rib { web: false, .. } => "RibFeature",
|
||||
FeatureKind::Rib { .. } => "WebFeature",
|
||||
@@ -647,6 +731,13 @@ impl FeatureKind {
|
||||
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
|
||||
FeatureKind::Pipe { .. } => "Pipe",
|
||||
FeatureKind::Emboss { .. } => "Emboss",
|
||||
FeatureKind::SheetBase { .. } => "BaseFlange",
|
||||
FeatureKind::SheetContour { .. } => "ContourFlange",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlange",
|
||||
FeatureKind::SheetHem { .. } => "Hem",
|
||||
FeatureKind::SheetUnfold { refold: false, .. } => "Unfold",
|
||||
FeatureKind::SheetUnfold { .. } => "Refold",
|
||||
FeatureKind::SheetConvert { .. } => "ConvertToSheetMetal",
|
||||
FeatureKind::Coil { .. } => "Coil",
|
||||
FeatureKind::Rib { web: false, .. } => "Rib",
|
||||
FeatureKind::Rib { .. } => "Web",
|
||||
@@ -728,6 +819,16 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(depth),
|
||||
FeatureKind::SheetContour { distance, .. } => v.push(distance),
|
||||
FeatureKind::SheetFlange { height, angle, radius, .. } => {
|
||||
v.extend([height.as_str(), angle]);
|
||||
v.extend(radius.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::SheetHem { length, gap, .. } => {
|
||||
v.push(length);
|
||||
v.extend(gap.iter().map(String::as_str));
|
||||
}
|
||||
FeatureKind::SheetBase { .. } | FeatureKind::SheetUnfold { .. } | FeatureKind::SheetConvert { .. } => {}
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, .. } => {
|
||||
v.extend([diameter.as_str(), pitch, turns, section_size]);
|
||||
v.extend(start_angle.iter().map(String::as_str));
|
||||
@@ -763,6 +864,10 @@ pub struct Feature {
|
||||
pub kind: FeatureKind,
|
||||
}
|
||||
|
||||
fn inside_str() -> String {
|
||||
"inside".into()
|
||||
}
|
||||
|
||||
fn zero_expr() -> String {
|
||||
"0".into()
|
||||
}
|
||||
@@ -795,6 +900,9 @@ pub struct Document {
|
||||
/// Joints between occurrences, joint origins and motion links.
|
||||
#[serde(default, skip_serializing_if = "crate::joints::Assembly::is_empty")]
|
||||
pub assembly: crate::joints::Assembly,
|
||||
/// Sheet metal rules.
|
||||
#[serde(default, skip_serializing_if = "crate::sheet::SheetSettings::is_empty")]
|
||||
pub sheet: crate::sheet::SheetSettings,
|
||||
/// Favourite parameters (by name).
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
pub favorites: std::collections::BTreeSet<String>,
|
||||
@@ -854,6 +962,7 @@ impl Document {
|
||||
components: Vec::new(),
|
||||
occurrences: Vec::new(),
|
||||
assembly: Default::default(),
|
||||
sheet: Default::default(),
|
||||
body_components: Default::default(),
|
||||
materials: Default::default(),
|
||||
favorites: Default::default(),
|
||||
@@ -995,6 +1104,7 @@ impl Document {
|
||||
FeatureKind::Loft { sections, .. } if sections.iter().any(|s| s.sketch == id) => gone.push(f.id),
|
||||
FeatureKind::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } if *path_sketch == id => gone.push(f.id),
|
||||
FeatureKind::Emboss { sketch, .. } | FeatureKind::Rib { sketch, .. } if *sketch == id => gone.push(f.id),
|
||||
FeatureKind::SheetBase { sketch, .. } | FeatureKind::SheetContour { sketch, .. } if *sketch == id => gone.push(f.id),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-1
@@ -185,6 +185,8 @@ pub struct ModelState {
|
||||
pub bodies: Vec<ModelBody>,
|
||||
pub sketches: Vec<SolvedSketch>,
|
||||
pub body_counter: usize,
|
||||
/// Sheet metal bodies (by body name) and their flat patterns.
|
||||
pub sheets: Vec<crate::sheet::SheetBody>,
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
@@ -287,7 +289,21 @@ impl Model {
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
for (i, f) in doc.features.iter().enumerate() {
|
||||
let rolled_back = i >= marker;
|
||||
let fp = fingerprint(prev_fp, f, &vals, rolled_back);
|
||||
let mut fp = fingerprint(prev_fp, f, &vals, rolled_back);
|
||||
// Sheet metal features also depend on the rules (and the parameters they use).
|
||||
if f.kind.is_sheet() {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
fp.hash(&mut h);
|
||||
serde_json::to_string(&doc.sheet).unwrap_or_default().hash(&mut h);
|
||||
for r in &doc.sheet.rules {
|
||||
for e in [&r.thickness, &r.k_factor, &r.bend_radius, &r.relief_width, &r.relief_depth, &r.corner_relief, &r.hem_gap, &r.gap] {
|
||||
for n in expr::references(e) {
|
||||
vals.get(&n).map(|v| v.v.to_bits()).hash(&mut h);
|
||||
}
|
||||
}
|
||||
}
|
||||
fp = h.finish();
|
||||
}
|
||||
prev_fp = fp;
|
||||
if reuse
|
||||
&& let Some(old) = self.results.get(i)
|
||||
@@ -1371,6 +1387,12 @@ mod more;
|
||||
|
||||
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
match &f.kind {
|
||||
FeatureKind::SheetBase { .. }
|
||||
| FeatureKind::SheetContour { .. }
|
||||
| FeatureKind::SheetFlange { .. }
|
||||
| FeatureKind::SheetHem { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. } => more::sheet_eval(doc, vals, f, st),
|
||||
FeatureKind::Emboss { .. }
|
||||
| FeatureKind::Rib { .. }
|
||||
| FeatureKind::ReplaceFace { .. }
|
||||
@@ -1400,6 +1422,22 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Coil { operation, targets, .. }
|
||||
| FeatureKind::Sweep { operation, targets, .. } => {
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
// Cuts through sheet metal become cut-outs of its flat pattern.
|
||||
if *operation == Operation::Cut && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
|
||||
let handled = more::sheet_cut(f, st, &tools, targets)?;
|
||||
if !handled.is_empty() {
|
||||
let rest: Vec<String> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.map(|b| b.name.clone())
|
||||
.filter(|n| !handled.contains(n) && (targets.is_empty() || targets.contains(n)))
|
||||
.collect();
|
||||
if rest.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
return apply_op(st, f, tools, *operation, &rest);
|
||||
}
|
||||
}
|
||||
apply_op(st, f, tools, *operation, targets)
|
||||
}
|
||||
FeatureKind::Hole { thread, diameter, .. } => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! kernel's extrude, boolean, offset and transform operations).
|
||||
|
||||
use super::*;
|
||||
use crate::apply_point;
|
||||
use solvecraft_geom::{Loop2, Seg2};
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
@@ -239,3 +240,307 @@ fn point_on_polyline(pts: &[Vec2], d: f64) -> Vec2 {
|
||||
}
|
||||
pts.last().copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Rebuild a sheet body's solid from its model.
|
||||
fn rebuild_sheet(st: &mut ModelState, idx: usize) -> Result<()> {
|
||||
let sheet = st.sheets.get(idx).cloned().ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
let solid = sheet.solid()?;
|
||||
let i = st.bodies.iter().position(|b| b.name == sheet.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", sheet.body)))?;
|
||||
if let Some(mb) = st.bodies.get_mut(i) {
|
||||
*mb = ModelBody::new(mb.name.clone(), solid, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The sheet a feature works on: by body name, else the one with an edge nearest the pick.
|
||||
fn sheet_for(st: &ModelState, body: &Option<String>, pick: Option<Vec3>) -> Result<usize> {
|
||||
if let Some(n) = body {
|
||||
return st.sheets.iter().position(|s| &s.body == n).ok_or_else(|| DocError::Invalid(format!("`{n}` is not a sheet metal body")));
|
||||
}
|
||||
let p = pick.ok_or_else(|| DocError::Invalid("no sheet metal body".into()))?;
|
||||
st.sheets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, s)| s.edge_at(p).map(|e| (i, e.4)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| DocError::Invalid("there is no sheet metal body".into()))
|
||||
}
|
||||
|
||||
/// A region mirrored in the sketch x axis (y → −y); curves other than lines and arcs as polylines.
|
||||
fn mirror_y(r: &Region2) -> Region2 {
|
||||
let f = |p: Vec2| Vec2::new(p.x, -p.y);
|
||||
let m = |l: &Loop2| {
|
||||
let segs: Vec<Seg2> = l
|
||||
.segs
|
||||
.iter()
|
||||
.flat_map(|s| match *s {
|
||||
Seg2::Line { a, b } => vec![Seg2::Line { a: f(a), b: f(b) }],
|
||||
Seg2::Arc { center, radius, start, sweep } => vec![Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep }],
|
||||
other => {
|
||||
let pts: Vec<Vec2> = (0..=16).map(|k| f(other.point_at(k as f64 / 16.0))).collect();
|
||||
pts.windows(2).map(|w| Seg2::Line { a: w[0], b: w[1] }).collect()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Loop2 { segs }.ccw()
|
||||
};
|
||||
Region2 { outer: m(&r.outer), holes: r.holes.iter().map(|h| m(h).reversed()).collect() }
|
||||
}
|
||||
|
||||
pub(super) fn sheet_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
use crate::sheet;
|
||||
match &f.kind {
|
||||
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let rv = doc.rule_values(vals, &r)?;
|
||||
for (k, region) in regions.into_iter().enumerate() {
|
||||
let (plane, region) = if *flip {
|
||||
(Plane { origin: ss.plane.origin, x: ss.plane.x, y: ss.plane.y * -1.0 }, mirror_y(®ion))
|
||||
} else {
|
||||
(ss.plane, region)
|
||||
};
|
||||
let name = new_name(st, f, k);
|
||||
let sh = sheet::base_flange(&name, &r.name, &rv, plane, region);
|
||||
let solid = sh.solid()?;
|
||||
st.bodies.push(ModelBody::new(name, solid, f.id));
|
||||
st.sheets.push(sh);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SheetContour { sketch, curves, distance, rule, flip, reverse } => {
|
||||
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
for id in curves {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
|
||||
if !ss.sketch.segs(ci).iter().all(|s| matches!(s, Seg2::Line { .. })) {
|
||||
return Err(DocError::Invalid("not supported yet: arcs in a contour flange (use lines; the bends are made for you)".into()));
|
||||
}
|
||||
}
|
||||
let pts = chain_points(&ss, curves)?;
|
||||
let w = val(vals, distance, Kind::Length)?;
|
||||
if !(w > 0.0) {
|
||||
return Err(DocError::Invalid("the contour flange distance must be positive".into()));
|
||||
}
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let rv = doc.rule_values(vals, &r)?;
|
||||
let plane = if *reverse { Plane { origin: ss.plane.origin, x: ss.plane.x, y: ss.plane.y * -1.0 } } else { ss.plane };
|
||||
let pts: Vec<Vec2> = if *reverse { pts.iter().map(|p| Vec2::new(p.x, -p.y)).collect() } else { pts };
|
||||
// Mirroring the sketch swaps the material side too.
|
||||
let name = new_name(st, f, 0);
|
||||
let sh = sheet::contour_flange(&name, &r.name, &rv, &plane, &pts, w, *flip != *reverse)?;
|
||||
let solid = sh.solid()?;
|
||||
st.bodies.push(ModelBody::new(name, solid, f.id));
|
||||
st.sheets.push(sh);
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body } => {
|
||||
let i = sheet_for(st, body, edges.first().copied())?;
|
||||
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
|
||||
let rr = match radius {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.bend_radius
|
||||
}
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
if sh.flat {
|
||||
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
|
||||
}
|
||||
sh.add_flanges(edges, h, a, rr, position, *flip)?;
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
|
||||
let i = sheet_for(st, body, edges.first().copied())?;
|
||||
let l = val(vals, length, Kind::Length)?;
|
||||
let g = match gap {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => {
|
||||
let rule = st.sheets.get(i).map(|s| s.rule.clone()).unwrap_or_default();
|
||||
doc.rule_values(vals, &doc.sheet_rule(Some(&rule)))?.hem_gap
|
||||
}
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
for e in edges {
|
||||
sh.add_hem(*e, l, g, *flip)?;
|
||||
}
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetUnfold { body, refold } => {
|
||||
let i = match body {
|
||||
Some(_) => sheet_for(st, body, None)?,
|
||||
None => st.sheets.iter().rposition(|s| s.flat == *refold).ok_or_else(|| {
|
||||
DocError::Invalid(if *refold { "no unfolded sheet to refold".into() } else { "no sheet metal body to unfold".into() })
|
||||
})?,
|
||||
};
|
||||
if let Some(sh) = st.sheets.get_mut(i) {
|
||||
sh.flat = !*refold;
|
||||
}
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetConvert { body, face, rule } => {
|
||||
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
|
||||
let tol = (mb.body.size() * 1e-3).max(1e-3);
|
||||
let mesh = mb.body.tessellate(tol)?;
|
||||
// Thickness: the body's extent along the face normal.
|
||||
let ds: Vec<f64> = mesh.positions.iter().map(|p| (*p - *face).dot(n)).collect();
|
||||
let (lo, hi) = (ds.iter().cloned().fold(f64::MAX, f64::min), ds.iter().cloned().fold(f64::MIN, f64::max));
|
||||
let t = hi - lo;
|
||||
// The face outline: boundary edges of the triangles on that face's plane.
|
||||
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
|
||||
let on: Vec<[u32; 3]> = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|t| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - *face).dot(n) - lo).abs() < tol)))
|
||||
.collect();
|
||||
// Boundary edges by position (meshes may repeat vertices per triangle).
|
||||
let pos = |k: u32| mesh.positions.get(k as usize).copied().unwrap_or_default();
|
||||
let q = |p: Vec3| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64, (p.z * 1e6).round() as i64);
|
||||
let mut count: std::collections::HashMap<((i64, i64, i64), (i64, i64, i64)), i32> = Default::default();
|
||||
let edges: Vec<(Vec3, Vec3)> = on.iter().flat_map(|t| [(pos(t[0]), pos(t[1])), (pos(t[1]), pos(t[2])), (pos(t[2]), pos(t[0]))]).collect();
|
||||
for (a, b) in &edges {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
*count.entry((ka.min(kb), ka.max(kb))).or_insert(0) += 1;
|
||||
}
|
||||
let mut boundary: Vec<(Vec3, Vec3)> = edges
|
||||
.into_iter()
|
||||
.filter(|(a, b)| {
|
||||
let (ka, kb) = (q(*a), q(*b));
|
||||
count.get(&(ka.min(kb), ka.max(kb))) == Some(&1)
|
||||
})
|
||||
.collect();
|
||||
// Chain into loops (plane coordinates).
|
||||
let mut loops: Vec<Vec<Vec2>> = Vec::new();
|
||||
for _ in 0..10_000 {
|
||||
let Some((s0, mut cur)) = boundary.pop() else { break };
|
||||
let mut lp = vec![s0];
|
||||
for _ in 0..boundary.len() + 1 {
|
||||
if q(cur) == q(s0) {
|
||||
break;
|
||||
}
|
||||
lp.push(cur);
|
||||
match boundary.iter().position(|(a, _)| q(*a) == q(cur)) {
|
||||
Some(k) => cur = boundary.remove(k).1,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
let raw: Vec<Vec2> = lp.iter().map(|p| plane.to_local(*p)).collect();
|
||||
// Drop points in the middle of straight runs.
|
||||
let m = raw.len();
|
||||
let pts: Vec<Vec2> = (0..m)
|
||||
.filter_map(|i| {
|
||||
let (p, c, n) = (*raw.get((i + m - 1) % m)?, *raw.get(i)?, *raw.get((i + 1) % m)?);
|
||||
((c - p).cross(n - c).abs() > 1e-9 * (c - p).len().max(1e-9) * (n - c).len().max(1e-9)).then_some(c)
|
||||
})
|
||||
.collect();
|
||||
if pts.len() >= 3 {
|
||||
loops.push(pts);
|
||||
}
|
||||
}
|
||||
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
|
||||
let mut it = loops.into_iter();
|
||||
let outer = Loop2::polygon(&it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?).ccw();
|
||||
let region = Region2 { outer, holes: it.map(|l| Loop2::polygon(&l).ccw().reversed()).collect() };
|
||||
let area = region.area().abs();
|
||||
let vol = kernel::measure(&mb.body)?.volume;
|
||||
if (area * t - vol).abs() > 1e-3 * vol.max(1.0) {
|
||||
return Err(DocError::Invalid("not supported yet: converting bodies that are not a plate of even thickness".into()));
|
||||
}
|
||||
let r = doc.sheet_rule(rule.as_deref());
|
||||
let mut rv = doc.rule_values(vals, &r)?;
|
||||
rv.thickness = t;
|
||||
let mut sh = sheet::base_flange(body, &r.name, &rv, plane, Region2 { outer: region.outer.clone(), holes: Vec::new() });
|
||||
sh.holes = region.holes.iter().map(|h| Region2 { outer: h.ccw(), holes: Vec::new() }).collect();
|
||||
let solid = sh.solid()?;
|
||||
if let Some(b) = st.bodies.iter_mut().find(|b| &b.name == body) {
|
||||
*b = ModelBody::new(b.name.clone(), solid, b.feature);
|
||||
}
|
||||
st.sheets.retain(|s| &s.body != body);
|
||||
st.sheets.push(sh);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("{} is not a sheet metal feature", f.name))),
|
||||
}
|
||||
}
|
||||
|
||||
fn poly_area(p: &[Vec2]) -> f64 {
|
||||
let m = p.len();
|
||||
(0..m).filter_map(|i| Some(p.get(i)?.cross(*p.get((i + 1) % m)?))).sum::<f64>() / 2.0
|
||||
}
|
||||
|
||||
/// An extrude cut through sheet metal: the profiles become cut-outs of the flat pattern
|
||||
/// (parallel to a panel, through its thickness). Returns the sheet bodies handled.
|
||||
pub(super) fn sheet_cut(f: &Feature, st: &mut ModelState, tools: &[Body], targets: &[String]) -> Result<Vec<String>> {
|
||||
let FeatureKind::Extrude { sketch, profiles, .. } = &f.kind else { return Ok(Vec::new()) };
|
||||
let Some(ss) = st.sketch(*sketch).cloned() else { return Ok(Vec::new()) };
|
||||
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let n = ss.plane.normal();
|
||||
// How far the tools reach along the sketch normal.
|
||||
let mut span = (f64::MAX, f64::MIN);
|
||||
for t in tools {
|
||||
for p in t.tessellate((t.size() * 1e-3).max(1e-3))?.positions {
|
||||
let d = (p - ss.plane.origin).dot(n);
|
||||
span = (span.0.min(d), span.1.max(d));
|
||||
}
|
||||
}
|
||||
let mut handled = Vec::new();
|
||||
for i in 0..st.sheets.len() {
|
||||
let Some(sh) = st.sheets.get(i).cloned() else { continue };
|
||||
if !targets.is_empty() && !targets.contains(&sh.body) {
|
||||
continue;
|
||||
}
|
||||
let candidates: Vec<(Option<usize>, Region2)> = if sh.flat {
|
||||
if sh.frame.normal().cross(n).len() > 1e-6 {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![(None, Region2 { outer: Loop2::polygon(&sh.outline()?.into_iter().next().unwrap_or_default()), holes: Vec::new() })]
|
||||
}
|
||||
} else {
|
||||
sh.parallel_panels(&ss.plane)
|
||||
};
|
||||
let mut added = Vec::new();
|
||||
for (pi, panel) in candidates {
|
||||
// The panel's faces along the sketch normal must both be inside the cut.
|
||||
let w = sh.world(pi);
|
||||
let z0 = (apply_point(&w, Vec3::ZERO) - ss.plane.origin).dot(n);
|
||||
let z1 = (apply_point(&w, Vec3::Z * sh.t) - ss.plane.origin).dot(n);
|
||||
if z0.min(z1) < span.0 - 1e-6 || z0.max(z1) > span.1 + 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let outline = if sh.flat { sh.outline()?.into_iter().flatten().collect::<Vec<_>>() } else { panel.outer.polyline(1e-3) };
|
||||
for r in ®ions {
|
||||
let Some(flat) = sh.region_to_flat(pi, &ss.plane, r) else { continue };
|
||||
let fp = flat.outer.polyline(1e-3);
|
||||
let overlaps = fp.iter().any(|p| point_in_poly(&outline, *p)) || outline.iter().any(|p| point_in_poly(&fp, *p));
|
||||
if overlaps && !added.iter().any(|x: &Region2| x == &flat) {
|
||||
added.push(flat);
|
||||
}
|
||||
}
|
||||
}
|
||||
if added.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = st.sheets.get_mut(i) {
|
||||
s.holes.extend(added);
|
||||
}
|
||||
rebuild_sheet(st, i)?;
|
||||
handled.push(sh.body.clone());
|
||||
}
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
fn point_in_poly(poly: &[Vec2], p: Vec2) -> bool {
|
||||
let mut inside = false;
|
||||
let m = poly.len();
|
||||
for i in 0..m {
|
||||
let (Some(&a), Some(&b)) = (poly.get(i), poly.get((i + m - 1) % m)) else { continue };
|
||||
if (a.y > p.y) != (b.y > p.y) && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub mod joints;
|
||||
mod params;
|
||||
pub mod project;
|
||||
mod project3d;
|
||||
pub mod sheet;
|
||||
|
||||
pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, mat_inverse, mat_mul, rigid};
|
||||
pub use document::*;
|
||||
|
||||
@@ -138,6 +138,20 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Emboss { depth, .. } => v.push(("Depth", depth, L)),
|
||||
FeatureKind::SheetContour { distance, .. } => v.push(("Distance", distance, L)),
|
||||
FeatureKind::SheetFlange { height, angle, radius, .. } => {
|
||||
v.push(("Height", height, L));
|
||||
v.push(("Angle", angle, A));
|
||||
if let Some(r) = radius {
|
||||
v.push(("Bend radius", r, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::SheetHem { length, gap, .. } => {
|
||||
v.push(("Length", length, L));
|
||||
if let Some(g) = gap {
|
||||
v.push(("Gap", g, L));
|
||||
}
|
||||
}
|
||||
FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, .. } => {
|
||||
v.push(("Diameter", diameter, L));
|
||||
v.push(("Pitch", pitch, L));
|
||||
@@ -155,6 +169,9 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::ReplaceFace { target, .. } => plane_inputs_mut(target, &mut v),
|
||||
FeatureKind::Combine { .. }
|
||||
| FeatureKind::SheetBase { .. }
|
||||
| FeatureKind::SheetUnfold { .. }
|
||||
| FeatureKind::SheetConvert { .. }
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user