Sketch on cut walls, chamfers, pattern copies, join-merged sides and split faces (#625)
A flat face that no construction certificate names on its own now attaches by its exact history name (a new lineage face certificate): cut walls and floors, chamfer faces, each pattern or mirror copy, side walls a Join merged, and the pieces a later feature split a face into. Construction certificates still come first; a seed keeps its certificate when it is patterned. Two flush extrudes of one outline joined end to end keep their exact history in the prism boolean. Face sketch 52% -> 71%, offset plane 52% -> 71%, press-pull/extrude from face 41% -> 60% in the action matrix; baseline blessed. Fixes #491, fixes #492, fixes #493, fixes #557. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27f20f97cb
commit
6d5df4b9b3
18 files changed
+786
-381
No files matched your search
@@ -6,6 +6,14 @@
|
||||
//! the operation's own operand list, which each operation documents. A body made by an
|
||||
//! operation carries its history (see [`Body::history`]); [`History::substitute`] composes an
|
||||
//! operation with one that made one of its operands.
|
||||
//!
|
||||
//! This is what persistent names and face attachments stand on. The document composes each
|
||||
//! feature's histories into face names (`F5:side:l1`, `F7:blend:…`, `F4:top@F6.2`, see
|
||||
//! `solidcraft_doc::history_names`); a name is exact only when every operation on the way
|
||||
//! reported its history. A flat face with an exact name takes a durable sketch attachment by that
|
||||
//! name (`FaceCertificate::Lineage`, docs/face-attachments.md), so an operation that records its
|
||||
//! history here gives its faces attachments with no attachment code of its own. An operation
|
||||
//! that can't should leave its result without one (`Body::without_history`), never guess.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
|
||||
@@ -1225,7 +1225,12 @@ fn sew_sections(
|
||||
for (r, piece) in crate::extrude(plane, section, levels[first], levels[end])?.into_iter().enumerate() {
|
||||
remaining_faces =
|
||||
remaining_faces.checked_sub(piece.face_count()).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?;
|
||||
pieces.push((piece, lineage::Built::Extruded { section: tagged.region(r), lo: levels[first], hi: levels[end] }));
|
||||
// Each slab's section, as read there: the same outline, but its walls
|
||||
// read beside whichever operand is there.
|
||||
let layers =
|
||||
(first..end).map(|k| Some((slabs.get(k)?.region(r), *levels.get(k)?, *levels.get(k + 1)?))).collect::<Option<Vec<_>>>();
|
||||
let layers = layers.ok_or_else(|| KernelError::Failed("missing prism section".into()))?;
|
||||
pieces.push((piece, lineage::Built::Extruded { layers }));
|
||||
}
|
||||
} else {
|
||||
let (piece, built) = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce, history)
|
||||
@@ -1507,6 +1512,13 @@ fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2
|
||||
if (u - x).abs() > tol * (1.0 + u) || (u - y).abs() > tol * (1.0 + u) {
|
||||
return sew_stack(&plane, d, &pa, &pb, tol, &history).map(|r| r.map(Some));
|
||||
}
|
||||
// One outline end to end: also slab by slab, so the history follows each operand's
|
||||
// walls (one longer prism, whose walls are made from both).
|
||||
if let Some(Ok(body)) = sew_stack(&plane, d, &pa, &pb, tol, &history)
|
||||
&& body.history().is_some()
|
||||
{
|
||||
return Some(Ok(Some(body)));
|
||||
}
|
||||
(pa.lo.min(pb.lo), pa.hi.max(pb.hi))
|
||||
} else {
|
||||
(pa.lo, pa.hi)
|
||||
@@ -1518,7 +1530,7 @@ fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2
|
||||
};
|
||||
// Each body over its region.
|
||||
let mut pieces: Vec<(Body, lineage::Built)> =
|
||||
bodies.into_iter().enumerate().map(|(r, b)| (b, lineage::Built::Extruded { section: section.region(r), lo, hi })).collect();
|
||||
bodies.into_iter().enumerate().map(|(r, b)| (b, lineage::Built::Extruded { layers: vec![(section.region(r), lo, hi)] })).collect();
|
||||
let last = pieces.pop()?;
|
||||
if !pieces.is_empty() {
|
||||
// Several pieces: one body of several lumps (the last first).
|
||||
|
||||
@@ -59,8 +59,11 @@ pub(super) struct Context<'a> {
|
||||
|
||||
/// How one piece of the result was built.
|
||||
pub(super) enum Built {
|
||||
/// One body of `crate::extrude` of the section's one region between levels `lo` and `hi`.
|
||||
Extruded { section: Section, lo: f64, hi: f64 },
|
||||
/// One body of `crate::extrude` of a section's one region through `layers`: successive
|
||||
/// slabs (the section as read in each, between its two levels), bottom first, all of one
|
||||
/// outline. Several where operands stacked end to end share an outline (one wall then lies
|
||||
/// over each operand's wall in turn, and is made from all of them).
|
||||
Extruded { layers: Vec<(Section, f64, f64)> },
|
||||
/// Sewn (`sew_connected_sections`) from these faces, the sewing saying which each face of
|
||||
/// the piece was built from (`None`: it could not).
|
||||
Sewn { specs: Vec<Spec>, faces: Option<Vec<usize>> },
|
||||
@@ -111,10 +114,11 @@ impl Spec {
|
||||
}
|
||||
}
|
||||
|
||||
/// A face of a piece: the wall over a segment with this tag between two levels, or a cap.
|
||||
/// A face of an extruded piece: a wall or a cap.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Kind {
|
||||
Wall(Tag, f64, f64),
|
||||
/// The wall over this segment of the section.
|
||||
Wall(usize),
|
||||
Cap(usize),
|
||||
}
|
||||
|
||||
@@ -139,7 +143,7 @@ impl<'a> Context<'a> {
|
||||
let mut edges = HashMap::new();
|
||||
for (body, built) in pieces {
|
||||
let (f, e) = match built {
|
||||
Built::Extruded { section, lo, hi } => self.extruded(caps, body, section, *lo, *hi)?,
|
||||
Built::Extruded { layers } => self.extruded(caps, body, layers)?,
|
||||
Built::Sewn { specs, faces } => self.sewn(caps, body, specs, faces.as_deref()?)?,
|
||||
Built::Traced => {
|
||||
let h = body.history()?;
|
||||
@@ -331,28 +335,35 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces and edges of a piece `crate::extrude` built from a section of one region. Its
|
||||
/// history says which faces are walls over which segment (numbered over the region's
|
||||
/// loops in order) and which are its caps; the start cap's wires run over the segments in
|
||||
/// loop order (as the extrude builds them), so the vertex after each wire edge is where one
|
||||
/// segment runs into the next, or splits.
|
||||
/// Faces and edges of a piece `crate::extrude` built from a section of one region through
|
||||
/// `layers` (see [`Built::Extruded`]). Its history says which faces are walls over which
|
||||
/// segment (numbered over the region's loops in order) and which are its caps; the start
|
||||
/// cap's wires run over the segments in loop order (as the extrude builds them), so the
|
||||
/// vertex after each wire edge is where one segment runs into the next, or splits. A wall
|
||||
/// through several layers is made from the wall each layer's segment was read beside, and
|
||||
/// an edge between two walls from the operand edges there in each layer.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn extruded(
|
||||
&self,
|
||||
caps: &[Cap],
|
||||
body: &Body,
|
||||
section: &Section,
|
||||
lo: f64,
|
||||
hi: f64,
|
||||
layers: &[(Section, f64, f64)],
|
||||
) -> Option<(Vec<(BTreeSet<FaceOrigin>, bool)>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
|
||||
let h = body.history()?;
|
||||
let tags = section.segments().1;
|
||||
let tops = [self.cap_origins(caps, section, lo, false), self.cap_origins(caps, section, hi, true)];
|
||||
let ((first, lo, _), (last, _, hi)) = (layers.first()?, layers.last()?);
|
||||
let (lo, hi) = (*lo, *hi);
|
||||
let tags: Vec<Vec<Tag>> = layers.iter().map(|(section, ..)| section.segments().1).collect();
|
||||
let count = tags.first()?.len();
|
||||
if tags.iter().any(|t| t.len() != count) {
|
||||
return None;
|
||||
}
|
||||
let tag = |layer: usize, segment: usize| tags.get(layer).and_then(|t| t.get(segment)).copied();
|
||||
let tops = [self.cap_origins(caps, first, lo, false), self.cap_origins(caps, last, hi, true)];
|
||||
let kinds: Vec<Kind> = h
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| match f.origins.as_slice() {
|
||||
[FaceOrigin::Profile { segment }] => Some(Kind::Wall(*tags.get(*segment)?, lo, hi)),
|
||||
[FaceOrigin::Profile { segment }] => (*segment < count).then_some(Kind::Wall(*segment)),
|
||||
[FaceOrigin::Cap { end }] => Some(Kind::Cap(usize::from(*end))),
|
||||
_ => None,
|
||||
})
|
||||
@@ -360,7 +371,14 @@ impl<'a> Context<'a> {
|
||||
let faces = kinds
|
||||
.iter()
|
||||
.map(|k| match *k {
|
||||
Kind::Wall(t, lo, hi) => Some((std::iter::once(Self::wall(t)?).collect(), self.whole(caps, t, lo, hi))),
|
||||
Kind::Wall(segment) => {
|
||||
let walls = (0..layers.len()).map(|layer| Self::wall(tag(layer, segment)?)).collect::<Option<BTreeSet<_>>>()?;
|
||||
let whole = match layers {
|
||||
[(_, lo, hi)] => self.whole(caps, tag(0, segment)?, *lo, *hi),
|
||||
_ => false,
|
||||
};
|
||||
Some((walls, whole))
|
||||
}
|
||||
Kind::Cap(end) => Some(tops.get(end).filter(|o| !o.is_empty())?.clone()).map(|o| (o, false)),
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
@@ -374,7 +392,9 @@ impl<'a> Context<'a> {
|
||||
})
|
||||
};
|
||||
let start = kinds.iter().position(|k| matches!(k, Kind::Cap(0)))?;
|
||||
let mut junctions: HashMap<mt::VertexID, (Tag, Tag)> = HashMap::new();
|
||||
// At each vertex of the start cap: the segment running into it, the one running out,
|
||||
// and whether that is one segment the extrude split there.
|
||||
let mut junctions: HashMap<mt::VertexID, (usize, usize, bool)> = HashMap::new();
|
||||
for w in body.solid.face_iter().nth(start)?.absolute_boundaries() {
|
||||
let es: Vec<mt::Edge> = w.edge_iter().cloned().collect();
|
||||
let segs: Vec<usize> = es.iter().map(segment_of).collect::<Option<_>>()?;
|
||||
@@ -383,20 +403,27 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
for (k, e) in es.iter().enumerate() {
|
||||
let (s, next) = (*segs.get(k)?, *segs.get((k + 1) % segs.len())?);
|
||||
let (t, u) = (*tags.get(s)?, *tags.get(next)?);
|
||||
// A segment the extrude split in two runs on through the vertex between.
|
||||
let pair = if s != next || k + 1 == segs.len() { (t, u) } else { (Tag { end: None, ..t }, Tag { start: None, ..t }) };
|
||||
junctions.insert(e.back().id(), pair);
|
||||
junctions.insert(e.back().id(), (s, next, s == next && k + 1 != segs.len()));
|
||||
}
|
||||
}
|
||||
let mut origins = HashMap::new();
|
||||
for (e, fs) in edges.iter().zip(&bounds) {
|
||||
let [f, g] = fs.as_slice() else { return None };
|
||||
let o = match (kinds.get(*f)?, kinds.get(*g)?) {
|
||||
(Kind::Wall(t, ..), Kind::Cap(end)) | (Kind::Cap(end), Kind::Wall(t, ..)) => self.cap_edge(*t, tops.get(*end)?)?,
|
||||
(Kind::Wall(segment), Kind::Cap(end)) | (Kind::Cap(end), Kind::Wall(segment)) => {
|
||||
let layer = if *end == 0 { 0 } else { layers.len() - 1 };
|
||||
self.cap_edge(tag(layer, *segment)?, tops.get(*end)?)?
|
||||
}
|
||||
(Kind::Wall(..), Kind::Wall(..)) => {
|
||||
let (t1, t2) = [e.absolute_front().id(), e.absolute_back().id()].iter().find_map(|v| junctions.get(v))?;
|
||||
self.junction(*t1, *t2, lo, hi)?
|
||||
let (s, next, split) = *[e.absolute_front().id(), e.absolute_back().id()].iter().find_map(|v| junctions.get(v))?;
|
||||
let mut o = BTreeSet::new();
|
||||
for (layer, (_, lo, hi)) in layers.iter().enumerate() {
|
||||
let (t, u) = (tag(layer, s)?, tag(layer, next)?);
|
||||
// A segment the extrude split in two runs on through the vertex between.
|
||||
let (t, u) = if split { (Tag { end: None, ..t }, Tag { start: None, ..t }) } else { (t, u) };
|
||||
o.extend(self.junction(t, u, *lo, *hi)?);
|
||||
}
|
||||
o.into_iter().collect()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user