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:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 05:26:29 -04:00
1 parent 27f20f97cb
commit 6d5df4b9b3
18 files changed
+786 -381

No files matched your search

+8
View File
@@ -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;
+14 -2
View File
@@ -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).
+52 -25
View File
@@ -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,
};