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

+161 -2
View File
@@ -179,6 +179,13 @@ pub enum FaceCertificate {
source: Box<FaceCertificate>,
inner: bool,
},
/// A flat face known by its exact history name (the target's `name`, without piece
/// numbers): what the kernel's face history says it came from, followed through every
/// feature since (a cut wall `F5:side:l1`, a chamfer `F7:blend:…`, a pattern copy
/// `F4:top@F6.2`, a face a join merged from two features). Used where no construction
/// certificate above names the face uniquely. Its plane is the face's exact current plane in
/// the frame [`lineage_frame`] gives it. See docs/face-attachments.md.
Lineage,
}
impl FaceCertificate {
@@ -210,8 +217,9 @@ impl FaceCertificate {
&& payload_sha256.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
Self::ShellPlanar { source_producer, source, .. } => {
*source_producer != 0 && !matches!(source.as_ref(), Self::ShellPlanar { .. }) && source.validate().is_ok()
*source_producer != 0 && !matches!(source.as_ref(), Self::ShellPlanar { .. } | Self::Lineage) && source.validate().is_ok()
}
Self::Lineage => true,
};
if valid { Ok(()) } else { Err(DocError::Invalid("invalid face source certificate".into())) }
}
@@ -258,13 +266,16 @@ pub(crate) fn validate_certificate_json(value: &serde_json::Value) -> Result<()>
.and_then(serde_json::Value::as_str)
.is_some_and(|digest| digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)))
}
Some("lineage") => fields.len() == 1,
Some("shell_planar") => {
fields.len() == 4
&& fields.keys().all(|key| ["kind", "source_producer", "source", "inner"].contains(&key.as_str()))
&& nonzero("source_producer")
&& fields.get("inner").and_then(serde_json::Value::as_bool).is_some()
&& fields.get("source").is_some_and(|source| {
source.as_object().is_some_and(|source| source.get("kind").and_then(serde_json::Value::as_str) != Some("shell_planar"))
source
.as_object()
.is_some_and(|source| !matches!(source.get("kind").and_then(serde_json::Value::as_str), Some("shell_planar" | "lineage")))
&& validate_certificate_json(source).is_ok()
})
}
@@ -286,6 +297,7 @@ pub(crate) fn shell_certificate_role(source: &FaceCertificate, inner: bool) -> R
FaceCertificate::ExtrudeCap { high, outer, holes, .. } => format!("cap:{}:{}", if *high { "high" } else { "low" }, cap_key(outer, holes)),
FaceCertificate::Imported { payload_sha256, item, face } => format!("import:{payload_sha256}:{item}:{face}"),
FaceCertificate::ShellPlanar { .. } => return Err(DocError::Invalid("nested shell source certificates are unsupported".into())),
FaceCertificate::Lineage => return Err(DocError::Invalid("a shell's source needs a construction certificate".into())),
};
Ok(format!("shell:{}:{role}", if inner { "inner" } else { "outer" }))
}
@@ -551,6 +563,153 @@ impl Document {
}
}
/// The frame a [`FaceCertificate::Lineage`] face's plane is given, from the plane alone (never
/// from how a kernel parametrizes the surface, so it survives rebuilds and kernel changes): its
/// origin where the body's definition origin projects onto the plane, its x axis the first of
/// the definition's X, Y and Z axes that is not steep to the plane (`|cos| < 0.6`; one always
/// is), projected onto it. A face that moves along its normal keeps the frame where it was;
/// one that moves within its plane (a Move feature) keeps it too, as a sketch on a face does
/// in the definition's coordinates.
pub fn lineage_frame(body: &ModelBody, plane: &solidcraft_geom::Plane) -> Option<solidcraft_geom::Plane> {
use solidcraft_geom::Vec3;
let n = plane.normal();
let m = &body.source_transform;
let origin = crate::apply_point(m, Vec3::ZERO);
let axis = [Vec3::X, Vec3::Y, Vec3::Z].into_iter().filter_map(|a| crate::apply_vector(m, a).normalized()).find(|a| a.dot(n).abs() < 0.6)?;
let x = axis - n * axis.dot(n);
let origin = origin - n * (origin - plane.origin).dot(n);
solidcraft_geom::Plane::new(origin, x, n.cross(x))
}
/// Whether two exact planes are one oriented plane (pieces of one face).
fn same_oriented_plane(a: &solidcraft_geom::Plane, b: &solidcraft_geom::Plane, scale: f64) -> bool {
a.normal().dot(b.normal()) >= 1. - 1e-8 && (b.origin - a.origin).dot(a.normal()).abs() <= 1e-7 * (1. + scale)
}
impl Document {
/// The exact plane an anchor on face `index` of `body` is measured in: the construction
/// certificate's plane, or for a [`FaceCertificate::Lineage`] face its current exact plane in
/// the [`lineage_frame`].
pub fn anchor_face_plane(&self, body: &ModelBody, index: usize, certificate: Option<&FaceCertificate>) -> Option<solidcraft_geom::Plane> {
match certificate {
Some(FaceCertificate::Lineage) => body.body.planar_face(index).and_then(|plane| lineage_frame(body, &plane)),
_ => body.body.face_plane(index),
}
}
/// A target for flat face `index` of `body` by its exact history name
/// ([`FaceCertificate::Lineage`]), when that name follows exact history, names a feature of
/// the design and finds this face (with its other pieces, all on one oriented plane).
pub(crate) fn lineage_face_target(
&self,
body: &ModelBody,
index: usize,
point: solidcraft_geom::Vec3,
) -> Option<crate::config::ConfigFaceTarget> {
let names = crate::naming::names(body);
if !names.verified(index) {
return None;
}
let name = crate::naming::strip_piece(names.faces.get(index)?).to_string();
let origin = name.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
let inverse = crate::mat_inverse(&body.source_transform)?;
let target = crate::config::ConfigFaceTarget {
body: body.definition_name().into(),
origin_feature: origin,
name,
point: crate::apply_point(&inverse, point),
certificate: Some(FaceCertificate::Lineage),
};
self.configuration_face_indices(body, &target).ok()?.contains(&index).then_some(target)
}
/// The faces of `body` an exact history name finds: those named `name` or a piece of it, else
/// (a later join merged it into another face) the face that answers to it. Every one must
/// follow exact history and lie on one oriented plane.
pub(crate) fn lineage_face_indices(&self, body: &ModelBody, name: &str) -> Result<Vec<usize>> {
let names = crate::naming::names(body);
let mut indices: Vec<usize> =
names.faces.iter().enumerate().filter(|(_, face)| crate::naming::strip_piece(face) == name).map(|(index, _)| index).collect();
if indices.is_empty() {
indices =
names.info.iter().enumerate().filter(|(_, info)| info.aliases.iter().any(|alias| alias == name)).map(|(index, _)| index).collect();
}
if indices.is_empty() {
return Err(DocError::Unknown(format!("face `{name}` on `{}` no longer exists", body.name)));
}
self.coplanar_pieces(body, &indices, |index| names.verified(index))?;
Ok(indices)
}
/// Check that `indices` are pieces of one flat face: each `exact` and planar, all on one
/// oriented plane, and not split on purpose (Split Face: its pieces are separate faces, so
/// a pick on one never stands for the whole; docs/face-attachments.md).
pub(crate) fn coplanar_pieces(&self, body: &ModelBody, indices: &[usize], exact: impl Fn(usize) -> bool) -> Result<()> {
let scale = body.body.size();
let mut first = None;
for index in indices {
if !exact(*index) {
return Err(DocError::Invalid("this face's history is not exact".into()));
}
let plane = body.body.planar_face(*index).ok_or_else(|| DocError::Invalid("attached face is not flat".into()))?;
match &first {
None => first = Some(plane),
Some(first) if same_oriented_plane(first, &plane, scale) => {}
Some(_) => return Err(DocError::Invalid("attached face pieces no longer share one oriented plane".into())),
}
}
if let (Some(plane), true) = (first, indices.len() > 1) {
let tolerance = 1e-6 * (1. + scale);
let on_plane = |line: &Vec<solidcraft_geom::Vec3>| {
!line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance)
};
if body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane)) {
return Err(DocError::Invalid(
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
));
}
}
Ok(())
}
/// Of several faces carrying one construction certificate of feature `origin`, the pieces of
/// that feature's own face: copies a pattern or mirror made carry their seed's certificate
/// but are named as copies (`…@F<pattern>.<instance>`, docs/face-attachments.md), and pieces
/// a later feature split the face into share its exact name and plane.
pub(crate) fn certified_pieces(&self, body: &ModelBody, indices: Vec<usize>, origin: u64) -> Result<Vec<usize>> {
if indices.len() < 2 {
return Ok(indices);
}
let names = crate::naming::names(body);
let own = format!("F{origin}:");
let stem = |index: usize| names.faces.get(index).map(|name| crate::naming::strip_piece(name));
let seed: Vec<usize> =
indices.into_iter().filter(|index| stem(*index).is_some_and(|name| name.starts_with(&own) && !name.contains('@'))).collect();
let ambiguous = || DocError::Unknown("attached planar face certificate is missing or ambiguous".into());
let first = seed.first().and_then(|index| stem(*index)).ok_or_else(ambiguous)?;
if seed.iter().any(|index| stem(*index) != Some(first)) {
return Err(ambiguous());
}
// The certificate is exact; the names only tell the seed from its copies here.
self.coplanar_pieces(body, &seed, |_| true).map_err(|_| ambiguous())?;
Ok(seed)
}
/// Of several faces carrying one certificate of feature `origin`, those that are not a
/// pattern's or mirror's copies (named `…@F<pattern>.<instance>`), when any is not.
pub(crate) fn without_copies(&self, body: &ModelBody, indices: Vec<usize>) -> Vec<usize> {
if indices.len() < 2 {
return indices;
}
let names = crate::naming::names(body);
let copy = |index: &usize| names.faces.get(*index).is_some_and(|name| crate::naming::strip_piece(name).contains('@'));
if indices.iter().all(copy) {
return indices;
}
indices.into_iter().filter(|index| !copy(index)).collect()
}
}
pub(crate) fn payload_digest(payload: &str) -> String {
use sha2::{Digest, Sha256};
format!("{:x}", Sha256::digest(payload.as_bytes()))
+8 -4
View File
@@ -145,6 +145,7 @@ impl Document {
&& self.feature(*sketch).is_some_and(|feature| matches!(&feature.kind, FeatureKind::Sketch { sketch, .. } if sketch.curves.iter().any(|candidate| candidate.id == *curve && !candidate.construction && matches!(candidate.kind, solidcraft_sketch::CurveKind::Line { .. } | solidcraft_sketch::CurveKind::Circle { .. } | solidcraft_sketch::CurveKind::Arc { .. }))))
}
(super::FaceCertificate::Imported { .. }, FeatureKind::Import { .. }) => role.strip_prefix("face:").is_some_and(|index| index.parse::<usize>().is_ok()),
(super::FaceCertificate::Lineage, _) => !role.is_empty(),
(super::FaceCertificate::ShellPlanar { source_producer, source, inner }, FeatureKind::Shell { .. }) => {
*source_producer != target.origin_feature
&& self.flat_certificate_producer_matches(*source_producer, source)
@@ -185,6 +186,9 @@ impl Document {
if body.body.is_mesh() || body.definition_name() != target.body {
return Err(DocError::Invalid("configured face definition does not match".into()));
}
if let Some(super::FaceCertificate::Lineage) = &target.certificate {
return self.lineage_face_indices(body, &target.name);
}
if let Some(super::FaceCertificate::ShellPlanar { source_producer, source, inner }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, current)| {
matches!(current, Some(solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), source: current_source, inner: current_inner, .. }) if *producer == target.origin_feature && inner == current_inner && super::flat_source_matches(source, current_source, *source_producer)).then_some(index)
@@ -238,10 +242,10 @@ impl Document {
matched.then_some(index)
})
.collect();
if indices.len() != 1 {
if indices.is_empty() {
return Err(DocError::Unknown("attached planar face certificate is missing or ambiguous".into()));
}
return Ok(indices);
return self.certified_pieces(body, indices, target.origin_feature);
}
if let Some(super::FaceCertificate::ExtrudeSide { sketch, curve }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, source)| {
@@ -250,7 +254,7 @@ impl Document {
if indices.is_empty() {
return Err(DocError::Unknown("constructed side certificate no longer exists".into()));
}
return Ok(indices);
return Ok(self.without_copies(body, indices));
}
if let Some(super::FaceCertificate::Imported { payload_sha256, item, face }) = &target.certificate {
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, source)| {
@@ -259,7 +263,7 @@ impl Document {
if indices.is_empty() {
return Err(DocError::Unknown("immutable import face certificate no longer exists".into()));
}
return Ok(indices);
return Ok(self.without_copies(body, indices));
}
let names = crate::naming::face_names(body);
let indices: Vec<usize> =
+28 -8
View File
@@ -144,15 +144,30 @@ impl Document {
author: u64,
author_path: &[u64],
) -> Result<FaceAnchor> {
let source = body
// A construction certificate that names this face alone, else its exact history name.
let certified = body
.body
.face_plane(index)
.ok_or_else(|| DocError::Invalid("this face has no exact planar source; choose a fixed plane instead".into()))
.and_then(|source| {
let target = self.strong_face_target(body, index, source.origin)?;
if target.certificate.is_none() {
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
}
let indices = self.configuration_face_indices(body, &target)?;
self.coplanar_pieces(body, &indices, |_| true)?;
Ok(target)
});
let target = match certified {
Ok(target) => target,
Err(error) => match body.body.planar_face(index).and_then(|plane| self.lineage_face_target(body, index, plane.origin)) {
Some(target) => target,
None => return Err(error),
},
};
let source = self
.anchor_face_plane(body, index, target.certificate.as_ref())
.ok_or_else(|| DocError::Invalid("this face has no exact planar source; choose a fixed plane instead".into()))?;
let target = self.strong_face_target(body, index, source.origin)?;
if target.certificate.is_none() {
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
}
self.configuration_face_indices(body, &target)?;
let mut anchor = FaceAnchor {
target,
component: self.model_body_component(body),
@@ -178,9 +193,14 @@ impl Document {
state.body(&anchor.target.body).ok_or_else(|| DocError::Unknown(format!("attached body `{}` no longer exists", anchor.target.body)))?;
let indices = self.configuration_face_indices(body, &anchor.target)?;
let first = indices.first().copied().ok_or_else(|| DocError::Unknown("attached face no longer exists".into()))?;
let plane = body.body.face_plane(first).ok_or_else(|| DocError::Invalid("attached face no longer has an exact planar source".into()))?;
let certificate = anchor.target.certificate.as_ref();
let plane = self
.anchor_face_plane(body, first, certificate)
.ok_or_else(|| DocError::Invalid("attached face no longer has an exact planar source".into()))?;
for index in indices.iter().skip(1) {
let other = body.body.face_plane(*index).ok_or_else(|| DocError::Invalid("attached face pieces are not exactly planar".into()))?;
let other = self
.anchor_face_plane(body, *index, certificate)
.ok_or_else(|| DocError::Invalid("attached face pieces are not exactly planar".into()))?;
if plane.normal().dot(other.normal()) < 1. - 1e-8 || (other.origin - plane.origin).dot(plane.normal()).abs() > 1e-7 {
return Err(DocError::Invalid("attached face pieces no longer share one oriented plane".into()));
}
+4
View File
@@ -66,6 +66,10 @@
//! A design can also keep what was hidden when it was saved (`visibility`: hidden bodies,
//! occurrence placements, sketches and origin items; `browser.visibility`). A design without
//! it shows everything, as it always did; a reader that ignores it loses only the hiding.
//! Sketch face anchors can name a flat face by its exact history name (a `lineage`
//! certificate: the target's `name`, e.g. a cut wall `F5:side:l1` or a pattern copy
//! `F4:top@F6.2`), where no construction certificate names it alone. Anchors with
//! construction certificates resolve as before.
use serde_json::Value;