diff --git a/crates/doc/src/config_certificates.rs b/crates/doc/src/config_certificates.rs index 66cc709b..21b27a33 100644 --- a/crates/doc/src/config_certificates.rs +++ b/crates/doc/src/config_certificates.rs @@ -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, holes: Vec> }, - ExtrudeSide { sketch: u64, curve: String }, - Imported { payload_sha256: String, item: u64, face: u64 }, - ShellPlanar { source_producer: u64, source: Box, 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, + holes: Vec>, + }, + ExtrudeSide { + sketch: u64, + curve: String, + }, + Imported { + payload_sha256: String, + item: u64, + face: u64, + }, + ShellPlanar { + source_producer: u64, + source: Box, + 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; diff --git a/crates/doc/src/config_styles.rs b/crates/doc/src/config_styles.rs index 2c6c9cde..e02b9b53 100644 --- a/crates/doc/src/config_styles.rs +++ b/crates/doc/src/config_styles.rs @@ -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 { diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index c5ae6388..88362813 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -3643,6 +3643,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, 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 } => { diff --git a/crates/doc/src/format.rs b/crates/doc/src/format.rs index 0816837f..54dc0e24 100644 --- a/crates/doc/src/format.rs +++ b/crates/doc/src/format.rs @@ -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 { /// 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 diff --git a/crates/doc/src/project.rs b/crates/doc/src/project.rs index 51da7fda..1c57f105 100644 --- a/crates/doc/src/project.rs +++ b/crates/doc/src/project.rs @@ -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 { .. } ) diff --git a/crates/engine/src/cmd/face_tests.rs b/crates/engine/src/cmd/face_tests.rs index 2c7c001f..ed698845 100644 --- a/crates/engine/src/cmd/face_tests.rs +++ b/crates/engine/src/cmd/face_tests.rs @@ -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 { + 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) + ); +} diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 11a23ef4..38861a51 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -42,13 +42,54 @@ pub struct Body { #[derive(Clone, Debug, PartialEq)] pub enum FaceSource { - ExtrudeSide { segment: solidcraft_geom::Seg2, plane: Option, coalesced: bool }, - CertifiedExtrudeSide { producer: u64, sketch: u64, curve: String, plane: Option }, - ExtrudeCap { high: bool, region: solidcraft_geom::Region2, plane: solidcraft_geom::Plane }, - CertifiedExtrudeCap { producer: u64, sketch: u64, high: bool, outer: Vec, holes: Vec>, plane: solidcraft_geom::Plane }, - Box { producer: Option, axis: u8, high: bool, plane: solidcraft_geom::Plane }, - ShellPlanar { producer: Option, source: Box, inner: bool, plane: solidcraft_geom::Plane }, - Imported { item: u64, face: u64, payload_sha256: Option, producer: Option }, + ExtrudeSide { + segment: solidcraft_geom::Seg2, + plane: Option, + coalesced: bool, + }, + CertifiedExtrudeSide { + producer: u64, + sketch: u64, + curve: String, + plane: Option, + }, + ExtrudeCap { + high: bool, + region: solidcraft_geom::Region2, + plane: solidcraft_geom::Plane, + }, + CertifiedExtrudeCap { + producer: u64, + sketch: u64, + high: bool, + outer: Vec, + holes: Vec>, + plane: solidcraft_geom::Plane, + }, + Box { + producer: Option, + 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, + source: Box, + inner: bool, + plane: solidcraft_geom::Plane, + }, + Imported { + item: u64, + face: u64, + payload_sha256: Option, + producer: Option, + }, } pub type CapSignature = (Vec, Vec>); @@ -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 { + 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 { if producer == 0 { return Err(KernelError::Invalid("invalid box producer".into())); diff --git a/crates/kernel/src/planar_sources.rs b/crates/kernel/src/planar_sources.rs index 3ab4b3fa..e99816e1 100644 --- a/crates/kernel/src/planar_sources.rs +++ b/crates/kernel/src/planar_sources.rs @@ -30,6 +30,7 @@ fn base_plane(source: &FaceSource, actual: &Plane) -> Option { 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 { 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 { } 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) diff --git a/docs/book/src/guide/files.md b/docs/book/src/guide/files.md index 6238dde5..af7fefac 100644 --- a/docs/book/src/guide/files.md +++ b/docs/book/src/guide/files.md @@ -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 diff --git a/docs/design-container.md b/docs/design-container.md index 8b7e26f3..2dd9d9a4 100644 --- a/docs/design-container.md +++ b/docs/design-container.md @@ -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.