Define a kernel-neutral native B-rep format and guarded adapter (#461)
A SolidCraft-owned portable B-rep schema (geometry schema 2 with analytic curves and surfaces, explicit frames and ranges, fitted intersection curves) and a versioned body envelope (SCBREP03) with its own metadata records, a bounded binary codec, a validator with resource budgets, and an adapter that refuses what it can't convert exactly. Pinned by a box-and-fillet golden fixture. Not yet written by application saves. Refs #303.
This commit is contained in:
1 parent
3159fc0dd0
commit
9f88537927
14 files changed
+3925
-1
No files matched your search
@@ -0,0 +1,369 @@
|
||||
//! The truck adapter. Persisted data belongs to solidcraft_geom::brep, never truck.
|
||||
use super::{Result, invalid};
|
||||
use mt::{BoundedCurve, EuclideanSpace, InnerSpace, Invertible, ParametricCurve, Transform, Transformed};
|
||||
use solidcraft_geom::brep as b;
|
||||
use truck_modeling as mt;
|
||||
use truck_topology::compress as t;
|
||||
|
||||
type Packed = t::CompressedSolid<mt::Point3, mt::Curve, mt::Surface>;
|
||||
#[path = "analytic.rs"]
|
||||
mod analytic;
|
||||
#[path = "fit.rs"]
|
||||
mod fit;
|
||||
use analytic::{arc, full_turn, load_analytic, revolved};
|
||||
use fit::fit_intersection;
|
||||
fn point(p: mt::Point3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
fn vector(p: mt::Vector3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
fn vector4(p: mt::Vector4) -> [f64; 4] {
|
||||
[p.x, p.y, p.z, p.w]
|
||||
}
|
||||
fn p([x, y, z]: [f64; 3]) -> mt::Point3 {
|
||||
mt::Point3::new(x, y, z)
|
||||
}
|
||||
fn v([x, y, z]: [f64; 3]) -> mt::Vector3 {
|
||||
mt::Vector3::new(x, y, z)
|
||||
}
|
||||
fn v4([x, y, z, w]: [f64; 4]) -> mt::Vector4 {
|
||||
mt::Vector4::new(x, y, z, w)
|
||||
}
|
||||
fn index(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).map_err(invalid)
|
||||
}
|
||||
fn depth(d: usize) -> Result<()> {
|
||||
if d > 16 { Err(invalid("geometry nesting")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
// Truck evaluates the full knot interval. The portable domain is the standard
|
||||
// active interval, so only coincident (clamped) domains can cross this adapter.
|
||||
fn clamped(knots: &[f64], count: usize) -> Result<()> {
|
||||
let degree = knots.len().checked_sub(count + 1).ok_or_else(|| invalid("spline degree"))?;
|
||||
if knots.first() != knots.get(degree) || knots.last() != knots.get(count) {
|
||||
return Err(invalid("unclamped spline domain unsupported by this adapter"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compatible_curve(c: &b::Curve) -> Result<()> {
|
||||
match c {
|
||||
b::Curve::Spline { knots, points } => clamped(knots, points.len()),
|
||||
b::Curve::RationalSpline { knots, homogeneous } => clamped(knots, homogeneous.len()),
|
||||
b::Curve::Intersection { knots, points, .. } => clamped(knots, points.len()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
fn compatible_surface(s: &b::Surface) -> Result<()> {
|
||||
match s {
|
||||
b::Surface::Plane { origin, u_axis, v_axis } => {
|
||||
let plane = mt::Plane::new(p(*origin), p(*origin) + v(*u_axis), p(*origin) + v(*v_axis));
|
||||
if vector(plane.u_axis()) != *u_axis || vector(plane.v_axis()) != *v_axis {
|
||||
return Err(invalid("plane axes cannot be reconstructed exactly"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
b::Surface::Spline { u_knots, v_knots, points } => {
|
||||
clamped(u_knots, points.len())?;
|
||||
clamped(v_knots, points.first().map_or(0, Vec::len))
|
||||
}
|
||||
b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => {
|
||||
clamped(u_knots, homogeneous.len())?;
|
||||
clamped(v_knots, homogeneous.first().map_or(0, Vec::len))
|
||||
}
|
||||
b::Surface::Revolution { profile, angle, .. } => {
|
||||
full_turn(angle)?;
|
||||
compatible_curve(profile)
|
||||
}
|
||||
b::Surface::Cylinder { angle, .. } | b::Surface::Cone { angle, .. } => full_turn(angle),
|
||||
b::Surface::Sphere { longitude, .. } | b::Surface::Torus { longitude, .. } => full_turn(longitude),
|
||||
}
|
||||
}
|
||||
fn compatible(body: &b::Brep) -> Result<()> {
|
||||
for shell in &body.shells {
|
||||
for edge in &shell.edges {
|
||||
compatible_curve(&edge.curve)?;
|
||||
}
|
||||
for face in &shell.faces {
|
||||
compatible_surface(&face.surface)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn encode(solid: &mt::Solid) -> Result<b::Brep> {
|
||||
let mut shells = Vec::new();
|
||||
let mut fitting_budget = 4096usize;
|
||||
for shell in solid.compress().boundaries {
|
||||
let vertices = shell.vertices.into_iter().map(point).collect();
|
||||
let edges = shell
|
||||
.edges
|
||||
.into_iter()
|
||||
.map(|e| Ok(b::Edge { vertices: [index(e.vertices.0)?, index(e.vertices.1)?], curve: save_curve(&e.curve, 0, &mut fitting_budget)? }))
|
||||
.collect::<Result<_>>()?;
|
||||
let faces = shell
|
||||
.faces
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let (surface, normal_flip) = save_surface(&f.surface, 0, &mut fitting_budget)?;
|
||||
Ok(b::Face {
|
||||
loops: f
|
||||
.boundaries
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let mut uses = w
|
||||
.into_iter()
|
||||
.map(|e| Ok(b::EdgeUse { edge: index(e.index)?, forward: e.orientation }))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// Wire direction is explicit; Face.forward changes only the normal.
|
||||
if !f.orientation {
|
||||
reverse_wire(&mut uses);
|
||||
}
|
||||
Ok(uses)
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
surface,
|
||||
forward: f.orientation != normal_flip,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
shells.push(b::Shell { vertices, edges, faces });
|
||||
}
|
||||
let result = b::Brep { version: b::VERSION, shells };
|
||||
result.validate().map_err(invalid)?;
|
||||
compatible(&result)?;
|
||||
Ok(result)
|
||||
}
|
||||
pub(super) fn decode(body: b::Brep) -> Result<mt::Solid> {
|
||||
body.validate().map_err(invalid)?;
|
||||
compatible(&body)?;
|
||||
if body.shells.iter().flat_map(|s| &s.edges).any(|e| e.vertices[0] == e.vertices[1]) {
|
||||
return Err(invalid("closed single-vertex edges are unsupported by this adapter"));
|
||||
}
|
||||
let packed = Packed {
|
||||
boundaries: body
|
||||
.shells
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
Ok(t::CompressedShell {
|
||||
vertices: s.vertices.into_iter().map(p).collect(),
|
||||
edges: s
|
||||
.edges
|
||||
.into_iter()
|
||||
.map(|e| Ok(t::CompressedEdge { vertices: (e.vertices[0] as usize, e.vertices[1] as usize), curve: load_curve(e.curve)? }))
|
||||
.collect::<Result<_>>()?,
|
||||
faces: s
|
||||
.faces
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let mut loops = f.loops;
|
||||
if !f.forward {
|
||||
for wire in &mut loops {
|
||||
reverse_wire(wire);
|
||||
}
|
||||
}
|
||||
Ok(t::CompressedFace {
|
||||
boundaries: loops
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
w.into_iter().map(|e| t::CompressedEdgeIndex { index: e.edge as usize, orientation: e.forward }).collect()
|
||||
})
|
||||
.collect(),
|
||||
orientation: f.forward,
|
||||
surface: load_surface(f.surface)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
};
|
||||
let shells = packed.boundaries.into_iter().map(mt::Shell::extract).collect::<std::result::Result<Vec<_>, _>>().map_err(invalid)?;
|
||||
if shells.iter().any(|s| s.shell_condition() == mt::ShellCondition::Irregular) {
|
||||
return Err(invalid("non-manifold shell"));
|
||||
}
|
||||
Ok(mt::Solid::new_unchecked(shells))
|
||||
}
|
||||
fn reverse_wire(wire: &mut [b::EdgeUse]) {
|
||||
wire.reverse();
|
||||
for edge in wire {
|
||||
edge.forward = !edge.forward;
|
||||
}
|
||||
}
|
||||
fn save_curve(c: &mt::Curve, d: usize, fitting_budget: &mut usize) -> Result<b::Curve> {
|
||||
depth(d)?;
|
||||
Ok(match c {
|
||||
mt::Curve::Line(c) => b::Curve::Line { start: point(c.0), end: point(c.1) },
|
||||
mt::Curve::BSplineCurve(c) => {
|
||||
b::Curve::Spline { knots: c.knot_vec().iter().copied().collect(), points: c.control_points().iter().copied().map(point).collect() }
|
||||
}
|
||||
mt::Curve::NurbsCurve(c) => b::Curve::RationalSpline {
|
||||
knots: c.knot_vec().iter().copied().collect(),
|
||||
homogeneous: c.control_points().iter().copied().map(vector4).collect(),
|
||||
},
|
||||
mt::Curve::IntersectionCurve(_) => fit_intersection(c, fitting_budget)?,
|
||||
})
|
||||
}
|
||||
fn save_surface(s: &mt::Surface, d: usize, fitting_budget: &mut usize) -> Result<(b::Surface, bool)> {
|
||||
depth(d)?;
|
||||
let mut normal_flip = false;
|
||||
let saved = match s {
|
||||
mt::Surface::Plane(s) => b::Surface::Plane { origin: point(s.origin()), u_axis: vector(s.u_axis()), v_axis: vector(s.v_axis()) },
|
||||
mt::Surface::BSplineSurface(s) => b::Surface::Spline {
|
||||
u_knots: s.uknot_vec().iter().copied().collect(),
|
||||
v_knots: s.vknot_vec().iter().copied().collect(),
|
||||
points: s.control_points().iter().map(|r| r.iter().copied().map(point).collect()).collect(),
|
||||
},
|
||||
mt::Surface::NurbsSurface(s) => b::Surface::RationalSpline {
|
||||
u_knots: s.uknot_vec().iter().copied().collect(),
|
||||
v_knots: s.vknot_vec().iter().copied().collect(),
|
||||
homogeneous: s.control_points().iter().map(|r| r.iter().copied().map(vector4).collect()).collect(),
|
||||
},
|
||||
mt::Surface::RevolutedCurve(s) => {
|
||||
let m = s.transform();
|
||||
let (x, y, z) = (m.x.truncate(), m.y.truncate(), m.z.truncate());
|
||||
let scale = x.magnitude();
|
||||
let tolerance = scale * 1e-10;
|
||||
if !scale.is_finite()
|
||||
|| scale <= 1e-12
|
||||
|| (y.magnitude() - scale).abs() > tolerance
|
||||
|| (z.magnitude() - scale).abs() > tolerance
|
||||
|| x.dot(y).abs() > scale * tolerance
|
||||
|| x.dot(z).abs() > scale * tolerance
|
||||
|| y.dot(z).abs() > scale * tolerance
|
||||
|| m.x.w != 0.
|
||||
|| m.y.w != 0.
|
||||
|| m.z.w != 0.
|
||||
|| m.w.w != 1.
|
||||
{
|
||||
return Err(invalid("non-similarity revolution transform needs an exact spline conversion"));
|
||||
}
|
||||
// Bake a similarity into the geometry, never into persisted processor data.
|
||||
// An improper rotation reverses the oriented axis in the rotation identity.
|
||||
let sign = if x.dot(y.cross(z)) < 0. { -1. } else { 1. };
|
||||
let axis = m.transform_vector(s.entity().axis()).normalize() * sign;
|
||||
let profile = s.entity().entity_curve().transformed(*m);
|
||||
normal_flip = !s.orientation();
|
||||
b::Surface::Revolution {
|
||||
profile: Box::new(save_curve(&profile, d + 1, fitting_budget)?),
|
||||
origin: point(m.transform_point(s.entity().origin())),
|
||||
axis: vector(axis),
|
||||
angle: [0., std::f64::consts::TAU],
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok((saved, normal_flip))
|
||||
}
|
||||
fn load_curve(c: b::Curve) -> Result<mt::Curve> {
|
||||
Ok(match c {
|
||||
b::Curve::Line { start, end } => mt::Curve::Line(mt::Line(p(start), p(end))),
|
||||
b::Curve::CircleArc { frame, radius, angle } => arc(&frame, [radius, radius], angle)?,
|
||||
b::Curve::EllipseArc { frame, radii, angle } => arc(&frame, radii, angle)?,
|
||||
b::Curve::Spline { knots, points } => {
|
||||
mt::Curve::BSplineCurve(mt::BSplineCurve::try_new(knots.into(), points.into_iter().map(p).collect()).map_err(invalid)?)
|
||||
}
|
||||
b::Curve::RationalSpline { knots, homogeneous } => mt::Curve::NurbsCurve(mt::NurbsCurve::new(
|
||||
mt::BSplineCurve::try_new(knots.into(), homogeneous.into_iter().map(v4).collect()).map_err(invalid)?,
|
||||
)),
|
||||
b::Curve::Polyline { .. } => return Err(invalid("standalone polyline unsupported by this adapter")),
|
||||
b::Curve::Intersection { knots, points, .. } => {
|
||||
mt::Curve::BSplineCurve(mt::BSplineCurve::try_new(knots.into(), points.into_iter().map(p).collect()).map_err(invalid)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
fn load_surface(s: b::Surface) -> Result<mt::Surface> {
|
||||
Ok(match s {
|
||||
b::Surface::Plane { origin, u_axis, v_axis } => mt::Surface::Plane(mt::Plane::new(p(origin), p(origin) + v(u_axis), p(origin) + v(v_axis))),
|
||||
b::Surface::Spline { u_knots, v_knots, points } => mt::Surface::BSplineSurface(
|
||||
mt::BSplineSurface::try_new((u_knots.into(), v_knots.into()), points.into_iter().map(|r| r.into_iter().map(p).collect()).collect())
|
||||
.map_err(invalid)?,
|
||||
),
|
||||
b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => mt::Surface::NurbsSurface(mt::NurbsSurface::new(
|
||||
mt::BSplineSurface::try_new((u_knots.into(), v_knots.into()), homogeneous.into_iter().map(|r| r.into_iter().map(v4).collect()).collect())
|
||||
.map_err(invalid)?,
|
||||
)),
|
||||
b::Surface::Revolution { profile, origin, axis, angle } => {
|
||||
full_turn(&angle)?;
|
||||
revolved(load_curve(*profile)?, p(origin), v(axis), false)
|
||||
}
|
||||
analytic => load_analytic(analytic)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn refuses_domain_or_axis_changes_instead_of_approximating() {
|
||||
let curve = b::Curve::Spline { knots: vec![0., 1., 2., 3.], points: vec![[0.; 3], [1.; 3]] };
|
||||
assert!(compatible_curve(&curve).is_err());
|
||||
let plane = b::Surface::Plane { origin: [1e12; 3], u_axis: [1e-6, 0., 0.], v_axis: [0., 1., 0.] };
|
||||
assert!(compatible_surface(&plane).is_err());
|
||||
assert!(clamped(&[0., 0., 1., 1.], 2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytic_records_decode_as_their_mathematical_loci_and_normals() {
|
||||
use mt::{MetricSpace, ParametricSurface, ParametricSurface3D};
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
let frame = b::Frame { origin: [1., 2., 3.], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] };
|
||||
let circle = load_curve(b::Curve::CircleArc { frame: frame.clone(), radius: 2., angle: [0., FRAC_PI_2] }).unwrap();
|
||||
assert!(circle.subs(0.).distance(p([3., 2., 3.])) < 1e-12);
|
||||
assert!(circle.subs(1.).distance(p([1., 4., 3.])) < 1e-12);
|
||||
let ellipse = load_curve(b::Curve::EllipseArc { frame: frame.clone(), radii: [2., 3.], angle: [0., FRAC_PI_2] }).unwrap();
|
||||
assert!(ellipse.subs(1.).distance(p([1., 5., 3.])) < 1e-12);
|
||||
let cases = [
|
||||
(b::Surface::Cylinder { frame: frame.clone(), radius: 2., angle: [0., TAU], height: [-1., 3.] }, 0.5, [3., 2., 4.], [1., 0., 0.]),
|
||||
(
|
||||
b::Surface::Cone { frame: frame.clone(), radius_at_origin: 2., half_angle: 0.2, angle: [0., TAU], height: [0., 3.] },
|
||||
0.5,
|
||||
[3. + 1.5 * 0.2f64.tan(), 2., 4.5],
|
||||
[1., 0., -0.2f64.tan()],
|
||||
),
|
||||
(
|
||||
b::Surface::Sphere { frame: frame.clone(), radius: 2., longitude: [0., TAU], latitude: [-FRAC_PI_2, FRAC_PI_2] },
|
||||
0.5,
|
||||
[3., 2., 3.],
|
||||
[1., 0., 0.],
|
||||
),
|
||||
(
|
||||
b::Surface::Torus { frame, major_radius: 4., minor_radius: 1., longitude: [0., TAU], meridian: [0., TAU] },
|
||||
0.,
|
||||
[6., 2., 3.],
|
||||
[1., 0., 0.],
|
||||
),
|
||||
];
|
||||
for (record, parameter, expected, normal) in cases {
|
||||
let surface = load_surface(record).unwrap();
|
||||
assert!(surface.subs(0., parameter).distance(p(expected)) < 1e-10);
|
||||
assert!(surface.normal(0., parameter).normalize().dot(v(normal).normalize()) > 1. - 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_fit_is_spatial_bounded_and_independent_of_surface_solvers() {
|
||||
use mt::MetricSpace;
|
||||
use solidcraft_geom::Vec3;
|
||||
let cyl = crate::cylinder(Vec3::ZERO, Vec3::Z, 5., 40.).unwrap();
|
||||
let block = crate::box_solid(Vec3::new(-20., -20., 20.), Vec3::new(20., 20., 60.)).unwrap();
|
||||
let block = crate::transform(&block, Vec3::ZERO, Vec3::new(0., 0., 20.), Vec3::X, 0.4).unwrap();
|
||||
let cut = crate::boolean(&cyl, &block, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let curve = cut.solid.edge_iter().map(|e| e.curve()).find(|c| matches!(c, mt::Curve::IntersectionCurve(_))).unwrap();
|
||||
assert!(save_curve(&curve, 0, &mut 0).is_err());
|
||||
let record = save_curve(&curve, 0, &mut 4096).unwrap();
|
||||
let encoded = serde_json::to_string(&record).unwrap();
|
||||
assert!(!encoded.contains("leader") && !encoded.contains("surfaces"));
|
||||
let b::Curve::Intersection { fitting_tolerance, .. } = &record else { panic!("explicit fitted record required") };
|
||||
let tolerance = *fitting_tolerance;
|
||||
let restored = load_curve(record).unwrap();
|
||||
let (a, b) = curve.range_tuple();
|
||||
for i in 0..=512 {
|
||||
let t = a + (b - a) * i as f64 / 512.;
|
||||
assert!(curve.subs(t).distance(restored.subs(t)) <= tolerance * 1.1, "sample {i}: error {}", curve.subs(t).distance(restored.subs(t)));
|
||||
}
|
||||
let decoded = crate::decode_native_body(&crate::encode_native_body(&cut).unwrap()).unwrap();
|
||||
let original = crate::measure(&cut).unwrap().volume;
|
||||
assert!((crate::measure(&decoded).unwrap().volume - original).abs() < original * 1e-5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Analytic wire records are mathematical loci, independent of backend parameterizations.
|
||||
use super::*;
|
||||
|
||||
pub(super) fn full_turn(angle: &[f64; 2]) -> Result<()> {
|
||||
if (angle[1] - angle[0] - std::f64::consts::TAU).abs() > 1e-12 {
|
||||
Err(invalid("partial angular surface domains need another adapter representation"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact rational conic locus. The adapter reparameterizes angle by rational spans.
|
||||
pub(super) fn arc(frame: &b::Frame, radii: [f64; 2], angle: [f64; 2]) -> Result<mt::Curve> {
|
||||
let span = angle[1] - angle[0];
|
||||
let count = (span / std::f64::consts::FRAC_PI_2).ceil().clamp(1., 4.) as usize;
|
||||
let step = span / count as f64;
|
||||
let weight = (step / 2.).cos();
|
||||
let at = |t: f64, scale: f64| p(frame.origin) + v(frame.x_axis) * (radii[0] * t.cos() / scale) + v(frame.y_axis) * (radii[1] * t.sin() / scale);
|
||||
let mut controls = vec![at(angle[0], 1.).to_homogeneous()];
|
||||
let mut knots = vec![0.; 3];
|
||||
for i in 0..count {
|
||||
let start = angle[0] + i as f64 * step;
|
||||
controls.push(at(start + step / 2., weight).to_homogeneous() * weight);
|
||||
controls.push(at(start + step, 1.).to_homogeneous());
|
||||
knots.extend(std::iter::repeat_n((i + 1) as f64 / count as f64, if i + 1 == count { 3 } else { 2 }));
|
||||
}
|
||||
Ok(mt::Curve::NurbsCurve(mt::NurbsCurve::new(mt::BSplineCurve::try_new(knots.into(), controls).map_err(invalid)?)))
|
||||
}
|
||||
|
||||
pub(super) fn revolved(profile: mt::Curve, origin: mt::Point3, axis: mt::Vector3, normal_flip: bool) -> mt::Surface {
|
||||
let mut s = mt::Processor::new(mt::RevolutedCurve::by_revolution(profile, origin, axis));
|
||||
// Internal parameter swapping is allowed; no such flag is persisted in a surface record.
|
||||
if normal_flip {
|
||||
s.invert();
|
||||
}
|
||||
mt::Surface::RevolutedCurve(s)
|
||||
}
|
||||
|
||||
pub(super) fn load_analytic(surface: b::Surface) -> Result<mt::Surface> {
|
||||
let axis = |f: &b::Frame| v(f.x_axis).cross(v(f.y_axis));
|
||||
Ok(match surface {
|
||||
b::Surface::Cylinder { frame, radius, angle, height } => {
|
||||
full_turn(&angle)?;
|
||||
let z = axis(&frame);
|
||||
let at = |h| p(frame.origin) + v(frame.x_axis) * radius + z * h;
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[0]), at(height[1]))), p(frame.origin), z, true)
|
||||
}
|
||||
b::Surface::Cone { frame, radius_at_origin, half_angle, angle, height } => {
|
||||
full_turn(&angle)?;
|
||||
let z = axis(&frame);
|
||||
let at = |h| p(frame.origin) + v(frame.x_axis) * (radius_at_origin + h * half_angle.tan()) + z * h;
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[0]), at(height[1]))), p(frame.origin), z, true)
|
||||
}
|
||||
b::Surface::Sphere { frame, radius, longitude, latitude } => {
|
||||
full_turn(&longitude)?;
|
||||
let z = axis(&frame);
|
||||
let meridian = b::Frame { origin: frame.origin, x_axis: frame.x_axis, y_axis: vector(z) };
|
||||
revolved(arc(&meridian, [radius; 2], latitude)?, p(frame.origin), z, true)
|
||||
}
|
||||
b::Surface::Torus { frame, major_radius, minor_radius, longitude, meridian } => {
|
||||
full_turn(&longitude)?;
|
||||
let z = axis(&frame);
|
||||
let profile = b::Frame { origin: point(p(frame.origin) + v(frame.x_axis) * major_radius), x_axis: frame.x_axis, y_axis: vector(z) };
|
||||
revolved(arc(&profile, [minor_radius; 2], meridian)?, p(frame.origin), z, true)
|
||||
}
|
||||
_ => return Err(invalid("expected analytic surface")),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Bounded tagged binary values: integer and f64 bits stay exact, no base64/text numbers.
|
||||
use super::*;
|
||||
fn bytes<'a>(input: &mut &'a [u8], n: usize) -> Result<&'a [u8]> {
|
||||
let head = input.get(..n).ok_or_else(|| invalid("truncated value"))?;
|
||||
*input = input.get(n..).ok_or_else(|| invalid("truncated value"))?;
|
||||
Ok(head)
|
||||
}
|
||||
fn length(input: &mut &[u8]) -> Result<usize> {
|
||||
let raw: [u8; 4] = bytes(input, 4)?.try_into().map_err(invalid)?;
|
||||
usize::try_from(u32::from_le_bytes(raw)).map_err(invalid)
|
||||
}
|
||||
fn string(input: &mut &[u8]) -> Result<String> {
|
||||
let n = length(input)?;
|
||||
if n > 1 << 20 {
|
||||
return Err(invalid("string budget"));
|
||||
}
|
||||
Ok(std::str::from_utf8(bytes(input, n)?).map_err(invalid)?.to_owned())
|
||||
}
|
||||
pub(super) fn read(input: &mut &[u8], depth: usize, budget: &mut usize) -> Result<Value> {
|
||||
if depth > 48 || *budget == 0 {
|
||||
return Err(invalid("value budget/depth"));
|
||||
}
|
||||
*budget -= 1;
|
||||
let tag = *bytes(input, 1)?.first().ok_or_else(|| invalid("tag"))?;
|
||||
Ok(match tag {
|
||||
0 => Value::Null,
|
||||
1 => Value::Bool(false),
|
||||
2 => Value::Bool(true),
|
||||
3..=5 => {
|
||||
let raw: [u8; 8] = bytes(input, 8)?.try_into().map_err(invalid)?;
|
||||
Value::Number(match tag {
|
||||
3 => Number::from(i64::from_le_bytes(raw)),
|
||||
4 => Number::from(u64::from_le_bytes(raw)),
|
||||
_ => Number::from_f64(f64::from_le_bytes(raw)).ok_or_else(|| invalid("non-finite number"))?,
|
||||
})
|
||||
}
|
||||
6 => Value::String(string(input)?),
|
||||
7 | 8 => {
|
||||
let n = length(input)?;
|
||||
if n > MAX_ITEMS || n > input.len() || n > *budget {
|
||||
return Err(invalid("collection budget"));
|
||||
}
|
||||
if tag == 7 {
|
||||
let mut a = Vec::new();
|
||||
for _ in 0..n {
|
||||
a.push(read(input, depth + 1, budget)?);
|
||||
}
|
||||
Value::Array(a)
|
||||
} else {
|
||||
let mut map = Map::new();
|
||||
for _ in 0..n {
|
||||
let key = string(input)?;
|
||||
if map.contains_key(&key) {
|
||||
return Err(invalid("duplicate key"));
|
||||
}
|
||||
map.insert(key, read(input, depth + 1, budget)?);
|
||||
}
|
||||
Value::Object(map)
|
||||
}
|
||||
}
|
||||
_ => return Err(invalid("unknown binary tag")),
|
||||
})
|
||||
}
|
||||
fn count(n: usize, out: &mut Vec<u8>) -> Result<()> {
|
||||
out.extend_from_slice(&u32::try_from(n).map_err(invalid)?.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
fn text(s: &str, out: &mut Vec<u8>) -> Result<()> {
|
||||
count(s.len(), out)?;
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn write(value: &Value, out: &mut Vec<u8>, depth: usize) -> Result<()> {
|
||||
if depth > 48 || out.len() > MAX_BYTES {
|
||||
return Err(invalid("encoding budget"));
|
||||
}
|
||||
match value {
|
||||
Value::Null => out.push(0),
|
||||
Value::Bool(b) => out.push(if *b { 2 } else { 1 }),
|
||||
Value::Number(n) => {
|
||||
if n.is_f64() {
|
||||
out.push(5);
|
||||
out.extend_from_slice(&n.as_f64().ok_or_else(|| invalid("number"))?.to_le_bytes());
|
||||
} else if let Some(n) = n.as_u64() {
|
||||
out.push(4);
|
||||
out.extend_from_slice(&n.to_le_bytes());
|
||||
} else {
|
||||
out.push(3);
|
||||
out.extend_from_slice(&n.as_i64().ok_or_else(|| invalid("integer"))?.to_le_bytes());
|
||||
}
|
||||
}
|
||||
Value::String(s) => {
|
||||
out.push(6);
|
||||
text(s, out)?;
|
||||
}
|
||||
Value::Array(a) => {
|
||||
out.push(7);
|
||||
count(a.len(), out)?;
|
||||
for v in a {
|
||||
write(v, out, depth + 1)?;
|
||||
}
|
||||
}
|
||||
Value::Object(map) => {
|
||||
out.push(8);
|
||||
count(map.len(), out)?;
|
||||
for (k, v) in map {
|
||||
text(k, out)?;
|
||||
write(v, out, depth + 1)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.len() > MAX_BYTES {
|
||||
return Err(invalid("encoding size"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Bounded export of procedural intersections as ordinary spatial cubic splines.
|
||||
//! The stored tolerance is the fitting target; the result is always provisional.
|
||||
use super::*;
|
||||
|
||||
fn point_at(c: &mt::Curve, t: f64, budget: &mut usize) -> Result<mt::Point3> {
|
||||
*budget = budget.checked_sub(1).ok_or_else(|| invalid("intersection fitting budget"))?;
|
||||
let p = c.subs(t);
|
||||
if !p.x.is_finite() || !p.y.is_finite() || !p.z.is_finite() {
|
||||
return Err(invalid("intersection sample"));
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
pub(super) fn fit_intersection(c: &mt::Curve, budget: &mut usize) -> Result<b::Curve> {
|
||||
let tolerance = 1e-4;
|
||||
let (start, end) = c.range_tuple();
|
||||
if !start.is_finite() || !end.is_finite() || start >= end {
|
||||
return Err(invalid("intersection range"));
|
||||
}
|
||||
// Fit each smooth parameter interval using spatial samples, without backend derivative conventions.
|
||||
// Only the resulting spatial spline crosses the persisted format boundary.
|
||||
let mt::Curve::IntersectionCurve(intersection) = c else {
|
||||
return Err(invalid("expected intersection"));
|
||||
};
|
||||
let mut breaks = vec![start, end];
|
||||
match intersection.leader() {
|
||||
mt::Leader::Polyline(points) => {
|
||||
if points.len() > 4096 {
|
||||
return Err(invalid("intersection parameter budget"));
|
||||
}
|
||||
breaks.extend((0..points.len()).map(|i| i as f64).filter(|t| *t > start && *t < end));
|
||||
}
|
||||
mt::Leader::BSpline(curve) => {
|
||||
if curve.knot_vec().len() > 4096 {
|
||||
return Err(invalid("intersection parameter budget"));
|
||||
}
|
||||
breaks.extend(curve.knot_vec().iter().copied().filter(|t| *t > start && *t < end));
|
||||
}
|
||||
}
|
||||
breaks.sort_by(f64::total_cmp);
|
||||
breaks.dedup();
|
||||
let mut stack = Vec::new();
|
||||
for pair in breaks.windows(2).rev() {
|
||||
let (a, b) = (pair[0], pair[1]);
|
||||
stack.push((a, b, point_at(c, a, budget)?, point_at(c, b, budget)?, 0u8));
|
||||
}
|
||||
let mut pieces = Vec::new();
|
||||
while let Some((a, b, pa, pb, depth)) = stack.pop() {
|
||||
let p1 = point_at(c, a + (b - a) / 3., budget)?.to_vec();
|
||||
let p2 = point_at(c, a + (b - a) * 2. / 3., budget)?.to_vec();
|
||||
let x = p1 * 27. - pa.to_vec() * 8. - pb.to_vec();
|
||||
let y = p2 * 27. - pa.to_vec() - pb.to_vec() * 8.;
|
||||
let controls = [pa, mt::Point3::from_vec((x * 2. - y) / 18.), mt::Point3::from_vec((y * 2. - x) / 18.), pb];
|
||||
let mut fits = true;
|
||||
for fraction in [0.125, 0.25, 0.5, 0.75, 0.875] {
|
||||
let actual = point_at(c, a + (b - a) * fraction, budget)?;
|
||||
let u = 1. - fraction;
|
||||
let fitted = controls[0].to_vec() * u * u * u
|
||||
+ controls[1].to_vec() * 3. * u * u * fraction
|
||||
+ controls[2].to_vec() * 3. * u * fraction * fraction
|
||||
+ controls[3].to_vec() * fraction * fraction * fraction;
|
||||
if (actual.to_vec() - fitted).magnitude() > tolerance * 0.1 {
|
||||
fits = false;
|
||||
}
|
||||
}
|
||||
if fits {
|
||||
pieces.push((a, b, controls));
|
||||
} else {
|
||||
if depth >= 12 || pieces.len() + stack.len() >= 1023 {
|
||||
return Err(invalid("intersection fit did not converge"));
|
||||
}
|
||||
let middle = a + (b - a) / 2.;
|
||||
let pm = point_at(c, middle, budget)?;
|
||||
stack.push((middle, b, pm, pb, depth + 1));
|
||||
stack.push((a, middle, pa, pm, depth + 1));
|
||||
}
|
||||
}
|
||||
let first = pieces.first().ok_or_else(|| invalid("empty intersection fit"))?;
|
||||
let mut points = vec![point(first.2[0])];
|
||||
let mut knots = vec![start; 4];
|
||||
for (i, (_, b, controls)) in pieces.iter().enumerate() {
|
||||
points.extend(controls.iter().skip(1).copied().map(point));
|
||||
knots.extend(std::iter::repeat_n(*b, if i + 1 == pieces.len() { 4 } else { 3 }));
|
||||
}
|
||||
Ok(b::Curve::Intersection { knots, points, fitting_tolerance: tolerance, faces: None })
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,349 @@
|
||||
//! Explicit conversions between runtime metadata and the versioned wire records.
|
||||
use super::{Result, invalid};
|
||||
use crate::{Body, FaceSource, history as h};
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3, brep::envelope as w};
|
||||
|
||||
fn index(n: usize) -> Result<u32> {
|
||||
u32::try_from(n).map_err(invalid)
|
||||
}
|
||||
fn point(p: Vec3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
fn point2(p: Vec2) -> [f64; 2] {
|
||||
[p.x, p.y]
|
||||
}
|
||||
fn p([x, y, z]: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(x, y, z)
|
||||
}
|
||||
fn p2([x, y]: [f64; 2]) -> Vec2 {
|
||||
Vec2::new(x, y)
|
||||
}
|
||||
fn plane(p: Plane) -> w::Plane {
|
||||
w::Plane { origin: point(p.origin), x_axis: point(p.x), y_axis: point(p.y) }
|
||||
}
|
||||
fn load_plane(v: w::Plane) -> Plane {
|
||||
Plane { origin: p(v.origin), x: p(v.x_axis), y: p(v.y_axis) }
|
||||
}
|
||||
fn segment(s: &Seg2) -> Result<w::Segment> {
|
||||
Ok(match *s {
|
||||
Seg2::Line { a, b } => w::Segment::Line { start: point2(a), end: point2(b) },
|
||||
Seg2::Arc { center, radius, start, sweep } => w::Segment::Arc { center: point2(center), radius, start, sweep },
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => w::Segment::Cubic { points: [p0, p1, p2, p3].map(point2) },
|
||||
Seg2::Conic { a, apex, b, w } => w::Segment::Conic { points: [a, apex, b].map(point2), middle_weight: w },
|
||||
Seg2::Bezier { n, p } => {
|
||||
let count = usize::from(n) + 1;
|
||||
if !(5..=8).contains(&count) || p.get(count..).is_none_or(|unused| unused.iter().any(|p| *p != Vec2::ZERO)) {
|
||||
return Err(invalid("unsupported certificate Bezier layout"));
|
||||
}
|
||||
w::Segment::Bezier { points: p.get(..count).ok_or_else(|| invalid("Bezier count"))?.iter().copied().map(point2).collect() }
|
||||
}
|
||||
})
|
||||
}
|
||||
fn load_segment(s: w::Segment) -> Result<Seg2> {
|
||||
Ok(match s {
|
||||
w::Segment::Line { start, end } => Seg2::Line { a: p2(start), b: p2(end) },
|
||||
w::Segment::Arc { center, radius, start, sweep } => Seg2::Arc { center: p2(center), radius, start, sweep },
|
||||
w::Segment::Cubic { points: [a, b, c, d] } => Seg2::Cubic { p0: p2(a), p1: p2(b), p2: p2(c), p3: p2(d) },
|
||||
w::Segment::Conic { points: [a, apex, b], middle_weight } => Seg2::Conic { a: p2(a), apex: p2(apex), b: p2(b), w: middle_weight },
|
||||
w::Segment::Bezier { points } => {
|
||||
if !(5..=8).contains(&points.len()) {
|
||||
return Err(invalid("certificate Bezier degree"));
|
||||
}
|
||||
let mut p = [Vec2::ZERO; 8];
|
||||
let n = (points.len() - 1) as u8;
|
||||
for (slot, value) in p.iter_mut().zip(points) {
|
||||
*slot = p2(value);
|
||||
}
|
||||
Seg2::Bezier { n, p }
|
||||
}
|
||||
})
|
||||
}
|
||||
fn region(r: &Region2) -> Result<w::Region> {
|
||||
Ok(w::Region {
|
||||
outer: r.outer.segs.iter().map(segment).collect::<Result<_>>()?,
|
||||
holes: r.holes.iter().map(|l| l.segs.iter().map(segment).collect()).collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
fn load_region(r: w::Region) -> Result<Region2> {
|
||||
let loop_ = |s: Vec<w::Segment>| Ok(Loop2 { segs: s.into_iter().map(load_segment).collect::<Result<_>>()? });
|
||||
Ok(Region2 { outer: loop_(r.outer)?, holes: r.holes.into_iter().map(loop_).collect::<Result<_>>()? })
|
||||
}
|
||||
fn source(s: &FaceSource, depth: usize) -> Result<w::FaceSource> {
|
||||
if depth > 32 {
|
||||
return Err(invalid("face-source nesting"));
|
||||
}
|
||||
Ok(match s {
|
||||
FaceSource::ExtrudeSide { segment: s, plane: p, coalesced } => {
|
||||
w::FaceSource::ExtrusionSide { segment: segment(s)?, plane: p.map(plane), coalesced: *coalesced }
|
||||
}
|
||||
FaceSource::CertifiedExtrudeSide { producer, sketch, curve, plane: p } => {
|
||||
w::FaceSource::CertifiedExtrusionSide { producer: *producer, sketch: *sketch, curve: curve.clone(), plane: p.map(plane) }
|
||||
}
|
||||
FaceSource::ExtrudeCap { high, region: r, plane: p } => w::FaceSource::ExtrusionCap { high: *high, region: region(r)?, plane: plane(*p) },
|
||||
FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, plane: p } => w::FaceSource::CertifiedExtrusionCap {
|
||||
producer: *producer,
|
||||
sketch: *sketch,
|
||||
high: *high,
|
||||
outer: outer.clone(),
|
||||
holes: holes.clone(),
|
||||
plane: plane(*p),
|
||||
},
|
||||
FaceSource::CylinderCap { producer, high, plane: p } => w::FaceSource::CylinderCap { producer: *producer, high: *high, plane: plane(*p) },
|
||||
FaceSource::Box { producer, axis, high, plane: p } => {
|
||||
w::FaceSource::BoxFace { producer: *producer, axis: *axis, high: *high, plane: plane(*p) }
|
||||
}
|
||||
FaceSource::ShellPlanar { producer, source: s, inner, plane: p } => {
|
||||
w::FaceSource::PlanarShell { producer: *producer, source: Box::new(source(s, depth + 1)?), inner: *inner, plane: plane(*p) }
|
||||
}
|
||||
FaceSource::Imported { item, face, payload_sha256, producer } => {
|
||||
w::FaceSource::Imported { item: *item, face: *face, payload_sha256: payload_sha256.clone(), producer: *producer }
|
||||
}
|
||||
})
|
||||
}
|
||||
fn load_source(s: w::FaceSource, depth: usize) -> Result<FaceSource> {
|
||||
if depth > 32 {
|
||||
return Err(invalid("face-source nesting"));
|
||||
}
|
||||
Ok(match s {
|
||||
w::FaceSource::ExtrusionSide { segment, plane, coalesced } => {
|
||||
FaceSource::ExtrudeSide { segment: load_segment(segment)?, plane: plane.map(load_plane), coalesced }
|
||||
}
|
||||
w::FaceSource::CertifiedExtrusionSide { producer, sketch, curve, plane } => {
|
||||
FaceSource::CertifiedExtrudeSide { producer, sketch, curve, plane: plane.map(load_plane) }
|
||||
}
|
||||
w::FaceSource::ExtrusionCap { high, region, plane } => {
|
||||
FaceSource::ExtrudeCap { high, region: load_region(region)?, plane: load_plane(plane) }
|
||||
}
|
||||
w::FaceSource::CertifiedExtrusionCap { producer, sketch, high, outer, holes, plane } => {
|
||||
FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, plane: load_plane(plane) }
|
||||
}
|
||||
w::FaceSource::CylinderCap { producer, high, plane } => FaceSource::CylinderCap { producer, high, plane: load_plane(plane) },
|
||||
w::FaceSource::BoxFace { producer, axis, high, plane } => FaceSource::Box { producer, axis, high, plane: load_plane(plane) },
|
||||
w::FaceSource::PlanarShell { producer, source, inner, plane } => {
|
||||
FaceSource::ShellPlanar { producer, source: Box::new(load_source(*source, depth + 1)?), inner, plane: load_plane(plane) }
|
||||
}
|
||||
w::FaceSource::Imported { item, face, payload_sha256, producer } => FaceSource::Imported { item, face, payload_sha256, producer },
|
||||
})
|
||||
}
|
||||
fn origin(o: h::FaceOrigin) -> Result<w::FaceOrigin> {
|
||||
Ok(match o {
|
||||
h::FaceOrigin::Face { operand, face } => w::FaceOrigin::Face { operand: index(operand)?, face: index(face)? },
|
||||
h::FaceOrigin::Profile { segment } => w::FaceOrigin::Profile { segment: index(segment)? },
|
||||
h::FaceOrigin::Cap { end } => w::FaceOrigin::Cap { end },
|
||||
h::FaceOrigin::Edge { operand, edge } => w::FaceOrigin::Edge { operand: index(operand)?, edge: index(edge)? },
|
||||
h::FaceOrigin::Offset { operand, face } => w::FaceOrigin::Offset { operand: index(operand)?, face: index(face)? },
|
||||
})
|
||||
}
|
||||
fn load_origin(o: w::FaceOrigin) -> h::FaceOrigin {
|
||||
match o {
|
||||
w::FaceOrigin::Face { operand, face } => h::FaceOrigin::Face { operand: operand as usize, face: face as usize },
|
||||
w::FaceOrigin::Profile { segment } => h::FaceOrigin::Profile { segment: segment as usize },
|
||||
w::FaceOrigin::Cap { end } => h::FaceOrigin::Cap { end },
|
||||
w::FaceOrigin::Edge { operand, edge } => h::FaceOrigin::Edge { operand: operand as usize, edge: edge as usize },
|
||||
w::FaceOrigin::Offset { operand, face } => h::FaceOrigin::Offset { operand: operand as usize, face: face as usize },
|
||||
}
|
||||
}
|
||||
fn change(c: h::Change) -> w::Change {
|
||||
match c {
|
||||
h::Change::Kept => w::Change::Kept,
|
||||
h::Change::Modified => w::Change::Modified,
|
||||
h::Change::Split => w::Change::Split,
|
||||
h::Change::Merged => w::Change::Merged,
|
||||
h::Change::Generated => w::Change::Generated,
|
||||
}
|
||||
}
|
||||
fn load_change(c: w::Change) -> h::Change {
|
||||
match c {
|
||||
w::Change::Kept => h::Change::Kept,
|
||||
w::Change::Modified => h::Change::Modified,
|
||||
w::Change::Split => h::Change::Split,
|
||||
w::Change::Merged => h::Change::Merged,
|
||||
w::Change::Generated => h::Change::Generated,
|
||||
}
|
||||
}
|
||||
fn edge_origin(o: &h::EdgeOrigin) -> Result<w::EdgeOrigin> {
|
||||
Ok(match o {
|
||||
h::EdgeOrigin::Edge { operand, edge } => w::EdgeOrigin::Edge { operand: index(*operand)?, edge: index(*edge)? },
|
||||
h::EdgeOrigin::Meet { a, b } => {
|
||||
let mut sides = [
|
||||
a.origins().iter().copied().map(origin).collect::<Result<Vec<_>>>()?,
|
||||
b.origins().iter().copied().map(origin).collect::<Result<Vec<_>>>()?,
|
||||
];
|
||||
for side in &mut sides {
|
||||
side.sort();
|
||||
side.dedup();
|
||||
}
|
||||
sides.sort();
|
||||
w::EdgeOrigin::Meeting { sides }
|
||||
}
|
||||
})
|
||||
}
|
||||
fn load_edge_origin(o: w::EdgeOrigin) -> Result<h::EdgeOrigin> {
|
||||
Ok(match o {
|
||||
w::EdgeOrigin::Edge { operand, edge } => h::EdgeOrigin::Edge { operand: operand as usize, edge: edge as usize },
|
||||
w::EdgeOrigin::Meeting { sides: [a, b] } => {
|
||||
if a > b || [&a, &b].iter().any(|s| s.is_empty() || s.windows(2).any(|pair| pair[0] >= pair[1])) {
|
||||
return Err(invalid("noncanonical wire history meeting"));
|
||||
}
|
||||
let side = |o: Vec<w::FaceOrigin>| h::Side::new(o.into_iter().map(load_origin)).ok_or_else(|| invalid("empty history side"));
|
||||
let (mut a, mut b) = (side(a)?, side(b)?);
|
||||
// The runtime may order its variants differently from the wire schema.
|
||||
if a > b {
|
||||
std::mem::swap(&mut a, &mut b);
|
||||
}
|
||||
h::EdgeOrigin::Meet { a, b }
|
||||
}
|
||||
})
|
||||
}
|
||||
fn history(v: &h::History) -> Result<w::History> {
|
||||
Ok(w::History {
|
||||
operands: index(v.operands)?,
|
||||
faces: v
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| Ok(w::FaceHistory { origins: f.origins.iter().copied().map(origin).collect::<Result<_>>()?, change: change(f.change) }))
|
||||
.collect::<Result<_>>()?,
|
||||
edges: v
|
||||
.edges
|
||||
.iter()
|
||||
.map(|e| {
|
||||
Ok(w::EdgeHistory {
|
||||
faces: e.faces.iter().copied().map(index).collect::<Result<_>>()?,
|
||||
origins: e.origins.iter().map(edge_origin).collect::<Result<_>>()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
fn load_history(v: w::History) -> Result<h::History> {
|
||||
Ok(h::History {
|
||||
operands: v.operands as usize,
|
||||
faces: v
|
||||
.faces
|
||||
.into_iter()
|
||||
.map(|f| h::FaceProv { origins: f.origins.into_iter().map(load_origin).collect(), change: load_change(f.change) })
|
||||
.collect(),
|
||||
edges: v
|
||||
.edges
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
Ok(h::EdgeProv {
|
||||
faces: e.faces.into_iter().map(|n| n as usize).collect(),
|
||||
origins: e.origins.into_iter().map(load_edge_origin).collect::<Result<_>>()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
pub(super) fn encode(body: &Body) -> Result<w::Metadata> {
|
||||
Ok(w::Metadata {
|
||||
version: w::VERSION,
|
||||
color: body.color,
|
||||
paint: body
|
||||
.paint
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
Ok(w::Paint {
|
||||
opacity: p.opacity,
|
||||
faces: p
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| Ok(w::FacePaint { face: index(f.face)?, color: f.color, opacity: f.opacity }))
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
boundaries: body.splits.as_ref().map(|s| s.iter().map(|line| line.iter().copied().map(point).collect()).collect()),
|
||||
face_sources: body
|
||||
.face_sources
|
||||
.as_ref()
|
||||
.map(|s| s.iter().map(|s| s.as_ref().map(|s| source(s, 0)).transpose()).collect::<Result<_>>())
|
||||
.transpose()?,
|
||||
history: body.history.as_ref().map(|h| history(h)).transpose()?,
|
||||
preserve_boundaries: body.regularization_protected,
|
||||
})
|
||||
}
|
||||
pub(super) fn apply(body: &mut Body, saved: w::Metadata) -> Result<()> {
|
||||
if saved.version != w::VERSION {
|
||||
return Err(invalid("metadata version"));
|
||||
}
|
||||
body.color = saved.color;
|
||||
body.paint = saved.paint.map(|p| {
|
||||
std::sync::Arc::new(crate::Paint {
|
||||
opacity: p.opacity,
|
||||
faces: p.faces.into_iter().map(|f| crate::FacePaint { face: f.face as usize, color: f.color, opacity: f.opacity }).collect(),
|
||||
})
|
||||
});
|
||||
body.splits = saved.boundaries.map(|s| std::sync::Arc::new(s.into_iter().map(|line| line.into_iter().map(p).collect()).collect()));
|
||||
body.face_sources = saved
|
||||
.face_sources
|
||||
.map(|s| s.into_iter().map(|s| s.map(|s| load_source(s, 0)).transpose()).collect::<Result<Vec<_>>>())
|
||||
.transpose()?
|
||||
.map(std::sync::Arc::new);
|
||||
body.history = saved.history.map(load_history).transpose()?.map(std::sync::Arc::new);
|
||||
body.regularization_protected = saved.preserve_boundaries;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn independent_certificate_records_preserve_all_variants() {
|
||||
let plane = w::Plane { origin: [1., 2., 3.], x_axis: [1., 0., 0.], y_axis: [0., 1., 0.] };
|
||||
let segments = vec![
|
||||
w::Segment::Line { start: [0., 0.], end: [1., 2.] },
|
||||
w::Segment::Arc { center: [1., 2.], radius: 3., start: 0.2, sweep: -0.7 },
|
||||
w::Segment::Cubic { points: [[0., 0.], [1., 2.], [3., 4.], [5., 6.]] },
|
||||
w::Segment::Conic { points: [[0., 0.], [1., 2.], [3., 4.]], middle_weight: 0.7 },
|
||||
w::Segment::Bezier { points: vec![[1., 2.]; 5] },
|
||||
w::Segment::Bezier { points: vec![[1., 2.]; 8] },
|
||||
];
|
||||
for value in &segments {
|
||||
assert_eq!(*value, segment(&load_segment(value.clone()).unwrap()).unwrap());
|
||||
}
|
||||
let region = w::Region { outer: segments.clone(), holes: vec![segments.clone()] };
|
||||
let mut sources: Vec<_> =
|
||||
segments.into_iter().map(|segment| w::FaceSource::ExtrusionSide { segment, plane: Some(plane.clone()), coalesced: true }).collect();
|
||||
sources.extend([
|
||||
w::FaceSource::CertifiedExtrusionSide { producer: 5, sketch: 7, curve: "curve-9".into(), plane: None },
|
||||
w::FaceSource::ExtrusionCap { high: true, region, plane: plane.clone() },
|
||||
w::FaceSource::CertifiedExtrusionCap {
|
||||
producer: 5,
|
||||
sketch: 7,
|
||||
high: false,
|
||||
outer: vec!["a".into()],
|
||||
holes: vec![vec!["b".into()]],
|
||||
plane: plane.clone(),
|
||||
},
|
||||
w::FaceSource::CylinderCap { producer: 8, high: true, plane: plane.clone() },
|
||||
w::FaceSource::BoxFace { producer: Some(5), axis: 2, high: true, plane: plane.clone() },
|
||||
w::FaceSource::Imported { item: 2, face: 3, payload_sha256: Some("a".repeat(64)), producer: Some(5) },
|
||||
]);
|
||||
sources.push(w::FaceSource::PlanarShell { producer: Some(6), source: Box::new(sources[0].clone()), inner: true, plane });
|
||||
for value in sources {
|
||||
assert_eq!(value, source(&load_source(value.clone(), 0).unwrap(), 0).unwrap());
|
||||
}
|
||||
assert!(load_segment(w::Segment::Bezier { points: vec![[0., 0.]; 9] }).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_wire_order_is_explicit_and_meetings_round_trip() {
|
||||
let origins = vec![
|
||||
w::FaceOrigin::Face { operand: 0, face: 2 },
|
||||
w::FaceOrigin::Profile { segment: 3 },
|
||||
w::FaceOrigin::Cap { end: true },
|
||||
w::FaceOrigin::Edge { operand: 1, edge: 4 },
|
||||
w::FaceOrigin::Offset { operand: 0, face: 5 },
|
||||
];
|
||||
for value in &origins {
|
||||
assert_eq!(*value, origin(load_origin(value.clone())).unwrap());
|
||||
}
|
||||
for value in [w::Change::Kept, w::Change::Modified, w::Change::Split, w::Change::Merged, w::Change::Generated] {
|
||||
assert_eq!(value, change(load_change(value)));
|
||||
}
|
||||
let value = w::EdgeOrigin::Meeting { sides: [origins.clone(), vec![w::FaceOrigin::Offset { operand: 1, face: 2 }]] };
|
||||
assert_eq!(value, edge_origin(&load_edge_origin(value.clone()).unwrap()).unwrap());
|
||||
assert!(load_edge_origin(w::EdgeOrigin::Meeting { sides: [vec![], origins] }).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user