feat: preserve body history and placed modelling workflows
Record ownership transfers in the timeline so earlier Join/Cut membership, world placement, machining data and styles survive component moves, edits, independent copying, undo and reopening. Preserve pending positions in history snapshots and keep constructor transfers ordered during feature redefinition. Retain shallow sheet pockets and exact planar intersection islands through unfold/refold, with depth-aware flat previews and DXF export. Store explicit 3D wire coordinate spaces and migrate known legacy provenance without moving saved points or analytic fits. Infer unique eligible cylindrical Emboss targets, align Revolve manipulators with actual extent geometry, and discard obsolete preview errors after input correction. Certify constructed and immutable imported face styles using exact source metadata while preserving legacy face names. Upgrade designs to format10 with bounded validation and geometry-preserving legacy migration.
This commit is contained in:
1 parent
0398a813c8
commit
c1d819f235
86 files changed
+6011
-488
No files matched your search
@@ -27,6 +27,15 @@ pub struct Body {
|
||||
/// Lines the user split faces along (Split Face), as polylines: export keeps the faces on
|
||||
/// either side apart where it would otherwise join pieces of one surface.
|
||||
pub(crate) splits: Option<std::sync::Arc<Vec<Vec<Vec3>>>>,
|
||||
/// Exact construction/import source identities, in this body's face order.
|
||||
pub(crate) face_sources: Option<std::sync::Arc<Vec<Option<FaceSource>>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FaceSource {
|
||||
ExtrudeSide { segment: solvecraft_geom::Seg2 },
|
||||
CertifiedExtrudeSide { producer: u64, sketch: u64, curve: String },
|
||||
Imported { item: u64, face: u64, payload_sha256: Option<String>, producer: Option<u64> },
|
||||
}
|
||||
|
||||
/// How a body looks beyond its colour, for export: its opacity and its faces' own colours.
|
||||
@@ -287,6 +296,7 @@ impl Body {
|
||||
&& self.color == other.color
|
||||
&& self.paint == other.paint
|
||||
&& self.splits == other.splits
|
||||
&& self.face_sources == other.face_sources
|
||||
}
|
||||
|
||||
pub(crate) fn new(solid: Solid) -> Result<Body> {
|
||||
@@ -324,9 +334,61 @@ impl Body {
|
||||
color: None,
|
||||
paint: None,
|
||||
splits: None,
|
||||
face_sources: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn face_sources(&self) -> Option<&[Option<FaceSource>]> {
|
||||
self.face_sources.as_deref().map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Bind constructed edges to uniquely proven source curves without changing face names.
|
||||
pub fn with_extrude_certificates(mut self, producer: u64, sketch: u64, curves: &[Option<String>]) -> Result<Self> {
|
||||
if producer == 0 || sketch == 0 || curves.iter().flatten().any(|curve| curve.is_empty() || curve.len() > 128) {
|
||||
return Err(KernelError::Invalid("invalid constructed side certificate".into()));
|
||||
}
|
||||
if let Some(sources) = &self.face_sources {
|
||||
if sources.len() != curves.len() {
|
||||
return Err(KernelError::Invalid("side certificate count mismatch".into()));
|
||||
}
|
||||
let sources = sources
|
||||
.iter()
|
||||
.zip(curves)
|
||||
.map(|(source, curve)| match (source, curve) {
|
||||
(Some(FaceSource::ExtrudeSide { .. }), Some(curve)) => {
|
||||
Some(FaceSource::CertifiedExtrudeSide { producer, sketch, curve: curve.clone() })
|
||||
}
|
||||
(Some(FaceSource::ExtrudeSide { .. }), None) => None,
|
||||
_ => source.clone(),
|
||||
})
|
||||
.collect();
|
||||
self.face_sources = Some(std::sync::Arc::new(sources));
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_import_certificate(mut self, digest: &str, producer: u64) -> Result<Self> {
|
||||
if producer == 0 || digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) {
|
||||
return Err(KernelError::Invalid("invalid immutable import certificate".into()));
|
||||
}
|
||||
if let Some(sources) = &self.face_sources {
|
||||
let mut sources = sources.as_ref().clone();
|
||||
for source in sources.iter_mut().flatten() {
|
||||
if let FaceSource::Imported { payload_sha256, producer: origin, .. } = source {
|
||||
*payload_sha256 = Some(digest.into());
|
||||
*origin = Some(producer);
|
||||
}
|
||||
}
|
||||
self.face_sources = Some(std::sync::Arc::new(sources));
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(crate) fn with_face_sources(mut self, sources: Option<std::sync::Arc<Vec<Option<FaceSource>>>>) -> Self {
|
||||
self.face_sources = sources;
|
||||
self
|
||||
}
|
||||
|
||||
/// A deep copy of the solid that can be mutated without affecting other bodies.
|
||||
pub(crate) fn deep_copy(&self) -> Solid {
|
||||
mt::builder::clone(&*self.solid)
|
||||
|
||||
@@ -15,6 +15,10 @@ fn finite(x: f64, what: &str) -> Result<f64> {
|
||||
|
||||
/// A closed wire in 3D from a sketch loop.
|
||||
pub(crate) fn wire(plane: &Plane, lp: &Loop2) -> Result<mt::Wire> {
|
||||
wire_sources(plane, lp).map(|(wire, _)| wire)
|
||||
}
|
||||
|
||||
fn wire_sources(plane: &Plane, lp: &Loop2) -> Result<(mt::Wire, Vec<(mt::Edge, Seg2)>)> {
|
||||
let n = lp.segs.len();
|
||||
if n == 0 || n > MAX_SEGS {
|
||||
return Err(KernelError::Invalid(format!("loop with {n} segments")));
|
||||
@@ -24,6 +28,7 @@ pub(crate) fn wire(plane: &Plane, lp: &Loop2) -> Result<mt::Wire> {
|
||||
}
|
||||
let verts: Vec<mt::Vertex> = lp.segs.iter().map(|s| builder::vertex(p3(plane.to_world(s.start())))).collect();
|
||||
let mut edges = Vec::with_capacity(n);
|
||||
let mut sources = Vec::with_capacity(n);
|
||||
for (i, s) in lp.segs.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (verts.get(i), verts.get((i + 1) % n)) else { continue };
|
||||
let e: mt::Edge = match *s {
|
||||
@@ -35,7 +40,9 @@ pub(crate) fn wire(plane: &Plane, lp: &Loop2) -> Result<mt::Wire> {
|
||||
// Split big arcs so every piece is well conditioned.
|
||||
if sweep.abs() > std::f64::consts::PI * 1.5 {
|
||||
let vm = builder::vertex(p3(plane.to_world(s.mid())));
|
||||
edges.push(builder::circle_arc(a, &vm, p3(plane.to_world(s.point_at(0.25)))));
|
||||
let first = builder::circle_arc(a, &vm, p3(plane.to_world(s.point_at(0.25))));
|
||||
sources.push((first.clone(), *s));
|
||||
edges.push(first);
|
||||
builder::circle_arc(&vm, b, p3(plane.to_world(s.point_at(0.75))))
|
||||
} else {
|
||||
builder::circle_arc(a, b, p3(plane.to_world(s.mid())))
|
||||
@@ -45,9 +52,10 @@ pub(crate) fn wire(plane: &Plane, lp: &Loop2) -> Result<mt::Wire> {
|
||||
crate::freeform::edge(plane, s, a, b).ok_or_else(|| KernelError::Invalid("profile curve".into()))?
|
||||
}
|
||||
};
|
||||
sources.push((e.clone(), *s));
|
||||
edges.push(e);
|
||||
}
|
||||
Ok(edges.into())
|
||||
Ok((edges.into(), sources))
|
||||
}
|
||||
|
||||
/// A planar face from a region (outer loop with holes).
|
||||
@@ -95,9 +103,35 @@ pub fn extrude(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Resu
|
||||
for r in regions {
|
||||
region_ok(r)?;
|
||||
let base = plane.offset(lo);
|
||||
let f = face(&base, r)?;
|
||||
let (outer, mut sources) = wire_sources(&base, &r.outer.ccw())?;
|
||||
let mut wires = vec![outer];
|
||||
for hole in &r.holes {
|
||||
let (wire, mut hole_sources) = wire_sources(&base, &hole.ccw().reversed())?;
|
||||
wires.push(wire);
|
||||
sources.append(&mut hole_sources);
|
||||
}
|
||||
let f = guard("extrude profile", || builder::try_attach_plane(&wires).map_err(|e| KernelError::Invalid(format!("profile: {e}"))))?;
|
||||
let solid: Solid = guard("extrude", || Ok(builder::tsweep(&f, v3(n * (hi - lo)))))?;
|
||||
out.push(Body::new(solid)?);
|
||||
let body = Body::new(solid)?;
|
||||
// tsweep retains each actual input boundary edge on its generated side.
|
||||
// Match topology identity, never face order, surface samples or centroids.
|
||||
let provenance = guard("extrude source identities", || {
|
||||
let source_edges: std::collections::HashMap<_, _> = sources.iter().map(|(edge, segment)| (edge.id(), *segment)).collect();
|
||||
let mut result = Vec::new();
|
||||
for generated in body.solid.face_iter() {
|
||||
let matching: Vec<_> = generated.absolute_boundaries().iter().flatten().filter_map(|edge| source_edges.get(&edge.id())).collect();
|
||||
let source = if generated.id() == f.id() {
|
||||
None
|
||||
} else if let [segment] = matching.as_slice() {
|
||||
Some(crate::FaceSource::ExtrudeSide { segment: **segment })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
result.push(source);
|
||||
}
|
||||
Ok(result)
|
||||
})?;
|
||||
out.push(body.with_face_sources(Some(std::sync::Arc::new(provenance))));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
+276
-19
@@ -17,6 +17,21 @@ fn unwrap(a: f64, around: f64) -> f64 {
|
||||
around + (a - around + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI
|
||||
}
|
||||
|
||||
fn wall_angles(face: &mt::Face, o: Vec3, axis: Vec3, radial: Vec3, tangent: Vec3, tol: f64) -> Option<(f64, f64)> {
|
||||
if face.boundaries().len() != 1 || face.edge_iter().count() != 4 {
|
||||
return None;
|
||||
}
|
||||
for edge in face.edge_iter() {
|
||||
if let Some(Some(mid)) = crate::offset::arc_mid(&edge, tol * 10.) {
|
||||
let am = angle(mid, o, axis, radial, tangent);
|
||||
let aa = unwrap(angle(from_p3(edge.front().point()), o, axis, radial, tangent), am);
|
||||
let ab = unwrap(angle(from_p3(edge.back().point()), o, axis, radial, tangent), am);
|
||||
return Some((aa.min(ab), aa.max(ab)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Certify a tessellated pick and project it onto the cylinder used by emboss.
|
||||
pub fn cylinder_emboss_anchor(body: &Body, face: usize, at: Vec3) -> Result<Vec3> {
|
||||
if !at.is_finite() || at.len() > 1e9 {
|
||||
@@ -40,7 +55,7 @@ pub fn cylinder_emboss_anchor(body: &Body, face: usize, at: Vec3) -> Result<Vec3
|
||||
/// Wrap sketch arc length around an exterior cylinder, retaining exact radial depth.
|
||||
/// Profiles must contain orthogonal lines and lie inside one uninterrupted wall face.
|
||||
pub fn emboss_cylinder(body: &Body, plane: &Plane, regions: &[Region2], depth: f64) -> Result<Body> {
|
||||
emboss_impl(body, None, plane, regions, depth)
|
||||
emboss_impl(body, None, plane, regions, depth, false).and_then(EmbossOutcome::body)
|
||||
}
|
||||
|
||||
/// As [`emboss_cylinder`], requiring the saved selected point to identify that wall.
|
||||
@@ -48,10 +63,101 @@ pub fn emboss_cylinder_at(body: &Body, at: Vec3, plane: &Plane, regions: &[Regio
|
||||
if !at.is_finite() || at.len() > 1e9 {
|
||||
return Err(fail("invalid selected face point"));
|
||||
}
|
||||
emboss_impl(body, Some(at), plane, regions, depth)
|
||||
emboss_impl(body, Some(at), plane, regions, depth, false).and_then(EmbossOutcome::body)
|
||||
}
|
||||
|
||||
fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Region2], depth: f64) -> Result<Body> {
|
||||
enum EmbossOutcome {
|
||||
Body(Body),
|
||||
Anchor(usize, Vec3),
|
||||
NoMatch,
|
||||
}
|
||||
impl EmbossOutcome {
|
||||
fn body(self) -> Result<Body> {
|
||||
match self {
|
||||
Self::Body(body) => Ok(body),
|
||||
Self::Anchor(..) | Self::NoMatch => Err(fail("unexpected qualification result")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Qualify a tangent cylindrical target without changing it. A tangent exterior
|
||||
/// cylinder with an unsupported profile or depth returns an explicit error;
|
||||
/// a body without such a tangent wall returns `None` for planar compatibility.
|
||||
/// The anchor lies on a validated profile boundary, clear of seams and endcaps.
|
||||
pub fn infer_cylinder_emboss(body: &Body, plane: &Plane, regions: &[Region2], depth: f64) -> Result<Option<(usize, Vec3)>> {
|
||||
if body.is_mesh() {
|
||||
return Ok(None);
|
||||
}
|
||||
guard("cylinder emboss inference", || {
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
let mut tangent = false;
|
||||
for (index, face) in body.solid.face_iter().enumerate() {
|
||||
if index >= 50_000 {
|
||||
return Err(fail("target search exceeds face work limit"));
|
||||
}
|
||||
if let Some(Surf::Cylinder { o, a, r, convex: true }) = surf_of(face, tol) {
|
||||
let w = plane.origin - o;
|
||||
let normal = plane.normal();
|
||||
let signed = w.dot(normal);
|
||||
if normal.dot(a).abs() < 1e-8 && (signed.abs() - r).abs() <= tol * 10.0 {
|
||||
let radial = if signed < 0.0 { -normal } else { normal };
|
||||
// Every supported uninterrupted wall uses one angle interval
|
||||
// around this tangent normal, contained in (-pi,pi). A remote
|
||||
// footprint entirely outside even that global interval cannot
|
||||
// select this body, regardless of other unsupported faces.
|
||||
if !profile_interval_overlaps(plane, regions, o, a.cross(radial), -std::f64::consts::PI * r, std::f64::consts::PI * r)? {
|
||||
continue;
|
||||
}
|
||||
if let Some((lo, hi)) = wall_angles(face, o, a, radial, a.cross(radial), tol) {
|
||||
if lo >= -tol / r || hi <= tol / r {
|
||||
continue;
|
||||
}
|
||||
if !profile_interval_overlaps(plane, regions, o, a.cross(radial), lo * r, hi * r)? {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let relevant = match face_axial_bounds(face, o, a)? {
|
||||
Some((lo, hi)) => profile_interval_overlaps(plane, regions, o, a, lo, hi)?,
|
||||
None => true,
|
||||
};
|
||||
if relevant && signed < 0.0 {
|
||||
return Err(fail("sketch normal must point outwards from the exterior wall"));
|
||||
}
|
||||
tangent |= relevant;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !tangent {
|
||||
return Ok(None);
|
||||
}
|
||||
match emboss_impl(body, None, plane, regions, depth, true)? {
|
||||
EmbossOutcome::Anchor(index, at) => Ok(Some((index, at))),
|
||||
EmbossOutcome::NoMatch => Ok(None),
|
||||
EmbossOutcome::Body(_) => Err(fail("unexpected construction result")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a body contains an analytic exterior cylindrical wall. This bounded
|
||||
/// preflight avoids resolving cylinder placement context for ordinary planar targets.
|
||||
pub fn has_cylinder_emboss_wall(body: &Body) -> Result<bool> {
|
||||
if body.is_mesh() {
|
||||
return Ok(false);
|
||||
}
|
||||
guard("cylinder emboss target", || {
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
for (index, face) in body.solid.face_iter().enumerate() {
|
||||
if index >= 50_000 {
|
||||
return Err(fail("target search exceeds face work limit"));
|
||||
}
|
||||
if matches!(surf_of(face, tol), Some(Surf::Cylinder { convex: true, .. })) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
}
|
||||
fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Region2], depth: f64, qualify: bool) -> Result<EmbossOutcome> {
|
||||
body.require_brep("cylinder emboss")?;
|
||||
if !depth.is_finite() || depth.abs() < 1e-6 || depth.abs() > 1e6 || regions.is_empty() || regions.len() > 64 {
|
||||
return Err(fail("needs finite nonzero depth and 1..64 profiles"));
|
||||
@@ -77,8 +183,8 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
for (fi, s) in surfaces.iter().enumerate() {
|
||||
let Surf::Cylinder { o, a, r, convex: true } = *s else { continue };
|
||||
let w = plane.origin - o;
|
||||
let radial = w - a * w.dot(a);
|
||||
if (radial.len() - r).abs() > tol * 10.0 || plane.normal().dot(radial / r) < 1.0 - 1e-8 {
|
||||
let radial = plane.normal();
|
||||
if radial.dot(a).abs() > 1e-8 || (w.dot(radial) - r).abs() > tol * 10.0 {
|
||||
continue;
|
||||
}
|
||||
let radial = radial.normalized().ok_or_else(|| fail("radial frame"))?;
|
||||
@@ -87,17 +193,9 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
if f.boundaries().len() != 1 || f.edge_iter().count() != 4 {
|
||||
continue;
|
||||
}
|
||||
let mut angular = None;
|
||||
for e in f.edge_iter() {
|
||||
if let Some(Some(mid)) = crate::offset::arc_mid(&e, tol * 10.0) {
|
||||
let am = angle(mid, o, a, radial, tangent);
|
||||
let aa = unwrap(angle(from_p3(e.front().point()), o, a, radial, tangent), am);
|
||||
let ab = unwrap(angle(from_p3(e.back().point()), o, a, radial, tangent), am);
|
||||
angular = Some((aa.min(ab), aa.max(ab)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some((alo, ahi)) = angular else { continue };
|
||||
let Some((alo, ahi)) = wall_angles(f, o, a, radial, tangent, tol) else {
|
||||
continue;
|
||||
};
|
||||
if alo >= -tol / r || ahi <= tol / r {
|
||||
continue;
|
||||
}
|
||||
@@ -108,6 +206,9 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
}
|
||||
let (fi, o, axis, r, radial, tangent, alo, ahi) =
|
||||
chosen.ok_or_else(|| fail("sketch must be tangent to one complete exterior cylindrical wall, clear of its seam"))?;
|
||||
if qualify && !profile_interval_overlaps(plane, regions, o, tangent, alo * r, ahi * r)? {
|
||||
return Ok(EmbossOutcome::NoMatch);
|
||||
}
|
||||
let mut caps = Vec::new();
|
||||
let mut inner: f64 = 0.0;
|
||||
for s in &surfaces {
|
||||
@@ -131,6 +232,12 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
}
|
||||
let lo = caps.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
let hi = caps.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||||
// An infinite cylinder can be tangent while its finite wall is far from
|
||||
// the selected footprint. Conservative analytic/control-polygon bounds
|
||||
// exclude that body before unsupported-profile or depth diagnostics.
|
||||
if qualify && !profile_interval_overlaps(plane, regions, o, axis, lo, hi)? {
|
||||
return Ok(EmbossOutcome::NoMatch);
|
||||
}
|
||||
let rr = r + depth;
|
||||
if rr <= inner + tol * 10.0 {
|
||||
return Err(fail("depth reaches the axis or inner wall"));
|
||||
@@ -144,14 +251,21 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
}
|
||||
}
|
||||
let z0 = (plane.origin - o).dot(axis);
|
||||
let map2 = |p: Vec2| Vec2::new(plane.dir_to_world(p).dot(tangent), z0 + plane.dir_to_world(p).dot(axis));
|
||||
let s0 = (plane.origin - o).dot(tangent);
|
||||
let map2 = |p: Vec2| Vec2::new(s0 + plane.dir_to_world(p).dot(tangent), z0 + plane.dir_to_world(p).dot(axis));
|
||||
let mut count = 0usize;
|
||||
let mut mapped = Vec::new();
|
||||
for reg in regions {
|
||||
let mut loops = Vec::new();
|
||||
for lp in std::iter::once(®.outer).chain(®.holes) {
|
||||
count = count.saturating_add(lp.segs.len());
|
||||
if count > 4096 || lp.segs.len() < 4 || !lp.is_closed(tol) {
|
||||
if count > 4096 {
|
||||
return Err(fail("profile geometry budget"));
|
||||
}
|
||||
if lp.segs.iter().any(|segment| !matches!(segment, Seg2::Line { .. })) {
|
||||
return Err(fail("only orthogonal line profiles are supported on cylinders"));
|
||||
}
|
||||
if lp.segs.len() < 4 || !lp.is_closed(tol) {
|
||||
return Err(fail("profile geometry budget or open loop"));
|
||||
}
|
||||
let mut segs = Vec::new();
|
||||
@@ -181,6 +295,12 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
mapped.push(Region2 { outer, holes });
|
||||
}
|
||||
validate_profiles(&mapped, tol)?;
|
||||
if qualify {
|
||||
let segment = mapped.first().and_then(|region| region.outer.segs.first()).ok_or_else(|| fail("empty profile"))?;
|
||||
let p = (segment.start() + segment.end()) * 0.5;
|
||||
let at = o + axis * p.y + (radial * (p.x / r).cos() + tangent * (p.x / r).sin()) * r;
|
||||
return Ok(EmbossOutcome::Anchor(fi, at));
|
||||
}
|
||||
let at = |p: Vec2, radius: f64| o + axis * p.y + (radial * (p.x / r).cos() + tangent * (p.x / r).sin()) * radius;
|
||||
let selected = faces.get(fi).ok_or_else(|| fail("face"))?;
|
||||
let mut base_patches = vec![selected.absolute_boundaries().clone()];
|
||||
@@ -252,10 +372,108 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
if !result.validity().is_empty() {
|
||||
return Err(fail("rebuilt cylinder is not a closed manifold"));
|
||||
}
|
||||
Ok(result)
|
||||
Ok(EmbossOutcome::Body(result))
|
||||
})
|
||||
}
|
||||
|
||||
fn profile_interval_overlaps(plane: &Plane, regions: &[Region2], o: Vec3, axis: Vec3, lo: f64, hi: f64) -> Result<bool> {
|
||||
let z0 = (plane.origin - o).dot(axis);
|
||||
let direction = Vec2::new(plane.x.dot(axis), plane.y.dot(axis));
|
||||
let mut count = 0usize;
|
||||
let mut overlaps = false;
|
||||
for region in regions {
|
||||
let mut min = f64::INFINITY;
|
||||
let mut max = f64::NEG_INFINITY;
|
||||
for segment in ®ion.outer.segs {
|
||||
count = count.saturating_add(1);
|
||||
if count > 4096 {
|
||||
return Err(fail("profile geometry budget"));
|
||||
}
|
||||
let mut add = |p: Vec2| -> Result<()> {
|
||||
if !p.is_finite() || p.len() > 1e7 {
|
||||
return Err(fail("nonfinite or excessive profile coordinates"));
|
||||
}
|
||||
let z = z0 + p.dot(direction);
|
||||
min = min.min(z);
|
||||
max = max.max(z);
|
||||
Ok(())
|
||||
};
|
||||
match *segment {
|
||||
Seg2::Line { a, b } => {
|
||||
add(a)?;
|
||||
add(b)?;
|
||||
}
|
||||
Seg2::Arc { center, radius, .. } => {
|
||||
add(center)?;
|
||||
if !radius.is_finite() || radius <= 0.0 || radius > 1e7 {
|
||||
return Err(fail("invalid profile radius"));
|
||||
}
|
||||
let z = z0 + center.dot(direction);
|
||||
min = min.min(z - radius * direction.len());
|
||||
max = max.max(z + radius * direction.len());
|
||||
}
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => {
|
||||
for p in [p0, p1, p2, p3] {
|
||||
add(p)?;
|
||||
}
|
||||
}
|
||||
Seg2::Conic { a, apex, b, w } if w.is_finite() && w > 0.0 => {
|
||||
for p in [a, apex, b] {
|
||||
add(p)?;
|
||||
}
|
||||
}
|
||||
Seg2::Bezier { n, p } if (1..=7).contains(&n) => {
|
||||
for p in p.iter().take(usize::from(n) + 1) {
|
||||
add(*p)?;
|
||||
}
|
||||
}
|
||||
_ => return Err(fail("unsupported profile geometry")),
|
||||
}
|
||||
}
|
||||
overlaps |= min < hi && max > lo;
|
||||
}
|
||||
Ok(overlaps)
|
||||
}
|
||||
|
||||
fn face_axial_bounds(face: &mt::Face, o: Vec3, axis: Vec3) -> Result<Option<(f64, f64)>> {
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = f64::NEG_INFINITY;
|
||||
let mut count = 0usize;
|
||||
let mut add = |point: Vec3| -> Result<()> {
|
||||
count = count.saturating_add(1);
|
||||
if count > 100_000 || !point.is_finite() {
|
||||
return Err(fail("target boundary work limit or nonfinite geometry"));
|
||||
}
|
||||
let z = (point - o).dot(axis);
|
||||
lo = lo.min(z);
|
||||
hi = hi.max(z);
|
||||
Ok(())
|
||||
};
|
||||
for edge in face.edge_iter() {
|
||||
match edge.curve() {
|
||||
mt::Curve::Line(_) => {
|
||||
add(from_p3(edge.front().point()))?;
|
||||
add(from_p3(edge.back().point()))?;
|
||||
}
|
||||
mt::Curve::BSplineCurve(curve) => {
|
||||
for point in curve.control_points() {
|
||||
add(from_p3(*point))?;
|
||||
}
|
||||
}
|
||||
mt::Curve::NurbsCurve(curve) => {
|
||||
for point in curve.control_points() {
|
||||
if !point.w.is_finite() || point.w <= 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
add(Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w))?;
|
||||
}
|
||||
}
|
||||
mt::Curve::IntersectionCurve(_) => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok((lo.is_finite() && hi.is_finite()).then_some((lo, hi)))
|
||||
}
|
||||
|
||||
fn wrapped_loop(lp: &Loop2, r: f64, rr: f64, at: &impl Fn(Vec2, f64) -> Vec3) -> Result<(mt::Wire, mt::Wire, Vec<mt::Face>)> {
|
||||
let base: Vec<_> = lp.segs.iter().map(|s| builder::vertex(p3(at(s.start(), r)))).collect();
|
||||
let top: Vec<_> = lp.segs.iter().map(|s| builder::vertex(p3(at(s.start(), rr)))).collect();
|
||||
@@ -346,6 +564,45 @@ mod tests {
|
||||
Plane::new(Vec3::Y * 10., -Vec3::X, Vec3::Z).unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn inference_uses_finite_wall_footprint_and_an_interior_profile_anchor() {
|
||||
let near = crate::cylinder(Vec3::ZERO, Vec3::Z, 10., 20.).unwrap();
|
||||
let far = crate::cylinder(Vec3::Z * 100., Vec3::Z, 10., 20.).unwrap();
|
||||
let other = crate::cylinder(Vec3::Z * 120., Vec3::Z, 8., 10.).unwrap();
|
||||
let unsupported = crate::ops::join_pieces(vec![far.clone(), other]).unwrap().unwrap();
|
||||
let regions = [profile()];
|
||||
for body in [&far, &unsupported] {
|
||||
assert!(infer_cylinder_emboss(body, &plane(), ®ions, 1.).unwrap().is_none());
|
||||
}
|
||||
for x in [20., 80.] {
|
||||
let base = crate::cylinder(Vec3::X * x, Vec3::Z, 10., 20.).unwrap();
|
||||
let step = crate::cylinder(Vec3::X * x + Vec3::Z * 20., Vec3::Z, 8., 10.).unwrap();
|
||||
let remote = crate::ops::join_pieces(vec![base, step]).unwrap().unwrap();
|
||||
let inferred = infer_cylinder_emboss(&remote, &plane(), ®ions, 1.);
|
||||
if x == 20. {
|
||||
// The offset maps this footprint across the remote wall's seam;
|
||||
// partial relevance must retain conservative unsupported refusal.
|
||||
assert!(inferred.is_err());
|
||||
} else {
|
||||
assert!(inferred.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
let (index, anchor) = infer_cylinder_emboss(&near, &plane(), ®ions, 1.).unwrap().unwrap();
|
||||
assert!(anchor.z > 0. && anchor.z < 20.);
|
||||
assert!((anchor.x.hypot(anchor.y) - 10.).abs() < 1e-9);
|
||||
assert!(cylinder_emboss_anchor(&near, index, anchor).unwrap().dist(anchor) < 1e-10);
|
||||
let shifted = Plane::new(Vec3::new(-3., 10., 0.), -Vec3::X, Vec3::Z).unwrap();
|
||||
let shifted_profile =
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(-5., 5.), Vec2::new(-1., 5.), Vec2::new(-1., 10.), Vec2::new(-5., 10.)]), holes: vec![] };
|
||||
let (_, other_anchor) = infer_cylinder_emboss(&near, &shifted, std::slice::from_ref(&shifted_profile), 1.).unwrap().unwrap();
|
||||
assert!(other_anchor.dist(anchor) < 1e-10);
|
||||
let before = near.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let result = emboss_cylinder_at(&near, other_anchor, &shifted, &[shifted_profile], 1.).unwrap();
|
||||
assert!((result.tessellate_with(1e-4, true).unwrap().measure().volume - before - 21.).abs() < 0.02);
|
||||
assert!(infer_cylinder_emboss(&near, &plane(), ®ions, -10.).is_err());
|
||||
let inward = Plane::new(Vec3::Y * 10., Vec3::X, Vec3::Z).unwrap();
|
||||
assert!(infer_cylinder_emboss(&near, &inward, ®ions, 1.).unwrap_err().to_string().contains("outwards"));
|
||||
}
|
||||
#[test]
|
||||
fn wrapped_nested_island_uses_its_enclosing_hole_patch() {
|
||||
let body = crate::cylinder(Vec3::ZERO, Vec3::Z, 10., 20.).unwrap();
|
||||
let mut ring = profile();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::{FaceSource, extrude, transform_matrix};
|
||||
use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
#[test]
|
||||
fn constructed_annulus_sides_follow_exact_source_edges() {
|
||||
let ring = Region2 { outer: Loop2::circle(Vec2::default(), 10.), holes: vec![Loop2::circle(Vec2::default(), 9.).reversed()] };
|
||||
for (start, end) in [(0., 7.), (-7., 0.), (-7., 7.)] {
|
||||
let bodies = extrude(&Plane::XY, std::slice::from_ref(&ring), start, end).unwrap();
|
||||
let body = &bodies[0];
|
||||
let sources = body.face_sources().unwrap();
|
||||
assert_eq!(sources.len(), body.face_count());
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mut inner = 0;
|
||||
let mut outer = 0;
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
let Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { radius, .. } }) = source else { continue };
|
||||
if (*radius - 10.).abs() < 1e-12 {
|
||||
outer += 1;
|
||||
} else {
|
||||
assert!((*radius - 9.).abs() < 1e-12);
|
||||
inner += 1;
|
||||
}
|
||||
let mut vertices = 0;
|
||||
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if *face as usize != index {
|
||||
continue;
|
||||
}
|
||||
for point in mesh.tri(triangle).unwrap() {
|
||||
assert!((point.x.hypot(point.y) - radius).abs() < 1e-8, "face {index}, source {radius}, {point:?}");
|
||||
vertices += 1;
|
||||
}
|
||||
}
|
||||
assert!(vertices > 0);
|
||||
}
|
||||
assert_eq!((outer, inner), (2, 2), "full circles split into two actual edges each");
|
||||
assert_eq!(sources.iter().filter(|source| source.is_none()).count(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructed_side_identity_survives_affine_placement_without_recognition() {
|
||||
let region = Region2 { outer: Loop2::circle(Vec2::default(), 10.), holes: vec![] };
|
||||
let bodies = extrude(&Plane::named("XY").unwrap(), &[region], 0., 7.).unwrap();
|
||||
let source = &bodies[0];
|
||||
let placed = transform_matrix(source, [[0., 1., 0., 0.], [-1., 0., 0., 0.], [0., 0., 1., 0.], [25., 30., 40., 1.]]).unwrap();
|
||||
assert_eq!(placed.face_sources(), source.face_sources());
|
||||
assert!((placed.size() - source.size()).abs() < 1e-8);
|
||||
assert!(placed.faces(0.001).unwrap().iter().all(|face| face.centroid.dist(Vec3::new(25., 30., 43.5)) < 11.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructed_large_arc_split_and_reversal_preserve_one_source_segment() {
|
||||
let arc = Seg2::Arc { center: Vec2::default(), radius: 10., start: 0., sweep: std::f64::consts::PI * 5. / 3. };
|
||||
let outline = Loop2 { segs: vec![arc, Seg2::Line { a: arc.end(), b: arc.start() }] };
|
||||
for outline in [outline.clone(), outline.reversed()] {
|
||||
let body = extrude(&Plane::XY, &[Region2 { outer: outline, holes: vec![] }], 0., 7.).unwrap().remove(0);
|
||||
let sources = body.face_sources().unwrap();
|
||||
let arcs: Vec<_> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { radius, sweep, .. } }) => Some((radius, sweep)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(arcs.len(), 2, "large input arc explicitly creates two actual boundary edges");
|
||||
assert!(arcs.iter().all(|(radius, sweep)| (**radius - 10.).abs() < 1e-12 && (sweep.abs() - std::f64::consts::PI * 5. / 3.).abs() < 1e-12));
|
||||
assert_eq!(sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeSide { segment: Seg2::Line { .. } }))).count(), 1);
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if matches!(sources[*face as usize], Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { .. } })) {
|
||||
assert!(mesh.tri(triangle).unwrap().iter().all(|point| (point.x.hypot(point.y) - 10.).abs() < 1e-7));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ mod coplanar;
|
||||
mod curveblend;
|
||||
mod delete_face;
|
||||
mod emboss;
|
||||
#[cfg(test)]
|
||||
mod face_source_tests;
|
||||
mod freeform;
|
||||
pub mod fuzz;
|
||||
mod heal;
|
||||
@@ -52,10 +54,10 @@ mod topo;
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{fillet_chord, fillet_variable, fillet_variable_points};
|
||||
pub use body::{Body, EdgeInfo, FaceInfo, FacePaint, Paint};
|
||||
pub use body::{Body, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint};
|
||||
pub use build::{PathSeg, box_solid, cylinder, extrude, extrude_tapered, loft, revolve, sphere, sweep, torus};
|
||||
pub use delete_face::delete_faces;
|
||||
pub use emboss::{cylinder_emboss_anchor, emboss_cylinder, emboss_cylinder_at};
|
||||
pub use emboss::{cylinder_emboss_anchor, emboss_cylinder, emboss_cylinder_at, has_cylinder_emboss_wall, infer_cylinder_emboss};
|
||||
pub use helix::{loft_to_point, sweep_helix, sweep_helix_mesh};
|
||||
pub use iges_in::{MAX_IGES_BYTES, iges_import, iges_to_step};
|
||||
pub use iges_out::iges_export_bodies;
|
||||
|
||||
@@ -108,6 +108,7 @@ pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result<Body> {
|
||||
color: None,
|
||||
paint: None,
|
||||
splits: None,
|
||||
face_sources: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,6 +200,7 @@ impl Body {
|
||||
color: self.color,
|
||||
paint: self.paint.clone(),
|
||||
splits: self.splits.clone(),
|
||||
face_sources: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -531,7 +531,8 @@ pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result<Body> {
|
||||
m[3][3],
|
||||
);
|
||||
// An affine placement retains face order and appearance metadata.
|
||||
Body::new(placed_topology(&body.solid, mat)?).map(|placed| placed.with_color(body.color()).with_paint(body.paint().cloned()))
|
||||
Body::new(placed_topology(&body.solid, mat)?)
|
||||
.map(|placed| placed.with_color(body.color()).with_paint(body.paint().cloned()).with_face_sources(body.face_sources.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -862,9 +862,13 @@ fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option<Vec<Prism>> {
|
||||
}
|
||||
|
||||
fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
// This path repairs joins with a common outer cap (a boss flush with a rim).
|
||||
// Other extents and subtractive operations retain their existing B-rep path.
|
||||
if op != BoolOp::Union {
|
||||
// Common-cap joins and partial-depth cuts of aligned stepped prisms can
|
||||
// retain their exact section boundaries instead of intersecting meshes.
|
||||
// Other extents retain their existing B-rep path.
|
||||
if !matches!(op, BoolOp::Union | BoolOp::Cut) {
|
||||
return None;
|
||||
}
|
||||
if op == BoolOp::Cut && a.face_count().saturating_add(b.face_count()) > 2048 {
|
||||
return None;
|
||||
}
|
||||
fn section(ps: &[Prism], lo: f64, hi: f64, tol: f64) -> &[Region2] {
|
||||
@@ -882,28 +886,46 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>>
|
||||
}
|
||||
}
|
||||
}
|
||||
let ends = |ps: &[Prism]| ps.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.lo), hi.max(p.hi)));
|
||||
let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| {
|
||||
let plane = Plane::from_normal(Vec3::ZERO, d)?;
|
||||
Some((d, plane, stack_of(a, d, &plane, tol)?, stack_of(b, d, &plane, tol)?))
|
||||
let pa = stack_of(a, d, &plane, tol)?;
|
||||
let pb = stack_of(b, d, &plane, tol)?;
|
||||
let (ea, eb) = (ends(&pa), ends(&pb));
|
||||
if (ea.0 - eb.0).abs() >= tol && (ea.1 - eb.1).abs() >= tol {
|
||||
return None;
|
||||
}
|
||||
Some((d, plane, pa, pb))
|
||||
})?;
|
||||
let ends = |ps: &[Prism]| ps.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.lo), hi.max(p.hi)));
|
||||
let (ea, eb) = (ends(&pa), ends(&pb));
|
||||
if (ea.0 - eb.0).abs() >= tol && (ea.1 - eb.1).abs() >= tol {
|
||||
return None;
|
||||
}
|
||||
let mut levels: Vec<f64> = pa.iter().chain(&pb).flat_map(|p| [p.lo, p.hi]).collect();
|
||||
levels.sort_by(f64::total_cmp);
|
||||
levels.dedup_by(|x, y| (*x - *y).abs() < tol);
|
||||
let mut slabs = Vec::new();
|
||||
let mut work = 0usize;
|
||||
let mut output_edges = 0usize;
|
||||
let edges = |regions: &[Region2]| regions.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).map(|l| l.segs.len()).sum::<usize>();
|
||||
for w in levels.windows(2) {
|
||||
let (lo, hi) = (*w.first()?, *w.get(1)?);
|
||||
let (x, y) = (section(&pa, lo, hi, tol), section(&pb, lo, hi, tol));
|
||||
if op == BoolOp::Cut {
|
||||
let count = edges(x).saturating_add(edges(y));
|
||||
work = work.saturating_add(count.saturating_mul(count));
|
||||
if work > 32_000_000 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let s = match (x.is_empty(), y.is_empty(), op) {
|
||||
(true, _, BoolOp::Cut | BoolOp::Intersect) | (_, true, BoolOp::Intersect) => Vec::new(),
|
||||
(true, _, BoolOp::Union) => y.to_vec(),
|
||||
(_, true, _) => x.to_vec(),
|
||||
_ => boolean2d(x, y, op, tol)?,
|
||||
};
|
||||
if op == BoolOp::Cut {
|
||||
output_edges = output_edges.saturating_add(edges(&s));
|
||||
if output_edges > 8192 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
slabs.push(oriented(&s));
|
||||
}
|
||||
if slabs.iter().all(Vec::is_empty) {
|
||||
@@ -923,6 +945,42 @@ mod planar_tests {
|
||||
Region2 { outer: Loop2::polygon(&[a, Vec2::new(b.x, a.y), b, Vec2::new(a.x, b.y)]), holes: vec![] }
|
||||
}
|
||||
#[test]
|
||||
fn partial_depth_cuts_touching_a_through_hole_use_exact_stacked_sections() {
|
||||
let plane = Plane::named("XY").unwrap();
|
||||
let mut plate = rectangle(Vec2::ZERO, Vec2::new(100., 60.));
|
||||
plate.holes.push(rectangle(Vec2::new(25., 20.), Vec2::new(30., 30.)).outer);
|
||||
let stock = crate::extrude(&plane, &[plate], 0., 2.5).unwrap().pop().unwrap();
|
||||
let low = crate::extrude(&plane, &[rectangle(Vec2::new(20., 20.), Vec2::new(30., 30.))], 0., 1.5).unwrap().pop().unwrap();
|
||||
let high = crate::extrude(&plane, &[rectangle(Vec2::new(25., 20.), Vec2::new(35., 30.))], 1., 2.5).unwrap().pop().unwrap();
|
||||
for tools in [[&low, &high], [&high, &low]] {
|
||||
let mut result = stock.clone();
|
||||
for tool in tools {
|
||||
result = stack_boolean(&result, tool, BoolOp::Cut).expect("exact stack path").unwrap().unwrap();
|
||||
}
|
||||
assert!(result.validity().is_empty());
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
assert!((crate::measure(&result).unwrap().volume - 14_725.).abs() < 1e-6);
|
||||
let mesh = result.tessellate(1e-4).unwrap();
|
||||
for (point, material) in [
|
||||
(Vec3::new(22.5, 25., 0.5), false),
|
||||
(Vec3::new(22.5, 25., 2.), true),
|
||||
(Vec3::new(32.5, 25., 0.5), true),
|
||||
(Vec3::new(32.5, 25., 2.), false),
|
||||
(Vec3::new(27.5, 25., 1.25), false),
|
||||
(Vec3::new(10., 10., 1.25), true),
|
||||
] {
|
||||
assert_eq!(mesh.contains(point), material, "{point:?}");
|
||||
}
|
||||
let imported = crate::step_import(&crate::step_export(&[&result], "layered cut").unwrap()).unwrap();
|
||||
assert!(imported.warnings.is_empty());
|
||||
assert!((crate::measure(&imported.bodies.first().unwrap().body).unwrap().volume - 14_725.).abs() < 1e-6);
|
||||
let next = crate::extrude(&plane, &[rectangle(Vec2::new(20., 20.), Vec2::new(25., 30.))], 1.5, 2.5).unwrap().pop().unwrap();
|
||||
let opened = crate::boolean(&imported.bodies.first().unwrap().body, &next, BoolOp::Cut).unwrap().unwrap();
|
||||
assert!((crate::measure(&opened).unwrap().volume - 14_675.).abs() < 1e-6);
|
||||
assert!(opened.validity().is_empty());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn compound_planar_extrusion_normalizes_overlap_and_retains_islands() {
|
||||
let plane = Plane::named("XY").unwrap();
|
||||
let a = rectangle(Vec2::ZERO, Vec2::new(10.0, 10.0));
|
||||
|
||||
@@ -878,7 +878,15 @@ pub(crate) fn split_any(body: &Body, faces: &[usize], tool: &SplitTool) -> Resul
|
||||
let shells = std::mem::take(&mut imp.shells);
|
||||
let solid = mt::Solid::new_unchecked(shells.into_iter().map(mt::Shell::from).collect());
|
||||
Ok((
|
||||
Body { solid: std::sync::Arc::new(solid), coarse_missing: None, mesh: None, color: body.color, paint: None, splits: body.splits.clone() },
|
||||
Body {
|
||||
solid: std::sync::Arc::new(solid),
|
||||
coarse_missing: None,
|
||||
mesh: None,
|
||||
color: body.color,
|
||||
paint: None,
|
||||
splits: body.splits.clone(),
|
||||
face_sources: None,
|
||||
},
|
||||
imp,
|
||||
))
|
||||
})
|
||||
|
||||
@@ -34,6 +34,10 @@ const MAX_VISITS: usize = 100_000;
|
||||
/// A body read from a STEP file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImportedBody {
|
||||
/// Immutable STEP source item, scoped to its embedded payload.
|
||||
pub source_item: u64,
|
||||
/// Original STEP face entity of each kernel face, including seam pieces.
|
||||
pub face_origin: Vec<u64>,
|
||||
/// Solid name (the brep's label), else the product name.
|
||||
pub name: String,
|
||||
pub body: Body,
|
||||
@@ -510,7 +514,21 @@ impl<'a> Reader<'a> {
|
||||
.collect();
|
||||
let paint = crate::Paint { opacity: self.opacity.get(&item).copied().unwrap_or(1.0), faces };
|
||||
let body = body.with_color(color).with_paint(Some(paint));
|
||||
self.bodies.push(ImportedBody { name, body, color, path: path.to_vec(), closed, file_faces });
|
||||
let identities = face_origin
|
||||
.iter()
|
||||
.map(|face| Some(crate::FaceSource::Imported { item, face: *face, payload_sha256: None, producer: None }))
|
||||
.collect();
|
||||
let body = body.with_face_sources(Some(std::sync::Arc::new(identities)));
|
||||
self.bodies.push(ImportedBody {
|
||||
name,
|
||||
body,
|
||||
color,
|
||||
path: path.to_vec(),
|
||||
closed,
|
||||
file_faces,
|
||||
source_item: item,
|
||||
face_origin,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
let label = self.label(item);
|
||||
|
||||
@@ -29,6 +29,7 @@ fn surface_body(faces: Vec<mt::Face>) -> Body {
|
||||
color: None,
|
||||
paint: None,
|
||||
splits: None,
|
||||
face_sources: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ fn body_of(faces: Vec<mt::Face>) -> Result<Body> {
|
||||
color: None,
|
||||
paint: None,
|
||||
splits: None,
|
||||
face_sources: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user