192 lines
5.3 KiB
Rust
192 lines
5.3 KiB
Rust
use crate::Shape;
|
|
|
|
/// A parallel geometry composition.
|
|
///
|
|
/// Instead of building shapes sequentially (union A then union B then
|
|
/// union C), a Branch groups independent operations that evaluate
|
|
/// simultaneously. The type system enforces that branches are truly
|
|
/// independent — each is a self-contained geometric expression that
|
|
/// shares nothing with its siblings except the evaluation point.
|
|
///
|
|
/// This maps directly to the TrigGraph's DAG structure: each branch
|
|
/// becomes an independent subtree, and the join operation (union,
|
|
/// intersection, etc.) combines results at the end.
|
|
///
|
|
/// ```ignore
|
|
/// let part = par::branch()
|
|
/// .add(sphere(2.0))
|
|
/// .add(cylinder(1.0, 5.0).translate(0.0, 0.0, 1.0))
|
|
/// .add(cube(1.5).rotate_z(45.0).translate(3.0, 0.0, 0.0))
|
|
/// .union();
|
|
/// ```
|
|
pub struct Branch {
|
|
shapes: Vec<Shape>,
|
|
}
|
|
|
|
/// Start a parallel branch composition.
|
|
pub fn branch() -> Branch {
|
|
Branch { shapes: Vec::new() }
|
|
}
|
|
|
|
impl Branch {
|
|
/// Add an independent shape to this parallel group.
|
|
pub fn add(mut self, shape: Shape) -> Self {
|
|
self.shapes.push(shape);
|
|
self
|
|
}
|
|
|
|
/// Join all branches via union (min). Additive parallel.
|
|
pub fn union(self) -> Shape {
|
|
Shape::union_all(self.shapes)
|
|
}
|
|
|
|
/// Join all branches via intersection (max). Divisive parallel.
|
|
pub fn intersection(self) -> Shape {
|
|
Shape::intersection_all(self.shapes)
|
|
}
|
|
|
|
/// Join all branches via smooth union. Additive parallel with blending.
|
|
pub fn smooth_union(self, k: f64) -> Shape {
|
|
let mut shapes = self.shapes;
|
|
assert!(!shapes.is_empty());
|
|
let mut result = shapes.remove(0);
|
|
for s in shapes {
|
|
result = result.smooth_union(s, k);
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Number of parallel branches.
|
|
pub fn width(&self) -> usize {
|
|
self.shapes.len()
|
|
}
|
|
}
|
|
|
|
/// A mapped parallel operation: apply a transform to N instances.
|
|
///
|
|
/// This is multiplicative parallelism — the same base shape evaluated
|
|
/// with different parameters. Each instance is independent.
|
|
///
|
|
/// ```ignore
|
|
/// let bolts = par::map(
|
|
/// &cylinder(0.5, 2.0),
|
|
/// (0..8).map(|i| {
|
|
/// let angle = i as f64 * 45.0;
|
|
/// move |s: Shape| s.rotate_z(angle).translate(5.0, 0.0, 0.0)
|
|
/// })
|
|
/// );
|
|
/// ```
|
|
pub fn map<F>(base: &Shape, transforms: impl IntoIterator<Item = F>) -> Shape
|
|
where
|
|
F: FnOnce(Shape) -> Shape,
|
|
{
|
|
let shapes: Vec<Shape> = transforms
|
|
.into_iter()
|
|
.map(|f| f(base.clone()))
|
|
.collect();
|
|
Shape::union_all(shapes)
|
|
}
|
|
|
|
/// Symmetric parallel: apply a shape and its mirror simultaneously.
|
|
///
|
|
/// Both halves evaluate in parallel, then join via union.
|
|
pub fn symmetric_x(shape: &Shape) -> Shape {
|
|
branch()
|
|
.add(shape.clone())
|
|
.add(shape.clone().scale(-1.0, 1.0, 1.0))
|
|
.union()
|
|
}
|
|
|
|
pub fn symmetric_y(shape: &Shape) -> Shape {
|
|
branch()
|
|
.add(shape.clone())
|
|
.add(shape.clone().scale(1.0, -1.0, 1.0))
|
|
.union()
|
|
}
|
|
|
|
pub fn symmetric_z(shape: &Shape) -> Shape {
|
|
branch()
|
|
.add(shape.clone())
|
|
.add(shape.clone().scale(1.0, 1.0, -1.0))
|
|
.union()
|
|
}
|
|
|
|
/// Full octant symmetry: mirror across all three planes.
|
|
/// 8 parallel evaluations.
|
|
pub fn symmetric_xyz(shape: &Shape) -> Shape {
|
|
let mut b = branch();
|
|
for sx in &[1.0, -1.0] {
|
|
for sy in &[1.0, -1.0] {
|
|
for sz in &[1.0, -1.0] {
|
|
b = b.add(shape.clone().scale(*sx, *sy, *sz));
|
|
}
|
|
}
|
|
}
|
|
b.union()
|
|
}
|
|
|
|
/// Polar parallel: N instances around the Z axis, all independent.
|
|
pub fn polar(shape: &Shape, count: usize) -> Shape {
|
|
let step = 360.0 / count as f64;
|
|
map(shape, (0..count).map(move |i| {
|
|
let angle = step * i as f64;
|
|
move |s: Shape| s.rotate_z(angle)
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::prelude::*;
|
|
|
|
#[test]
|
|
fn branch_union() {
|
|
let part = branch()
|
|
.add(sphere(1.0))
|
|
.add(sphere(1.0).translate(3.0, 0.0, 0.0))
|
|
.union();
|
|
|
|
assert!(part.eval(0.0, 0.0, 0.0) < 0.0);
|
|
assert!(part.eval(3.0, 0.0, 0.0) < 0.0);
|
|
assert!(part.eval(1.5, 5.0, 0.0) > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn map_polar() {
|
|
let ring = polar(&sphere(0.3).translate(2.0, 0.0, 0.0), 6);
|
|
// Point at (2,0,0) should be inside one copy
|
|
assert!(ring.eval(2.0, 0.0, 0.0) < 0.0);
|
|
// Origin should be outside all copies
|
|
assert!(ring.eval(0.0, 0.0, 0.0) > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn symmetric_x_creates_mirror() {
|
|
let half = sphere(1.0).translate(2.0, 0.0, 0.0);
|
|
let full = symmetric_x(&half);
|
|
// Both sides present
|
|
assert!(full.eval(2.0, 0.0, 0.0) < 0.0);
|
|
assert!(full.eval(-2.0, 0.0, 0.0) < 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_width() {
|
|
let b = branch()
|
|
.add(sphere(1.0))
|
|
.add(cube(1.0))
|
|
.add(cylinder(1.0, 2.0));
|
|
assert_eq!(b.width(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn map_linear() {
|
|
let row = map(&sphere(0.5), (0..4).map(|i| {
|
|
let x = i as f64 * 2.0;
|
|
move |s: Shape| s.translate(x, 0.0, 0.0)
|
|
}));
|
|
assert!(row.eval(0.0, 0.0, 0.0) < 0.0);
|
|
assert!(row.eval(6.0, 0.0, 0.0) < 0.0);
|
|
assert!(row.eval(1.0, 0.0, 0.0) > 0.0);
|
|
}
|
|
}
|