mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #2519 from tangency-labs/layer-record
solidcore-record: the B-rep record, its validation, the native body file and Vec/Plane as plain data (layer split stage 1)
This commit is contained in:
Generated
+9
@@ -205,6 +205,15 @@ dependencies = [
|
||||
"step-import",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore-record"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore-ssi"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/solidcore", "crates/solidcore-ffi", "crates/difftest", "crates/nurbs-grid-mesh", "crates/ssi", "crates/solidcore-ssi", "crates/step-brep", "crates/step-export", "crates/step-import", "crates/xtask"]
|
||||
members = ["crates/solidcore", "crates/solidcore-record", "crates/solidcore-ffi", "crates/difftest", "crates/nurbs-grid-mesh", "crates/ssi", "crates/solidcore-ssi", "crates/step-brep", "crates/step-export", "crates/step-import", "crates/xtask"]
|
||||
default-members = ["crates/solidcore"]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "solidcore-record"
|
||||
description = "Geometry as data: the portable B-rep record, its serde form, structural validation and the native body file. Computes no geometry."
|
||||
edition = "2024"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
# Let chains (edition 2024, Rust 1.88).
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/tangency-labs/solidcore"
|
||||
keywords = ["cad", "brep", "nurbs", "geometry", "serde"]
|
||||
categories = ["data-structures", "encoding", "science"]
|
||||
|
||||
# Layers §3: the record depends on no geometry crate, and on no maths crate (§4).
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,41 @@
|
||||
# solidcore-record computes no geometric quantity (docs/layers.md §1, §4). Clippy reads the
|
||||
# nearest clippy.toml, so this repeats the workspace's settings and adds the record's ban.
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
allow-panic-in-tests = true
|
||||
allow-indexing-slicing-in-tests = true
|
||||
doc-valid-idents = ["SMLib", "NLib", "NVIDIA", "SolidCraft", "OpenUSD", ".."]
|
||||
disallowed-methods = [
|
||||
{ path = "f64::sqrt", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::cbrt", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::hypot", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::sin", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::cos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::tan", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::sin_cos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::asin", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::acos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::atan", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::atan2", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::powi", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::powf", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::exp", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::ln", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f64::mul_add", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::sqrt", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::cbrt", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::hypot", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::sin", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::cos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::tan", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::sin_cos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::asin", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::acos", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::atan", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::atan2", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::powi", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::powf", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::exp", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::ln", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "f32::mul_add", reason = "the record computes no geometry (docs/layers.md §4)" },
|
||||
]
|
||||
@@ -0,0 +1,830 @@
|
||||
//! Portable B-rep data, independent of a modelling backend: SolidCraft's native B-rep record.
|
||||
//! Millimetres, radians, right-handed Z-up. See SolidCraft's docs/native-brep.md for semantics.
|
||||
//! Validation checks structure and resource budgets only: counts, indices, finite numbers, knot
|
||||
//! vectors, parameter ranges, topology and canonical order. It computes no geometry; the kernel
|
||||
//! checks that frames are orthonormal, planes and placements not degenerate, and that the
|
||||
//! geometry agrees with the topology. Neither certifies geometric validity or trust.
|
||||
//!
|
||||
//! The records follow SMLib's flat form (`SmBrepData`): one record per logical use, the mated
|
||||
//! halves a kernel keeps at run time left for it to rebuild. Names are STEP's B-rep names; each
|
||||
//! type says its SMLib counterpart and STEP entity. Curves and surfaces are unbounded: an edge's
|
||||
//! bounds are its `interval`, a face's its `domain` and loops.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod curve2;
|
||||
pub mod envelope;
|
||||
pub mod history;
|
||||
pub mod part;
|
||||
mod topology;
|
||||
pub use curve2::Curve2;
|
||||
|
||||
pub const VERSION: u32 = 4;
|
||||
/// The most items (records, points, knots, uses) [`Brep::validate`] takes in a record that comes
|
||||
/// from outside the kernel: a file, the control channel, pasted data. A record the kernel built
|
||||
/// ([`Brep::validate_built`]) has no such cap.
|
||||
pub const MAX_ITEMS: usize = 4_000_000;
|
||||
pub const MAX_SHELLS: usize = 1024;
|
||||
const MAX_DEPTH: usize = 16;
|
||||
/// The highest spline degree a record may carry.
|
||||
pub const MAX_SPLINE_DEGREE: usize = 16;
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
/// A body (SMLib `SmBrep`, STEP `MANIFOLD_SOLID_BREP`/`BREP_WITH_VOIDS`/`SHELL_BASED_SURFACE_MODEL`).
|
||||
/// Every list is in canonical order (docs/native-brep.md, "Canonical order"): a face's index
|
||||
/// here is its face index everywhere, and so for edges and vertices.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Brep {
|
||||
pub version: u32,
|
||||
/// Intersection tolerance, mm (SMLib's per-brep `XSectTol3d`).
|
||||
pub tolerance: f64,
|
||||
/// `regions[0]` is the infinite region, then the bounded regions.
|
||||
pub regions: Vec<Region>,
|
||||
pub shells: Vec<Shell>,
|
||||
pub faces: Vec<Face>,
|
||||
pub edges: Vec<Edge>,
|
||||
pub vertices: Vec<Vertex>,
|
||||
}
|
||||
|
||||
/// A connected volume of space (SMLib `SmRegionData`). The infinite region has no outer shell
|
||||
/// and is never a void. A bounded region's `shells[0]` is its outer shell, a closed face shell;
|
||||
/// the rest are inner shells. A closed shell separates two regions and is listed by both: as
|
||||
/// the outer shell of the one it encloses and as an inner shell of the one around it. An open
|
||||
/// shell, a wire shell or a vertex shell lies in one region and is listed by that one only.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Region {
|
||||
pub shells: Vec<u32>,
|
||||
/// A bounded region that is empty space (a void), not material. False for the infinite region.
|
||||
pub void: bool,
|
||||
}
|
||||
|
||||
/// A connected boundary component (SMLib `SmShellData`; STEP `CLOSED_SHELL`/`OPEN_SHELL`,
|
||||
/// `WIRE_SHELL`, `VERTEX_SHELL`). One record stands for both runtime sides of a shell.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Shell {
|
||||
/// Connected faces, with wire edges embedded in them (`wires`, edges no face uses).
|
||||
Faces {
|
||||
faces: Vec<OrientedFace>,
|
||||
wires: Vec<u32>,
|
||||
},
|
||||
/// A connected graph of wire edges and no faces.
|
||||
Wire { edges: Vec<u32> },
|
||||
/// One isolated point.
|
||||
Vertex { vertex: u32 },
|
||||
}
|
||||
|
||||
/// A face as one shell uses it (SMLib `SmFaceuseData`, STEP `ORIENTED_FACE`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OrientedFace {
|
||||
pub face: u32,
|
||||
/// The face's normal in this shell is the natural normal (∂S/∂u × ∂S/∂v) of the surface its
|
||||
/// (u, v) are on, [`Face::parameters`]: its NURBS form when it has one. In a closed shell the
|
||||
/// normals point out of the material.
|
||||
pub forward: bool,
|
||||
}
|
||||
|
||||
/// A bounded piece of a surface (SMLib `SmFaceData`, STEP `ADVANCED_FACE`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Face {
|
||||
/// What the face is: its surface as the producer names it (a cylinder, a NURBS…).
|
||||
pub surface: Surface,
|
||||
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
|
||||
/// parameters: a cylinder, cone, sphere, torus or placed revolution as SMLib holds it (every
|
||||
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters), and a
|
||||
/// ball patch's own net. When given, `domain` and every pcurve are in this surface's (u, v),
|
||||
/// and `surface` only says what it is; it is a `Spline` or `RationalSpline` and `surface` is a
|
||||
/// cylinder, cone, sphere, ball patch, torus or placed revolution
|
||||
/// (docs/native-brep.md, "Surfaces").
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nurbs: Option<Surface>,
|
||||
/// The modelled rectangle in surface parameters, `[[u0, u1], [v0, v1]]`, inside the
|
||||
/// surface's natural domain and around every loop.
|
||||
pub domain: [[f64; 2]; 2],
|
||||
/// One outer loop and any inner ones, in the producer's order (truck lists some holes
|
||||
/// first; SMLib's import puts the outer loop first, as SMLib requires). Loops are stored
|
||||
/// relative to the surface's natural normal, whatever side a shell uses: seen from that
|
||||
/// normal, the outer loop runs counter-clockwise and inner loops clockwise, which is what
|
||||
/// tells them apart. The first loop is an edge loop.
|
||||
pub loops: Vec<Loop>,
|
||||
}
|
||||
|
||||
/// A face boundary (SMLib `SmLoopData`; STEP `FACE_BOUND` of an `EDGE_LOOP` or `VERTEX_LOOP`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Loop {
|
||||
/// A closed chain of oriented edges, in traversal order (STEP `EDGE_LOOP`).
|
||||
Edges { edges: Vec<OrientedEdge> },
|
||||
/// A one-vertex loop: a pole or a collapsed boundary (STEP `VERTEX_LOOP`). Inner only.
|
||||
Vertex { vertex: u32 },
|
||||
}
|
||||
|
||||
/// One occurrence of an edge in a face's loop (SMLib `SmEUData`, one mated pair of runtime
|
||||
/// edgeuses; STEP `ORIENTED_EDGE` with its `PCURVE`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OrientedEdge {
|
||||
pub edge: u32,
|
||||
/// The loop runs along the edge from interval min (start vertex) to max (end vertex).
|
||||
pub forward: bool,
|
||||
/// The edge in this face's (u, v), on the edge's own interval and running min → max
|
||||
/// whatever `forward` says. A seam edge has two uses on one face, each with its pcurve.
|
||||
pub pcurve: Curve2,
|
||||
}
|
||||
|
||||
/// A bounded piece of a curve (SMLib `SmEdgeData`, STEP `EDGE_CURVE`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Edge {
|
||||
/// What the edge is: its curve as the producer names it (a line, a circle…).
|
||||
pub curve: Curve,
|
||||
/// The NURBS form the edge is parameterised on, when it differs from `curve`'s own
|
||||
/// parameter: a circle or ellipse as SMLib holds it (every SMLib conic is a B-spline curve,
|
||||
/// the trims on it in that NURBS's parameter). When given, `interval` and the pcurves' parameter
|
||||
/// are this curve's, and `curve` only says what it is; it is a `Spline` or `RationalSpline` and
|
||||
/// `curve` is a line, circle or ellipse (docs/native-brep.md, "Curves").
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nurbs: Option<Curve>,
|
||||
/// The modelled interval in the edge's parameter ([`Edge::parameters`]), min < max.
|
||||
pub interval: [f64; 2],
|
||||
/// `[start, end]`, at interval min and max; equal for a closed edge.
|
||||
pub vertices: [u32; 2],
|
||||
/// mm.
|
||||
pub tolerance: f64,
|
||||
/// Every oriented edge on this edge, each once, the primary use (the first in canonical
|
||||
/// order) first and the rest in radial order: right-hand rule about the edge's direction.
|
||||
/// Empty for a wire edge.
|
||||
pub uses: Vec<EdgeUseRef>,
|
||||
}
|
||||
|
||||
/// Where one use of an edge sits (SMLib `SmEUData`'s radial fields): `faces[face]`,
|
||||
/// `loops[loop]`, `edges[position]`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EdgeUseRef {
|
||||
pub face: u32,
|
||||
pub r#loop: u32,
|
||||
pub position: u32,
|
||||
/// Going round the edge, the radial cycle enters this use at the side on the surface's
|
||||
/// natural normal (SMLib's selected member of the mated pair). It follows from the loop
|
||||
/// convention: always `!forward` of the use it names.
|
||||
pub upper: bool,
|
||||
}
|
||||
|
||||
/// A point (SMLib `SmVertexData`, STEP `VERTEX_POINT`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Vertex {
|
||||
pub point: [f64; 3],
|
||||
/// mm.
|
||||
pub tolerance: f64,
|
||||
}
|
||||
|
||||
/// Right-handed orthonormal frame; z = `x_axis` × `y_axis` (STEP `AXIS2_PLACEMENT_3D`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Frame {
|
||||
pub origin: [f64; 3],
|
||||
pub x_axis: [f64; 3],
|
||||
pub y_axis: [f64; 3],
|
||||
}
|
||||
|
||||
/// Unbounded 3D curves, each with its parameter `t` (docs/native-brep.md, "Curves").
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Curve {
|
||||
/// The line through both points: `start + t * (end - start)`, t = 0 at `start` and 1 at
|
||||
/// `end` (STEP `LINE`; truck's `Line`).
|
||||
Line { start: [f64; 3], end: [f64; 3] },
|
||||
/// `origin + radius * (cos t * x + sin t * y)`, t the angle from x (STEP `CIRCLE`).
|
||||
Circle { frame: Frame, radius: f64 },
|
||||
/// `origin + radii[0] cos t * x + radii[1] sin t * y` (STEP `ELLIPSE`).
|
||||
Ellipse { frame: Frame, radii: [f64; 2] },
|
||||
/// Straight segments; segment i spans t in [i, i + 1] (STEP `POLYLINE`).
|
||||
Polyline { points: Vec<[f64; 3]> },
|
||||
/// A B-spline on its knots (STEP `B_SPLINE_CURVE_WITH_KNOTS`).
|
||||
Spline {
|
||||
knots: Vec<f64>,
|
||||
points: Vec<[f64; 3]>,
|
||||
},
|
||||
/// A NURBS curve: homogeneous `[x*w, y*w, z*w, w]` coefficients (STEP `RATIONAL_B_SPLINE_CURVE`).
|
||||
RationalSpline {
|
||||
knots: Vec<f64>,
|
||||
homogeneous: Vec<[f64; 4]>,
|
||||
},
|
||||
/// The intersection of two surfaces, traced from `leader` (a polyline or spline near it)
|
||||
/// to within `tolerance`: the procedural curve itself, exactly as its producer holds it
|
||||
/// (STEP `INTERSECTION_CURVE`). t is the leader's parameter.
|
||||
SurfaceIntersection {
|
||||
surfaces: [Box<Surface>; 2],
|
||||
leader: Box<Curve>,
|
||||
tolerance: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Unbounded surfaces, each with its (u, v) (docs/native-brep.md, "Surfaces"). Natural
|
||||
/// normals are ∂S/∂u × ∂S/∂v.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Surface {
|
||||
/// `origin + u * u_axis + v * v_axis` (STEP `PLANE`).
|
||||
Plane {
|
||||
origin: [f64; 3],
|
||||
u_axis: [f64; 3],
|
||||
v_axis: [f64; 3],
|
||||
},
|
||||
/// `origin + u * (u_point - origin) + v * (v_point - origin)`: the plane through three points,
|
||||
/// as its producer holds them (axes worked out from them would round; truck's `Plane`).
|
||||
PlaneThrough {
|
||||
origin: [f64; 3],
|
||||
u_point: [f64; 3],
|
||||
v_point: [f64; 3],
|
||||
},
|
||||
/// `origin + v z + radius (cos u x + sin u y)`: u the angle, v the height (STEP
|
||||
/// `CYLINDRICAL_SURFACE`).
|
||||
Cylinder { frame: Frame, radius: f64 },
|
||||
/// `origin + v z + r(v) (cos u x + sin u y)`, `r(v) = radius_at_origin + v tan(half_angle)`,
|
||||
/// on the side of the apex where r(v) ≥ 0: above it for a positive half angle, below it for
|
||||
/// a negative one (a cone narrowing along z, as SMLib makes them) (STEP `CONICAL_SURFACE`).
|
||||
Cone {
|
||||
frame: Frame,
|
||||
radius_at_origin: f64,
|
||||
half_angle: f64,
|
||||
},
|
||||
/// `origin + radius (cos v (cos u x + sin u y) + sin v z)`: u the longitude, v the latitude
|
||||
/// in [-π/2, π/2] (STEP `SPHERICAL_SURFACE`).
|
||||
Sphere { frame: Frame, radius: f64 },
|
||||
/// `origin + (R + r cos w)(cos u x + sin u y) + r sin w z`, `w = v + tube_start`: u the
|
||||
/// longitude, v the meridian angle measured from `tube_start` (radians; 0, the outer equator,
|
||||
/// in STEP `TOROIDAL_SURFACE` and older records). The tube start is the tube's own reference,
|
||||
/// as the frame's x axis is the longitude's: a face's form starts its tube on a quarter line
|
||||
/// from there (`form`).
|
||||
Torus {
|
||||
frame: Frame,
|
||||
major_radius: f64,
|
||||
minor_radius: f64,
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
tube_start: f64,
|
||||
},
|
||||
/// A rational patch on the sphere of `center` and `radius`: the corner of three equal rounds as
|
||||
/// SMLib holds it (`SmBallPatch`). It has no (u, v) of its own: the face's `nurbs` is the patch,
|
||||
/// and this says it lies on that sphere, whose offsets are exact and collapse at its centre.
|
||||
BallPatch { center: [f64; 3], radius: f64 },
|
||||
/// A tensor-product B-spline: `points[i][j]`, i along u (STEP `B_SPLINE_SURFACE_WITH_KNOTS`).
|
||||
Spline {
|
||||
u_knots: Vec<f64>,
|
||||
v_knots: Vec<f64>,
|
||||
points: Vec<Vec<[f64; 3]>>,
|
||||
},
|
||||
/// A NURBS surface: homogeneous `[x*w, y*w, z*w, w]` coefficients.
|
||||
RationalSpline {
|
||||
u_knots: Vec<f64>,
|
||||
v_knots: Vec<f64>,
|
||||
homogeneous: Vec<Vec<[f64; 4]>>,
|
||||
},
|
||||
/// `profile` revolved about `origin` and unit `axis` (right-hand rule), then mapped by the
|
||||
/// column-major affine `matrix`: the revolution exactly as its producer holds it, nothing
|
||||
/// baked into the profile (truck's `RevolutedCurve` under a `Processor`). When `forward`,
|
||||
/// u is the profile's parameter and v the angle in [0, 2π); otherwise u and v swap (u the
|
||||
/// angle, v the profile's), which reverses the natural normal.
|
||||
PlacedRevolution {
|
||||
profile: Box<Curve>,
|
||||
origin: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
matrix: [[f64; 4]; 4],
|
||||
forward: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Why a record is refused.
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
#[error("portable B-rep: {0}")]
|
||||
pub struct Invalid(pub &'static str);
|
||||
|
||||
/// A record check's result.
|
||||
pub type Result<T> = std::result::Result<T, Invalid>;
|
||||
|
||||
/// `Ok` if `condition` holds, else [`Invalid`] with `message`.
|
||||
pub fn require(condition: bool, message: &'static str) -> Result<()> {
|
||||
if condition {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Invalid(message))
|
||||
}
|
||||
}
|
||||
|
||||
/// The items counted so far and the most allowed, shared by the checks of one input.
|
||||
#[derive(Debug)]
|
||||
pub struct Budget {
|
||||
used: usize,
|
||||
limit: usize,
|
||||
}
|
||||
impl Budget {
|
||||
/// The budget of a record from outside the kernel: [`MAX_ITEMS`].
|
||||
pub fn input() -> Budget {
|
||||
Budget {
|
||||
used: 0,
|
||||
limit: MAX_ITEMS,
|
||||
}
|
||||
}
|
||||
fn add(&mut self, n: usize) -> Result<()> {
|
||||
self.used = self.used.checked_add(n).ok_or(Invalid("item budget"))?;
|
||||
require(self.used <= self.limit, "item budget")
|
||||
}
|
||||
}
|
||||
fn numbers(values: impl IntoIterator<Item = f64>) -> Result<()> {
|
||||
require(
|
||||
values.into_iter().all(|v| v.is_finite() && v.abs() <= 1e12),
|
||||
"non-finite or excessive coordinate",
|
||||
)
|
||||
}
|
||||
fn points<const N: usize>(values: &[[f64; N]], budget: &mut Budget) -> Result<()> {
|
||||
budget.add(values.len())?;
|
||||
numbers(values.iter().flatten().copied())
|
||||
}
|
||||
fn knots(values: &[f64], count: usize, budget: &mut Budget) -> Result<()> {
|
||||
budget.add(values.len())?;
|
||||
numbers(values.iter().copied())?;
|
||||
let degree = values
|
||||
.len()
|
||||
.checked_sub(count.saturating_add(1))
|
||||
.ok_or(Invalid("spline degree"))?;
|
||||
require(
|
||||
(2..=4096).contains(&count) && (1..=MAX_SPLINE_DEGREE).contains(°ree) && degree < count,
|
||||
"spline degree or size",
|
||||
)?;
|
||||
require(
|
||||
values.windows(2).all(|w| w.first() <= w.get(1)),
|
||||
"unordered knots",
|
||||
)?;
|
||||
require(
|
||||
values.get(degree) < values.get(count),
|
||||
"empty spline domain",
|
||||
)?;
|
||||
// No knot may have multiplicity greater than degree + 1.
|
||||
let mut run = 1;
|
||||
for pair in values.windows(2) {
|
||||
run = if pair.first() == pair.get(1) {
|
||||
run + 1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
require(run <= degree + 1, "knot multiplicity")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn weights(values: &[[f64; 4]]) -> Result<()> {
|
||||
require(
|
||||
values.iter().all(|p| p.get(3).is_some_and(|w| *w >= 1e-12)),
|
||||
"non-positive rational weight",
|
||||
)
|
||||
}
|
||||
fn grid<const N: usize>(
|
||||
u: &[f64],
|
||||
v: &[f64],
|
||||
rows: &[Vec<[f64; N]>],
|
||||
budget: &mut Budget,
|
||||
) -> Result<()> {
|
||||
let columns = rows.first().map_or(0, Vec::len);
|
||||
knots(u, rows.len(), budget)?;
|
||||
knots(v, columns, budget)?;
|
||||
for row in rows {
|
||||
require(row.len() == columns, "ragged surface")?;
|
||||
points(row, budget)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// A frame's numbers. That its axes are orthonormal is the kernel's check.
|
||||
fn frame(f: &Frame, budget: &mut Budget) -> Result<()> {
|
||||
points(&[f.origin, f.x_axis, f.y_axis], budget)
|
||||
}
|
||||
/// A field left out of a record where it is zero (its default). By reference: serde's
|
||||
/// `skip_serializing_if` passes one.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_zero(x: &f64) -> bool {
|
||||
*x == 0.0
|
||||
}
|
||||
fn radius(r: f64) -> Result<()> {
|
||||
require(r.is_finite() && (1e-12..=1e12).contains(&r), "radius")
|
||||
}
|
||||
/// A tolerance in mm.
|
||||
fn tolerance(t: f64) -> Result<()> {
|
||||
require(t.is_finite() && t > 0. && t <= 1e3, "tolerance")
|
||||
}
|
||||
|
||||
/// A parameter's natural range: periodic with period 2π, or between two bounds (infinite for
|
||||
/// an unbounded direction).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Natural {
|
||||
Periodic,
|
||||
Range(f64, f64),
|
||||
}
|
||||
/// The natural range of an unbounded direction.
|
||||
pub const UNBOUNDED: Natural = Natural::Range(f64::NEG_INFINITY, f64::INFINITY);
|
||||
|
||||
/// The active interval of a spline's knots (already validated): `[knots[degree], knots[count]]`
|
||||
/// for `count` control points.
|
||||
pub fn knot_domain(k: &[f64], count: usize) -> Natural {
|
||||
let degree = k.len().saturating_sub(count + 1);
|
||||
match (k.get(degree), k.get(count)) {
|
||||
(Some(a), Some(b)) => Natural::Range(*a, *b),
|
||||
_ => Natural::Range(0., 0.),
|
||||
}
|
||||
}
|
||||
fn curve_natural(c: &Curve) -> Natural {
|
||||
match c {
|
||||
Curve::Line { .. } => UNBOUNDED,
|
||||
Curve::Circle { .. } | Curve::Ellipse { .. } => Natural::Periodic,
|
||||
Curve::Polyline { points } => Natural::Range(0., points.len().saturating_sub(1) as f64),
|
||||
Curve::Spline { knots, points } => knot_domain(knots, points.len()),
|
||||
Curve::RationalSpline { knots, homogeneous } => knot_domain(knots, homogeneous.len()),
|
||||
Curve::SurfaceIntersection { leader, .. } => curve_natural(leader),
|
||||
}
|
||||
}
|
||||
/// The natural ranges of the (u, v) a face's domain and pcurves are on, [`Face::parameters`]:
|
||||
/// a plane's or a spline's. Other surfaces are labels whose (u, v) are their NURBS form's, which
|
||||
/// validation requires; their natural ranges are the kernel's (a cone's apex is computed).
|
||||
fn parameter_natural(s: &Surface) -> Result<[Natural; 2]> {
|
||||
match s {
|
||||
Surface::Plane { .. } | Surface::PlaneThrough { .. } => Ok([UNBOUNDED; 2]),
|
||||
Surface::Spline {
|
||||
u_knots,
|
||||
v_knots,
|
||||
points,
|
||||
} => Ok([
|
||||
knot_domain(u_knots, points.len()),
|
||||
knot_domain(v_knots, points.first().map_or(0, Vec::len)),
|
||||
]),
|
||||
Surface::RationalSpline {
|
||||
u_knots,
|
||||
v_knots,
|
||||
homogeneous,
|
||||
} => Ok([
|
||||
knot_domain(u_knots, homogeneous.len()),
|
||||
knot_domain(v_knots, homogeneous.first().map_or(0, Vec::len)),
|
||||
]),
|
||||
_ => Err(Invalid("a face's parameters on a surface without its own")),
|
||||
}
|
||||
}
|
||||
impl Curve {
|
||||
/// The range of `t`. Call on a validated record.
|
||||
pub fn natural(&self) -> Natural {
|
||||
curve_natural(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The structure of one curve record from outside the kernel, as [`Brep::validate`] checks an
|
||||
/// edge's, counted against `budget`.
|
||||
pub fn check_curve(c: &Curve, budget: &mut Budget) -> Result<()> {
|
||||
curve(c, 0, budget)
|
||||
}
|
||||
|
||||
/// The structure of one surface record from outside the kernel, as [`Brep::validate`] checks a
|
||||
/// face's, counted against `budget`.
|
||||
pub fn check_surface(s: &Surface, budget: &mut Budget) -> Result<()> {
|
||||
surface(s, 0, budget)
|
||||
}
|
||||
|
||||
/// The structure of one pcurve record from outside the kernel, as [`Brep::validate`] checks an
|
||||
/// oriented edge's, counted against `budget`.
|
||||
pub fn check_curve2(c: &Curve2, budget: &mut Budget) -> Result<()> {
|
||||
curve2::curve2(c, budget)
|
||||
}
|
||||
|
||||
/// `[min, max]` is a parameter interval inside `natural`, as [`Brep::validate`] checks an edge's
|
||||
/// and a face's: finite, min < max, and at most one turn long where periodic.
|
||||
pub fn check_interval(r: &[f64; 2], natural: Natural) -> Result<()> {
|
||||
interval(r, natural)
|
||||
}
|
||||
|
||||
/// `[min, max]`: finite, min < max, inside `natural` (at most one turn long where periodic).
|
||||
fn interval(r: &[f64; 2], natural: Natural) -> Result<()> {
|
||||
numbers(*r)?;
|
||||
require(r[0] < r[1], "empty parameter interval")?;
|
||||
match natural {
|
||||
Natural::Periodic => {
|
||||
let slack = 1e-12 * (1. + r[0].abs().max(r[1].abs()));
|
||||
require(
|
||||
r[1] - r[0] <= TAU + slack,
|
||||
"parameter interval exceeds one turn",
|
||||
)
|
||||
}
|
||||
Natural::Range(a, b) => require(
|
||||
a <= r[0] && r[1] <= b,
|
||||
"parameter interval outside the natural domain",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn curve(c: &Curve, depth: usize, budget: &mut Budget) -> Result<()> {
|
||||
require(depth <= MAX_DEPTH, "geometry depth")?;
|
||||
budget.add(1)?;
|
||||
match c {
|
||||
Curve::Line { start, end } => {
|
||||
points(&[*start, *end], budget)?;
|
||||
require(start != end, "collapsed line")
|
||||
}
|
||||
Curve::Circle {
|
||||
frame: f,
|
||||
radius: r,
|
||||
} => {
|
||||
frame(f, budget)?;
|
||||
radius(*r)
|
||||
}
|
||||
Curve::Ellipse { frame: f, radii } => {
|
||||
frame(f, budget)?;
|
||||
radii.iter().try_for_each(|r| radius(*r))
|
||||
}
|
||||
Curve::Polyline { points: p } => {
|
||||
require(p.len() >= 2, "empty polyline")?;
|
||||
points(p, budget)?;
|
||||
require(
|
||||
p.windows(2).all(|p| p.first() != p.get(1)),
|
||||
"collapsed polyline segment",
|
||||
)
|
||||
}
|
||||
Curve::Spline {
|
||||
knots: k,
|
||||
points: p,
|
||||
} => {
|
||||
knots(k, p.len(), budget)?;
|
||||
points(p, budget)
|
||||
}
|
||||
Curve::RationalSpline {
|
||||
knots: k,
|
||||
homogeneous: p,
|
||||
} => {
|
||||
knots(k, p.len(), budget)?;
|
||||
points(p, budget)?;
|
||||
weights(p)
|
||||
}
|
||||
Curve::SurfaceIntersection {
|
||||
surfaces,
|
||||
leader,
|
||||
tolerance,
|
||||
} => {
|
||||
require(
|
||||
tolerance.is_finite() && (0.0..=1e3).contains(tolerance),
|
||||
"intersection tolerance",
|
||||
)?;
|
||||
for s in surfaces {
|
||||
surface(s, depth + 1, budget)?;
|
||||
}
|
||||
// The leader is only a starting guess for the trace: its knots are the producer's,
|
||||
// and a traced polyline may repeat a point.
|
||||
require(depth < MAX_DEPTH, "geometry depth")?;
|
||||
budget.add(1)?;
|
||||
match &**leader {
|
||||
Curve::Polyline { points: p } => {
|
||||
require(p.len() >= 2, "empty intersection leader")?;
|
||||
points(p, budget)
|
||||
}
|
||||
Curve::Spline {
|
||||
knots: k,
|
||||
points: p,
|
||||
} => {
|
||||
knots(k, p.len(), budget)?;
|
||||
points(p, budget)
|
||||
}
|
||||
_ => Err(Invalid("intersection leader")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
|
||||
require(depth <= MAX_DEPTH, "geometry depth")?;
|
||||
budget.add(1)?;
|
||||
match s {
|
||||
Surface::Plane {
|
||||
origin,
|
||||
u_axis,
|
||||
v_axis,
|
||||
} => {
|
||||
points(&[*origin, *u_axis, *v_axis], budget)?;
|
||||
Ok(())
|
||||
}
|
||||
Surface::PlaneThrough {
|
||||
origin,
|
||||
u_point,
|
||||
v_point,
|
||||
} => points(&[*origin, *u_point, *v_point], budget),
|
||||
Surface::Cylinder {
|
||||
frame: f,
|
||||
radius: r,
|
||||
}
|
||||
| Surface::Sphere {
|
||||
frame: f,
|
||||
radius: r,
|
||||
} => {
|
||||
frame(f, budget)?;
|
||||
radius(*r)
|
||||
}
|
||||
Surface::Cone {
|
||||
frame: f,
|
||||
radius_at_origin,
|
||||
half_angle,
|
||||
} => {
|
||||
frame(f, budget)?;
|
||||
numbers([*radius_at_origin, *half_angle])?;
|
||||
require(
|
||||
*radius_at_origin >= 0.
|
||||
&& half_angle.abs() >= 1e-12
|
||||
&& half_angle.abs() < std::f64::consts::FRAC_PI_2,
|
||||
"cone dimensions",
|
||||
)
|
||||
}
|
||||
Surface::Torus {
|
||||
frame: f,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
tube_start,
|
||||
} => {
|
||||
frame(f, budget)?;
|
||||
radius(*major_radius)?;
|
||||
radius(*minor_radius)?;
|
||||
require(
|
||||
tube_start.is_finite() && tube_start.abs() <= std::f64::consts::TAU,
|
||||
"torus tube start",
|
||||
)?;
|
||||
require(major_radius > minor_radius, "ring torus radii")
|
||||
}
|
||||
Surface::BallPatch { center, radius: r } => {
|
||||
points(&[*center], budget)?;
|
||||
radius(*r)
|
||||
}
|
||||
Surface::Spline {
|
||||
u_knots,
|
||||
v_knots,
|
||||
points: p,
|
||||
} => grid(u_knots, v_knots, p, budget),
|
||||
Surface::RationalSpline {
|
||||
u_knots,
|
||||
v_knots,
|
||||
homogeneous: p,
|
||||
} => {
|
||||
grid(u_knots, v_knots, p, budget)?;
|
||||
for row in p {
|
||||
weights(row)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Surface::PlacedRevolution {
|
||||
profile,
|
||||
origin,
|
||||
axis,
|
||||
matrix,
|
||||
..
|
||||
} => {
|
||||
points(&[*origin, *axis], budget)?;
|
||||
points(matrix, budget)?;
|
||||
curve(profile, depth + 1, budget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
/// The curve the edge's parameter is on: its NURBS form when it has one, else its curve.
|
||||
/// Everything that evaluates the edge at a parameter goes through this, with `interval`.
|
||||
pub fn parameters(&self) -> &Curve {
|
||||
self.nurbs.as_ref().unwrap_or(&self.curve)
|
||||
}
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// The surface the face's (u, v) are on: its NURBS form when it has one, else its surface.
|
||||
/// Everything that evaluates the face's domain or pcurves goes through this.
|
||||
pub fn parameters(&self) -> &Surface {
|
||||
self.nurbs.as_ref().unwrap_or(&self.surface)
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// Validate portable structure without calling a kernel or evaluating geometry: resource
|
||||
/// budgets, geometry records, parameter ranges, references, loops, regions, every edge's
|
||||
/// list of uses and the canonical order. Computes nothing: that frames are orthonormal and
|
||||
/// planes and placements not degenerate, watertightness, that edges lie on their surfaces and
|
||||
/// that pcurves lift onto their edges are the kernel's checks.
|
||||
///
|
||||
/// For a record from outside the kernel (a file, the control channel, pasted data): it is
|
||||
/// refused past [`MAX_ITEMS`] items, so hostile input is bounded before anything walks it.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.validate_within(Budget::input())
|
||||
}
|
||||
|
||||
/// [`Brep::validate`] for a record the kernel built in memory (an operation's result, a
|
||||
/// body's own record, a part of a validated record): every check but the item budget. The
|
||||
/// record already sits in memory, its size set by inputs that were bounded where they came
|
||||
/// in, and the checks are linear in it; a cap here would only refuse a real result.
|
||||
pub fn validate_built(&self) -> Result<()> {
|
||||
self.validate_within(Budget {
|
||||
used: 0,
|
||||
limit: usize::MAX,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_within(&self, mut budget: Budget) -> Result<()> {
|
||||
require(self.version == VERSION, "unsupported version")?;
|
||||
tolerance(self.tolerance)?;
|
||||
require(
|
||||
!self.shells.is_empty() && self.shells.len() <= MAX_SHELLS,
|
||||
"shell count",
|
||||
)?;
|
||||
require(
|
||||
!self.regions.is_empty() && self.regions.len() <= 2 * MAX_SHELLS + 1,
|
||||
"region count",
|
||||
)?;
|
||||
budget.add(self.regions.len() + self.faces.len() + self.edges.len())?;
|
||||
for v in &self.vertices {
|
||||
points(&[v.point], &mut budget)?;
|
||||
tolerance(v.tolerance)?;
|
||||
}
|
||||
for e in &self.edges {
|
||||
curve(&e.curve, 0, &mut budget)?;
|
||||
if let Some(n) = &e.nurbs {
|
||||
curve(n, 0, &mut budget)?;
|
||||
require(
|
||||
matches!(n, Curve::Spline { .. } | Curve::RationalSpline { .. }),
|
||||
"NURBS form is not a NURBS",
|
||||
)?;
|
||||
require(
|
||||
matches!(
|
||||
e.curve,
|
||||
Curve::Line { .. } | Curve::Circle { .. } | Curve::Ellipse { .. }
|
||||
),
|
||||
"NURBS form on a curve that is not a line, circle or ellipse",
|
||||
)?;
|
||||
}
|
||||
// A circle or ellipse is a label: its geometry is its NURBS form.
|
||||
require(
|
||||
e.nurbs.is_some()
|
||||
|| !matches!(e.curve, Curve::Circle { .. } | Curve::Ellipse { .. }),
|
||||
"a circle or ellipse without its NURBS form",
|
||||
)?;
|
||||
interval(&e.interval, curve_natural(e.parameters()))?;
|
||||
require(
|
||||
e.vertices
|
||||
.iter()
|
||||
.all(|&v| (v as usize) < self.vertices.len()),
|
||||
"edge vertices",
|
||||
)?;
|
||||
tolerance(e.tolerance)?;
|
||||
budget.add(e.uses.len())?;
|
||||
}
|
||||
for f in &self.faces {
|
||||
surface(&f.surface, 0, &mut budget)?;
|
||||
if let Some(n) = &f.nurbs {
|
||||
surface(n, 0, &mut budget)?;
|
||||
require(
|
||||
matches!(n, Surface::Spline { .. } | Surface::RationalSpline { .. }),
|
||||
"NURBS form is not a NURBS",
|
||||
)?;
|
||||
require(
|
||||
matches!(
|
||||
f.surface,
|
||||
Surface::Cylinder { .. }
|
||||
| Surface::Cone { .. }
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
),
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus or revolution",
|
||||
)?;
|
||||
}
|
||||
// A cylinder, cone, sphere, ball patch, torus or revolution is a label: its geometry is
|
||||
// its form.
|
||||
require(
|
||||
f.nurbs.is_some()
|
||||
|| !matches!(
|
||||
f.surface,
|
||||
Surface::Cylinder { .. }
|
||||
| Surface::Cone { .. }
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
),
|
||||
"a cylinder, cone, sphere, ball patch, torus or revolution without its NURBS form",
|
||||
)?;
|
||||
let [u, v] = parameter_natural(f.parameters())?;
|
||||
interval(&f.domain[0], u)?;
|
||||
interval(&f.domain[1], v)?;
|
||||
budget.add(f.loops.len())?;
|
||||
for l in &f.loops {
|
||||
if let Loop::Edges { edges } = l {
|
||||
budget.add(edges.len())?;
|
||||
for e in edges {
|
||||
curve2::curve2(&e.pcurve, &mut budget)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
topology::check(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Pcurves: an edge in its face's surface parameters, (u, v). 2D B-splines only, as SMLib keeps
|
||||
//! its UV trim curves (`SmBSplineCurve`); 2D lines and arcs are exact as degree-1 and rational
|
||||
//! quadratic splines. Evaluating one is the kernel's.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Budget, Result, knots, numbers, require};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Curve2 {
|
||||
Spline {
|
||||
knots: Vec<f64>,
|
||||
points: Vec<[f64; 2]>,
|
||||
},
|
||||
/// Homogeneous `[u*w, v*w, w]` coefficients.
|
||||
RationalSpline {
|
||||
knots: Vec<f64>,
|
||||
homogeneous: Vec<[f64; 3]>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Curve2 {
|
||||
/// The active parameter interval `[knots[degree], knots[count]]`.
|
||||
pub fn interval(&self) -> Option<[f64; 2]> {
|
||||
let (k, count) = match self {
|
||||
Curve2::Spline { knots, points } => (knots, points.len()),
|
||||
Curve2::RationalSpline { knots, homogeneous } => (knots, homogeneous.len()),
|
||||
};
|
||||
let degree = k.len().checked_sub(count.checked_add(1)?)?;
|
||||
Some([*k.get(degree)?, *k.get(count)?])
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn curve2(c: &Curve2, budget: &mut Budget) -> Result<()> {
|
||||
budget.add(1)?;
|
||||
match c {
|
||||
Curve2::Spline { knots: k, points } => {
|
||||
knots(k, points.len(), budget)?;
|
||||
budget.add(points.len())?;
|
||||
numbers(points.iter().flatten().copied())
|
||||
}
|
||||
Curve2::RationalSpline {
|
||||
knots: k,
|
||||
homogeneous,
|
||||
} => {
|
||||
knots(k, homogeneous.len(), budget)?;
|
||||
budget.add(homogeneous.len())?;
|
||||
numbers(homogeneous.iter().flatten().copied())?;
|
||||
require(
|
||||
homogeneous.iter().all(|p| p[2] >= 1e-12),
|
||||
"non-positive rational weight",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pcurves_validate_and_round_trip() {
|
||||
let line = Curve2::Spline {
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., 0.], [1., 2.]],
|
||||
};
|
||||
curve2(&line, &mut Budget::input()).unwrap();
|
||||
assert_eq!(line.interval(), Some([0., 1.]));
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Curve2>(&serde_json::to_string(&line).unwrap()).unwrap(),
|
||||
line
|
||||
);
|
||||
let bad = Curve2::RationalSpline {
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
homogeneous: vec![[0., 0., 1.], [1., 2., 0.]],
|
||||
};
|
||||
assert!(curve2(&bad, &mut Budget::input()).is_err());
|
||||
let nan = Curve2::Spline {
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., f64::NAN], [1., 2.]],
|
||||
};
|
||||
assert!(curve2(&nan, &mut Budget::input()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! Stable version-one body metadata records. Runtime model types never define this wire contract.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Metadata {
|
||||
pub version: u32,
|
||||
pub color: Option<[f32; 3]>,
|
||||
pub paint: Option<Paint>,
|
||||
pub boundaries: Option<Vec<Vec<[f64; 3]>>>,
|
||||
pub face_sources: Option<Vec<Option<FaceSource>>>,
|
||||
pub history: Option<History>,
|
||||
pub preserve_boundaries: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Plane {
|
||||
pub origin: [f64; 3],
|
||||
pub x_axis: [f64; 3],
|
||||
pub y_axis: [f64; 3],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Segment {
|
||||
Line {
|
||||
start: [f64; 2],
|
||||
end: [f64; 2],
|
||||
},
|
||||
Arc {
|
||||
center: [f64; 2],
|
||||
radius: f64,
|
||||
start: f64,
|
||||
sweep: f64,
|
||||
},
|
||||
Cubic {
|
||||
points: [[f64; 2]; 4],
|
||||
},
|
||||
Conic {
|
||||
points: [[f64; 2]; 3],
|
||||
middle_weight: f64,
|
||||
},
|
||||
Bezier {
|
||||
points: Vec<[f64; 2]>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Region {
|
||||
pub outer: Vec<Segment>,
|
||||
pub holes: Vec<Vec<Segment>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum FaceSource {
|
||||
ExtrusionSide {
|
||||
segment: Segment,
|
||||
plane: Option<Plane>,
|
||||
coalesced: bool,
|
||||
},
|
||||
CertifiedExtrusionSide {
|
||||
producer: u64,
|
||||
sketch: u64,
|
||||
curve: String,
|
||||
plane: Option<Plane>,
|
||||
},
|
||||
ExtrusionCap {
|
||||
high: bool,
|
||||
region: Region,
|
||||
plane: Plane,
|
||||
},
|
||||
CertifiedExtrusionCap {
|
||||
producer: u64,
|
||||
sketch: u64,
|
||||
high: bool,
|
||||
outer: Vec<String>,
|
||||
holes: Vec<Vec<String>>,
|
||||
plane: Plane,
|
||||
},
|
||||
CylinderCap {
|
||||
producer: u64,
|
||||
high: bool,
|
||||
plane: Plane,
|
||||
},
|
||||
BoxFace {
|
||||
producer: Option<u64>,
|
||||
axis: u8,
|
||||
high: bool,
|
||||
plane: Plane,
|
||||
},
|
||||
PlanarShell {
|
||||
producer: Option<u64>,
|
||||
source: Box<FaceSource>,
|
||||
inner: bool,
|
||||
plane: Plane,
|
||||
},
|
||||
Imported {
|
||||
item: u64,
|
||||
face: u64,
|
||||
payload_sha256: Option<String>,
|
||||
producer: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Paint {
|
||||
pub opacity: f32,
|
||||
pub faces: Vec<FacePaint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FacePaint {
|
||||
pub face: u32,
|
||||
pub color: [f32; 3],
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct History {
|
||||
pub operands: u32,
|
||||
pub faces: Vec<FaceHistory>,
|
||||
pub edges: Vec<EdgeHistory>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FaceHistory {
|
||||
pub origins: Vec<FaceOrigin>,
|
||||
pub change: Change,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EdgeHistory {
|
||||
pub faces: Vec<u32>,
|
||||
pub origins: Vec<EdgeOrigin>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum FaceOrigin {
|
||||
Face { operand: u32, face: u32 },
|
||||
Profile { segment: u32 },
|
||||
Cap { end: bool },
|
||||
Edge { operand: u32, edge: u32 },
|
||||
Offset { operand: u32, face: u32 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum EdgeOrigin {
|
||||
Edge { operand: u32, edge: u32 },
|
||||
Meeting { sides: [Vec<FaceOrigin>; 2] },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Change {
|
||||
Kept,
|
||||
Modified,
|
||||
Split,
|
||||
Merged,
|
||||
Generated,
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
//! Exact face and edge history: for each face and edge of an operation's result, what it came
|
||||
//! from among the operation's operands, recorded by the code that built it (never sampled).
|
||||
//!
|
||||
//! Indices are canonical face and edge indices (docs/native-brep.md, "Canonical order"), the
|
||||
//! positions in a body's face order and edge order. Operand numbers are positions in the
|
||||
//! operation's own operand list, which each operation documents. An operation returns its
|
||||
//! history beside the B-rep; a backend that cannot report it returns none and never guesses.
|
||||
//! These are the runtime types; the saved form is [`super::envelope`], kept separate so that a
|
||||
//! change here cannot redefine saved fields. `solidcraft_kernel::history` re-exports them and
|
||||
//! adds what needs a kernel body.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
/// Why a history could not be composed or checked.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("history: {0}")]
|
||||
pub struct Failed(pub &'static str);
|
||||
|
||||
type Result<T> = std::result::Result<T, Failed>;
|
||||
|
||||
fn failed(detail: &'static str) -> Failed {
|
||||
Failed(detail)
|
||||
}
|
||||
|
||||
/// Where a result face came from.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum FaceOrigin {
|
||||
/// A face of input `operand` (its index in the operand's face order).
|
||||
Face { operand: usize, face: usize },
|
||||
/// Swept from segment `segment` of the profile's loops (in order: outer, then holes).
|
||||
Profile { segment: usize },
|
||||
/// The start or end cap of a sweep (the profile itself, or where it ends up).
|
||||
Cap { end: bool },
|
||||
/// Made in place of edge `edge` of input `operand` (a fillet or chamfer).
|
||||
Edge { operand: usize, edge: usize },
|
||||
/// An offset of face `face` of input `operand` (a shell's inner wall).
|
||||
Offset { operand: usize, face: usize },
|
||||
}
|
||||
|
||||
/// How a result face relates to its origins.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Change {
|
||||
/// The same face as its origin (same region).
|
||||
Kept,
|
||||
/// Its origin's surface, trimmed differently.
|
||||
Modified,
|
||||
/// One of several result faces made from one input face.
|
||||
Split,
|
||||
/// Made from several input faces (coplanar or cosurface faces joined).
|
||||
Merged,
|
||||
/// New to the operation (no input face under it).
|
||||
Generated,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FaceProv {
|
||||
pub origins: Vec<FaceOrigin>,
|
||||
pub change: Change,
|
||||
}
|
||||
|
||||
/// One side of a meeting: the origins of the face on that side of the edge. One origin, or
|
||||
/// several where that face was made from several (a merged face) and the edge lies along it
|
||||
/// as a whole. Never empty; sorted, without repeats, so equal sides compare equal.
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct Side(Vec<FaceOrigin>);
|
||||
|
||||
impl Side {
|
||||
/// The side made of `origins` (in any order, repeats allowed); `None` when there are none.
|
||||
pub fn new(origins: impl IntoIterator<Item = FaceOrigin>) -> Option<Side> {
|
||||
let side = Side::canonical(origins.into_iter().collect());
|
||||
(!side.0.is_empty()).then_some(side)
|
||||
}
|
||||
|
||||
/// Its origins, sorted.
|
||||
pub fn origins(&self) -> &[FaceOrigin] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// The side's one origin, when it has exactly one.
|
||||
pub fn single(&self) -> Option<FaceOrigin> {
|
||||
match self.0.as_slice() {
|
||||
[o] => Some(*o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same side with each origin mapped by `f` (failing where `f` does).
|
||||
pub fn try_map<E>(
|
||||
&self,
|
||||
f: impl Fn(FaceOrigin) -> std::result::Result<FaceOrigin, E>,
|
||||
) -> std::result::Result<Side, E> {
|
||||
Ok(Side::canonical(
|
||||
self.0
|
||||
.iter()
|
||||
.map(|o| f(*o))
|
||||
.collect::<std::result::Result<_, E>>()?,
|
||||
))
|
||||
}
|
||||
|
||||
/// The same side with each origin's operand renumbered by `f`.
|
||||
#[must_use]
|
||||
pub fn map_operand(&self, f: impl Fn(usize) -> usize) -> Side {
|
||||
Side::canonical(self.0.iter().map(|o| o.map_operand(&f)).collect())
|
||||
}
|
||||
|
||||
/// `origins` sorted, without repeats.
|
||||
fn canonical(mut origins: Vec<FaceOrigin>) -> Side {
|
||||
origins.sort_unstable();
|
||||
origins.dedup();
|
||||
Side(origins)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FaceOrigin> for Side {
|
||||
fn from(origin: FaceOrigin) -> Side {
|
||||
Side(vec![origin])
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a result edge came from.
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum EdgeOrigin {
|
||||
/// (Part of) edge `edge` of input `operand`.
|
||||
Edge { operand: usize, edge: usize },
|
||||
/// Where two faces meet that met nowhere before (a boolean's intersection curve): the
|
||||
/// origins of the face on each side, `a <= b` (made with [`EdgeOrigin::meet`]).
|
||||
Meet { a: Side, b: Side },
|
||||
}
|
||||
|
||||
/// A result edge: the result faces it separates and what it came from (empty when it is new
|
||||
/// and lies between faces new to the operation, or between two pieces of one face: a split
|
||||
/// line).
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EdgeProv {
|
||||
pub faces: Vec<usize>,
|
||||
pub origins: Vec<EdgeOrigin>,
|
||||
}
|
||||
|
||||
/// History of an operation's result: one entry per face and per edge, in the result's orders.
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct History {
|
||||
/// How many operands the operation had.
|
||||
pub operands: usize,
|
||||
pub faces: Vec<FaceProv>,
|
||||
pub edges: Vec<EdgeProv>,
|
||||
}
|
||||
|
||||
impl FaceOrigin {
|
||||
/// The same origin with its operand (if it has one) renumbered by `f`.
|
||||
#[must_use]
|
||||
pub fn map_operand(self, f: impl Fn(usize) -> usize) -> FaceOrigin {
|
||||
match self {
|
||||
FaceOrigin::Face { operand, face } => FaceOrigin::Face {
|
||||
operand: f(operand),
|
||||
face,
|
||||
},
|
||||
FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge {
|
||||
operand: f(operand),
|
||||
edge,
|
||||
},
|
||||
FaceOrigin::Offset { operand, face } => FaceOrigin::Offset {
|
||||
operand: f(operand),
|
||||
face,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EdgeOrigin {
|
||||
/// Where the faces with origins `a` and `b` (sides, or single origins) meet, in canonical
|
||||
/// order (the lesser side first).
|
||||
pub fn meet(a: impl Into<Side>, b: impl Into<Side>) -> EdgeOrigin {
|
||||
let (a, b) = (a.into(), b.into());
|
||||
if a <= b {
|
||||
EdgeOrigin::Meet { a, b }
|
||||
} else {
|
||||
EdgeOrigin::Meet { a: b, b: a }
|
||||
}
|
||||
}
|
||||
|
||||
/// The same origin with its operands renumbered by `f`.
|
||||
#[must_use]
|
||||
pub fn map_operand(&self, f: impl Fn(usize) -> usize) -> EdgeOrigin {
|
||||
match self {
|
||||
EdgeOrigin::Edge { operand, edge } => EdgeOrigin::Edge {
|
||||
operand: f(*operand),
|
||||
edge: *edge,
|
||||
},
|
||||
EdgeOrigin::Meet { a, b } => EdgeOrigin::meet(a.map_operand(&f), b.map_operand(&f)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History {
|
||||
/// Check that this history describes a result with `result` (faces, edges) made from operands
|
||||
/// with `counts`: one entry per face and edge, and every origin within its operand's faces
|
||||
/// and edges.
|
||||
pub fn check_counts(&self, result: (usize, usize), counts: &[(usize, usize)]) -> Result<()> {
|
||||
if self.operands != counts.len() {
|
||||
return Err(failed("operand count does not match"));
|
||||
}
|
||||
if self.faces.len() != result.0 || self.edges.len() != result.1 {
|
||||
return Err(failed("entries do not match the result's faces and edges"));
|
||||
}
|
||||
let face_ok = |o: &FaceOrigin| match *o {
|
||||
FaceOrigin::Face { operand, face } | FaceOrigin::Offset { operand, face } => {
|
||||
counts.get(operand).is_some_and(|c| face < c.0)
|
||||
}
|
||||
FaceOrigin::Edge { operand, edge } => counts.get(operand).is_some_and(|c| edge < c.1),
|
||||
FaceOrigin::Profile { .. } | FaceOrigin::Cap { .. } => true,
|
||||
};
|
||||
if !self.faces.iter().all(|f| f.origins.iter().all(face_ok)) {
|
||||
return Err(failed("a face origin lies outside its operand"));
|
||||
}
|
||||
for e in &self.edges {
|
||||
if e.faces.iter().any(|f| *f >= self.faces.len()) {
|
||||
return Err(failed("an edge lies on a face outside the result"));
|
||||
}
|
||||
let ok = e.origins.iter().all(|o| match o {
|
||||
EdgeOrigin::Edge { operand, edge } => {
|
||||
counts.get(*operand).is_some_and(|c| *edge < c.1)
|
||||
}
|
||||
EdgeOrigin::Meet { a, b } => a.origins().iter().chain(b.origins()).all(face_ok),
|
||||
});
|
||||
if !ok {
|
||||
return Err(failed("an edge origin lies outside its operand"));
|
||||
}
|
||||
if e.origins
|
||||
.iter()
|
||||
.any(|o| matches!(o, EdgeOrigin::Meet { a, b } if a > b))
|
||||
{
|
||||
return Err(failed("a meeting's sides are out of order"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose with the history of the operation that made operand `k`: origins on operand `k`
|
||||
/// become that operation's origins, its operands (none, for a body made from a sketch)
|
||||
/// taking `k`'s place in the operand list; a meeting's side becomes the origins of the faces
|
||||
/// it names (several where that operation merged faces). Fails, without changing `self`,
|
||||
/// where a lineage cannot be expressed (an offset of a face that operation generated or
|
||||
/// merged, a blend of an edge it generated, a meeting with a face it made from nothing).
|
||||
pub fn substitute(&mut self, k: usize, inner: &History) -> Result<()> {
|
||||
if k >= self.operands {
|
||||
return Err(failed("no such operand"));
|
||||
}
|
||||
let n = inner.operands;
|
||||
// Operands before k stay, k's come in at k, those after k move by n - 1.
|
||||
let outer = |op: usize| if op < k { op } else { op + n - 1 };
|
||||
let lifted = |op: usize| op + k;
|
||||
let inner_face = |face: usize| {
|
||||
inner
|
||||
.faces
|
||||
.get(face)
|
||||
.ok_or_else(|| failed("a face origin lies outside the inner result"))
|
||||
};
|
||||
let inner_edge = |edge: usize| {
|
||||
inner
|
||||
.edges
|
||||
.get(edge)
|
||||
.ok_or_else(|| failed("an edge origin lies outside the inner result"))
|
||||
};
|
||||
// A face origin as origins of the composed operation, with the inner face's change.
|
||||
let map_face = |o: FaceOrigin| -> Result<(Vec<FaceOrigin>, Option<Change>)> {
|
||||
match o {
|
||||
FaceOrigin::Face { operand, face } if operand == k => {
|
||||
let p = inner_face(face)?;
|
||||
Ok((
|
||||
p.origins.iter().map(|x| x.map_operand(lifted)).collect(),
|
||||
Some(p.change),
|
||||
))
|
||||
}
|
||||
FaceOrigin::Offset { operand, face } if operand == k => {
|
||||
match inner_face(face)?.origins.as_slice() {
|
||||
[FaceOrigin::Face { operand, face }] => Ok((
|
||||
vec![FaceOrigin::Offset {
|
||||
operand: lifted(*operand),
|
||||
face: *face,
|
||||
}],
|
||||
None,
|
||||
)),
|
||||
_ => Err(failed(
|
||||
"an offset of a face the inner operation generated or merged",
|
||||
)),
|
||||
}
|
||||
}
|
||||
FaceOrigin::Edge { operand, edge } if operand == k => {
|
||||
match inner_edge(edge)?.origins.as_slice() {
|
||||
[EdgeOrigin::Edge { operand, edge }] => Ok((
|
||||
vec![FaceOrigin::Edge {
|
||||
operand: lifted(*operand),
|
||||
edge: *edge,
|
||||
}],
|
||||
None,
|
||||
)),
|
||||
_ => Err(failed("a blend of an edge the inner operation generated")),
|
||||
}
|
||||
}
|
||||
other => Ok((vec![other.map_operand(outer)], None)),
|
||||
}
|
||||
};
|
||||
let mut faces = Vec::with_capacity(self.faces.len());
|
||||
for f in &self.faces {
|
||||
let mut origins = BTreeSet::new();
|
||||
let mut kept = f.change == Change::Kept;
|
||||
for o in &f.origins {
|
||||
let (os, change) = map_face(*o)?;
|
||||
origins.extend(os);
|
||||
kept &= change.is_none_or(|c| c == Change::Kept);
|
||||
}
|
||||
let origins: Vec<FaceOrigin> = origins.into_iter().collect();
|
||||
let change = if f.change == Change::Generated
|
||||
|| !origins.iter().any(|o| matches!(o, FaceOrigin::Face { .. }))
|
||||
{
|
||||
Change::Generated
|
||||
} else if origins.len() > 1 {
|
||||
Change::Merged
|
||||
} else if kept {
|
||||
Change::Kept
|
||||
} else {
|
||||
Change::Modified
|
||||
};
|
||||
faces.push(FaceProv { origins, change });
|
||||
}
|
||||
// Split where an origin lies under several composed faces (pieces the inner operation
|
||||
// split and this one joined back, or dropped all but one of, are not).
|
||||
mark_shared_split(&mut faces);
|
||||
// One side of a meeting: the origins of what its faces were made from.
|
||||
let side = |s: &Side| -> Result<Side> {
|
||||
let mut origins = Vec::new();
|
||||
for o in s.origins() {
|
||||
origins.extend(map_face(*o)?.0);
|
||||
}
|
||||
Side::new(origins)
|
||||
.ok_or_else(|| failed("an edge meets a face the inner operation made from nothing"))
|
||||
};
|
||||
let mut edges = Vec::with_capacity(self.edges.len());
|
||||
for e in &self.edges {
|
||||
let mut origins = BTreeSet::new();
|
||||
for o in &e.origins {
|
||||
match o {
|
||||
EdgeOrigin::Edge { operand, edge } if *operand == k => {
|
||||
origins.extend(
|
||||
inner_edge(*edge)?
|
||||
.origins
|
||||
.iter()
|
||||
.map(|x| x.map_operand(lifted)),
|
||||
);
|
||||
}
|
||||
EdgeOrigin::Edge { .. } => {
|
||||
origins.insert(o.map_operand(outer));
|
||||
}
|
||||
EdgeOrigin::Meet { a, b } => {
|
||||
origins.insert(EdgeOrigin::meet(side(a)?, side(b)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
edges.push(EdgeProv {
|
||||
faces: e.faces.clone(),
|
||||
origins: origins.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
self.operands = self.operands + n - 1;
|
||||
self.faces = faces;
|
||||
self.edges = edges;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renumber the operands: operand `i` becomes `map[i]` of `operands` (several may become
|
||||
/// one, which joins their origins), or is dropped (`None`), which fails, without changing
|
||||
/// `self`, where an origin lies on it.
|
||||
pub fn renumber(&mut self, map: &[Option<usize>], operands: usize) -> Result<()> {
|
||||
if map.len() != self.operands || map.iter().flatten().any(|op| *op >= operands) {
|
||||
return Err(failed("operand map does not match"));
|
||||
}
|
||||
let to = |op: usize| {
|
||||
map.get(op)
|
||||
.copied()
|
||||
.flatten()
|
||||
.ok_or_else(|| failed("an origin lies on a dropped operand"))
|
||||
};
|
||||
let face = |o: FaceOrigin| -> Result<FaceOrigin> {
|
||||
Ok(match o {
|
||||
FaceOrigin::Face { operand, face } => FaceOrigin::Face {
|
||||
operand: to(operand)?,
|
||||
face,
|
||||
},
|
||||
FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge {
|
||||
operand: to(operand)?,
|
||||
edge,
|
||||
},
|
||||
FaceOrigin::Offset { operand, face } => FaceOrigin::Offset {
|
||||
operand: to(operand)?,
|
||||
face,
|
||||
},
|
||||
other => other,
|
||||
})
|
||||
};
|
||||
let mut faces = Vec::with_capacity(self.faces.len());
|
||||
for f in &self.faces {
|
||||
let origins: BTreeSet<FaceOrigin> =
|
||||
f.origins.iter().map(|o| face(*o)).collect::<Result<_>>()?;
|
||||
// Origins that became one are one face: no longer a merge.
|
||||
let change = if f.change == Change::Merged && origins.len() == 1 {
|
||||
Change::Modified
|
||||
} else {
|
||||
f.change
|
||||
};
|
||||
faces.push(FaceProv {
|
||||
origins: origins.into_iter().collect(),
|
||||
change,
|
||||
});
|
||||
}
|
||||
let mut edges = Vec::with_capacity(self.edges.len());
|
||||
for e in &self.edges {
|
||||
let origins: BTreeSet<EdgeOrigin> = e
|
||||
.origins
|
||||
.iter()
|
||||
.map(|o| match o {
|
||||
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge {
|
||||
operand: to(*operand)?,
|
||||
edge: *edge,
|
||||
}),
|
||||
EdgeOrigin::Meet { a, b } => {
|
||||
Ok(EdgeOrigin::meet(a.try_map(face)?, b.try_map(face)?))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
edges.push(EdgeProv {
|
||||
faces: e.faces.clone(),
|
||||
origins: origins.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
self.operands = operands;
|
||||
self.faces = faces;
|
||||
self.edges = edges;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`History::renumber`], consuming.
|
||||
pub fn renumbered(mut self, map: &[Option<usize>], operands: usize) -> Result<History> {
|
||||
self.renumber(map, operands)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// [`History::substitute`], consuming.
|
||||
pub fn substituted(mut self, k: usize, inner: &History) -> Result<History> {
|
||||
self.substitute(k, inner)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// This history, whose operand `i` was made by the operation `parts[i]` describes (all of
|
||||
/// them over the same operands), as a history over those operands.
|
||||
pub fn over(mut self, parts: &[&History]) -> Result<History> {
|
||||
let n = parts.first().map_or(0, |p| p.operands);
|
||||
if parts.len() != self.operands || parts.iter().any(|p| p.operands != n) {
|
||||
return Err(failed("operand histories do not match"));
|
||||
}
|
||||
// Each part's operands come in at its place: then part i's operand j is i * n + j.
|
||||
for (k, part) in parts.iter().enumerate().rev() {
|
||||
self.substitute(k, part)?;
|
||||
}
|
||||
let map: Vec<Option<usize>> = (0..self.operands).map(|op| op.checked_rem(n)).collect();
|
||||
self.renumber(&map, n)?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces of one origin that is under other faces too, and that still say Kept or Modified,
|
||||
/// are Split (also where histories are put side by side: pieces of one body can share a
|
||||
/// source).
|
||||
pub fn mark_shared_split(faces: &mut [FaceProv]) {
|
||||
let mut uses: HashMap<FaceOrigin, usize> = HashMap::new();
|
||||
for o in faces.iter().flat_map(|f| &f.origins) {
|
||||
*uses.entry(*o).or_default() += 1;
|
||||
}
|
||||
for f in faces.iter_mut() {
|
||||
if matches!(f.change, Change::Kept | Change::Modified)
|
||||
&& let [o] = f.origins.as_slice()
|
||||
&& uses.get(o).copied().unwrap_or(0) > 1
|
||||
{
|
||||
f.change = Change::Split;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
//! Part of a record: the bounded material regions asked for, each with the voids inside it, as a
|
||||
//! record of its own in canonical order (docs/native-brep.md, "Canonical order"), and where each
|
||||
//! of its faces, edges and vertices was in the whole. A lump of a body is the part of one of its
|
||||
//! material regions; material inside one of its voids (an island) is a lump of its own. Records
|
||||
//! of bodies apart from each other join into one by [`compound`].
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Brep, Edge, EdgeUseRef, Invalid, Loop, OrientedFace, Region, Result, Shell, require};
|
||||
|
||||
/// A record cut from a larger one, and the source index of each of its items.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Part {
|
||||
pub brep: Brep,
|
||||
pub faces: Vec<u32>,
|
||||
pub edges: Vec<u32>,
|
||||
pub vertices: Vec<u32>,
|
||||
}
|
||||
|
||||
/// The most two material regions of a record share, least first: a point, a curve, an area.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Contact {
|
||||
/// A vertex and no edge or face: they touch at a point.
|
||||
Vertex,
|
||||
/// An edge and no face: they touch along a curve.
|
||||
Edge,
|
||||
/// A face: they meet over an area.
|
||||
Face,
|
||||
}
|
||||
|
||||
/// Two material regions that meet (positions in [`Brep::material_regions`], the lower first),
|
||||
/// and the most they share.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RegionContact {
|
||||
pub regions: [usize; 2],
|
||||
pub contact: Contact,
|
||||
}
|
||||
|
||||
/// First-appearance numbering of a subset of `n` source items.
|
||||
struct Renumber {
|
||||
new: Vec<Option<u32>>,
|
||||
old: Vec<u32>,
|
||||
}
|
||||
impl Renumber {
|
||||
fn new(n: usize) -> Renumber {
|
||||
Renumber {
|
||||
new: vec![None; n],
|
||||
old: Vec::new(),
|
||||
}
|
||||
}
|
||||
/// The new index of source item `i`, numbering it now if it is new.
|
||||
fn visit(&mut self, i: u32) -> Result<u32> {
|
||||
let slot = self
|
||||
.new
|
||||
.get_mut(i as usize)
|
||||
.ok_or(Invalid("part reference"))?;
|
||||
if let Some(k) = slot {
|
||||
return Ok(*k);
|
||||
}
|
||||
let k = u32::try_from(self.old.len()).map_err(|_| Invalid("part index"))?;
|
||||
*slot = Some(k);
|
||||
self.old.push(i);
|
||||
Ok(k)
|
||||
}
|
||||
fn get(&self, i: u32) -> Option<u32> {
|
||||
self.new.get(i as usize).copied().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// Indices of the bounded material regions (not voids), in region order.
|
||||
pub fn material_regions(&self) -> Vec<usize> {
|
||||
self.regions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
.filter(|(_, r)| !r.void)
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// How the material regions meet, read from the record: each pair of regions that shares a
|
||||
/// face, an edge or a vertex, with the most they share. `regions` index
|
||||
/// [`Brep::material_regions`] (so a body's lumps); pairs not listed are apart. Regions share
|
||||
/// an edge where it is used by faces of both (more than two faces) or each has a copy of it
|
||||
/// (an edge of the same curve and interval between vertices at the same points), a vertex
|
||||
/// where edges or vertex loops of both end at it or at a copy of it (a vertex at the same
|
||||
/// point). Sorted by pair.
|
||||
pub fn region_contacts(&self) -> Result<Vec<RegionContact>> {
|
||||
let material = self.material_regions();
|
||||
// The material regions (positions in `material`) each face, edge and vertex belongs to.
|
||||
let mut on_face: Vec<Vec<usize>> = vec![Vec::new(); self.faces.len()];
|
||||
let mut on_edge: Vec<Vec<usize>> = vec![Vec::new(); self.edges.len()];
|
||||
let mut on_vertex: Vec<Vec<usize>> = vec![Vec::new(); self.vertices.len()];
|
||||
let mark = |list: &mut Vec<Vec<usize>>, i: u32, k: usize| -> Result<()> {
|
||||
let owners = list
|
||||
.get_mut(i as usize)
|
||||
.ok_or(Invalid("contact reference"))?;
|
||||
if owners.last() != Some(&k) {
|
||||
owners.push(k);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for (k, &r) in material.iter().enumerate() {
|
||||
let region = self.regions.get(r).ok_or(Invalid("region reference"))?;
|
||||
for &s in ®ion.shells {
|
||||
match self
|
||||
.shells
|
||||
.get(s as usize)
|
||||
.ok_or(Invalid("shell reference"))?
|
||||
{
|
||||
Shell::Faces { faces, wires } => {
|
||||
for f in faces {
|
||||
mark(&mut on_face, f.face, k)?;
|
||||
for l in &self
|
||||
.faces
|
||||
.get(f.face as usize)
|
||||
.ok_or(Invalid("face reference"))?
|
||||
.loops
|
||||
{
|
||||
match l {
|
||||
Loop::Edges { edges } => {
|
||||
for u in edges {
|
||||
mark(&mut on_edge, u.edge, k)?;
|
||||
}
|
||||
}
|
||||
Loop::Vertex { vertex } => mark(&mut on_vertex, *vertex, k)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
for &e in wires {
|
||||
mark(&mut on_edge, e, k)?;
|
||||
}
|
||||
}
|
||||
Shell::Wire { edges } => {
|
||||
for &e in edges {
|
||||
mark(&mut on_edge, e, k)?;
|
||||
}
|
||||
}
|
||||
Shell::Vertex { vertex } => mark(&mut on_vertex, *vertex, k)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
for (e, owners) in on_edge.iter().enumerate() {
|
||||
for &v in &self.edges.get(e).ok_or(Invalid("edge reference"))?.vertices {
|
||||
for &k in owners {
|
||||
let list = on_vertex
|
||||
.get_mut(v as usize)
|
||||
.ok_or(Invalid("contact reference"))?;
|
||||
if !list.contains(&k) {
|
||||
list.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A producer that keeps each solid's boundary closed on its own gives solids that touch a
|
||||
// copy each of the edge or vertex they share (SMLib's export splits them so): the same
|
||||
// curve over the same interval between vertices at the same points, a vertex at the same
|
||||
// point. Copies are one edge or vertex here; their owners are listed together.
|
||||
let point = |v: u32| {
|
||||
self.vertices
|
||||
.get(v as usize)
|
||||
.map(|x| x.point.map(f64::to_bits))
|
||||
.ok_or(Invalid("vertex reference"))
|
||||
};
|
||||
let mut at_point = std::collections::BTreeMap::<[u64; 3], Vec<usize>>::new();
|
||||
for (v, owners) in on_vertex.iter().enumerate().filter(|(_, o)| !o.is_empty()) {
|
||||
at_point
|
||||
.entry(point(
|
||||
u32::try_from(v).map_err(|_| Invalid("vertex index"))?,
|
||||
)?)
|
||||
.or_default()
|
||||
.extend(owners);
|
||||
}
|
||||
let mut edge_copies =
|
||||
std::collections::BTreeMap::<([[u64; 3]; 2], [u64; 2]), Vec<(&Edge, Vec<usize>)>>::new(
|
||||
);
|
||||
for (e, owners) in on_edge.iter().enumerate().filter(|(_, o)| !o.is_empty()) {
|
||||
let edge = self.edges.get(e).ok_or(Invalid("edge reference"))?;
|
||||
let key = (
|
||||
[point(edge.vertices[0])?, point(edge.vertices[1])?],
|
||||
edge.interval.map(f64::to_bits),
|
||||
);
|
||||
let same = edge_copies.entry(key).or_default();
|
||||
match same
|
||||
.iter_mut()
|
||||
.find(|(other, _)| other.curve == edge.curve && other.nurbs == edge.nurbs)
|
||||
{
|
||||
Some((_, all)) => all.extend(owners),
|
||||
None => same.push((edge, owners.clone())),
|
||||
}
|
||||
}
|
||||
let copied = |lists: Vec<Vec<usize>>| -> Vec<Vec<usize>> {
|
||||
lists
|
||||
.into_iter()
|
||||
.map(|mut l| {
|
||||
l.sort_unstable();
|
||||
l.dedup();
|
||||
l
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let edge_copies = copied(
|
||||
edge_copies
|
||||
.into_values()
|
||||
.flatten()
|
||||
.map(|(_, owners)| owners)
|
||||
.collect(),
|
||||
);
|
||||
let vertex_copies = copied(at_point.into_values().collect());
|
||||
let mut pairs = std::collections::BTreeMap::<[usize; 2], Contact>::new();
|
||||
for (owners, contact) in [
|
||||
(&on_face, Contact::Face),
|
||||
(&on_edge, Contact::Edge),
|
||||
(&edge_copies, Contact::Edge),
|
||||
(&on_vertex, Contact::Vertex),
|
||||
(&vertex_copies, Contact::Vertex),
|
||||
] {
|
||||
for list in owners.iter().filter(|l| l.len() > 1) {
|
||||
for (i, &a) in list.iter().enumerate() {
|
||||
for &b in list.iter().skip(i + 1) {
|
||||
let most = pairs.entry([a.min(b), a.max(b)]).or_insert(contact);
|
||||
*most = (*most).max(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(pairs
|
||||
.into_iter()
|
||||
.map(|(regions, contact)| RegionContact { regions, contact })
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The part made of the material regions `regions` (indices into `self.regions`, bounded,
|
||||
/// not voids): each with its outer shell, its open and wire shells, and a void for each
|
||||
/// closed inner shell. An island inside one of those voids is in the part, inside it, when
|
||||
/// its own region is among `regions`, and left out otherwise.
|
||||
pub fn part(&self, regions: &[usize]) -> Result<Part> {
|
||||
// Which region each closed shell encloses (the region listing it first), and which
|
||||
// region lies around it (the one listing it after its own outer shell; 0 for the open).
|
||||
let mut encloses: Vec<Option<usize>> = vec![None; self.shells.len()];
|
||||
let mut around: Vec<usize> = vec![0; self.shells.len()];
|
||||
for (r, region) in self.regions.iter().enumerate() {
|
||||
for (k, &s) in region.shells.iter().enumerate() {
|
||||
let i = s as usize;
|
||||
if r > 0 && k == 0 {
|
||||
*encloses
|
||||
.get_mut(i)
|
||||
.ok_or(Invalid("region shell reference"))? = Some(r);
|
||||
} else {
|
||||
*around.get_mut(i).ok_or(Invalid("region shell reference"))? = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The part's regions over source shells: each material region as it is, each void
|
||||
// inside one by its outer shell, and the infinite region around every outer shell whose
|
||||
// surrounding void is not in the part. A material region inside a void that is in the
|
||||
// part (an island in a hollow both are taken from) stays inside it.
|
||||
let mut voids: Vec<(usize, Vec<u32>)> = Vec::new();
|
||||
let mut kept: Vec<(Vec<u32>, bool)> = Vec::new();
|
||||
for &r in regions {
|
||||
let region = self.regions.get(r).ok_or(Invalid("part region"))?;
|
||||
require(r > 0 && !region.void, "a part's region is bounded material")?;
|
||||
require(
|
||||
!region.shells.is_empty(),
|
||||
"bounded region without an outer shell",
|
||||
)?;
|
||||
kept.push((region.shells.clone(), false));
|
||||
for &s in region.shells.iter().skip(1) {
|
||||
if let Some(v) = encloses.get(s as usize).copied().flatten() {
|
||||
require(
|
||||
self.regions.get(v).is_some_and(|v| v.void),
|
||||
"a closed inner shell of material encloses a void",
|
||||
)?;
|
||||
voids.push((v, vec![s]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut outer = Vec::new();
|
||||
for &r in regions {
|
||||
let Some(&s) = self.regions.get(r).and_then(|region| region.shells.first()) else {
|
||||
continue;
|
||||
};
|
||||
let container = around.get(s as usize).copied().unwrap_or(0);
|
||||
match voids.iter_mut().find(|(v, _)| *v == container) {
|
||||
Some((_, list)) => list.push(s),
|
||||
None => outer.push(s),
|
||||
}
|
||||
}
|
||||
let source_regions: Vec<(Vec<u32>, bool)> = std::iter::once((outer, false))
|
||||
.chain(kept)
|
||||
.chain(voids.into_iter().map(|(_, list)| (list, true)))
|
||||
.collect();
|
||||
self.renumbered(&source_regions)
|
||||
}
|
||||
|
||||
/// The record in canonical order (docs/native-brep.md, "Canonical order"), every region and
|
||||
/// shell kept, open, wire and vertex shells included, and where each item was in it. For a
|
||||
/// record whose parts a builder put together in its own order: each edge's uses are worked
|
||||
/// out from the walk (a list that already names exactly those uses keeps its radial order).
|
||||
/// A face no shell uses is left out.
|
||||
pub fn canonical(&self) -> Result<Part> {
|
||||
let regions: Vec<(Vec<u32>, bool)> = self
|
||||
.regions
|
||||
.iter()
|
||||
.map(|r| (r.shells.clone(), r.void))
|
||||
.collect();
|
||||
self.renumbered(®ions)
|
||||
}
|
||||
|
||||
/// The record of `source_regions` (each the source shells it lists and whether it is a void,
|
||||
/// the infinite region first), renumbered in canonical order.
|
||||
fn renumbered(&self, source_regions: &[(Vec<u32>, bool)]) -> Result<Part> {
|
||||
// Canonical order: shells and faces walking regions → shells → oriented faces.
|
||||
let mut shells = Renumber::new(self.shells.len());
|
||||
let mut faces = Renumber::new(self.faces.len());
|
||||
let mut direction = vec![true; self.faces.len()];
|
||||
let mut new_regions = Vec::with_capacity(source_regions.len());
|
||||
for (list, void) in source_regions {
|
||||
let mut mapped = Vec::with_capacity(list.len());
|
||||
for &s in list {
|
||||
mapped.push(shells.visit(s)?);
|
||||
if let Some(Shell::Faces { faces: uses, .. }) = self.shells.get(s as usize) {
|
||||
for f in uses {
|
||||
if faces.get(f.face).is_none()
|
||||
&& let Some(d) = direction.get_mut(f.face as usize)
|
||||
{
|
||||
*d = f.forward;
|
||||
}
|
||||
faces.visit(f.face)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
new_regions.push(Region {
|
||||
shells: mapped,
|
||||
void: *void,
|
||||
});
|
||||
}
|
||||
// Edges walking faces → loops → oriented edges (backwards where the face's first side is
|
||||
// reversed), then wire edges walking shells.
|
||||
// Each edge's first use in that walk is its primary use: (new face, loop, position).
|
||||
let mut edges = Renumber::new(self.edges.len());
|
||||
let mut walked: Vec<Vec<EdgeUseRef>> = vec![Vec::new(); self.edges.len()];
|
||||
for (k, &f) in faces.old.iter().enumerate() {
|
||||
let face = self
|
||||
.faces
|
||||
.get(f as usize)
|
||||
.ok_or(Invalid("face reference"))?;
|
||||
let forward = direction.get(f as usize).copied().unwrap_or(true);
|
||||
let k = index(k)?;
|
||||
for (li, l) in face.loops.iter().enumerate() {
|
||||
let Loop::Edges { edges: uses } = l else {
|
||||
continue;
|
||||
};
|
||||
let n = uses.len();
|
||||
for step in 0..n {
|
||||
let p = if forward { step } else { n - 1 - step };
|
||||
let u = uses.get(p).ok_or(Invalid("edge reference"))?;
|
||||
edges.visit(u.edge)?;
|
||||
let at = EdgeUseRef {
|
||||
face: k,
|
||||
r#loop: index(li)?,
|
||||
position: index(p)?,
|
||||
upper: !u.forward,
|
||||
};
|
||||
walked
|
||||
.get_mut(u.edge as usize)
|
||||
.ok_or(Invalid("edge reference"))?
|
||||
.push(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
for &s in &shells.old {
|
||||
let wires = match self
|
||||
.shells
|
||||
.get(s as usize)
|
||||
.ok_or(Invalid("shell reference"))?
|
||||
{
|
||||
Shell::Faces { wires, .. } => wires.as_slice(),
|
||||
Shell::Wire { edges } => edges.as_slice(),
|
||||
Shell::Vertex { .. } => &[],
|
||||
};
|
||||
for &e in wires {
|
||||
edges.visit(e)?;
|
||||
}
|
||||
}
|
||||
// Vertices walking edges (start, end), then vertex loops, then vertex shells.
|
||||
let mut vertices = Renumber::new(self.vertices.len());
|
||||
for &e in &edges.old {
|
||||
for &v in &self
|
||||
.edges
|
||||
.get(e as usize)
|
||||
.ok_or(Invalid("edge reference"))?
|
||||
.vertices
|
||||
{
|
||||
vertices.visit(v)?;
|
||||
}
|
||||
}
|
||||
for &f in &faces.old {
|
||||
for l in &self
|
||||
.faces
|
||||
.get(f as usize)
|
||||
.ok_or(Invalid("face reference"))?
|
||||
.loops
|
||||
{
|
||||
if let Loop::Vertex { vertex } = l {
|
||||
vertices.visit(*vertex)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for &s in &shells.old {
|
||||
if let Some(Shell::Vertex { vertex }) = self.shells.get(s as usize) {
|
||||
vertices.visit(*vertex)?;
|
||||
}
|
||||
}
|
||||
|
||||
let map = |r: &Renumber, i: u32| r.get(i).ok_or(Invalid("part reference outside the part"));
|
||||
let new_shells = shells
|
||||
.old
|
||||
.iter()
|
||||
.map(|&s| {
|
||||
Ok(
|
||||
match self
|
||||
.shells
|
||||
.get(s as usize)
|
||||
.ok_or(Invalid("shell reference"))?
|
||||
{
|
||||
Shell::Faces { faces: uses, wires } => Shell::Faces {
|
||||
faces: uses
|
||||
.iter()
|
||||
.map(|f| {
|
||||
Ok(OrientedFace {
|
||||
face: map(&faces, f.face)?,
|
||||
forward: f.forward,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
wires: wires
|
||||
.iter()
|
||||
.map(|&e| map(&edges, e))
|
||||
.collect::<Result<_>>()?,
|
||||
},
|
||||
Shell::Wire { edges: wire } => Shell::Wire {
|
||||
edges: wire
|
||||
.iter()
|
||||
.map(|&e| map(&edges, e))
|
||||
.collect::<Result<_>>()?,
|
||||
},
|
||||
Shell::Vertex { vertex } => Shell::Vertex {
|
||||
vertex: map(&vertices, *vertex)?,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let new_faces = faces
|
||||
.old
|
||||
.iter()
|
||||
.map(|&f| {
|
||||
let mut face = self
|
||||
.faces
|
||||
.get(f as usize)
|
||||
.ok_or(Invalid("face reference"))?
|
||||
.clone();
|
||||
for l in &mut face.loops {
|
||||
match l {
|
||||
Loop::Edges { edges: uses } => {
|
||||
for u in uses {
|
||||
u.edge = map(&edges, u.edge)?;
|
||||
}
|
||||
}
|
||||
Loop::Vertex { vertex } => *vertex = map(&vertices, *vertex)?,
|
||||
}
|
||||
}
|
||||
Ok(face)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let new_edges = edges
|
||||
.old
|
||||
.iter()
|
||||
.map(|&e| {
|
||||
let mut edge = self
|
||||
.edges
|
||||
.get(e as usize)
|
||||
.ok_or(Invalid("edge reference"))?
|
||||
.clone();
|
||||
edge.vertices = [
|
||||
map(&vertices, edge.vertices[0])?,
|
||||
map(&vertices, edge.vertices[1])?,
|
||||
];
|
||||
edge.uses = part_uses(
|
||||
&edge.uses,
|
||||
&faces,
|
||||
walked.get(e as usize).map_or(&[][..], Vec::as_slice),
|
||||
);
|
||||
Ok(edge)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let new_vertices = vertices
|
||||
.old
|
||||
.iter()
|
||||
.map(|&v| {
|
||||
self.vertices
|
||||
.get(v as usize)
|
||||
.cloned()
|
||||
.ok_or(Invalid("vertex reference"))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
let brep = Brep {
|
||||
version: self.version,
|
||||
tolerance: self.tolerance,
|
||||
regions: new_regions,
|
||||
shells: new_shells,
|
||||
faces: new_faces,
|
||||
edges: new_edges,
|
||||
vertices: new_vertices,
|
||||
};
|
||||
brep.validate_built()?;
|
||||
Ok(Part {
|
||||
brep,
|
||||
faces: faces.old,
|
||||
edges: edges.old,
|
||||
vertices: vertices.old,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge's uses on the part's faces (`walked`: in the canonical walk's order, the first its
|
||||
/// primary use): the source's radial cycle with the other faces' uses left out (a sub-cycle keeps
|
||||
/// its order), turned to start at the primary use, where the source names exactly the walked
|
||||
/// uses; the walk's own order otherwise (a record whose builder left them to be worked out).
|
||||
fn part_uses(uses: &[EdgeUseRef], faces: &Renumber, walked: &[EdgeUseRef]) -> Vec<EdgeUseRef> {
|
||||
let mut kept: Vec<EdgeUseRef> = uses
|
||||
.iter()
|
||||
.filter_map(|u| faces.get(u.face).map(|face| EdgeUseRef { face, ..*u }))
|
||||
.collect();
|
||||
if kept.len() != walked.len() || !walked.iter().all(|w| kept.contains(w)) {
|
||||
return walked.to_vec();
|
||||
}
|
||||
if let Some(first) = walked
|
||||
.first()
|
||||
.and_then(|p| kept.iter().position(|u| u == p))
|
||||
{
|
||||
kept.rotate_left(first);
|
||||
}
|
||||
kept
|
||||
}
|
||||
|
||||
fn index(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).map_err(|_| Invalid("part index"))
|
||||
}
|
||||
|
||||
/// Bodies' records joined into one record of all their material, side by side, in canonical
|
||||
/// order: what bodies apart from each other are together. The part's `faces`, `edges` and
|
||||
/// `vertices` number the inputs one after another (input 0's items first).
|
||||
pub fn compound(records: &[&Brep]) -> Result<Part> {
|
||||
let mut all = Brep {
|
||||
version: super::VERSION,
|
||||
tolerance: records.iter().map(|r| r.tolerance).fold(0.0, f64::max),
|
||||
regions: vec![Region {
|
||||
shells: Vec::new(),
|
||||
void: false,
|
||||
}],
|
||||
shells: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
vertices: Vec::new(),
|
||||
};
|
||||
for r in records {
|
||||
let offset = |n: usize| u32::try_from(n).map_err(|_| Invalid("compound size"));
|
||||
let (s0, f0, e0, v0) = (
|
||||
offset(all.shells.len())?,
|
||||
offset(all.faces.len())?,
|
||||
offset(all.edges.len())?,
|
||||
offset(all.vertices.len())?,
|
||||
);
|
||||
let (infinite, bounded) = r.regions.split_first().ok_or(Invalid("region count"))?;
|
||||
if let Some(open) = all.regions.first_mut() {
|
||||
open.shells.extend(infinite.shells.iter().map(|s| s + s0));
|
||||
}
|
||||
all.regions.extend(bounded.iter().map(|region| Region {
|
||||
shells: region.shells.iter().map(|s| s + s0).collect(),
|
||||
void: region.void,
|
||||
}));
|
||||
all.shells.extend(r.shells.iter().map(|shell| {
|
||||
match shell {
|
||||
Shell::Faces { faces, wires } => Shell::Faces {
|
||||
faces: faces
|
||||
.iter()
|
||||
.map(|f| OrientedFace {
|
||||
face: f.face + f0,
|
||||
forward: f.forward,
|
||||
})
|
||||
.collect(),
|
||||
wires: wires.iter().map(|e| e + e0).collect(),
|
||||
},
|
||||
Shell::Wire { edges } => Shell::Wire {
|
||||
edges: edges.iter().map(|e| e + e0).collect(),
|
||||
},
|
||||
Shell::Vertex { vertex } => Shell::Vertex {
|
||||
vertex: vertex + v0,
|
||||
},
|
||||
}
|
||||
}));
|
||||
all.faces.extend(r.faces.iter().map(|f| {
|
||||
let mut f = f.clone();
|
||||
for l in &mut f.loops {
|
||||
match l {
|
||||
Loop::Edges { edges } => edges.iter_mut().for_each(|u| u.edge += e0),
|
||||
Loop::Vertex { vertex } => *vertex += v0,
|
||||
}
|
||||
}
|
||||
f
|
||||
}));
|
||||
all.edges.extend(r.edges.iter().map(|e| {
|
||||
let mut e = e.clone();
|
||||
e.vertices = e.vertices.map(|v| v + v0);
|
||||
e.uses.iter_mut().for_each(|u| u.face += f0);
|
||||
e
|
||||
}));
|
||||
all.vertices.extend(r.vertices.iter().cloned());
|
||||
}
|
||||
all.part(&all.material_regions())
|
||||
}
|
||||
@@ -0,0 +1,788 @@
|
||||
//! The record's structural checks and its serde form. Fixtures are built by hand: planar faces
|
||||
//! with placeholder pcurves, which validation reads structurally (geometric checks, forms and
|
||||
//! fitted pcurves are the kernel's, and tested there).
|
||||
use super::*;
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
fn segment(a: f64, b: f64) -> Curve2 {
|
||||
Curve2::Spline {
|
||||
knots: vec![a, a, b, b],
|
||||
points: vec![[a, 0.], [b, 1.]],
|
||||
}
|
||||
}
|
||||
fn a_frame() -> Frame {
|
||||
Frame {
|
||||
origin: [1., 2., 3.],
|
||||
x_axis: [1., 0., 0.],
|
||||
y_axis: [0., 1., 0.],
|
||||
}
|
||||
}
|
||||
|
||||
/// Every edge's uses as the canonical walk meets them (faces are all forward here).
|
||||
fn list_uses(b: &mut Brep) {
|
||||
for e in &mut b.edges {
|
||||
e.uses.clear();
|
||||
}
|
||||
for (fi, f) in b.faces.iter().enumerate() {
|
||||
for (li, l) in f.loops.iter().enumerate() {
|
||||
if let Loop::Edges { edges } = l {
|
||||
for (p, u) in edges.iter().enumerate() {
|
||||
let at = EdgeUseRef {
|
||||
face: fi as u32,
|
||||
r#loop: li as u32,
|
||||
position: p as u32,
|
||||
upper: !u.forward,
|
||||
};
|
||||
b.edges[u.edge as usize].uses.push(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Planar faces, one per vertex cycle, grouped into shells; edges and vertices numbered in
|
||||
/// canonical order as the cycles first meet them.
|
||||
fn faceted(points: &[[f64; 3]], shells: &[&[&[usize]]], regions: Vec<Region>) -> Brep {
|
||||
let mut b = Brep {
|
||||
version: VERSION,
|
||||
tolerance: 1e-7,
|
||||
regions,
|
||||
shells: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
vertices: Vec::new(),
|
||||
};
|
||||
let mut vertex_of = vec![None; points.len()];
|
||||
let mut vertex = |b: &mut Brep, p: usize| -> u32 {
|
||||
*vertex_of[p].get_or_insert_with(|| {
|
||||
b.vertices.push(Vertex {
|
||||
point: points[p],
|
||||
tolerance: 1e-7,
|
||||
});
|
||||
b.vertices.len() as u32 - 1
|
||||
})
|
||||
};
|
||||
let mut by_pair = std::collections::HashMap::new();
|
||||
for cycles in shells {
|
||||
let mut uses = Vec::new();
|
||||
for cycle in *cycles {
|
||||
let mut loop_ = Vec::new();
|
||||
for k in 0..cycle.len() {
|
||||
let (a, z) = (cycle[k], cycle[(k + 1) % cycle.len()]);
|
||||
let (edge, forward) = if let Some(&e) = by_pair.get(&(z, a)) {
|
||||
(e, false)
|
||||
} else {
|
||||
let vs = [vertex(&mut b, a), vertex(&mut b, z)];
|
||||
b.edges.push(Edge {
|
||||
nurbs: None,
|
||||
curve: Curve::Line {
|
||||
start: points[a],
|
||||
end: points[z],
|
||||
},
|
||||
interval: [0., 1.],
|
||||
vertices: vs,
|
||||
tolerance: 1e-7,
|
||||
uses: vec![],
|
||||
});
|
||||
let e = b.edges.len() as u32 - 1;
|
||||
by_pair.insert((a, z), e);
|
||||
(e, true)
|
||||
};
|
||||
loop_.push(OrientedEdge {
|
||||
edge,
|
||||
forward,
|
||||
pcurve: segment(0., 1.),
|
||||
});
|
||||
}
|
||||
uses.push(OrientedFace {
|
||||
face: b.faces.len() as u32,
|
||||
forward: true,
|
||||
});
|
||||
// The plane through the cycle's first three points: its natural normal is the cycle's.
|
||||
let [o, p, q] = [0, 1, 2].map(|k| points[cycle[k]]);
|
||||
let axis = |a: [f64; 3]| [a[0] - o[0], a[1] - o[1], a[2] - o[2]];
|
||||
let surface = Surface::Plane {
|
||||
origin: o,
|
||||
u_axis: axis(p),
|
||||
v_axis: axis(q),
|
||||
};
|
||||
b.faces.push(Face {
|
||||
nurbs: None,
|
||||
surface,
|
||||
domain: [[-10., 10.], [-10., 10.]],
|
||||
loops: vec![Loop::Edges { edges: loop_ }],
|
||||
});
|
||||
}
|
||||
b.shells.push(Shell::Faces {
|
||||
faces: uses,
|
||||
wires: vec![],
|
||||
});
|
||||
}
|
||||
list_uses(&mut b);
|
||||
b
|
||||
}
|
||||
|
||||
fn infinite(shells: Vec<u32>) -> Region {
|
||||
Region {
|
||||
shells,
|
||||
void: false,
|
||||
}
|
||||
}
|
||||
const SQUARE: [[f64; 3]; 4] = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]];
|
||||
fn sheet() -> Brep {
|
||||
faceted(&SQUARE, &[&[&[0, 1, 2, 3]]], vec![infinite(vec![0])])
|
||||
}
|
||||
const TETRA: [[f64; 3]; 4] = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]];
|
||||
const TETRA_FACES: [&[usize]; 4] = [&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[0, 3, 2]];
|
||||
fn tetra() -> Brep {
|
||||
faceted(
|
||||
&TETRA,
|
||||
&[&TETRA_FACES],
|
||||
vec![
|
||||
infinite(vec![0]),
|
||||
Region {
|
||||
shells: vec![0],
|
||||
void: false,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
/// A tetrahedron with a tetrahedral void inside it.
|
||||
fn hollow() -> Brep {
|
||||
let mut points = TETRA.to_vec();
|
||||
points.extend(TETRA.map(|p| p.map(|x| 0.2 + 0.1 * x)));
|
||||
let inner: Vec<Vec<usize>> = TETRA_FACES
|
||||
.iter()
|
||||
.map(|c| c.iter().rev().map(|i| i + 4).collect())
|
||||
.collect();
|
||||
let inner: Vec<&[usize]> = inner.iter().map(Vec::as_slice).collect();
|
||||
faceted(
|
||||
&points,
|
||||
&[&TETRA_FACES, &inner],
|
||||
vec![
|
||||
infinite(vec![0]),
|
||||
Region {
|
||||
shells: vec![0, 1],
|
||||
void: false,
|
||||
},
|
||||
Region {
|
||||
shells: vec![1],
|
||||
void: true,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
/// The sheet with a vertex loop inside it, a wire shell and a vertex shell beside it.
|
||||
fn sheet_with_lower_forms() -> Brep {
|
||||
let mut b = sheet();
|
||||
let n = b.vertices.len() as u32;
|
||||
// Vertex order: edge vertices first (the wire edge's), then the vertex loop's, then the shell's.
|
||||
b.vertices.extend(
|
||||
[[2., 0., 0.], [3., 0., 0.], [0.5, 0.5, 0.], [5., 5., 5.]].map(|point| Vertex {
|
||||
point,
|
||||
tolerance: 1e-7,
|
||||
}),
|
||||
);
|
||||
b.edges.push(Edge {
|
||||
nurbs: None,
|
||||
curve: Curve::Line {
|
||||
start: [2., 0., 0.],
|
||||
end: [3., 0., 0.],
|
||||
},
|
||||
interval: [0., 1.],
|
||||
vertices: [n, n + 1],
|
||||
tolerance: 1e-7,
|
||||
uses: vec![],
|
||||
});
|
||||
b.faces[0].loops.push(Loop::Vertex { vertex: n + 2 });
|
||||
b.shells.push(Shell::Wire { edges: vec![4] });
|
||||
b.shells.push(Shell::Vertex { vertex: n + 3 });
|
||||
b.regions[0].shells.extend([1, 2]);
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_structure_validates_and_round_trips() {
|
||||
for (name, body) in [
|
||||
("sheet", sheet()),
|
||||
("closed", tetra()),
|
||||
("void", hollow()),
|
||||
("lower forms", sheet_with_lower_forms()),
|
||||
] {
|
||||
body.validate().unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let bytes = serde_json::to_vec(&body).unwrap();
|
||||
let restored: Brep = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(body, restored, "{name}");
|
||||
assert!(!String::from_utf8(bytes).unwrap().contains("truck"));
|
||||
}
|
||||
}
|
||||
|
||||
/// One defect each; every one is refused.
|
||||
#[test]
|
||||
fn one_defect_fixtures_are_refused() {
|
||||
let cases: Vec<(&str, &str, Brep)> = vec![
|
||||
("version", "unsupported version", {
|
||||
let mut b = sheet();
|
||||
b.version = 3;
|
||||
b
|
||||
}),
|
||||
("tolerance", "tolerance", {
|
||||
let mut b = sheet();
|
||||
b.tolerance = f64::NAN;
|
||||
b
|
||||
}),
|
||||
("vertex tolerance", "tolerance", {
|
||||
let mut b = sheet();
|
||||
b.vertices[0].tolerance = 0.;
|
||||
b
|
||||
}),
|
||||
("coordinate", "non-finite or excessive coordinate", {
|
||||
let mut b = sheet();
|
||||
b.vertices[0].point[0] = f64::INFINITY;
|
||||
b
|
||||
}),
|
||||
("edge reference", "edge reference", {
|
||||
let mut b = sheet();
|
||||
if let Loop::Edges { edges } = &mut b.faces[0].loops[0] {
|
||||
edges[0].edge = u32::MAX;
|
||||
}
|
||||
b
|
||||
}),
|
||||
("edge vertices", "edge vertices", {
|
||||
let mut b = sheet();
|
||||
b.edges[0].vertices[0] = u32::MAX;
|
||||
b
|
||||
}),
|
||||
("disconnected loop", "disconnected loop", {
|
||||
let mut b = sheet();
|
||||
if let Loop::Edges { edges } = &mut b.faces[0].loops[0] {
|
||||
edges.swap(1, 2);
|
||||
}
|
||||
list_uses(&mut b);
|
||||
b
|
||||
}),
|
||||
("reversed use", "disconnected loop", {
|
||||
let mut b = sheet();
|
||||
if let Loop::Edges { edges } = &mut b.faces[0].loops[0] {
|
||||
edges[1].forward = false;
|
||||
}
|
||||
list_uses(&mut b);
|
||||
b
|
||||
}),
|
||||
("outer vertex loop", "outer loop is not an edge loop", {
|
||||
let mut b = sheet_with_lower_forms();
|
||||
b.faces[0].loops.swap(0, 1);
|
||||
b
|
||||
}),
|
||||
("missing use", "edge uses do not list the edge's uses", {
|
||||
let mut b = tetra();
|
||||
b.edges[0].uses.pop();
|
||||
b
|
||||
}),
|
||||
(
|
||||
"repeated use",
|
||||
"edge use listed twice or not a use of the edge",
|
||||
{
|
||||
let mut b = tetra();
|
||||
let first = b.edges[0].uses[0];
|
||||
b.edges[0].uses[1] = first;
|
||||
b
|
||||
},
|
||||
),
|
||||
(
|
||||
"primary not first",
|
||||
"edge's primary use is not its first",
|
||||
{
|
||||
let mut b = tetra();
|
||||
b.edges[0].uses.reverse();
|
||||
b
|
||||
},
|
||||
),
|
||||
("upper", "edge use listed twice or not a use of the edge", {
|
||||
let mut b = tetra();
|
||||
b.edges[0].uses[1].upper = !b.edges[0].uses[1].upper;
|
||||
b
|
||||
}),
|
||||
("wire edge with uses", "wire edge with face uses", {
|
||||
let mut b = sheet_with_lower_forms();
|
||||
let u = b.edges[0].uses[0];
|
||||
b.edges[4].uses.push(u);
|
||||
b
|
||||
}),
|
||||
(
|
||||
"edge interval outside a spline",
|
||||
"parameter interval outside the natural domain",
|
||||
{
|
||||
let mut b = sheet();
|
||||
b.edges[0].curve = Curve::Spline {
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
points: vec![SQUARE[0], SQUARE[1]],
|
||||
};
|
||||
b.edges[0].interval = [0., 1.5];
|
||||
b
|
||||
},
|
||||
),
|
||||
("empty edge interval", "empty parameter interval", {
|
||||
let mut b = sheet();
|
||||
b.edges[0].interval = [1., 1.];
|
||||
b
|
||||
}),
|
||||
(
|
||||
"face domain outside a spline surface",
|
||||
"parameter interval outside the natural domain",
|
||||
{
|
||||
let mut b = sheet();
|
||||
b.faces[0].surface = Surface::Spline {
|
||||
u_knots: vec![0., 0., 1., 1.],
|
||||
v_knots: vec![0., 0., 1., 1.],
|
||||
points: vec![
|
||||
vec![[0.; 3], [0., 1., 0.]],
|
||||
vec![[1., 0., 0.], [1., 1., 0.]],
|
||||
],
|
||||
};
|
||||
b
|
||||
},
|
||||
),
|
||||
("edges out of order", "edges out of canonical order", {
|
||||
let mut b = sheet();
|
||||
b.edges.swap(0, 1);
|
||||
for f in &mut b.faces {
|
||||
for l in &mut f.loops {
|
||||
if let Loop::Edges { edges } = l {
|
||||
for u in edges {
|
||||
u.edge = [1, 0, 2, 3][u.edge as usize];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
list_uses(&mut b);
|
||||
b
|
||||
}),
|
||||
(
|
||||
"vertices out of order",
|
||||
"vertices out of canonical order",
|
||||
{
|
||||
let mut b = sheet();
|
||||
b.vertices.swap(0, 1);
|
||||
for e in &mut b.edges {
|
||||
e.vertices = e.vertices.map(|v| [1, 0, 2, 3][v as usize]);
|
||||
}
|
||||
b
|
||||
},
|
||||
),
|
||||
("faces out of order", "faces out of canonical order", {
|
||||
let mut b = tetra();
|
||||
if let Shell::Faces { faces, .. } = &mut b.shells[0] {
|
||||
faces.swap(0, 1);
|
||||
}
|
||||
b
|
||||
}),
|
||||
("orphan vertex", "vertices out of canonical order", {
|
||||
let mut b = sheet();
|
||||
b.vertices.push(Vertex {
|
||||
point: [9.; 3],
|
||||
tolerance: 1e-7,
|
||||
});
|
||||
b
|
||||
}),
|
||||
(
|
||||
"shell twice in a region",
|
||||
"shell listed twice in a region",
|
||||
{
|
||||
// The void lists its one shell twice: still within the shell count.
|
||||
let mut b = hollow();
|
||||
let void = b.regions.last_mut().unwrap();
|
||||
void.shells.push(void.shells[0]);
|
||||
b
|
||||
},
|
||||
),
|
||||
("void infinite region", "the infinite region is a void", {
|
||||
let mut b = sheet();
|
||||
b.regions[0].void = true;
|
||||
b
|
||||
}),
|
||||
(
|
||||
"open outer shell",
|
||||
"an open, wire or vertex shell lies in one region",
|
||||
{
|
||||
let mut b = sheet();
|
||||
b.regions.push(Region {
|
||||
shells: vec![0],
|
||||
void: false,
|
||||
});
|
||||
b
|
||||
},
|
||||
),
|
||||
(
|
||||
"closed shell in one region",
|
||||
"a closed shell bounds two regions and encloses one",
|
||||
{
|
||||
let mut b = tetra();
|
||||
b.regions.pop();
|
||||
b
|
||||
},
|
||||
),
|
||||
(
|
||||
"bounded region without shells",
|
||||
"bounded region without an outer shell",
|
||||
{
|
||||
let mut b = tetra();
|
||||
b.regions.push(Region {
|
||||
shells: vec![],
|
||||
void: true,
|
||||
});
|
||||
b
|
||||
},
|
||||
),
|
||||
(
|
||||
"unlisted shell",
|
||||
"a closed shell bounds two regions and encloses one",
|
||||
{
|
||||
let mut b = hollow();
|
||||
b.regions[1].shells.pop();
|
||||
b.regions.pop();
|
||||
b
|
||||
},
|
||||
),
|
||||
(
|
||||
"face used three times",
|
||||
"face used by neither one nor two oriented faces",
|
||||
{
|
||||
let mut b = tetra();
|
||||
if let Shell::Faces { faces, .. } = &mut b.shells[0] {
|
||||
faces.push(OrientedFace {
|
||||
face: 0,
|
||||
forward: false,
|
||||
});
|
||||
faces.push(OrientedFace {
|
||||
face: 0,
|
||||
forward: true,
|
||||
});
|
||||
}
|
||||
b
|
||||
},
|
||||
),
|
||||
("face twice in one shell", "a face's two sides", {
|
||||
let mut b = tetra();
|
||||
if let Shell::Faces { faces, .. } = &mut b.shells[0] {
|
||||
faces.push(OrientedFace {
|
||||
face: 0,
|
||||
forward: false,
|
||||
});
|
||||
}
|
||||
b
|
||||
}),
|
||||
];
|
||||
for (name, expected, body) in cases {
|
||||
let error = body
|
||||
.validate()
|
||||
.err()
|
||||
.unwrap_or_else(|| panic!("{name} was accepted"));
|
||||
assert_eq!(error.0, expected, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reversed_face_is_walked_backwards() {
|
||||
// The sheet seen from below: its first (only) side is reversed, so the canonical walk
|
||||
// meets its loop's edges last to first.
|
||||
let mut b = sheet();
|
||||
if let Shell::Faces { faces, .. } = &mut b.shells[0] {
|
||||
faces[0].forward = false;
|
||||
}
|
||||
assert!(b.validate().is_err());
|
||||
// Renumber the edges (and vertices) in the backward walk's order.
|
||||
let Loop::Edges { edges } = &mut b.faces[0].loops[0] else {
|
||||
panic!("an edge loop")
|
||||
};
|
||||
let order: Vec<u32> = edges.iter().rev().map(|u| u.edge).collect();
|
||||
let mut new = vec![0; order.len()];
|
||||
for (i, e) in order.iter().enumerate() {
|
||||
new[*e as usize] = i as u32;
|
||||
}
|
||||
for u in edges.iter_mut() {
|
||||
u.edge = new[u.edge as usize];
|
||||
}
|
||||
let mut edges_ = b.edges.clone();
|
||||
for (old, e) in b.edges.iter().enumerate() {
|
||||
edges_[new[old] as usize] = e.clone();
|
||||
}
|
||||
b.edges = edges_;
|
||||
let mut seen = Vec::new();
|
||||
for e in &b.edges {
|
||||
for v in e.vertices {
|
||||
if !seen.contains(&v) {
|
||||
seen.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
let vertices = seen
|
||||
.iter()
|
||||
.map(|&v| b.vertices[v as usize].clone())
|
||||
.collect();
|
||||
b.vertices = vertices;
|
||||
for e in &mut b.edges {
|
||||
e.vertices = e
|
||||
.vertices
|
||||
.map(|v| seen.iter().position(|&s| s == v).unwrap() as u32);
|
||||
}
|
||||
for e in &mut b.edges {
|
||||
e.uses.clear();
|
||||
}
|
||||
let Loop::Edges { edges } = &b.faces[0].loops[0] else {
|
||||
panic!("an edge loop")
|
||||
};
|
||||
for (p, u) in edges.iter().enumerate().rev() {
|
||||
b.edges[u.edge as usize].uses.push(EdgeUseRef {
|
||||
face: 0,
|
||||
r#loop: 0,
|
||||
position: p as u32,
|
||||
upper: !u.forward,
|
||||
});
|
||||
}
|
||||
b.validate().unwrap();
|
||||
}
|
||||
|
||||
/// A valid record of more than [`MAX_ITEMS`] items: the sheet with its first edge a polyline of
|
||||
/// that many points (#3992).
|
||||
fn over_the_item_budget() -> Brep {
|
||||
let mut b = sheet();
|
||||
b.edges[0].curve = Curve::Polyline {
|
||||
points: (0..MAX_ITEMS).map(|i| [i as f64 * 1e-6, 0., 0.]).collect(),
|
||||
};
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_item_budget_bounds_input_records_and_not_the_kernels_own() {
|
||||
// A record from a file or the control channel is refused past the budget; the same record
|
||||
// built by the kernel in memory (a boolean's result) is its own size and is taken (#3992).
|
||||
let big = over_the_item_budget();
|
||||
assert_eq!(big.validate().map_err(|e| e.0), Err("item budget"));
|
||||
big.validate_built().unwrap();
|
||||
// Every other check still holds for a built record.
|
||||
let mut collapsed = big;
|
||||
if let Curve::Polyline { points } = &mut collapsed.edges[0].curve {
|
||||
points[1] = points[0];
|
||||
}
|
||||
assert_eq!(
|
||||
collapsed.validate_built().map_err(|e| e.0),
|
||||
Err("collapsed polyline segment")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_domains_weights_and_nesting_are_bounded() {
|
||||
let mut budget = Budget::input();
|
||||
assert!(knots(&[0., 0., 1., 1.], 2, &mut budget).is_ok());
|
||||
for bad in [
|
||||
vec![0., 0., 0., 0.],
|
||||
vec![0., 1., 0., 1.],
|
||||
vec![0., 0., f64::INFINITY, 1.],
|
||||
] {
|
||||
assert!(knots(&bad, 2, &mut Budget::input()).is_err());
|
||||
}
|
||||
let c = Curve::RationalSpline {
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
homogeneous: vec![[0., 0., 0., 0.], [1., 0., 0., 1.]],
|
||||
};
|
||||
assert!(curve(&c, 0, &mut Budget::input()).is_err());
|
||||
assert!(
|
||||
curve(
|
||||
&Curve::Line {
|
||||
start: [0.; 3],
|
||||
end: [1.; 3]
|
||||
},
|
||||
MAX_DEPTH + 1,
|
||||
&mut Budget::input()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
Budget {
|
||||
used: MAX_ITEMS,
|
||||
limit: MAX_ITEMS
|
||||
}
|
||||
.add(1)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
curve_natural(&Curve::Spline {
|
||||
knots: vec![0., 0., 0., 1., 2., 2., 2.],
|
||||
points: vec![[0.; 3]; 4]
|
||||
}),
|
||||
Natural::Range(0., 2.)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_procedural_records_are_validated_and_round_trip() {
|
||||
let plane = |z: f64| {
|
||||
Box::new(Surface::Plane {
|
||||
origin: [0., 0., z],
|
||||
u_axis: [1., 0., 0.],
|
||||
v_axis: [0., 1., 0.],
|
||||
})
|
||||
};
|
||||
let leader = Box::new(Curve::Polyline {
|
||||
points: vec![[0.; 3], [0.; 3], [1., 0., 0.]],
|
||||
});
|
||||
let c = Curve::SurfaceIntersection {
|
||||
surfaces: [plane(0.), plane(1.)],
|
||||
leader: leader.clone(),
|
||||
tolerance: 1e-6,
|
||||
};
|
||||
curve(&c, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(curve_natural(&c), Natural::Range(0., 2.));
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Curve>(&serde_json::to_string(&c).unwrap()).unwrap(),
|
||||
c
|
||||
);
|
||||
let line = Box::new(Curve::Line {
|
||||
start: [0.; 3],
|
||||
end: [1.; 3],
|
||||
});
|
||||
for bad in [
|
||||
Curve::SurfaceIntersection {
|
||||
surfaces: [plane(0.), plane(1.)],
|
||||
leader: line.clone(),
|
||||
tolerance: 1e-6,
|
||||
},
|
||||
Curve::SurfaceIntersection {
|
||||
surfaces: [plane(0.), plane(1.)],
|
||||
leader: leader.clone(),
|
||||
tolerance: f64::NAN,
|
||||
},
|
||||
Curve::SurfaceIntersection {
|
||||
surfaces: [plane(f64::INFINITY), plane(1.)],
|
||||
leader,
|
||||
tolerance: 1e-6,
|
||||
},
|
||||
] {
|
||||
assert!(curve(&bad, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
let identity = [
|
||||
[1., 0., 0., 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
];
|
||||
let s = Surface::PlacedRevolution {
|
||||
profile: line.clone(),
|
||||
origin: [0.; 3],
|
||||
axis: [0., 0., 1.],
|
||||
matrix: identity,
|
||||
forward: false,
|
||||
};
|
||||
surface(&s, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Surface>(&serde_json::to_string(&s).unwrap()).unwrap(),
|
||||
s
|
||||
);
|
||||
let bad = Surface::PlacedRevolution {
|
||||
profile: line,
|
||||
origin: [0.; 3],
|
||||
axis: [0., 0., 1.],
|
||||
matrix: [[f64::NAN; 4]; 4],
|
||||
forward: true,
|
||||
};
|
||||
assert!(surface(&bad, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytic_records_are_unbounded_with_independent_frames() {
|
||||
let f = a_frame();
|
||||
let surfaces = [
|
||||
Surface::Cylinder {
|
||||
frame: f.clone(),
|
||||
radius: 2.,
|
||||
},
|
||||
Surface::Cone {
|
||||
frame: f.clone(),
|
||||
radius_at_origin: 2.,
|
||||
half_angle: 0.2,
|
||||
},
|
||||
Surface::Cone {
|
||||
frame: f.clone(),
|
||||
radius_at_origin: 2.,
|
||||
half_angle: -0.2,
|
||||
},
|
||||
Surface::Sphere {
|
||||
frame: f.clone(),
|
||||
radius: 2.,
|
||||
},
|
||||
Surface::Torus {
|
||||
frame: f.clone(),
|
||||
major_radius: 4.,
|
||||
minor_radius: 1.,
|
||||
tube_start: 0.0,
|
||||
},
|
||||
];
|
||||
for flat in [
|
||||
0.,
|
||||
1e-13,
|
||||
-1e-13,
|
||||
std::f64::consts::FRAC_PI_2,
|
||||
-std::f64::consts::FRAC_PI_2,
|
||||
] {
|
||||
assert!(
|
||||
surface(
|
||||
&Surface::Cone {
|
||||
frame: f.clone(),
|
||||
radius_at_origin: 2.,
|
||||
half_angle: flat
|
||||
},
|
||||
0,
|
||||
&mut Budget::input()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
for s in surfaces {
|
||||
surface(&s, 0, &mut Budget::input()).unwrap();
|
||||
let encoded = serde_json::to_string(&s).unwrap();
|
||||
assert_eq!(serde_json::from_str::<Surface>(&encoded).unwrap(), s);
|
||||
assert!(
|
||||
!encoded.contains("\"angle\"")
|
||||
&& !encoded.contains("height")
|
||||
&& !encoded.contains("longitude")
|
||||
);
|
||||
}
|
||||
for c in [
|
||||
Curve::Circle {
|
||||
frame: f.clone(),
|
||||
radius: 2.,
|
||||
},
|
||||
Curve::Ellipse {
|
||||
frame: f.clone(),
|
||||
radii: [2., 3.],
|
||||
},
|
||||
] {
|
||||
curve(&c, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Curve>(&serde_json::to_string(&c).unwrap()).unwrap(),
|
||||
c
|
||||
);
|
||||
assert_eq!(curve_natural(&c), Natural::Periodic);
|
||||
// A periodic edge may start anywhere and run up to one turn.
|
||||
interval(&[-1., TAU - 1.], Natural::Periodic).unwrap();
|
||||
}
|
||||
let mut bad = f;
|
||||
bad.y_axis[1] = f64::NAN;
|
||||
assert!(frame(&bad, &mut Budget::input()).is_err());
|
||||
assert!(interval(&[2., 1.], UNBOUNDED).is_err());
|
||||
assert!(radius(f64::NAN).is_err());
|
||||
assert!(
|
||||
surface(
|
||||
&Surface::Torus {
|
||||
frame: a_frame(),
|
||||
major_radius: 1.,
|
||||
minor_radius: 2.,
|
||||
tube_start: 0.0
|
||||
},
|
||||
0,
|
||||
&mut Budget::input()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
//! The structural rules of the topology records: references, loops, regions and shells, each
|
||||
//! edge's list of uses and the canonical order (docs/native-brep.md). Indices are hostile:
|
||||
//! every lookup is checked, and work is linear in the records.
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{Brep, EdgeUseRef, Invalid, Loop, Result, Shell, require};
|
||||
|
||||
/// How far a pcurve's interval may differ from its edge's: this fraction of the interval's
|
||||
/// magnitude (at least 1), room for a linear remap's rounding and nothing more.
|
||||
pub(super) const PCURVE_INTERVAL: f64 = 1e-9;
|
||||
|
||||
fn get<'a, T>(items: &'a [T], i: u32, what: &'static str) -> Result<&'a T> {
|
||||
items.get(i as usize).ok_or(Invalid(what))
|
||||
}
|
||||
|
||||
/// The next index in a first-appearance order: `seen[i]` is set the first time `i` turns up,
|
||||
/// and that must be in index order (the first new one is `next`).
|
||||
struct Order {
|
||||
seen: Vec<bool>,
|
||||
next: usize,
|
||||
what: &'static str,
|
||||
}
|
||||
impl Order {
|
||||
fn new(n: usize, what: &'static str) -> Order {
|
||||
Order {
|
||||
seen: vec![false; n],
|
||||
next: 0,
|
||||
what,
|
||||
}
|
||||
}
|
||||
fn visit(&mut self, i: u32) -> Result<bool> {
|
||||
let slot = self.seen.get_mut(i as usize).ok_or(Invalid(self.what))?;
|
||||
if *slot {
|
||||
return Ok(false);
|
||||
}
|
||||
require(i as usize == self.next, self.what)?;
|
||||
*slot = true;
|
||||
self.next += 1;
|
||||
Ok(true)
|
||||
}
|
||||
fn complete(&self) -> Result<()> {
|
||||
require(self.next == self.seen.len(), self.what)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check(b: &Brep) -> Result<()> {
|
||||
// Regions and shells: which regions list each shell, and as whose outer shell.
|
||||
let mut listed = vec![0u32; b.shells.len()];
|
||||
let mut outer_of = vec![0u32; b.shells.len()];
|
||||
let infinite = b.regions.first().ok_or(Invalid("region count"))?;
|
||||
require(!infinite.void, "the infinite region is a void")?;
|
||||
// The last region (numbered from 1) that listed each shell: a repeat within one region shows
|
||||
// in one lookup, so the check stays linear however many shells and regions there are.
|
||||
let mut last_region = vec![0usize; b.shells.len()];
|
||||
for (r, region) in b.regions.iter().enumerate() {
|
||||
require(region.shells.len() <= b.shells.len(), "region shells")?;
|
||||
for (k, &s) in region.shells.iter().enumerate() {
|
||||
let last = last_region
|
||||
.get_mut(s as usize)
|
||||
.ok_or(Invalid("region shell reference"))?;
|
||||
require(*last != r + 1, "shell listed twice in a region")?;
|
||||
*last = r + 1;
|
||||
let n = listed
|
||||
.get_mut(s as usize)
|
||||
.ok_or(Invalid("region shell reference"))?;
|
||||
*n += 1;
|
||||
if r > 0 && k == 0 {
|
||||
require(
|
||||
matches!(
|
||||
get(&b.shells, s, "region shell reference")?,
|
||||
Shell::Faces { .. }
|
||||
),
|
||||
"outer shell is not a face shell",
|
||||
)?;
|
||||
if let Some(n) = outer_of.get_mut(s as usize) {
|
||||
*n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
require(
|
||||
r == 0 || !region.shells.is_empty(),
|
||||
"bounded region without an outer shell",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Faces: who uses each one. Loops: references, shape and connectivity.
|
||||
let mut face_uses: Vec<Vec<(usize, bool)>> = vec![Vec::new(); b.faces.len()];
|
||||
for (s, shell) in b.shells.iter().enumerate() {
|
||||
match shell {
|
||||
Shell::Faces { faces, wires } => {
|
||||
require(!faces.is_empty(), "empty shell")?;
|
||||
for f in faces {
|
||||
face_uses
|
||||
.get_mut(f.face as usize)
|
||||
.ok_or(Invalid("oriented face reference"))?
|
||||
.push((s, f.forward));
|
||||
}
|
||||
for &e in wires {
|
||||
require(
|
||||
get(&b.edges, e, "wire edge reference")?.uses.is_empty(),
|
||||
"wire edge with face uses",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Shell::Wire { edges } => {
|
||||
require(!edges.is_empty(), "empty wire shell")?;
|
||||
for &e in edges {
|
||||
require(
|
||||
get(&b.edges, e, "wire edge reference")?.uses.is_empty(),
|
||||
"wire edge with face uses",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Shell::Vertex { vertex } => {
|
||||
get(&b.vertices, *vertex, "vertex shell reference")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for uses in &face_uses {
|
||||
match uses.as_slice() {
|
||||
[_] => {}
|
||||
[(a, fa), (b, fb)] => require(a != b && fa != fb, "a face's two sides")?,
|
||||
_ => return Err(Invalid("face used by neither one nor two oriented faces")),
|
||||
}
|
||||
}
|
||||
for f in &b.faces {
|
||||
require(
|
||||
matches!(f.loops.first(), Some(Loop::Edges { .. })),
|
||||
"outer loop is not an edge loop",
|
||||
)?;
|
||||
for l in &f.loops {
|
||||
match l {
|
||||
Loop::Vertex { vertex } => {
|
||||
get(&b.vertices, *vertex, "vertex loop reference")?;
|
||||
}
|
||||
Loop::Edges { edges } => {
|
||||
require(!edges.is_empty(), "empty loop")?;
|
||||
let (mut first, mut last) = (None, None);
|
||||
for u in edges {
|
||||
let edge = get(&b.edges, u.edge, "edge reference")?;
|
||||
let [a, z] = edge.vertices;
|
||||
let (start, end) = if u.forward { (a, z) } else { (z, a) };
|
||||
require(last.is_none_or(|v| v == start), "disconnected loop")?;
|
||||
first.get_or_insert(start);
|
||||
last = Some(end);
|
||||
let p = u.pcurve.interval().ok_or(Invalid("pcurve interval"))?;
|
||||
let scale = PCURVE_INTERVAL
|
||||
* (1f64)
|
||||
.max(edge.interval[0].abs())
|
||||
.max(edge.interval[1].abs());
|
||||
require(
|
||||
(p[0] - edge.interval[0]).abs() <= scale
|
||||
&& (p[1] - edge.interval[1]).abs() <= scale,
|
||||
"pcurve interval differs from its edge's",
|
||||
)?;
|
||||
}
|
||||
require(first == last, "open loop")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shells: a closed face shell (every edge on its faces used at least twice among them)
|
||||
// separates two regions and encloses one; any other shell lies in one region.
|
||||
let mut count = vec![0u32; b.edges.len()];
|
||||
for (s, shell) in b.shells.iter().enumerate() {
|
||||
let (listed, outer) = (
|
||||
listed.get(s).copied().unwrap_or(0),
|
||||
outer_of.get(s).copied().unwrap_or(0),
|
||||
);
|
||||
let closed = match shell {
|
||||
Shell::Faces { faces, .. } => {
|
||||
let edges = || {
|
||||
faces
|
||||
.iter()
|
||||
.filter_map(|f| b.faces.get(f.face as usize))
|
||||
.flat_map(|f| &f.loops)
|
||||
.flat_map(|l| match l {
|
||||
Loop::Edges { edges } => edges.as_slice(),
|
||||
Loop::Vertex { .. } => &[],
|
||||
})
|
||||
};
|
||||
for u in edges() {
|
||||
if let Some(n) = count.get_mut(u.edge as usize) {
|
||||
*n = n.saturating_add(1);
|
||||
}
|
||||
}
|
||||
let closed = edges().all(|u| count.get(u.edge as usize).is_some_and(|n| *n >= 2));
|
||||
for u in edges() {
|
||||
if let Some(n) = count.get_mut(u.edge as usize) {
|
||||
*n = 0;
|
||||
}
|
||||
}
|
||||
closed
|
||||
}
|
||||
Shell::Wire { .. } | Shell::Vertex { .. } => false,
|
||||
};
|
||||
if closed {
|
||||
require(
|
||||
listed == 2 && outer == 1,
|
||||
"a closed shell bounds two regions and encloses one",
|
||||
)?;
|
||||
} else {
|
||||
require(
|
||||
listed == 1 && outer == 0,
|
||||
"an open, wire or vertex shell lies in one region",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Canonical order. Shells and faces in first appearance walking regions → shells →
|
||||
// oriented faces; each face is walked in the direction its first oriented face gives.
|
||||
let mut shells = Order::new(b.shells.len(), "shells out of canonical order");
|
||||
let mut faces = Order::new(b.faces.len(), "faces out of canonical order");
|
||||
let mut direction = vec![true; b.faces.len()];
|
||||
for region in &b.regions {
|
||||
for &s in ®ion.shells {
|
||||
if !shells.visit(s)? {
|
||||
continue;
|
||||
}
|
||||
if let Some(Shell::Faces { faces: uses, .. }) = b.shells.get(s as usize) {
|
||||
for f in uses {
|
||||
if faces.visit(f.face)?
|
||||
&& let Some(d) = direction.get_mut(f.face as usize)
|
||||
{
|
||||
*d = f.forward;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shells.complete()?;
|
||||
faces.complete()?;
|
||||
|
||||
// Edges in first appearance walking faces → loops → oriented edges (backwards in a face
|
||||
// whose first side is reversed), then wire edges walking shells; each edge's uses in that
|
||||
// walk's order, the first one primary.
|
||||
let mut edges = Order::new(b.edges.len(), "edges out of canonical order");
|
||||
let mut walked: Vec<Vec<EdgeUseRef>> = vec![Vec::new(); b.edges.len()];
|
||||
for (fi, f) in b.faces.iter().enumerate() {
|
||||
let forward = direction.get(fi).copied().unwrap_or(true);
|
||||
for (li, l) in f.loops.iter().enumerate() {
|
||||
let Loop::Edges { edges: uses } = l else {
|
||||
continue;
|
||||
};
|
||||
let n = uses.len();
|
||||
for k in 0..n {
|
||||
let p = if forward { k } else { n - 1 - k };
|
||||
let u = uses.get(p).ok_or(Invalid("edge reference"))?;
|
||||
edges.visit(u.edge)?;
|
||||
let at = EdgeUseRef {
|
||||
face: index(fi)?,
|
||||
r#loop: index(li)?,
|
||||
position: index(p)?,
|
||||
upper: !u.forward,
|
||||
};
|
||||
walked
|
||||
.get_mut(u.edge as usize)
|
||||
.ok_or(Invalid("edge reference"))?
|
||||
.push(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
for shell in &b.shells {
|
||||
let wires = match shell {
|
||||
Shell::Faces { wires, .. } => wires.as_slice(),
|
||||
Shell::Wire { edges } => edges.as_slice(),
|
||||
Shell::Vertex { .. } => &[],
|
||||
};
|
||||
for &e in wires {
|
||||
edges.visit(e)?;
|
||||
}
|
||||
}
|
||||
edges.complete()?;
|
||||
for (edge, walked) in b.edges.iter().zip(&walked) {
|
||||
require(
|
||||
edge.uses.len() == walked.len(),
|
||||
"edge uses do not list the edge's uses",
|
||||
)?;
|
||||
require(
|
||||
edge.uses.first() == walked.first(),
|
||||
"edge's primary use is not its first",
|
||||
)?;
|
||||
// As many entries as uses, and each one a use not listed before: each use once.
|
||||
let mut left: HashSet<EdgeUseRef> = walked.iter().copied().collect();
|
||||
for u in &edge.uses {
|
||||
require(
|
||||
left.remove(u),
|
||||
"edge use listed twice or not a use of the edge",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertices in first appearance walking edges (start, end), then vertex loops walking
|
||||
// faces → loops, then vertex shells walking shells.
|
||||
let mut vertices = Order::new(b.vertices.len(), "vertices out of canonical order");
|
||||
for e in &b.edges {
|
||||
for &v in &e.vertices {
|
||||
vertices.visit(v)?;
|
||||
}
|
||||
}
|
||||
for f in &b.faces {
|
||||
for l in &f.loops {
|
||||
if let Loop::Vertex { vertex } = l {
|
||||
vertices.visit(*vertex)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for shell in &b.shells {
|
||||
if let Shell::Vertex { vertex } = shell {
|
||||
vertices.visit(*vertex)?;
|
||||
}
|
||||
}
|
||||
vertices.complete()
|
||||
}
|
||||
|
||||
fn index(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).map_err(|_| Invalid("index"))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Geometry as data (docs/layers.md, "Record"): the portable B-rep record with its serde form
|
||||
//! and structural validation ([`brep`]), its history and metadata records, the native body
|
||||
//! file ([`native`]), and the plain data of vectors ([`Vec2`], [`Vec3`]), sketch planes
|
||||
//! ([`Plane`]) and planar profiles ([`Seg2`], [`Loop2`], [`Region2`]).
|
||||
//!
|
||||
//! The record computes no geometric quantity: no square roots, trigonometry, dot or cross
|
||||
//! products, normalising or solves (clippy `disallowed-methods`, this crate's `clippy.toml`). It
|
||||
//! depends on no geometry or maths crate. Evaluating, measuring and checking the geometry a
|
||||
//! record holds is the kernel's.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::disallowed_methods)]
|
||||
|
||||
// The record's types are SolidCraft's wire format, carried over as it is: each type says what it
|
||||
// is, its formula and its SMLib and STEP counterparts, and its fields are the names in that
|
||||
// formula (SolidCraft's docs/native-brep.md), so fields and variants are not documented one by
|
||||
// one.
|
||||
#[allow(missing_docs)]
|
||||
pub mod brep;
|
||||
pub mod native;
|
||||
pub mod plane;
|
||||
// Profiles are SolidCraft's sketch wire format, carried over as it is: each variant says what it
|
||||
// is and its fields are the names in its formula.
|
||||
#[allow(missing_docs)]
|
||||
pub mod profile;
|
||||
pub mod vec;
|
||||
|
||||
pub use plane::Plane;
|
||||
pub use profile::{Loop2, MAX_BEZIER_CTRL, MAX_BEZIER_DEGREE, Region2, Seg2};
|
||||
pub use vec::{Vec2, Vec3};
|
||||
@@ -0,0 +1,295 @@
|
||||
//! SolidCraft's native, versioned binary body file: a body's record, its metadata and the
|
||||
//! values it measured about itself, as bounded tagged binary values. Exact: a decoded record is
|
||||
//! the encoded one, number for number. Checks are structural only (the record's
|
||||
//! [`Brep::validate`], the metadata's counts and ranges); the kernel checks the geometry and
|
||||
//! builds the body.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Number, Value};
|
||||
|
||||
use crate::brep::{Brep, envelope};
|
||||
|
||||
mod binary;
|
||||
|
||||
// 01/02 were development-only prototypes; independent metadata records start at 03; 04 stores
|
||||
// procedural intersections and placed revolutions exactly, and the measured runtime values; 05
|
||||
// nests geometry schema 4 (regions, unbounded geometry, pcurves, canonical order).
|
||||
/// The file's first eight bytes.
|
||||
pub const MAGIC: &[u8; 8] = b"SCBREP05";
|
||||
// A body's own limits. A design's cache holds many bodies within the container's budgets, and
|
||||
// decoding is linear in the bytes, so one large part (an imported casting) fits.
|
||||
/// The most bytes a file may have.
|
||||
pub const MAX_BYTES: usize = 256 << 20;
|
||||
/// The most items in one collection, and faces or edges in one body.
|
||||
pub const MAX_ITEMS: usize = 1_000_000;
|
||||
/// The most values a file may hold.
|
||||
pub const MAX_VALUES: usize = 20_000_000;
|
||||
|
||||
/// Why a file was refused or could not be written.
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
#[error("native B-rep: {0}")]
|
||||
pub struct Error(pub String);
|
||||
|
||||
/// A native-file operation's result.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// An [`Error`] saying `detail`.
|
||||
pub fn invalid(detail: impl std::fmt::Display) -> Error {
|
||||
Error(detail.to_string())
|
||||
}
|
||||
|
||||
/// One body as saved.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Saved {
|
||||
/// The body's record.
|
||||
pub solid: Brep,
|
||||
/// What it carries: colour, paint, boundaries, face sources, history.
|
||||
pub metadata: envelope::Metadata,
|
||||
/// What it measured about itself.
|
||||
pub runtime: Runtime,
|
||||
}
|
||||
|
||||
/// What a body measured about itself that later operations read: kept so a decoded body works
|
||||
/// to the same tolerances as the one encoded, without measuring again.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Runtime {
|
||||
/// The body's size as stored (0: not measured).
|
||||
pub size: f64,
|
||||
/// Saved by earlier versions (an orientation mesh's missing-face count); read and ignored.
|
||||
pub coarse_missing: Option<(f64, u64)>,
|
||||
}
|
||||
|
||||
/// `saved` as a file: [`check`]ed, then written as it serializes.
|
||||
pub fn encode(saved: &Saved) -> Result<Vec<u8>> {
|
||||
check(saved)?;
|
||||
let mut out = MAGIC.to_vec();
|
||||
binary::write_serialized(saved, &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A file's body, from bounded untrusted bytes: its size, header, values and structure checked
|
||||
/// ([`check_value`], [`check`]) before anything walks it.
|
||||
pub fn decode(bytes: &[u8]) -> Result<Saved> {
|
||||
if bytes.len() > MAX_BYTES || bytes.get(..8) != Some(MAGIC.as_slice()) {
|
||||
return Err(invalid("version or size"));
|
||||
}
|
||||
let mut input = bytes.get(8..).ok_or_else(|| invalid("header"))?;
|
||||
let mut remaining = MAX_VALUES;
|
||||
let value = binary::read(&mut input, 0, &mut remaining)?;
|
||||
if !input.is_empty() {
|
||||
return Err(invalid("trailing data"));
|
||||
}
|
||||
check_value(&value)?;
|
||||
let saved: Saved = serde_json::from_value(value).map_err(invalid)?;
|
||||
check(&saved)?;
|
||||
let size = saved.runtime.size;
|
||||
if !(size.is_finite() && size >= 0.0) {
|
||||
return Err(invalid("runtime values"));
|
||||
}
|
||||
Ok(saved)
|
||||
}
|
||||
|
||||
/// The tagged binary value at the start of `input`, which it moves past, taking from `budget`
|
||||
/// one per value read.
|
||||
pub fn read_value(input: &mut &[u8], budget: &mut usize) -> Result<Value> {
|
||||
binary::read(input, 0, budget)
|
||||
}
|
||||
|
||||
/// `value` appended to `out` as a tagged binary value. [`encode`] writes a body as it
|
||||
/// serializes; this writes a value already built (a hostile one, in tests).
|
||||
pub fn write_value(value: &Value, out: &mut Vec<u8>) -> Result<()> {
|
||||
binary::write(value, out, 0)
|
||||
}
|
||||
|
||||
/// The record's structure ([`Brep::validate`]) and the metadata's agreement with it.
|
||||
pub fn check(saved: &Saved) -> Result<()> {
|
||||
saved.solid.validate().map_err(invalid)?;
|
||||
let (faces, edges) = (saved.solid.faces.len(), saved.solid.edges.len());
|
||||
let saved = &saved.metadata;
|
||||
if saved.version != envelope::VERSION {
|
||||
return Err(invalid("metadata version"));
|
||||
}
|
||||
if saved
|
||||
.face_sources
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.len() != faces)
|
||||
{
|
||||
return Err(invalid("face source count"));
|
||||
}
|
||||
if let Some(history) = &saved.history
|
||||
&& (history.faces.len() != faces
|
||||
|| history.edges.len() != edges
|
||||
|| history.operands as usize > MAX_ITEMS
|
||||
|| history
|
||||
.edges
|
||||
.iter()
|
||||
.flat_map(|e| &e.faces)
|
||||
.any(|&f| f as usize >= faces))
|
||||
{
|
||||
return Err(invalid("history topology"));
|
||||
}
|
||||
if saved
|
||||
.color
|
||||
.is_some_and(|c| c.iter().any(|v| !v.is_finite() || !(0.0..=1.0).contains(v)))
|
||||
{
|
||||
return Err(invalid("colour"));
|
||||
}
|
||||
if let Some(paint) = &saved.paint
|
||||
&& (!paint.opacity.is_finite()
|
||||
|| !(0.0..=1.0).contains(&paint.opacity)
|
||||
|| paint.faces.iter().any(|p| {
|
||||
p.face as usize >= faces
|
||||
|| !p.opacity.is_finite()
|
||||
|| !(0.0..=1.0).contains(&p.opacity)
|
||||
|| p.color
|
||||
.iter()
|
||||
.any(|v| !v.is_finite() || !(0.0..=1.0).contains(v))
|
||||
}))
|
||||
{
|
||||
return Err(invalid("face paint"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The binary reader bounds allocation before deserialization. Geometry semantics
|
||||
// are checked by the independent schema validator, not by backend serde internals.
|
||||
/// A value's nesting, string sizes, numbers and array sizes within the file's budgets.
|
||||
pub fn check_value(v: &Value) -> Result<()> {
|
||||
value_within(v, 0)
|
||||
}
|
||||
|
||||
fn value_within(v: &Value, depth: usize) -> Result<()> {
|
||||
if depth > 48 {
|
||||
return Err(invalid("nesting"));
|
||||
}
|
||||
match v {
|
||||
Value::String(s) if s.len() > 1 << 20 => return Err(invalid("string budget")),
|
||||
Value::Number(n)
|
||||
if n.is_f64() && n.as_f64().is_none_or(|x| !x.is_finite() || x.abs() > 1e12) =>
|
||||
{
|
||||
return Err(invalid("non-finite metadata"));
|
||||
}
|
||||
Value::Array(a) => {
|
||||
if a.len() > MAX_ITEMS {
|
||||
return Err(invalid("array budget"));
|
||||
}
|
||||
for x in a {
|
||||
value_within(x, depth + 1)?;
|
||||
}
|
||||
}
|
||||
Value::Object(o) => {
|
||||
for x in o.values() {
|
||||
value_within(x, depth + 1)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::brep::{Region, Shell, Vertex};
|
||||
|
||||
/// One isolated point: the smallest valid body, with colour and nothing else.
|
||||
fn point() -> Saved {
|
||||
let solid = Brep {
|
||||
version: crate::brep::VERSION,
|
||||
tolerance: 1e-7,
|
||||
regions: vec![Region {
|
||||
shells: vec![0],
|
||||
void: false,
|
||||
}],
|
||||
shells: vec![Shell::Vertex { vertex: 0 }],
|
||||
faces: vec![],
|
||||
edges: vec![],
|
||||
vertices: vec![Vertex {
|
||||
point: [1., -2.5, 3e5],
|
||||
tolerance: 1e-7,
|
||||
}],
|
||||
};
|
||||
let metadata = envelope::Metadata {
|
||||
version: envelope::VERSION,
|
||||
color: Some([0.1, 0.5, 0.9]),
|
||||
paint: None,
|
||||
boundaries: None,
|
||||
face_sources: None,
|
||||
history: None,
|
||||
preserve_boundaries: false,
|
||||
};
|
||||
Saved {
|
||||
solid,
|
||||
metadata,
|
||||
runtime: Runtime {
|
||||
size: 2.5,
|
||||
coarse_missing: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_its_record_number_for_number() {
|
||||
let saved = point();
|
||||
let bytes = encode(&saved).unwrap();
|
||||
assert!(bytes.starts_with(MAGIC));
|
||||
let back = decode(&bytes).unwrap();
|
||||
assert_eq!(back.solid, saved.solid);
|
||||
assert_eq!(back.metadata, saved.metadata);
|
||||
assert_eq!(back.runtime.size.to_bits(), saved.runtime.size.to_bits());
|
||||
assert_eq!(encode(&back).unwrap(), bytes);
|
||||
// Written as it serializes, the bytes are those of its JSON value.
|
||||
let mut through_a_value = MAGIC.to_vec();
|
||||
write_value(&serde_json::to_value(&saved).unwrap(), &mut through_a_value).unwrap();
|
||||
assert_eq!(through_a_value, bytes);
|
||||
let mut raw = bytes.get(8..).unwrap();
|
||||
assert_eq!(
|
||||
read_value(&mut raw, &mut 1000).unwrap(),
|
||||
serde_json::to_value(&saved).unwrap()
|
||||
);
|
||||
assert!(raw.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_files_are_refused_and_never_panic() {
|
||||
let bytes = encode(&point()).unwrap();
|
||||
for n in [0, 1, 7, 8, 9, bytes.len() - 1] {
|
||||
assert!(decode(&bytes[..n]).is_err());
|
||||
}
|
||||
for i in 0..bytes.len() {
|
||||
let mut bad = bytes.clone();
|
||||
bad[i] ^= 0xff;
|
||||
assert!(std::panic::catch_unwind(|| decode(&bad)).is_ok());
|
||||
}
|
||||
let mut huge = MAGIC.to_vec();
|
||||
huge.push(7);
|
||||
huge.extend(u32::MAX.to_le_bytes());
|
||||
assert!(decode(&huge).is_err());
|
||||
let mut trailing = bytes.clone();
|
||||
trailing.push(0);
|
||||
assert_eq!(
|
||||
decode(&trailing).err().map(|e| e.to_string()),
|
||||
Some("native B-rep: trailing data".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_that_disagrees_with_its_record_is_refused() {
|
||||
let mut saved = point();
|
||||
saved.metadata.face_sources = Some(vec![None]);
|
||||
assert_eq!(
|
||||
check(&saved).err().map(|e| e.to_string()),
|
||||
Some("native B-rep: face source count".into())
|
||||
);
|
||||
let mut saved = point();
|
||||
saved.metadata.color = Some([2., 0., 0.]);
|
||||
assert!(encode(&saved).is_err());
|
||||
let mut saved = point();
|
||||
saved.runtime.size = f64::NAN;
|
||||
let mut bytes = MAGIC.to_vec();
|
||||
write_value(&serde_json::to_value(&saved).unwrap(), &mut bytes).unwrap();
|
||||
assert!(decode(&bytes).is_err());
|
||||
assert!(check_value(&serde_json::json!({ "x": 1e13 })).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
//! Bounded tagged binary values: integer and f64 bits stay exact, no base64/text numbers.
|
||||
use super::{MAX_BYTES, MAX_ITEMS, MAX_VALUES, Map, Number, Result, Serialize, Value, invalid};
|
||||
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(())
|
||||
}
|
||||
/// `value` appended to `out`. The encoder writes a body as it serializes
|
||||
/// ([`write_serialized`]); this writes a value already built, as tests build hostile ones.
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Why a value could not be written (a serde error carries only its message).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct WriteError(String);
|
||||
|
||||
impl std::fmt::Display for WriteError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for WriteError {}
|
||||
|
||||
impl serde::ser::Error for WriteError {
|
||||
fn custom<T: std::fmt::Display>(msg: T) -> Self {
|
||||
WriteError(msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn refuse(what: &str) -> WriteError {
|
||||
WriteError(what.into())
|
||||
}
|
||||
|
||||
type Written<T = ()> = std::result::Result<T, WriteError>;
|
||||
|
||||
/// `value` appended to `out` as the tagged binary value it is as JSON: the bytes [`write`] gives
|
||||
/// for `serde_json::to_value(value)`, refused where that, the checks on its result
|
||||
/// (`validate_value`, the [`MAX_VALUES`] budget) or [`write`] refuse. Written as it is
|
||||
/// serialized, with no JSON value in between: a body's record is millions of values, and building
|
||||
/// them all first was most of the cost of encoding it.
|
||||
///
|
||||
/// As JSON: a struct or a map is an object of its entries in key order (a JSON object's), its
|
||||
/// keys strings (a map's keys may be strings, characters, booleans or integers, written as JSON
|
||||
/// writes such keys); an enum variant without data its name, one with data an object of its name
|
||||
/// and the data; an option its value or null; bytes an array of numbers. A number is an integer
|
||||
/// (unsigned from 0 up, signed below) or a finite float; a float that is not finite is null.
|
||||
///
|
||||
/// Refused: nesting past 48, a string over 1 MiB, an array of more than [`MAX_ITEMS`], a float
|
||||
/// over 1e12 in size, more than [`MAX_VALUES`] values, more than [`MAX_BYTES`] in `out`.
|
||||
pub(super) fn write_serialized<T: Serialize + ?Sized>(value: &T, out: &mut Vec<u8>) -> Result<()> {
|
||||
let mut w = Writer { out, values: 0 };
|
||||
value
|
||||
.serialize(Ser {
|
||||
w: &mut w,
|
||||
depth: 0,
|
||||
})
|
||||
.map_err(invalid)
|
||||
}
|
||||
|
||||
struct Writer<'a> {
|
||||
out: &'a mut Vec<u8>,
|
||||
values: usize,
|
||||
}
|
||||
|
||||
impl Writer<'_> {
|
||||
/// Count a value at `depth` and write its tag.
|
||||
fn start(&mut self, depth: usize, tag: u8) -> Written {
|
||||
if depth > 48 {
|
||||
return Err(refuse("nesting"));
|
||||
}
|
||||
self.values = self.values.saturating_add(1);
|
||||
if self.values > MAX_VALUES {
|
||||
return Err(refuse("value budget"));
|
||||
}
|
||||
self.out.push(tag);
|
||||
self.fits()
|
||||
}
|
||||
fn fits(&self) -> Written {
|
||||
if self.out.len() > MAX_BYTES {
|
||||
Err(refuse("encoding size"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn number(&mut self, depth: usize, tag: u8, bits: [u8; 8]) -> Written {
|
||||
self.start(depth, tag)?;
|
||||
self.out.extend_from_slice(&bits);
|
||||
self.fits()
|
||||
}
|
||||
fn unsigned(&mut self, depth: usize, v: u64) -> Written {
|
||||
self.number(depth, 4, v.to_le_bytes())
|
||||
}
|
||||
fn signed(&mut self, depth: usize, v: i64) -> Written {
|
||||
match u64::try_from(v) {
|
||||
Ok(u) => self.unsigned(depth, u),
|
||||
Err(_) => self.number(depth, 3, v.to_le_bytes()),
|
||||
}
|
||||
}
|
||||
fn float(&mut self, depth: usize, v: f64) -> Written {
|
||||
if !v.is_finite() {
|
||||
return self.start(depth, 0);
|
||||
}
|
||||
if v.abs() > 1e12 {
|
||||
return Err(refuse("non-finite metadata"));
|
||||
}
|
||||
self.number(depth, 5, v.to_le_bytes())
|
||||
}
|
||||
fn length(&mut self, n: usize) -> Written {
|
||||
self.out.extend_from_slice(
|
||||
&u32::try_from(n)
|
||||
.map_err(|_| refuse("length"))?
|
||||
.to_le_bytes(),
|
||||
);
|
||||
self.fits()
|
||||
}
|
||||
fn text(&mut self, s: &str) -> Written {
|
||||
self.length(s.len())?;
|
||||
self.out.extend_from_slice(s.as_bytes());
|
||||
self.fits()
|
||||
}
|
||||
fn string(&mut self, depth: usize, s: &str) -> Written {
|
||||
if s.len() > 1 << 20 {
|
||||
return Err(refuse("string budget"));
|
||||
}
|
||||
self.start(depth, 6)?;
|
||||
self.text(s)
|
||||
}
|
||||
/// Open a collection at `depth`: its tag and room for its count, which goes at the place
|
||||
/// returned.
|
||||
fn open(&mut self, depth: usize, tag: u8) -> Written<usize> {
|
||||
self.start(depth, tag)?;
|
||||
let at = self.out.len();
|
||||
self.out.extend_from_slice(&[0; 4]);
|
||||
self.fits()?;
|
||||
Ok(at)
|
||||
}
|
||||
fn count_at(&mut self, at: usize, n: usize) -> Written {
|
||||
let n = u32::try_from(n)
|
||||
.map_err(|_| refuse("length"))?
|
||||
.to_le_bytes();
|
||||
self.out
|
||||
.get_mut(at..at + 4)
|
||||
.ok_or_else(|| refuse("count"))?
|
||||
.copy_from_slice(&n);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A value to write at a depth.
|
||||
pub(super) struct Ser<'w, 'a> {
|
||||
w: &'w mut Writer<'a>,
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl<'w, 'a> Ser<'w, 'a> {
|
||||
/// An object of one entry, named `variant`, whose value comes next (at the depth returned).
|
||||
fn variant(self, variant: &str) -> Written<Ser<'w, 'a>> {
|
||||
let at = self.w.open(self.depth, 8)?;
|
||||
self.w.count_at(at, 1)?;
|
||||
self.w.text(variant)?;
|
||||
Ok(Ser {
|
||||
w: self.w,
|
||||
depth: self.depth + 1,
|
||||
})
|
||||
}
|
||||
fn array(self) -> Written<Array<'w, 'a>> {
|
||||
let at = self.w.open(self.depth, 7)?;
|
||||
Ok(Array {
|
||||
w: self.w,
|
||||
depth: self.depth,
|
||||
at,
|
||||
items: 0,
|
||||
})
|
||||
}
|
||||
fn object(self) -> Written<Object<'w, 'a>> {
|
||||
let at = self.w.open(self.depth, 8)?;
|
||||
Ok(Object {
|
||||
w: self.w,
|
||||
depth: self.depth,
|
||||
at,
|
||||
entries: Vec::new(),
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An array being written: where its count goes, and how many items it has.
|
||||
pub(super) struct Array<'w, 'a> {
|
||||
w: &'w mut Writer<'a>,
|
||||
depth: usize,
|
||||
at: usize,
|
||||
items: usize,
|
||||
}
|
||||
|
||||
impl Array<'_, '_> {
|
||||
fn item<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.items += 1;
|
||||
if self.items > MAX_ITEMS {
|
||||
return Err(refuse("array budget"));
|
||||
}
|
||||
value.serialize(Ser {
|
||||
w: self.w,
|
||||
depth: self.depth + 1,
|
||||
})
|
||||
}
|
||||
fn close(self) -> Written {
|
||||
self.w.count_at(self.at, self.items)
|
||||
}
|
||||
}
|
||||
|
||||
/// An object being written: where its count goes, and each entry's key and where it ends. The
|
||||
/// entries are written as they come and put in key order when it closes.
|
||||
pub(super) struct Object<'w, 'a> {
|
||||
w: &'w mut Writer<'a>,
|
||||
depth: usize,
|
||||
at: usize,
|
||||
entries: Vec<(String, usize)>,
|
||||
/// The key of the entry whose value comes next.
|
||||
key: Option<String>,
|
||||
}
|
||||
|
||||
impl Object<'_, '_> {
|
||||
fn key(&mut self, key: String) -> Written {
|
||||
self.w.text(&key)?;
|
||||
self.key = Some(key);
|
||||
Ok(())
|
||||
}
|
||||
fn value<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
let key = self
|
||||
.key
|
||||
.take()
|
||||
.ok_or_else(|| refuse("an object's value without its key"))?;
|
||||
value.serialize(Ser {
|
||||
w: self.w,
|
||||
depth: self.depth + 1,
|
||||
})?;
|
||||
self.entries.push((key, self.w.out.len()));
|
||||
Ok(())
|
||||
}
|
||||
fn entry<T: Serialize + ?Sized>(&mut self, key: &str, value: &T) -> Written {
|
||||
self.key(key.to_owned())?;
|
||||
self.value(value)
|
||||
}
|
||||
fn close(self) -> Written {
|
||||
let Object { w, at, entries, .. } = self;
|
||||
let first = at + 4;
|
||||
if !entries.windows(2).all(|p| matches!(p, [a, b] if a.0 < b.0)) {
|
||||
// In key order, as a JSON object (a map by key) holds them. A key twice would keep
|
||||
// only its last value there; no record repeats one.
|
||||
let mut spans: Vec<(&str, usize, usize)> = Vec::with_capacity(entries.len());
|
||||
let mut start = first;
|
||||
for (key, end) in &entries {
|
||||
spans.push((key.as_str(), start, *end));
|
||||
start = *end;
|
||||
}
|
||||
spans.sort_by(|a, b| a.0.cmp(b.0));
|
||||
if spans.windows(2).any(|p| matches!(p, [a, b] if a.0 == b.0)) {
|
||||
return Err(refuse("duplicate key"));
|
||||
}
|
||||
let written = w.out.split_off(first);
|
||||
for (_, start, end) in spans {
|
||||
w.out.extend_from_slice(
|
||||
written
|
||||
.get(start - first..end - first)
|
||||
.ok_or_else(|| refuse("object"))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
w.count_at(at, entries.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// A map key as JSON writes it: a string.
|
||||
struct Key;
|
||||
|
||||
impl serde::Serializer for Key {
|
||||
type Ok = String;
|
||||
type Error = WriteError;
|
||||
type SerializeSeq = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeTuple = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeTupleStruct = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeTupleVariant = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeMap = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeStruct = serde::ser::Impossible<String, WriteError>;
|
||||
type SerializeStructVariant = serde::ser::Impossible<String, WriteError>;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_i8(self, v: i8) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_i16(self, v: i16) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_i32(self, v: i32) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_i64(self, v: i64) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_i128(self, v: i128) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_u8(self, v: u8) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_u16(self, v: u16) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_u32(self, v: u32) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_u64(self, v: u64) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_u128(self, v: u128) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_f32(self, _: f32) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_f64(self, _: f64) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_char(self, v: char) -> Written<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
fn serialize_str(self, v: &str) -> Written<String> {
|
||||
Ok(v.to_owned())
|
||||
}
|
||||
fn serialize_bytes(self, _: &[u8]) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_none(self) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_some<T: Serialize + ?Sized>(self, _: &T) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_unit(self) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_unit_struct(self, _: &'static str) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
variant: &'static str,
|
||||
) -> Written<String> {
|
||||
Ok(variant.to_owned())
|
||||
}
|
||||
fn serialize_newtype_struct<T: Serialize + ?Sized>(
|
||||
self,
|
||||
_: &'static str,
|
||||
value: &T,
|
||||
) -> Written<String> {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_newtype_variant<T: Serialize + ?Sized>(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: &T,
|
||||
) -> Written<String> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_seq(self, _: Option<usize>) -> Written<Self::SerializeSeq> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_tuple(self, _: usize) -> Written<Self::SerializeTuple> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Written<Self::SerializeTupleStruct> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Written<Self::SerializeTupleVariant> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_map(self, _: Option<usize>) -> Written<Self::SerializeMap> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_struct(self, _: &'static str, _: usize) -> Written<Self::SerializeStruct> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Written<Self::SerializeStructVariant> {
|
||||
Err(refuse("a map key must be a string"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'w, 'a> serde::Serializer for Ser<'w, 'a> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
type SerializeSeq = Array<'w, 'a>;
|
||||
type SerializeTuple = Array<'w, 'a>;
|
||||
type SerializeTupleStruct = Array<'w, 'a>;
|
||||
type SerializeTupleVariant = Array<'w, 'a>;
|
||||
type SerializeMap = Object<'w, 'a>;
|
||||
type SerializeStruct = Object<'w, 'a>;
|
||||
type SerializeStructVariant = Object<'w, 'a>;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Written {
|
||||
self.w.start(self.depth, if v { 2 } else { 1 })
|
||||
}
|
||||
fn serialize_i8(self, v: i8) -> Written {
|
||||
self.w.signed(self.depth, v.into())
|
||||
}
|
||||
fn serialize_i16(self, v: i16) -> Written {
|
||||
self.w.signed(self.depth, v.into())
|
||||
}
|
||||
fn serialize_i32(self, v: i32) -> Written {
|
||||
self.w.signed(self.depth, v.into())
|
||||
}
|
||||
fn serialize_i64(self, v: i64) -> Written {
|
||||
self.w.signed(self.depth, v)
|
||||
}
|
||||
fn serialize_i128(self, v: i128) -> Written {
|
||||
match (u64::try_from(v), i64::try_from(v)) {
|
||||
(Ok(u), _) => self.w.unsigned(self.depth, u),
|
||||
(_, Ok(i)) => self.w.signed(self.depth, i),
|
||||
_ => Err(refuse("number out of range")),
|
||||
}
|
||||
}
|
||||
fn serialize_u8(self, v: u8) -> Written {
|
||||
self.w.unsigned(self.depth, v.into())
|
||||
}
|
||||
fn serialize_u16(self, v: u16) -> Written {
|
||||
self.w.unsigned(self.depth, v.into())
|
||||
}
|
||||
fn serialize_u32(self, v: u32) -> Written {
|
||||
self.w.unsigned(self.depth, v.into())
|
||||
}
|
||||
fn serialize_u64(self, v: u64) -> Written {
|
||||
self.w.unsigned(self.depth, v)
|
||||
}
|
||||
fn serialize_u128(self, v: u128) -> Written {
|
||||
self.w.unsigned(
|
||||
self.depth,
|
||||
u64::try_from(v).map_err(|_| refuse("number out of range"))?,
|
||||
)
|
||||
}
|
||||
fn serialize_f32(self, v: f32) -> Written {
|
||||
self.w.float(self.depth, v.into())
|
||||
}
|
||||
fn serialize_f64(self, v: f64) -> Written {
|
||||
self.w.float(self.depth, v)
|
||||
}
|
||||
fn serialize_char(self, v: char) -> Written {
|
||||
self.w.string(self.depth, v.encode_utf8(&mut [0; 4]))
|
||||
}
|
||||
fn serialize_str(self, v: &str) -> Written {
|
||||
self.w.string(self.depth, v)
|
||||
}
|
||||
fn serialize_bytes(self, v: &[u8]) -> Written {
|
||||
let mut a = self.array()?;
|
||||
for b in v {
|
||||
a.item(b)?;
|
||||
}
|
||||
a.close()
|
||||
}
|
||||
fn serialize_none(self) -> Written {
|
||||
self.w.start(self.depth, 0)
|
||||
}
|
||||
fn serialize_some<T: Serialize + ?Sized>(self, value: &T) -> Written {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_unit(self) -> Written {
|
||||
self.w.start(self.depth, 0)
|
||||
}
|
||||
fn serialize_unit_struct(self, _: &'static str) -> Written {
|
||||
self.w.start(self.depth, 0)
|
||||
}
|
||||
fn serialize_unit_variant(self, _: &'static str, _: u32, variant: &'static str) -> Written {
|
||||
self.w.string(self.depth, variant)
|
||||
}
|
||||
fn serialize_newtype_struct<T: Serialize + ?Sized>(
|
||||
self,
|
||||
_: &'static str,
|
||||
value: &T,
|
||||
) -> Written {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_newtype_variant<T: Serialize + ?Sized>(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
variant: &'static str,
|
||||
value: &T,
|
||||
) -> Written {
|
||||
value.serialize(self.variant(variant)?)
|
||||
}
|
||||
fn serialize_seq(self, _: Option<usize>) -> Written<Array<'w, 'a>> {
|
||||
self.array()
|
||||
}
|
||||
fn serialize_tuple(self, _: usize) -> Written<Array<'w, 'a>> {
|
||||
self.array()
|
||||
}
|
||||
fn serialize_tuple_struct(self, _: &'static str, _: usize) -> Written<Array<'w, 'a>> {
|
||||
self.array()
|
||||
}
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
variant: &'static str,
|
||||
_: usize,
|
||||
) -> Written<Array<'w, 'a>> {
|
||||
self.variant(variant)?.array()
|
||||
}
|
||||
fn serialize_map(self, _: Option<usize>) -> Written<Object<'w, 'a>> {
|
||||
self.object()
|
||||
}
|
||||
fn serialize_struct(self, _: &'static str, _: usize) -> Written<Object<'w, 'a>> {
|
||||
self.object()
|
||||
}
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
variant: &'static str,
|
||||
_: usize,
|
||||
) -> Written<Object<'w, 'a>> {
|
||||
self.variant(variant)?.object()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeSeq for Array<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.item(value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTuple for Array<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.item(value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTupleStruct for Array<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_field<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.item(value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTupleVariant for Array<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_field<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.item(value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeMap for Object<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_key<T: Serialize + ?Sized>(&mut self, key: &T) -> Written {
|
||||
let key = key.serialize(Key)?;
|
||||
self.key(key)
|
||||
}
|
||||
fn serialize_value<T: Serialize + ?Sized>(&mut self, value: &T) -> Written {
|
||||
self.value(value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for Object<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_field<T: Serialize + ?Sized>(&mut self, key: &'static str, value: &T) -> Written {
|
||||
self.entry(key, value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStructVariant for Object<'_, '_> {
|
||||
type Ok = ();
|
||||
type Error = WriteError;
|
||||
fn serialize_field<T: Serialize + ?Sized>(&mut self, key: &'static str, value: &T) -> Written {
|
||||
self.entry(key, value)
|
||||
}
|
||||
fn end(self) -> Written {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! A sketch plane as data: an origin and two in-plane axes. Making one from directions and
|
||||
//! mapping points to and from it are the kernel's.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vec::Vec3;
|
||||
|
||||
/// A sketch plane: an origin and two orthonormal in-plane axes. The normal is `x × y`.
|
||||
///
|
||||
/// World point of sketch point `(u, v)` = `origin + u·x + v·y`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Plane {
|
||||
/// The sketch origin.
|
||||
pub origin: Vec3,
|
||||
/// Sketch x, a unit vector.
|
||||
#[serde(rename = "x_dir")]
|
||||
pub x: Vec3,
|
||||
/// Sketch y, a unit vector perpendicular to `x`.
|
||||
#[serde(rename = "y_dir")]
|
||||
pub y: Vec3,
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
/// Top: sketch x = +X, y = +Y, normal +Z.
|
||||
pub const XY: Plane = Plane {
|
||||
origin: Vec3::ZERO,
|
||||
x: Vec3::X,
|
||||
y: Vec3::Y,
|
||||
};
|
||||
/// Front: sketch x = +X, y = −Z, normal +Y.
|
||||
pub const XZ: Plane = Plane {
|
||||
origin: Vec3::ZERO,
|
||||
x: Vec3::X,
|
||||
y: Vec3::new(0.0, 0.0, -1.0),
|
||||
};
|
||||
/// Right: sketch x = +Y, y = +Z, normal +X.
|
||||
pub const YZ: Plane = Plane {
|
||||
origin: Vec3::ZERO,
|
||||
x: Vec3::Y,
|
||||
y: Vec3::Z,
|
||||
};
|
||||
|
||||
/// Named origin plane: `XY`, `XZ`, `YZ` (case-insensitive).
|
||||
pub fn named(name: &str) -> Option<Plane> {
|
||||
match name.to_ascii_uppercase().as_str() {
|
||||
"XY" | "TOP" => Some(Plane::XY),
|
||||
"XZ" | "FRONT" => Some(Plane::XZ),
|
||||
"YZ" | "RIGHT" => Some(Plane::YZ),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Planar profiles as data: closed loops of lines, circular arcs, Béziers and conics, and
|
||||
//! regions (an outer loop minus holes). Sketches produce them; the kernel evaluates them and
|
||||
//! turns them into faces.
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::vec::Vec2;
|
||||
|
||||
/// One boundary segment in sketch coordinates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Seg2 {
|
||||
Line {
|
||||
a: Vec2,
|
||||
b: Vec2,
|
||||
},
|
||||
/// Arc from `start` angle sweeping `sweep` radians (positive = counter-clockwise).
|
||||
Arc {
|
||||
center: Vec2,
|
||||
radius: f64,
|
||||
start: f64,
|
||||
sweep: f64,
|
||||
},
|
||||
/// Cubic Bézier.
|
||||
Cubic {
|
||||
p0: Vec2,
|
||||
p1: Vec2,
|
||||
p2: Vec2,
|
||||
p3: Vec2,
|
||||
},
|
||||
/// Rational quadratic Bézier from `a` to `b` with control `apex` of weight `w` (end
|
||||
/// weights 1): exact conic arcs (ellipses, parabolas, hyperbolas).
|
||||
Conic {
|
||||
a: Vec2,
|
||||
apex: Vec2,
|
||||
b: Vec2,
|
||||
w: f64,
|
||||
},
|
||||
/// Bézier of degree `n` (4…[`MAX_BEZIER_DEGREE`]) over `p[0..=n]` (the rest unused): pieces of
|
||||
/// higher-degree splines.
|
||||
Bezier {
|
||||
n: u8,
|
||||
p: [Vec2; MAX_BEZIER_CTRL],
|
||||
},
|
||||
}
|
||||
|
||||
/// Highest degree of [`Seg2::Bezier`]: a degree-9 piece is the single Bézier a G4 blend curve
|
||||
/// (`sketch.blend_curve` with `continuity: "g4"`) is made of.
|
||||
pub const MAX_BEZIER_DEGREE: usize = 9;
|
||||
|
||||
/// Control points a [`Seg2::Bezier`] carries: its highest degree plus one (the unused tail is
|
||||
/// zero).
|
||||
pub const MAX_BEZIER_CTRL: usize = MAX_BEZIER_DEGREE + 1;
|
||||
|
||||
/// A closed loop of segments, each starting where the previous one ends.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Loop2 {
|
||||
pub segs: Vec<Seg2>,
|
||||
}
|
||||
|
||||
impl Loop2 {
|
||||
/// Full circle as one arc from angle 0 (counter-clockwise): one closed curve, so a solid
|
||||
/// swept from it has one wall, and an edge round it is one edge.
|
||||
pub fn circle(center: Vec2, radius: f64) -> Loop2 {
|
||||
Loop2::circle_from(center, radius, 0.0)
|
||||
}
|
||||
/// Full circle as one arc starting (and ending) at angle `start`, counter-clockwise.
|
||||
pub fn circle_from(center: Vec2, radius: f64, start: f64) -> Loop2 {
|
||||
Loop2 {
|
||||
segs: vec![Seg2::Arc {
|
||||
center,
|
||||
radius,
|
||||
start,
|
||||
sweep: TAU,
|
||||
}],
|
||||
}
|
||||
}
|
||||
pub fn polygon(pts: &[Vec2]) -> Loop2 {
|
||||
let n = pts.len();
|
||||
Loop2 {
|
||||
segs: (0..n)
|
||||
.filter_map(|i| {
|
||||
Some(Seg2::Line {
|
||||
a: *pts.get(i)?,
|
||||
b: *pts.get((i + 1) % n)?,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A planar region: outer loop (counter-clockwise) minus holes (clockwise).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Region2 {
|
||||
pub outer: Loop2,
|
||||
pub holes: Vec<Loop2>,
|
||||
}
|
||||
|
||||
impl Region2 {
|
||||
/// The outer loop, then the holes.
|
||||
pub fn loops(&self) -> impl Iterator<Item = &Loop2> + Clone {
|
||||
std::iter::once(&self.outer).chain(&self.holes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Vectors and points as data: [`Vec2`] (sketch space) and [`Vec3`] (model space, Z up), with
|
||||
//! their component-wise operators (`+`, `-`, scalar `*` and `/`, negation). Every geometric
|
||||
//! quantity of them (dot and cross products, lengths, distances, directions) is the kernel's.
|
||||
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 2D vector / point (sketch space).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(from = "[f64; 2]", into = "[f64; 2]")]
|
||||
pub struct Vec2 {
|
||||
/// X component.
|
||||
pub x: f64,
|
||||
/// Y component.
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// 3D vector / point (model space, Z up).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(from = "[f64; 3]", into = "[f64; 3]")]
|
||||
pub struct Vec3 {
|
||||
/// X component.
|
||||
pub x: f64,
|
||||
/// Y component.
|
||||
pub y: f64,
|
||||
/// Z component.
|
||||
pub z: f64,
|
||||
}
|
||||
|
||||
impl From<[f64; 2]> for Vec2 {
|
||||
fn from(a: [f64; 2]) -> Self {
|
||||
Vec2::new(a[0], a[1])
|
||||
}
|
||||
}
|
||||
impl From<Vec2> for [f64; 2] {
|
||||
fn from(v: Vec2) -> Self {
|
||||
[v.x, v.y]
|
||||
}
|
||||
}
|
||||
impl From<[f64; 3]> for Vec3 {
|
||||
fn from(a: [f64; 3]) -> Self {
|
||||
Vec3::new(a[0], a[1], a[2])
|
||||
}
|
||||
}
|
||||
impl From<Vec3> for [f64; 3] {
|
||||
fn from(v: Vec3) -> Self {
|
||||
[v.x, v.y, v.z]
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
/// The origin.
|
||||
pub const ZERO: Vec2 = Vec2 { x: 0.0, y: 0.0 };
|
||||
/// Unit X.
|
||||
pub const X: Vec2 = Vec2 { x: 1.0, y: 0.0 };
|
||||
/// Unit Y.
|
||||
pub const Y: Vec2 = Vec2 { x: 0.0, y: 1.0 };
|
||||
/// `(x, y)`.
|
||||
#[must_use]
|
||||
pub const fn new(x: f64, y: f64) -> Self {
|
||||
Vec2 { x, y }
|
||||
}
|
||||
/// Both components finite.
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite()
|
||||
}
|
||||
/// `(x, y, z)`.
|
||||
#[must_use]
|
||||
pub fn extend(self, z: f64) -> Vec3 {
|
||||
Vec3::new(self.x, self.y, z)
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec3 {
|
||||
/// The origin.
|
||||
pub const ZERO: Vec3 = Vec3 {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
};
|
||||
/// Unit X.
|
||||
pub const X: Vec3 = Vec3 {
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
};
|
||||
/// Unit Y.
|
||||
pub const Y: Vec3 = Vec3 {
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
z: 0.0,
|
||||
};
|
||||
/// Unit Z.
|
||||
pub const Z: Vec3 = Vec3 {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 1.0,
|
||||
};
|
||||
/// `(x, y, z)`.
|
||||
#[must_use]
|
||||
pub const fn new(x: f64, y: f64, z: f64) -> Self {
|
||||
Vec3 { x, y, z }
|
||||
}
|
||||
/// Every component finite.
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
|
||||
}
|
||||
/// The components in single precision (for drawing).
|
||||
pub fn to_f32(self) -> [f32; 3] {
|
||||
[self.x as f32, self.y as f32, self.z as f32]
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ops {
|
||||
($t:ident, $($f:ident),+) => {
|
||||
impl Add for $t { type Output = $t; fn add(self, o: $t) -> $t { $t { $($f: self.$f + o.$f),+ } } }
|
||||
impl Sub for $t { type Output = $t; fn sub(self, o: $t) -> $t { $t { $($f: self.$f - o.$f),+ } } }
|
||||
impl Mul<f64> for $t { type Output = $t; fn mul(self, k: f64) -> $t { $t { $($f: self.$f * k),+ } } }
|
||||
impl Div<f64> for $t { type Output = $t; fn div(self, k: f64) -> $t { $t { $($f: self.$f / k),+ } } }
|
||||
impl Neg for $t { type Output = $t; fn neg(self) -> $t { $t { $($f: -self.$f),+ } } }
|
||||
impl AddAssign for $t { fn add_assign(&mut self, o: $t) { $(self.$f += o.$f;)+ } }
|
||||
impl SubAssign for $t { fn sub_assign(&mut self, o: $t) { $(self.$f -= o.$f;)+ } }
|
||||
};
|
||||
}
|
||||
ops!(Vec2, x, y);
|
||||
ops!(Vec3, x, y, z);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn operators_are_component_wise_and_serde_is_an_array() {
|
||||
let (a, b) = (Vec3::new(1., 2., 3.), Vec3::new(-4., 0.5, 8.));
|
||||
assert_eq!(a + b, Vec3::new(-3., 2.5, 11.));
|
||||
assert_eq!(a - b, Vec3::new(5., 1.5, -5.));
|
||||
assert_eq!(a * 2., Vec3::new(2., 4., 6.));
|
||||
assert_eq!(b / 2., Vec3::new(-2., 0.25, 4.));
|
||||
assert_eq!(-a, Vec3::new(-1., -2., -3.));
|
||||
let mut c = a;
|
||||
c += b;
|
||||
c -= a;
|
||||
assert_eq!(c, b);
|
||||
assert_eq!(serde_json::to_string(&a).unwrap(), "[1.0,2.0,3.0]");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Vec2>("[0.5,-1.0]").unwrap(),
|
||||
Vec2::new(0.5, -1.)
|
||||
);
|
||||
assert_eq!(Vec2::new(1., 2.).extend(3.), a);
|
||||
assert!(!Vec3::new(f64::NAN, 0., 0.).is_finite());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user