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:
jyviko
2026-11-03 06:38:31 -05:00
committed by GitHub
17 changed files with 4750 additions and 1 deletions
Generated
+9
View File
@@ -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
View File
@@ -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]
+20
View File
@@ -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
+41
View File
@@ -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)" },
]
+830
View File
@@ -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(&degree) && 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,
}
+493
View File
@@ -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;
}
}
}
+624
View File
@@ -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 &region.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(&regions)
}
/// 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())
}
+788
View File
@@ -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 &region.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"))
}
+29
View File
@@ -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};
+295
View File
@@ -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()
}
}
+51
View File
@@ -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,
}
}
}
+106
View File
@@ -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)
}
}
+152
View File
@@ -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());
}
}