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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

* Format 15 also covers sketch_axis links (#448)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
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

+76 -6
View File
@@ -151,11 +151,34 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solidcraft_k
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum FaceCertificate {
Box { axis: u8, high: bool },
ExtrudeCap { sketch: u64, high: bool, outer: Vec<String>, holes: Vec<Vec<String>> },
ExtrudeSide { sketch: u64, curve: String },
Imported { payload_sha256: String, item: u64, face: u64 },
ShellPlanar { source_producer: u64, source: Box<FaceCertificate>, inner: bool },
Box {
axis: u8,
high: bool,
},
/// A cylinder primitive's cap: `high` is its `top` (the far end), else its `bottom`.
CylinderCap {
high: bool,
},
ExtrudeCap {
sketch: u64,
high: bool,
outer: Vec<String>,
holes: Vec<Vec<String>>,
},
ExtrudeSide {
sketch: u64,
curve: String,
},
Imported {
payload_sha256: String,
item: u64,
face: u64,
},
ShellPlanar {
source_producer: u64,
source: Box<FaceCertificate>,
inner: bool,
},
}
impl FaceCertificate {
@@ -177,6 +200,7 @@ impl FaceCertificate {
pub fn validate(&self) -> Result<()> {
let valid = match self {
Self::Box { axis, .. } => *axis <= 2,
Self::CylinderCap { .. } => true,
Self::ExtrudeCap { sketch, outer, holes, .. } => *sketch != 0 && valid_signature(outer, holes),
Self::ExtrudeSide { sketch, curve } => *sketch != 0 && !curve.is_empty() && curve.len() <= 128,
Self::Imported { payload_sha256, item, face } => {
@@ -206,6 +230,11 @@ pub(crate) fn validate_certificate_json(value: &serde_json::Value) -> Result<()>
&& fields.get("axis").and_then(serde_json::Value::as_u64).is_some_and(|axis| axis <= 2)
&& fields.get("high").and_then(serde_json::Value::as_bool).is_some()
}
Some("cylinder_cap") => {
fields.len() == 2
&& fields.keys().all(|key| ["kind", "high"].contains(&key.as_str()))
&& fields.get("high").and_then(serde_json::Value::as_bool).is_some()
}
Some("extrude_cap") => {
fields.len() == 5
&& fields.keys().all(|key| ["kind", "sketch", "high", "outer", "holes"].contains(&key.as_str()))
@@ -252,6 +281,7 @@ pub(crate) fn shell_certificate_role(source: &FaceCertificate, inner: bool) -> R
let axis = ["x", "y", "z"].get(*axis as usize).ok_or_else(|| DocError::Invalid("invalid shell source axis".into()))?;
format!("box:{}{axis}", if *high { "+" } else { "-" })
}
FaceCertificate::CylinderCap { high } => format!("cylinder:{}", cylinder_cap_role(*high)),
FaceCertificate::ExtrudeSide { curve, .. } => format!("side:{curve}"),
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}"),
@@ -260,10 +290,16 @@ pub(crate) fn shell_certificate_role(source: &FaceCertificate, inner: bool) -> R
Ok(format!("shell:{}:{role}", if inner { "inner" } else { "outer" }))
}
/// A cylinder cap's role, as the cylinder's face names call it.
pub(crate) fn cylinder_cap_role(high: bool) -> &'static str {
if high { "top" } else { "bottom" }
}
pub(crate) fn flat_source_certificate(source: &solidcraft_kernel::FaceSource) -> Option<(u64, FaceCertificate)> {
use solidcraft_kernel::FaceSource;
match source {
FaceSource::Box { producer: Some(producer), axis, high, .. } => Some((*producer, FaceCertificate::Box { axis: *axis, high: *high })),
FaceSource::CylinderCap { producer, high, .. } => Some((*producer, FaceCertificate::CylinderCap { high: *high })),
FaceSource::CertifiedExtrudeSide { producer, sketch, curve, .. } => {
Some((*producer, FaceCertificate::ExtrudeSide { sketch: *sketch, curve: curve.clone() }))
}
@@ -283,6 +319,9 @@ pub(crate) fn flat_source_matches(certificate: &FaceCertificate, source: &solidc
(FaceCertificate::Box { axis, high }, FaceSource::Box { producer: Some(current), axis: current_axis, high: current_high, .. }) => {
producer == *current && axis == current_axis && high == current_high
}
(FaceCertificate::CylinderCap { high }, FaceSource::CylinderCap { producer: current, high: current_high, .. }) => {
producer == *current && high == current_high
}
(
FaceCertificate::ExtrudeSide { sketch, curve },
FaceSource::CertifiedExtrudeSide { producer: current, sketch: current_sketch, curve: current_curve, .. },
@@ -411,7 +450,8 @@ impl Document {
| Some(solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, .. })
| Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { producer, .. })
| Some(solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), .. }) => *producer,
| Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::CylinderCap { producer, .. }) => *producer,
_ => name
.strip_prefix('F')
.and_then(|name| name.split_once(':'))
@@ -481,6 +521,11 @@ impl Document {
target.name = format!("F{producer}:{}{axis_name}", if *high { "+" } else { "-" });
target.certificate = Some(FaceCertificate::Box { axis: *axis, high: *high });
}
Some(solidcraft_kernel::FaceSource::CylinderCap { producer, high, .. }) => {
target.origin_feature = *producer;
target.name = format!("F{producer}:{}", cylinder_cap_role(*high));
target.certificate = Some(FaceCertificate::CylinderCap { high: *high });
}
Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, .. }) => {
target.origin_feature = *producer;
target.name = format!("F{producer}:cap:{}:{}", if *high { "high" } else { "low" }, cap_key(outer, holes));
@@ -512,6 +557,31 @@ mod tests {
SolvedSketch { feature: 1, name: "sources".into(), plane: solidcraft_geom::Plane::XY, sketch, report, profiles: Vec::new() }
}
#[test]
fn cylinder_cap_certificate_json_is_exact() {
for high in [true, false] {
let valid = json!({"kind":"cylinder_cap","high":high});
assert!(validate_certificate_json(&valid).is_ok());
let certificate: FaceCertificate = serde_json::from_value(valid.clone()).unwrap();
assert_eq!(certificate, FaceCertificate::CylinderCap { high });
assert_eq!(serde_json::to_value(&certificate).unwrap(), valid);
}
let shelled = json!({"kind":"shell_planar","source_producer":3,"source":{"kind":"cylinder_cap","high":false},"inner":true});
assert!(validate_certificate_json(&shelled).is_ok());
let certificate: FaceCertificate = serde_json::from_value(shelled).unwrap();
assert_eq!(shell_certificate_role(&FaceCertificate::CylinderCap { high: false }, true).unwrap(), "shell:inner:cylinder:bottom");
assert!(certificate.validate().is_ok());
for bad in [
json!({"kind":"cylinder_cap"}),
json!({"kind":"cylinder_cap","high":1}),
json!({"kind":"cylinder_cap","high":null}),
json!({"kind":"cylinder_cap","high":true,"axis":2}),
json!({"kind":"cylinder_cap","high":true,"extra":[[[]]]}),
] {
assert!(validate_certificate_json(&bad).is_err(), "{bad}");
}
}
#[test]
fn prepared_sources_keep_exact_identity_and_reject_ambiguity() {
use solidcraft_geom::Vec2;
+12 -1
View File
@@ -109,6 +109,7 @@ impl Document {
let Some(feature) = self.feature(producer) else { return false };
match (certificate, &feature.kind) {
(FaceCertificate::Box { .. }, FeatureKind::Box { operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => true,
(FaceCertificate::CylinderCap { .. }, FeatureKind::Cylinder { operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => true,
(FaceCertificate::ExtrudeCap { sketch, .. }, FeatureKind::Extrude { sketch: current, extent, operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => sketch == current && extent.taper.is_none(),
(FaceCertificate::ExtrudeSide { sketch, curve }, FeatureKind::Extrude { sketch: current, extent, operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => {
sketch == current && extent.taper.is_none()
@@ -132,6 +133,9 @@ impl Document {
(super::FaceCertificate::Box { axis, high }, FeatureKind::Box { operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => {
["x", "y", "z"].get(*axis as usize).is_some_and(|axis| role == format!("{}{axis}", if *high { "+" } else { "-" }))
}
(super::FaceCertificate::CylinderCap { high }, FeatureKind::Cylinder { operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => {
role == super::cylinder_cap_role(*high)
}
(super::FaceCertificate::ExtrudeCap { sketch, high, outer, holes }, FeatureKind::Extrude { sketch: producer_sketch, extent, operation: crate::Operation::NewBody | crate::Operation::Join, .. }) => {
sketch == producer_sketch && extent.taper.is_none()
&& role == format!("cap:{}:{}", if *high { "high" } else { "low" }, super::cap_key(outer, holes))
@@ -192,7 +196,10 @@ impl Document {
}
return Ok(indices);
}
if matches!(target.certificate, Some(super::FaceCertificate::Box { .. } | super::FaceCertificate::ExtrudeCap { .. })) {
if matches!(
target.certificate,
Some(super::FaceCertificate::Box { .. } | super::FaceCertificate::CylinderCap { .. } | super::FaceCertificate::ExtrudeCap { .. })
) {
let indices: Vec<_> = body
.body
.face_sources()
@@ -205,6 +212,10 @@ impl Document {
Some(super::FaceCertificate::Box { axis, high }),
Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), axis: current_axis, high: current_high, .. }),
) => *producer == target.origin_feature && axis == current_axis && high == current_high,
(
Some(super::FaceCertificate::CylinderCap { high }),
Some(solidcraft_kernel::FaceSource::CylinderCap { producer, high: current_high, .. }),
) => *producer == target.origin_feature && high == current_high,
(
Some(super::FaceCertificate::ExtrudeCap { sketch, high, outer, holes }),
Some(solidcraft_kernel::FaceSource::CertifiedExtrudeCap {
+1
View File
@@ -3643,6 +3643,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
let b = kernel::cylinder(point(vals, base)?, *axis, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?)?;
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
apply_traced(st, f, vec![crate::lineage::cylinder_tool(b)], *operation, &[])
}
FeatureKind::Sphere { center, radius, operation } => {
+7 -2
View File
@@ -46,13 +46,17 @@
//! legacy mark (`?F2:top#2`) and are found again as the build that saved them found them.
//! Pieces a feature splits a body into are named after their faces; features saved before
//! keep the kernel's shell order (`shell_piece_order`), which their stored body names mean.
//! - 15: sketch face anchors and face bindings can name a cylinder primitive's cap
//! (`cylinder_cap`), and sketches can link lines to their own axes (`sketch_axis`). Version 14
//! designs need no transformation; the version stamp lets older readers request an update
//! instead of encountering an unknown certificate or link kind.
use serde_json::Value;
use crate::{DocError, Document, Result};
/// The format this build writes.
pub const FORMAT: u32 = 14;
pub const FORMAT: u32 = 15;
pub fn format_string(n: u32) -> String {
format!("solidcraft/{n}")
@@ -69,7 +73,7 @@ pub fn version_of(s: &str) -> Option<u32> {
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
type JsonStep = fn(&mut Value) -> Result<()>;
const JSON_STEPS: [JsonStep; 13] = [
const JSON_STEPS: [JsonStep; 14] = [
|_| Ok(()),
v2_to_v3,
v3_to_v4,
@@ -83,6 +87,7 @@ const JSON_STEPS: [JsonStep; 13] = [
|_| Ok(()),
|_| Ok(()),
v13_to_v14,
|_| Ok(()),
];
/// Stored face and edge names were worked out from geometry before format 14; they get the
+1
View File
@@ -324,6 +324,7 @@ fn certified_planar_construction(body: &crate::ModelBody, index: usize) -> bool
Some(
solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(_), .. }
| solidcraft_kernel::FaceSource::Box { producer: Some(_), .. }
| solidcraft_kernel::FaceSource::CylinderCap { .. }
| solidcraft_kernel::FaceSource::CertifiedExtrudeSide { plane: Some(_), .. }
| solidcraft_kernel::FaceSource::CertifiedExtrudeCap { .. }
)
+63
View File
@@ -317,3 +317,66 @@ fn join_covering_a_whole_attached_face_loses_the_attachment_without_moving_it()
assert!(!failed(&s, top));
assert!((s.model.state().sketch(top).unwrap().plane.origin.z - 10.).abs() < 1e-9);
}
fn certificate_of(s: &Session, sketch: u64) -> Option<solidcraft_doc::config::FaceCertificate> {
let FeatureKind::Sketch { plane: PlaneRef::Face { anchor: Some(anchor), .. }, .. } = &s.doc.feature(sketch).unwrap().kind else {
return None;
};
anchor.target.certificate.clone()
}
#[test]
fn cylinder_primitive_caps_take_attachments_that_follow_edits_and_reopen() {
use solidcraft_doc::config::FaceCertificate;
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius":10,"height":40,"name":"Post"}));
let top = run(&mut s, "sketch.create", json!({"plane":{"face":[2,1,40]}}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.finish", json!({}));
let bottom = run(&mut s, "sketch.create", json!({"plane":{"face":[2,1,0]}}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.finish", json!({}));
assert_eq!(certificate_of(&s, top), Some(FaceCertificate::CylinderCap { high: true }));
assert_eq!(certificate_of(&s, bottom), Some(FaceCertificate::CylinderCap { high: false }));
let heights = |s: &Session| {
let state = s.model.state();
(state.sketch(top).unwrap().plane.origin.z, state.sketch(bottom).unwrap().plane.origin.z)
};
run(&mut s, "timeline.edit", json!({"feature":"Post","set":{"height":"55","base":[0,0,5]}}));
s.refresh();
assert!(s.model.results.iter().all(|result| result.error.is_none()), "{:?}", s.model.results);
let (high, low) = heights(&s);
assert!((high - 60.).abs() < 1e-9 && (low - 5.).abs() < 1e-9, "{high} {low}");
let mut reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
reopened.refresh();
assert!(reopened.model.results.iter().all(|result| result.error.is_none()), "{:?}", reopened.model.results);
assert_eq!(heights(&reopened), (high, low));
}
#[test]
fn crossed_cylinder_primitives_keep_their_caps_attachable() {
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius":10,"height":40}));
run(&mut s, "solid.cylinder", json!({"radius":5,"height":40,"base":[-20,0,20],"axis":"X","operation":"join"}));
assert_eq!(s.model.state().bodies.len(), 1);
for face in [[0., 0., 40.], [20., 0., 20.]] {
let sketch = run(&mut s, "sketch.create", json!({"plane":{"face":face}}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.finish", json!({}));
assert!(certificate_of(&s, sketch).is_some(), "{face:?}");
}
}
#[test]
fn shelled_cylinder_floor_keeps_its_cap_identity() {
use solidcraft_doc::config::FaceCertificate;
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius":10,"height":40}));
run(&mut s, "solid.shell", json!({"faces":[[2,1,40]],"thickness":2}));
let floor = run(&mut s, "sketch.create", json!({"plane":{"face":[2,1,2]}}))["sketch"].as_u64().unwrap();
assert!(
matches!(
certificate_of(&s, floor),
Some(FaceCertificate::ShellPlanar { source, inner: true, .. }) if *source == FaceCertificate::CylinderCap { high: false }
),
"{:?}",
certificate_of(&s, floor)
);
}
+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)
+3 -2
View File
@@ -51,8 +51,9 @@ keeps the current design. Previews cannot be edited or measured. This experiment
requires `preview: true` in an engine Save/Save As call; ordinary saves omit display caches.
The repository's `docs/design-container.md` describes limits and encoding details.
Current designs use format 14, including persistent names from exact face history,
exact planar shell-face anchors, persistent analytic offset boundary roles and
Current designs use format 15, including sketch face anchors on cylinder caps, sketch axis
links, persistent
names from exact face history, exact planar shell-face anchors, persistent analytic offset boundary roles and
association profile references, durable sketch face anchors, periodic fit splines and versioned
revolve conventions. It also retains recorded body transfers, explicit 3D curve coordinate
spaces and certified face bindings, alongside semantic configuration styles, associative
+1 -1
View File
@@ -7,7 +7,7 @@ self-contained input/output format. `io::write_design` keeps its JSON contract;
`io::write_design_container` is the fallible container writer.
Packages have `container.json` with `{"format":"solidcraft-container/1"}` and
`document.json` with the versioned parametric document (currently `solidcraft/14`).
`document.json` with the versioned parametric document (currently `solidcraft/15`).
The independent versions let document migrations evolve without changing blob encodings.
Existing serialized document fields are retained automatically, without a second copy
of the document schema or a deep clone of embedded payloads.