feat(kernel): history across coalesced stepped walls (#136)

A cut of stepped prisms sews a wall over one segment through successive
slabs as one face, and the generated planar walls are then joined on
their common supports. Until now such a piece carried no history.

- prism_lineage: a sewn wall records what it is over in each slab it
  spans (Spec::Wall { levels, slabs }), so a coalesced wall comes from
  the operand walls under it in every slab
- the sewn piece's history is attached before the joining and carried
  through it (Built::Traced); planar_regularize composes the history of
  the body it joins with the join itself
- a composed face is Split only where record::mark_shared_split finds an
  origin under several faces; an inner Split alone no longer makes one
- Body::same ignores history: an operation that leaves a body as it is
  gives the same body, carrying that operation's history

The two boolean history tests that waited for this run again, and the
coalesced and regularized step cuts are covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and bherbruck committed 2026-10-12 11:52:53 -04:00
1 parent 97bcdb4d06
commit 08d797235d
11 files changed
+334 -159

No files matched your search

+25 -1
View File
@@ -1,6 +1,7 @@
//! Exact topological removal of construction-induced planar subdivisions.
//! Shared edge identity and complete construction lineage select the groups.
use super::{Body, from_p3};
use crate::history::FaceOrigin;
use crate::{Result, guard};
use solvecraft_geom::Plane;
use std::collections::{BTreeMap, HashMap, HashSet};
@@ -115,6 +116,24 @@ fn merged_face(faces: &[&mt::Face], members: &[usize], plane: Plane, eligible: &
mt::Face::try_new(wires, faces.get(*members.first()?)?.oriented_surface()).ok()
}
/// `body` with its history: `input`'s faces with each output face made of the input faces
/// `members` lists for it (every edge of `body` one of `input`'s, by identity), composed with
/// the history of the operation that made `input`; none where `input` has none.
fn with_joined_history(body: Body, input: &Body, members: Vec<Vec<usize>>) -> Body {
let Some(inner) = input.history() else { return body };
let faces = members.into_iter().map(|m| (m.iter().map(|&face| FaceOrigin::Face { operand: 0, face }).collect(), m.len() == 1)).collect();
let joined = crate::record::traced(
body,
&crate::record::counts(&[input]),
Some(crate::record::regions(faces)),
Some(&crate::record::edge_origins(&input.solid, 0)),
);
// Both steps are checked (`traced` against `input`, `through` composing over its checked
// history); the composed operands' counts are not known here, so it is set in place.
let history = crate::boolean_record::through(&joined, 0, Some(inner));
Body { history: history.map(Arc::new), ..joined }
}
impl Body {
pub(crate) fn regularize_certified_planar_faces(self) -> Result<Self> {
self.regularize_planar_faces(false)
@@ -252,6 +271,8 @@ impl Body {
let mut shells = Vec::new();
let mut output_sources = Vec::new();
let mut output_ids = Vec::new();
// The input faces each output face is made of, for its history.
let mut output_members: Vec<Vec<usize>> = Vec::new();
let mut offset = 0;
for shell in self.solid.boundaries() {
let mut output = Vec::new();
@@ -260,12 +281,15 @@ impl Body {
if let Some(merged) = replacements.get(&leader) {
if offset == leader {
output.push(merged.clone());
let Some(members) = groups.remove(&leader) else { return Ok(None) };
output_members.push(members);
} else {
offset += 1;
continue;
}
} else {
output.push(face.clone());
output_members.push(vec![offset]);
}
output_ids.push(output.last().ok_or_else(|| crate::KernelError::Failed("missing regularized face".into()))?.id());
let source = sources[offset].as_ref();
@@ -291,7 +315,7 @@ impl Body {
if !body.solid.face_iter().map(|f| f.id()).eq(output_ids) {
return Ok(None);
}
Ok(Some(body.with_face_sources(Some(Arc::new(output_sources)))))
Ok(Some(with_joined_history(body.with_face_sources(Some(Arc::new(output_sources))), &self, output_members)))
})?;
Ok(result.unwrap_or(self))
}