M0: workspace, geometry foundation, sketch constraint solver, kernel boundary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
dafce1f4e2
47 files changed
+5624
No files matched your search
@@ -0,0 +1,30 @@
|
||||
//! Geometry foundation for SolveCraft: 2D/3D vectors, sketch planes, axis-aligned boxes and
|
||||
//! triangle meshes with their measures (volume, area, centroid). Units are millimetres.
|
||||
//!
|
||||
//! This crate has no knowledge of sketches, documents or the B-rep kernel; every other crate
|
||||
//! builds on it.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod mesh;
|
||||
mod plane;
|
||||
mod profile;
|
||||
mod vec;
|
||||
|
||||
pub use mesh::{Aabb3, Mesh, MeshMeasure};
|
||||
pub use plane::Plane;
|
||||
pub use profile::{Loop2, Region2, Seg2};
|
||||
pub use vec::{Vec2, Vec3};
|
||||
|
||||
/// Geometric tolerance for coincidence tests (mm).
|
||||
pub const EPS: f64 = 1e-9;
|
||||
/// Linear tolerance used for "same point" decisions in modelling (mm).
|
||||
pub const LINEAR_TOL: f64 = 1e-6;
|
||||
|
||||
/// `x` if finite, else `fallback`.
|
||||
pub fn finite_or(x: f64, fallback: f64) -> f64 {
|
||||
if x.is_finite() { x } else { fallback }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,141 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Vec3;
|
||||
|
||||
/// Axis-aligned bounding box.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Aabb3 {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
|
||||
impl Default for Aabb3 {
|
||||
fn default() -> Self {
|
||||
Aabb3::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
impl Aabb3 {
|
||||
pub const EMPTY: Aabb3 = Aabb3 {
|
||||
min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
|
||||
max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
|
||||
};
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!(self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z)
|
||||
}
|
||||
pub fn add(&mut self, p: Vec3) {
|
||||
if p.is_finite() {
|
||||
self.min = self.min.min(p);
|
||||
self.max = self.max.max(p);
|
||||
}
|
||||
}
|
||||
pub fn union(&self, o: &Aabb3) -> Aabb3 {
|
||||
if self.is_empty() {
|
||||
return *o;
|
||||
}
|
||||
if o.is_empty() {
|
||||
return *self;
|
||||
}
|
||||
Aabb3 { min: self.min.min(o.min), max: self.max.max(o.max) }
|
||||
}
|
||||
pub fn center(&self) -> Vec3 {
|
||||
if self.is_empty() { Vec3::ZERO } else { (self.min + self.max) * 0.5 }
|
||||
}
|
||||
pub fn size(&self) -> Vec3 {
|
||||
if self.is_empty() { Vec3::ZERO } else { self.max - self.min }
|
||||
}
|
||||
pub fn diagonal(&self) -> f64 {
|
||||
self.size().len()
|
||||
}
|
||||
}
|
||||
|
||||
/// An indexed triangle mesh with per-vertex normals, a face id per triangle (the B-rep face it
|
||||
/// came from) and edge polylines (the B-rep edges, for display and picking).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Mesh {
|
||||
pub positions: Vec<Vec3>,
|
||||
pub normals: Vec<Vec3>,
|
||||
/// Triangles as vertex index triples (counter-clockwise seen from outside).
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
/// B-rep face index per triangle.
|
||||
pub tri_face: Vec<u32>,
|
||||
/// B-rep edges as polylines.
|
||||
pub edges: Vec<Vec<Vec3>>,
|
||||
}
|
||||
|
||||
/// Mass properties of a closed mesh.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MeshMeasure {
|
||||
pub volume: f64,
|
||||
pub area: f64,
|
||||
pub centroid: Vec3,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
pub fn tri(&self, t: &[u32; 3]) -> Option<[Vec3; 3]> {
|
||||
Some([*self.positions.get(t[0] as usize)?, *self.positions.get(t[1] as usize)?, *self.positions.get(t[2] as usize)?])
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Aabb3 {
|
||||
let mut b = Aabb3::EMPTY;
|
||||
for p in &self.positions {
|
||||
b.add(*p);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Signed volume (divergence theorem), surface area and volume centroid.
|
||||
pub fn measure(&self) -> MeshMeasure {
|
||||
let (mut v6, mut area, mut c) = (0.0, 0.0, Vec3::ZERO);
|
||||
for t in &self.triangles {
|
||||
let Some([a, b, cc]) = self.tri(t) else { continue };
|
||||
let d = a.dot(b.cross(cc));
|
||||
v6 += d;
|
||||
c += (a + b + cc) * d;
|
||||
area += (b - a).cross(cc - a).len() * 0.5;
|
||||
}
|
||||
let volume = v6 / 6.0;
|
||||
let centroid = if v6.abs() > 1e-12 { c / (4.0 * v6) } else { self.bounds().center() };
|
||||
MeshMeasure { volume, area, centroid }
|
||||
}
|
||||
|
||||
/// Append another mesh (face ids offset by `face_offset`).
|
||||
pub fn append(&mut self, o: &Mesh, face_offset: u32) {
|
||||
let base = u32::try_from(self.positions.len()).unwrap_or(u32::MAX);
|
||||
self.positions.extend_from_slice(&o.positions);
|
||||
self.normals.extend_from_slice(&o.normals);
|
||||
self.triangles.extend(o.triangles.iter().map(|t| [t[0].saturating_add(base), t[1].saturating_add(base), t[2].saturating_add(base)]));
|
||||
self.tri_face.extend(o.tri_face.iter().map(|f| f.saturating_add(face_offset)));
|
||||
self.edges.extend(o.edges.iter().cloned());
|
||||
}
|
||||
|
||||
/// Closest intersection of a ray with the mesh: (distance along `dir`, triangle index).
|
||||
pub fn raycast(&self, o: Vec3, dir: Vec3) -> Option<(f64, usize)> {
|
||||
let mut best: Option<(f64, usize)> = None;
|
||||
for (i, t) in self.triangles.iter().enumerate() {
|
||||
let Some([a, b, c]) = self.tri(t) else { continue };
|
||||
let (e1, e2) = (b - a, c - a);
|
||||
let p = dir.cross(e2);
|
||||
let det = e1.dot(p);
|
||||
if det.abs() < 1e-14 {
|
||||
continue;
|
||||
}
|
||||
let inv = 1.0 / det;
|
||||
let s = o - a;
|
||||
let u = s.dot(p) * inv;
|
||||
if !(0.0..=1.0).contains(&u) {
|
||||
continue;
|
||||
}
|
||||
let q = s.cross(e1);
|
||||
let v = dir.dot(q) * inv;
|
||||
if v < 0.0 || u + v > 1.0 {
|
||||
continue;
|
||||
}
|
||||
let t = e2.dot(q) * inv;
|
||||
if t > 1e-9 && best.is_none_or(|(bt, _)| t < bt) {
|
||||
best = Some((t, i));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Vec2, 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 {
|
||||
pub origin: Vec3,
|
||||
#[serde(rename = "x_dir")]
|
||||
pub x: Vec3,
|
||||
#[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::Z };
|
||||
/// Right: sketch x = +Y, y = +Z, normal +X.
|
||||
pub const YZ: Plane = Plane { origin: Vec3::ZERO, x: Vec3::Y, y: Vec3::Z };
|
||||
|
||||
/// Plane through `origin` with the given axes, re-orthonormalised. `None` if degenerate.
|
||||
pub fn new(origin: Vec3, x: Vec3, y: Vec3) -> Option<Plane> {
|
||||
let x = x.normalized()?;
|
||||
let n = x.cross(y).normalized()?;
|
||||
let y = n.cross(x).normalized()?;
|
||||
origin.is_finite().then_some(Plane { origin, x, y })
|
||||
}
|
||||
|
||||
/// Plane through `origin` with normal `n` (in-plane axes chosen deterministically).
|
||||
pub fn from_normal(origin: Vec3, n: Vec3) -> Option<Plane> {
|
||||
let n = n.normalized()?;
|
||||
// Horizontal planes keep world X as sketch x; others use the horizontal direction Z × n.
|
||||
let cand = if n.dot(Vec3::Z).abs() > 0.9 { Vec3::X } else { Vec3::Z.cross(n) };
|
||||
let x = (cand - n * cand.dot(n)).normalized()?;
|
||||
Plane::new(origin, x, n.cross(x))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normal(&self) -> Vec3 {
|
||||
self.x.cross(self.y)
|
||||
}
|
||||
pub fn to_world(&self, p: Vec2) -> Vec3 {
|
||||
self.origin + self.x * p.x + self.y * p.y
|
||||
}
|
||||
pub fn dir_to_world(&self, d: Vec2) -> Vec3 {
|
||||
self.x * d.x + self.y * d.y
|
||||
}
|
||||
pub fn to_local(&self, p: Vec3) -> Vec2 {
|
||||
let d = p - self.origin;
|
||||
Vec2::new(d.dot(self.x), d.dot(self.y))
|
||||
}
|
||||
/// Signed distance of `p` above the plane (along the normal).
|
||||
pub fn height(&self, p: Vec3) -> f64 {
|
||||
(p - self.origin).dot(self.normal())
|
||||
}
|
||||
/// The plane moved along its normal by `d`.
|
||||
pub fn offset(&self, d: f64) -> Plane {
|
||||
Plane { origin: self.origin + self.normal() * d, ..*self }
|
||||
}
|
||||
/// Intersection of the ray `o + t·dir` with the plane (t may be negative).
|
||||
pub fn intersect_ray(&self, o: Vec3, dir: Vec3) -> Option<Vec3> {
|
||||
let n = self.normal();
|
||||
let den = dir.dot(n);
|
||||
if den.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let t = (self.origin - o).dot(n) / den;
|
||||
t.is_finite().then(|| o + dir * t)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! Planar profiles: closed loops of lines and circular arcs, and regions (an outer loop minus
|
||||
//! holes). Sketches produce them; the kernel turns them into faces.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::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,
|
||||
},
|
||||
}
|
||||
|
||||
impl Seg2 {
|
||||
pub fn start(&self) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { a, .. } => a,
|
||||
Seg2::Arc { center, radius, start, .. } => center + Vec2::from_angle(start) * radius,
|
||||
}
|
||||
}
|
||||
pub fn end(&self) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { b, .. } => b,
|
||||
Seg2::Arc { center, radius, start, sweep } => center + Vec2::from_angle(start + sweep) * radius,
|
||||
}
|
||||
}
|
||||
pub fn point_at(&self, t: f64) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => a.lerp(b, t),
|
||||
Seg2::Arc { center, radius, start, sweep } => center + Vec2::from_angle(start + sweep * t) * radius,
|
||||
}
|
||||
}
|
||||
pub fn mid(&self) -> Vec2 {
|
||||
self.point_at(0.5)
|
||||
}
|
||||
/// Unit tangent at the start (direction of travel).
|
||||
pub fn start_tangent(&self) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => (b - a).normalized().unwrap_or(Vec2::X),
|
||||
Seg2::Arc { start, sweep, .. } => Vec2::from_angle(start).perp() * sweep.signum(),
|
||||
}
|
||||
}
|
||||
/// Unit tangent at the end (direction of travel).
|
||||
pub fn end_tangent(&self) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => (b - a).normalized().unwrap_or(Vec2::X),
|
||||
Seg2::Arc { start, sweep, .. } => Vec2::from_angle(start + sweep).perp() * sweep.signum(),
|
||||
}
|
||||
}
|
||||
pub fn reversed(&self) -> Seg2 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => Seg2::Line { a: b, b: a },
|
||||
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center, radius, start: start + sweep, sweep: -sweep },
|
||||
}
|
||||
}
|
||||
pub fn length(&self) -> f64 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => a.dist(b),
|
||||
Seg2::Arc { radius, sweep, .. } => (radius * sweep).abs(),
|
||||
}
|
||||
}
|
||||
/// Contribution to the loop's signed area (shoelace integral ½∮(x dy − y dx)).
|
||||
pub fn area_term(&self) -> f64 {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => 0.5 * a.cross(b),
|
||||
Seg2::Arc { radius, sweep, .. } => {
|
||||
let (a, b) = (self.start(), self.end());
|
||||
// Chord term plus the circular segment between chord and arc.
|
||||
0.5 * a.cross(b) + 0.5 * radius * radius * (sweep - sweep.sin())
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Polyline approximation (including both end points) with at most `tol` chord error.
|
||||
pub fn polyline(&self, tol: f64) -> Vec<Vec2> {
|
||||
match *self {
|
||||
Seg2::Line { a, b } => vec![a, b],
|
||||
Seg2::Arc { radius, sweep, .. } => {
|
||||
let tol = tol.max(radius.abs() * 1e-6).max(1e-9);
|
||||
let step = if tol < radius { 2.0 * (1.0 - tol / radius).clamp(-1.0, 1.0).acos() } else { TAU / 8.0 };
|
||||
let n = ((sweep.abs() / step.max(1e-6)).ceil() as usize).clamp(2, 4096);
|
||||
(0..=n).map(|i| self.point_at(i as f64 / n as f64)).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 two half arcs (counter-clockwise).
|
||||
pub fn circle(center: Vec2, radius: f64) -> Loop2 {
|
||||
Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center, radius, start: 0.0, sweep: std::f64::consts::PI },
|
||||
Seg2::Arc { center, radius, start: std::f64::consts::PI, sweep: std::f64::consts::PI },
|
||||
],
|
||||
}
|
||||
}
|
||||
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() }
|
||||
}
|
||||
/// Signed area: positive when counter-clockwise.
|
||||
pub fn signed_area(&self) -> f64 {
|
||||
self.segs.iter().map(Seg2::area_term).sum()
|
||||
}
|
||||
pub fn reversed(&self) -> Loop2 {
|
||||
Loop2 { segs: self.segs.iter().rev().map(Seg2::reversed).collect() }
|
||||
}
|
||||
/// Counter-clockwise copy.
|
||||
pub fn ccw(&self) -> Loop2 {
|
||||
if self.signed_area() < 0.0 { self.reversed() } else { self.clone() }
|
||||
}
|
||||
pub fn polyline(&self, tol: f64) -> Vec<Vec2> {
|
||||
let mut out = Vec::new();
|
||||
for s in &self.segs {
|
||||
let p = s.polyline(tol);
|
||||
out.extend(p.iter().take(p.len().saturating_sub(1)));
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Even-odd point containment against the polyline approximation.
|
||||
pub fn contains(&self, p: Vec2) -> bool {
|
||||
let poly = self.polyline(1e-3);
|
||||
let n = poly.len();
|
||||
let mut inside = false;
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (poly.get(i), poly.get((i + 1) % n)) else { continue };
|
||||
if (a.y > p.y) != (b.y > p.y) {
|
||||
let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
|
||||
if p.x < x {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
/// Area-weighted centroid (of the polyline approximation).
|
||||
pub fn centroid(&self) -> Vec2 {
|
||||
let poly = self.polyline(1e-3);
|
||||
let n = poly.len();
|
||||
let (mut a2, mut c) = (0.0, Vec2::ZERO);
|
||||
for i in 0..n {
|
||||
let (Some(p), Some(q)) = (poly.get(i), poly.get((i + 1) % n)) else { continue };
|
||||
let k = p.cross(*q);
|
||||
a2 += k;
|
||||
c += (*p + *q) * k;
|
||||
}
|
||||
if a2.abs() < 1e-15 { poly.first().copied().unwrap_or_default() } else { c / (3.0 * a2) }
|
||||
}
|
||||
/// Closed within `tol`?
|
||||
pub fn is_closed(&self, tol: f64) -> bool {
|
||||
let n = self.segs.len();
|
||||
n > 0
|
||||
&& (0..n).all(|i| match (self.segs.get(i), self.segs.get((i + 1) % n)) {
|
||||
(Some(a), Some(b)) => a.end().dist(b.start()) <= tol,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub fn area(&self) -> f64 {
|
||||
self.outer.signed_area().abs() - self.holes.iter().map(|h| h.signed_area().abs()).sum::<f64>()
|
||||
}
|
||||
/// A point strictly inside the region (for picking and labels).
|
||||
pub fn interior_point(&self) -> Vec2 {
|
||||
let c = self.centroid();
|
||||
if self.contains(c) {
|
||||
return c;
|
||||
}
|
||||
// Scan a few horizontal lines for the midpoint of the widest inside span.
|
||||
let poly = self.outer.polyline(1e-2);
|
||||
let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for p in &poly {
|
||||
y0 = y0.min(p.y);
|
||||
y1 = y1.max(p.y);
|
||||
}
|
||||
for k in 1..16 {
|
||||
let y = y0 + (y1 - y0) * k as f64 / 16.0;
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for l in std::iter::once(&self.outer).chain(&self.holes) {
|
||||
let pl = l.polyline(1e-2);
|
||||
let n = pl.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (pl.get(i), pl.get((i + 1) % n)) else { continue };
|
||||
if (a.y > y) != (b.y > y) {
|
||||
xs.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
|
||||
}
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
for w in xs.chunks(2) {
|
||||
if let [a, b] = w {
|
||||
let p = Vec2::new((a + b) * 0.5, y);
|
||||
if self.contains(p) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
pub fn contains(&self, p: Vec2) -> bool {
|
||||
self.outer.contains(p) && !self.holes.iter().any(|h| h.contains(p))
|
||||
}
|
||||
pub fn centroid(&self) -> Vec2 {
|
||||
let ao = self.outer.signed_area().abs();
|
||||
let mut c = self.outer.centroid() * ao;
|
||||
let mut a = ao;
|
||||
for h in &self.holes {
|
||||
let ah = h.signed_area().abs();
|
||||
c -= h.centroid() * ah;
|
||||
a -= ah;
|
||||
}
|
||||
if a.abs() < 1e-15 { self.outer.centroid() } else { c / a }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn areas() {
|
||||
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(4.0, 3.0), Vec2::new(0.0, 3.0)]);
|
||||
assert!((sq.signed_area() - 12.0).abs() < 1e-12);
|
||||
assert!((sq.reversed().signed_area() + 12.0).abs() < 1e-12);
|
||||
let c = Loop2::circle(Vec2::new(5.0, 5.0), 2.0);
|
||||
assert!((c.signed_area() - std::f64::consts::PI * 4.0).abs() < 1e-9);
|
||||
assert!(c.is_closed(1e-9));
|
||||
let r = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![Loop2::circle(Vec2::ZERO, 5.0).reversed()] };
|
||||
assert!((r.area() - std::f64::consts::PI * 75.0).abs() < 1e-9);
|
||||
let ip = r.interior_point();
|
||||
assert!(r.contains(ip), "{ip:?}");
|
||||
assert!(sq.contains(Vec2::new(1.0, 1.0)) && !sq.contains(Vec2::new(5.0, 1.0)));
|
||||
assert!(sq.centroid().dist(Vec2::new(2.0, 1.5)) < 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::*;
|
||||
|
||||
fn cube_mesh(s: f64) -> Mesh {
|
||||
let p = |x: f64, y: f64, z: f64| Vec3::new(x * s, y * s, z * s);
|
||||
let positions = vec![p(0., 0., 0.), p(1., 0., 0.), p(1., 1., 0.), p(0., 1., 0.), p(0., 0., 1.), p(1., 0., 1.), p(1., 1., 1.), p(0., 1., 1.)];
|
||||
let triangles =
|
||||
vec![[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]];
|
||||
let n = triangles.len();
|
||||
Mesh { normals: vec![Vec3::Z; 8], positions, triangles, tri_face: vec![0; n], edges: vec![] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cube_measure() {
|
||||
let m = cube_mesh(2.0).measure();
|
||||
assert!((m.volume - 8.0).abs() < 1e-12);
|
||||
assert!((m.area - 24.0).abs() < 1e-12);
|
||||
assert!(m.centroid.dist(Vec3::new(1.0, 1.0, 1.0)) < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raycast_hits_front_face() {
|
||||
let m = cube_mesh(2.0);
|
||||
let (t, _) = m.raycast(Vec3::new(1.0, 1.0, 10.0), Vec3::new(0.0, 0.0, -1.0)).unwrap();
|
||||
assert!((t - 8.0).abs() < 1e-9);
|
||||
assert!(m.raycast(Vec3::new(5.0, 5.0, 10.0), Vec3::new(0.0, 0.0, -1.0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planes() {
|
||||
assert_eq!(Plane::XY.normal(), Vec3::Z);
|
||||
assert_eq!(Plane::XZ.normal(), Vec3::new(0.0, -1.0, 0.0));
|
||||
assert_eq!(Plane::YZ.normal(), Vec3::X);
|
||||
let p = Plane::XZ.to_world(Vec2::new(3.0, 4.0));
|
||||
assert_eq!(p, Vec3::new(3.0, 0.0, 4.0));
|
||||
assert_eq!(Plane::XZ.to_local(p), Vec2::new(3.0, 4.0));
|
||||
let hit = Plane::XY.offset(5.0).intersect_ray(Vec3::new(1.0, 2.0, 10.0), Vec3::new(0.0, 0.0, -2.0)).unwrap();
|
||||
assert!(hit.dist(Vec3::new(1.0, 2.0, 5.0)) < 1e-12);
|
||||
assert!(Plane::new(Vec3::ZERO, Vec3::X, Vec3::X).is_none());
|
||||
let f = Plane::from_normal(Vec3::ZERO, Vec3::new(0.0, 0.0, 3.0)).unwrap();
|
||||
assert!((f.normal() - Vec3::Z).len() < 1e-12);
|
||||
assert!(Plane::from_normal(Vec3::ZERO, Vec3::new(f64::NAN, 0.0, 1.0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec_ops_and_serde() {
|
||||
let a = Vec3::new(1.0, 2.0, 3.0);
|
||||
assert_eq!(a.cross(Vec3::X), Vec3::new(0.0, 3.0, -2.0));
|
||||
assert_eq!(Vec2::new(3.0, 4.0).len(), 5.0);
|
||||
assert!(Vec2::ZERO.normalized().is_none());
|
||||
assert!(Vec3::new(0.5, 0.0, 0.0).dist_to_segment(Vec3::ZERO, Vec3::X) < 1e-12);
|
||||
assert_eq!(serde_json::to_string(&a).unwrap(), "[1.0,2.0,3.0]");
|
||||
let b: Vec2 = serde_json::from_str("[1,2]").unwrap();
|
||||
assert_eq!(b, Vec2::new(1.0, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aabb() {
|
||||
let mut b = Aabb3::EMPTY;
|
||||
assert!(b.is_empty());
|
||||
b.add(Vec3::new(1.0, 2.0, 3.0));
|
||||
b.add(Vec3::new(f64::NAN, 0.0, 0.0));
|
||||
b.add(Vec3::new(-1.0, 0.0, 0.0));
|
||||
assert_eq!(b.size(), Vec3::new(2.0, 2.0, 3.0));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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 {
|
||||
pub x: f64,
|
||||
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 {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
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 {
|
||||
pub const ZERO: Vec2 = Vec2 { x: 0.0, y: 0.0 };
|
||||
pub const X: Vec2 = Vec2 { x: 1.0, y: 0.0 };
|
||||
pub const Y: Vec2 = Vec2 { x: 0.0, y: 1.0 };
|
||||
pub const fn new(x: f64, y: f64) -> Self {
|
||||
Vec2 { x, y }
|
||||
}
|
||||
pub fn dot(self, o: Vec2) -> f64 {
|
||||
self.x * o.x + self.y * o.y
|
||||
}
|
||||
/// z component of the 3D cross product.
|
||||
pub fn cross(self, o: Vec2) -> f64 {
|
||||
self.x * o.y - self.y * o.x
|
||||
}
|
||||
pub fn len(self) -> f64 {
|
||||
self.x.hypot(self.y)
|
||||
}
|
||||
pub fn len2(self) -> f64 {
|
||||
self.dot(self)
|
||||
}
|
||||
pub fn dist(self, o: Vec2) -> f64 {
|
||||
(self - o).len()
|
||||
}
|
||||
pub fn normalized(self) -> Option<Vec2> {
|
||||
let l = self.len();
|
||||
(l > crate::EPS && l.is_finite()).then(|| self / l)
|
||||
}
|
||||
/// Rotated +90°.
|
||||
pub fn perp(self) -> Vec2 {
|
||||
Vec2::new(-self.y, self.x)
|
||||
}
|
||||
pub fn angle(self) -> f64 {
|
||||
self.y.atan2(self.x)
|
||||
}
|
||||
pub fn from_angle(a: f64) -> Vec2 {
|
||||
Vec2::new(a.cos(), a.sin())
|
||||
}
|
||||
pub fn lerp(self, o: Vec2, t: f64) -> Vec2 {
|
||||
self + (o - self) * t
|
||||
}
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite()
|
||||
}
|
||||
pub fn extend(self, z: f64) -> Vec3 {
|
||||
Vec3::new(self.x, self.y, z)
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec3 {
|
||||
pub const ZERO: Vec3 = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
pub const X: Vec3 = Vec3 { x: 1.0, y: 0.0, z: 0.0 };
|
||||
pub const Y: Vec3 = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
|
||||
pub const Z: Vec3 = Vec3 { x: 0.0, y: 0.0, z: 1.0 };
|
||||
pub const fn new(x: f64, y: f64, z: f64) -> Self {
|
||||
Vec3 { x, y, z }
|
||||
}
|
||||
pub fn dot(self, o: Vec3) -> f64 {
|
||||
self.x * o.x + self.y * o.y + self.z * o.z
|
||||
}
|
||||
pub fn cross(self, o: Vec3) -> Vec3 {
|
||||
Vec3::new(self.y * o.z - self.z * o.y, self.z * o.x - self.x * o.z, self.x * o.y - self.y * o.x)
|
||||
}
|
||||
pub fn len(self) -> f64 {
|
||||
self.len2().sqrt()
|
||||
}
|
||||
pub fn len2(self) -> f64 {
|
||||
self.dot(self)
|
||||
}
|
||||
pub fn dist(self, o: Vec3) -> f64 {
|
||||
(self - o).len()
|
||||
}
|
||||
pub fn normalized(self) -> Option<Vec3> {
|
||||
let l = self.len();
|
||||
(l > crate::EPS && l.is_finite()).then(|| self / l)
|
||||
}
|
||||
pub fn lerp(self, o: Vec3, t: f64) -> Vec3 {
|
||||
self + (o - self) * t
|
||||
}
|
||||
pub fn is_finite(self) -> bool {
|
||||
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
|
||||
}
|
||||
pub fn min(self, o: Vec3) -> Vec3 {
|
||||
Vec3::new(self.x.min(o.x), self.y.min(o.y), self.z.min(o.z))
|
||||
}
|
||||
pub fn max(self, o: Vec3) -> Vec3 {
|
||||
Vec3::new(self.x.max(o.x), self.y.max(o.y), self.z.max(o.z))
|
||||
}
|
||||
/// Any unit vector perpendicular to `self` (which should be non-zero).
|
||||
pub fn any_perp(self) -> Vec3 {
|
||||
let a = if self.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
|
||||
self.cross(a).normalized().unwrap_or(Vec3::Z)
|
||||
}
|
||||
pub fn to_f32(self) -> [f32; 3] {
|
||||
[self.x as f32, self.y as f32, self.z as f32]
|
||||
}
|
||||
/// Distance from this point to the segment `a`–`b`.
|
||||
pub fn dist_to_segment(self, a: Vec3, b: Vec3) -> f64 {
|
||||
let ab = b - a;
|
||||
let l2 = ab.len2();
|
||||
let t = if l2 > 0.0 { ((self - a).dot(ab) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
self.dist(a + ab * t)
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
Reference in new issue
Block a user