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:
1 parent
b24e478bc1
commit
49956c2797
10 files changed
+247
-23
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 } => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { .. }
|
||||
)
|
||||
|
||||
@@ -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
@@ -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()));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user