Sketch on a cylinder primitive's caps; format 15 (#460)

* Sketch on a cylinder primitive's caps (#420)

A solid.cylinder's caps carried no construction certificate, so a sketch
on its top or bottom was refused unless fixed:true (#420's crossed
cylinders, built from primitives).

- kernel: FaceSource::CylinderCap { producer, high, plane };
  Body::with_cylinder_certificate turns a cylinder's two extrude caps into
  certified caps. kernel::cylinder's output is unchanged for its other
  callers.
- doc: FaceCertificate::CylinderCap { high }, named F<id>:top / F<id>:bottom
  as the cylinder's face names already are; New Body and Join cylinders
  are certified, shells carry it as their planar source.
- format 15: designs can store the new certificate kind; version 14
  designs need no change, and older builds ask for an update instead of
  meeting an unknown kind.

Tests: top and bottom attachments follow a height and base edit and a
reopen; crossed joined cylinders keep both caps attachable; a shelled
cylinder's floor keeps its cap identity; malformed certificate JSON is
refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

* Format 15 also covers sketch_axis links (#448)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Irakli KourtisandClaude Opus 5.5 authored and GitHub committed 2026-10-12 21:32:07 -04:00
1 parent b24e478bc1
commit 49956c2797
10 files changed
+247 -23

No files matched your search

+80 -10
View File
@@ -42,13 +42,54 @@ pub struct Body {
#[derive(Clone, Debug, PartialEq)]
pub enum FaceSource {
ExtrudeSide { segment: solidcraft_geom::Seg2, plane: Option<solidcraft_geom::Plane>, coalesced: bool },
CertifiedExtrudeSide { producer: u64, sketch: u64, curve: String, plane: Option<solidcraft_geom::Plane> },
ExtrudeCap { high: bool, region: solidcraft_geom::Region2, plane: solidcraft_geom::Plane },
CertifiedExtrudeCap { producer: u64, sketch: u64, high: bool, outer: Vec<String>, holes: Vec<Vec<String>>, plane: solidcraft_geom::Plane },
Box { producer: Option<u64>, axis: u8, high: bool, plane: solidcraft_geom::Plane },
ShellPlanar { producer: Option<u64>, source: Box<FaceSource>, inner: bool, plane: solidcraft_geom::Plane },
Imported { item: u64, face: u64, payload_sha256: Option<String>, producer: Option<u64> },
ExtrudeSide {
segment: solidcraft_geom::Seg2,
plane: Option<solidcraft_geom::Plane>,
coalesced: bool,
},
CertifiedExtrudeSide {
producer: u64,
sketch: u64,
curve: String,
plane: Option<solidcraft_geom::Plane>,
},
ExtrudeCap {
high: bool,
region: solidcraft_geom::Region2,
plane: solidcraft_geom::Plane,
},
CertifiedExtrudeCap {
producer: u64,
sketch: u64,
high: bool,
outer: Vec<String>,
holes: Vec<Vec<String>>,
plane: solidcraft_geom::Plane,
},
Box {
producer: Option<u64>,
axis: u8,
high: bool,
plane: solidcraft_geom::Plane,
},
/// A cylinder primitive's cap: `high` at the far end (its `top`), else at the base.
CylinderCap {
producer: u64,
high: bool,
plane: solidcraft_geom::Plane,
},
ShellPlanar {
producer: Option<u64>,
source: Box<FaceSource>,
inner: bool,
plane: solidcraft_geom::Plane,
},
Imported {
item: u64,
face: u64,
payload_sha256: Option<String>,
producer: Option<u64>,
},
}
pub type CapSignature = (Vec<String>, Vec<Vec<String>>);
@@ -58,9 +99,11 @@ impl FaceSource {
fn plane_mut(&mut self) -> Option<&mut solidcraft_geom::Plane> {
match self {
Self::ExtrudeSide { plane, .. } | Self::CertifiedExtrudeSide { plane, .. } => plane.as_mut(),
Self::ExtrudeCap { plane, .. } | Self::CertifiedExtrudeCap { plane, .. } | Self::Box { plane, .. } | Self::ShellPlanar { plane, .. } => {
Some(plane)
}
Self::ExtrudeCap { plane, .. }
| Self::CertifiedExtrudeCap { plane, .. }
| Self::Box { plane, .. }
| Self::CylinderCap { plane, .. }
| Self::ShellPlanar { plane, .. } => Some(plane),
Self::Imported { .. } => None,
}
}
@@ -504,6 +547,7 @@ impl Body {
FaceSource::ExtrudeCap { plane, .. }
| FaceSource::CertifiedExtrudeCap { plane, .. }
| FaceSource::Box { plane, .. }
| FaceSource::CylinderCap { plane, .. }
| FaceSource::ShellPlanar { plane, .. } => Some(*plane),
FaceSource::Imported { .. } => self.planar_face(index),
}
@@ -529,6 +573,32 @@ impl Body {
.flatten()
}
/// Certify the two caps of a body built by [`crate::cylinder`]. Its wall stays uncertified.
pub fn with_cylinder_certificate(mut self, producer: u64) -> Result<Self> {
if producer == 0 {
return Err(KernelError::Invalid("invalid cylinder producer".into()));
}
let Some(sources) = &self.face_sources else {
return Err(KernelError::Failed("cylinder construction source".into()));
};
let mut sources = sources.as_ref().clone();
let mut ends = [false; 2];
for source in sources.iter_mut() {
if let Some(FaceSource::ExtrudeCap { high, plane, .. }) = source {
let end = ends.get_mut(usize::from(*high)).ok_or_else(|| KernelError::Failed("cylinder cap role".into()))?;
if std::mem::replace(end, true) {
return Err(KernelError::Failed("duplicate cylinder cap role".into()));
}
*source = Some(FaceSource::CylinderCap { producer, high: *high, plane: *plane });
}
}
if ends != [true; 2] {
return Err(KernelError::Failed("cylinder cap role count".into()));
}
self.face_sources = Some(std::sync::Arc::new(sources));
Ok(self)
}
pub fn with_box_certificate(mut self, producer: u64) -> Result<Self> {
if producer == 0 {
return Err(KernelError::Invalid("invalid box producer".into()));
+3 -1
View File
@@ -30,6 +30,7 @@ fn base_plane(source: &FaceSource, actual: &Plane) -> Option<Plane> {
FaceSource::CertifiedExtrudeSide { plane: Some(p), .. }
| FaceSource::CertifiedExtrudeCap { plane: p, .. }
| FaceSource::Box { producer: Some(_), plane: p, .. }
| FaceSource::CylinderCap { plane: p, .. }
| FaceSource::ShellPlanar { producer: Some(_), plane: p, .. } => Some(*p),
FaceSource::Imported { producer: Some(_), payload_sha256: Some(_), .. } => Some(*actual),
_ => None,
@@ -41,6 +42,7 @@ pub(super) fn certified_plane(source: &FaceSource) -> Option<Plane> {
FaceSource::CertifiedExtrudeSide { plane: Some(p), .. }
| FaceSource::CertifiedExtrudeCap { plane: p, .. }
| FaceSource::Box { producer: Some(_), plane: p, .. }
| FaceSource::CylinderCap { plane: p, .. }
| FaceSource::ShellPlanar { producer: Some(_), plane: p, .. } => Some(*p),
_ => None,
}
@@ -93,7 +95,7 @@ pub(super) fn descriptor_cost(source: &FaceSource) -> Option<usize> {
}
cost = cost.checked_add(count.checked_mul(128)?)?;
}
FaceSource::ExtrudeSide { .. } | FaceSource::Imported { .. } | FaceSource::Box { .. } => {}
FaceSource::ExtrudeSide { .. } | FaceSource::Imported { .. } | FaceSource::Box { .. } | FaceSource::CylinderCap { .. } => {}
_ => return None,
}
Some(cost)