1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
use crate::float_types::parry3d::bounding_volume::Aabb;
use crate::float_types::{EPSILON, Real};
use crate::mesh::plane::Plane;
use nalgebra::{Matrix3, Matrix4, Rotation3, Translation3, Vector3};
/// Boolean operations + transformations
pub trait CSG: Sized + Clone {
fn new() -> Self;
fn union(&self, other: &Self) -> Self;
fn difference(&self, other: &Self) -> Self;
fn intersection(&self, other: &Self) -> Self;
fn xor(&self, other: &Self) -> Self;
fn transform(&self, matrix: &Matrix4<Real>) -> Self;
fn inverse(&self) -> Self;
fn bounding_box(&self) -> Aabb;
fn invalidate_bounding_box(&mut self);
/// Returns a new Self translated by vector.
fn translate_vector(&self, vector: Vector3<Real>) -> Self {
self.transform(&Translation3::from(vector).to_homogeneous())
}
/// Returns a new Self translated by x, y, and z.
fn translate(&self, x: Real, y: Real, z: Real) -> Self {
self.translate_vector(Vector3::new(x, y, z))
}
/// Returns a new Self translated so that its bounding-box center is at the origin (0,0,0).
fn center(&self) -> Self {
let aabb = self.bounding_box();
// Compute the AABB center
let center_x = (aabb.mins.x + aabb.maxs.x) * 0.5;
let center_y = (aabb.mins.y + aabb.maxs.y) * 0.5;
let center_z = (aabb.mins.z + aabb.maxs.z) * 0.5;
// Translate so that the bounding-box center goes to the origin
self.translate(-center_x, -center_y, -center_z)
}
/// Translates Self so that its bottommost point(s) sit exactly at z=0.
///
/// - Shifts all vertices up or down such that the minimum z coordinate of the bounding box becomes 0.
///
/// # Example
/// ```
/// use csgrs::mesh::Mesh;
/// use crate::csgrs::traits::CSG;
/// let mesh = Mesh::<()>::cube(1.0, None).translate(2.0, 1.0, -2.0);
/// let floated = mesh.float();
/// assert_eq!(floated.bounding_box().mins.z, 0.0);
/// ```
fn float(&self) -> Self {
let aabb = self.bounding_box();
let min_z = aabb.mins.z;
self.translate(0.0, 0.0, -min_z)
}
/// Rotates Self by x_degrees, y_degrees, z_degrees
fn rotate(&self, x_deg: Real, y_deg: Real, z_deg: Real) -> Self {
let rx = Rotation3::from_axis_angle(&Vector3::x_axis(), x_deg.to_radians());
let ry = Rotation3::from_axis_angle(&Vector3::y_axis(), y_deg.to_radians());
let rz = Rotation3::from_axis_angle(&Vector3::z_axis(), z_deg.to_radians());
// Compose them in the desired order
let rot = rz * ry * rx;
self.transform(&rot.to_homogeneous())
}
/// Scales Self by scale_x, scale_y, scale_z
fn scale(&self, sx: Real, sy: Real, sz: Real) -> Self {
let mat4 = Matrix4::new_nonuniform_scaling(&Vector3::new(sx, sy, sz));
self.transform(&mat4)
}
/// **Mathematical Foundation: Reflection Across Arbitrary Planes**
///
/// Reflect (mirror) this object about an arbitrary plane `plane`.
/// This implements the complete mathematical theory of 3D reflections:
///
/// ## **Reflection Mathematics**
///
/// ### **Plane Representation**
/// The plane is specified by:
/// - `plane.normal` = the plane's normal vector n⃗ (need not be unit)
/// - `plane.offset` = the signed distance d from origin to plane
/// - **Plane Equation**: n⃗·p⃗ + d = 0
///
/// ### **Reflection Matrix Derivation**
/// For a unit normal n̂ and plane through origin, the reflection matrix is:
/// ```text
/// R = I - 2n̂n̂ᵀ
/// ```
/// **Proof**: For any vector v⃗, the reflection is:
/// - **Component parallel to n̂**: v∥ = (v⃗·n̂)n̂ → reflected to -v∥
/// - **Component perpendicular**: v⊥ = v⃗ - v∥ → unchanged
/// - **Result**: v'⃗ = v⊥ - v∥ = v⃗ - 2(v⃗·n̂)n̂ = (I - 2n̂n̂ᵀ)v⃗
///
/// ### **General Plane Reflection Algorithm**
/// 1. **Normalize**: n̂ = n⃗/|n⃗|, d̂ = d/|n⃗|
/// 2. **Translate to Origin**: T₁ = translate by -d̂n̂
/// 3. **Reflect at Origin**: R = I - 2n̂n̂ᵀ
/// 4. **Translate Back**: T₂ = translate by +d̂n̂
/// 5. **Compose**: M = T₂ · R · T₁
///
/// ### **Normal Vector Transformation**
/// Normals transform by the inverse transpose: n'⃗ = (M⁻¹)ᵀn⃗
/// For reflections, this simplifies to the same matrix M.
///
/// ## **Geometric Properties**
/// - **Isometry**: Preserves distances and angles
/// - **Orientation Reversal**: Changes handedness (det(M) = -1)
/// - **Involution**: M² = I (reflecting twice gives identity)
/// - **Plane Invariance**: Points on the plane remain fixed
///
/// **Note**: The result is inverted (.inverse()) because reflection reverses
/// the orientation of polygons, affecting inside/outside semantics in CSG.
///
/// Returns a new Self whose geometry is mirrored accordingly.
fn mirror(&self, plane: Plane) -> Self {
// Normal might not be unit, so compute its length:
let len = plane.normal().norm();
if len.abs() < EPSILON {
// Degenerate plane? Just return clone (no transform)
return self.clone();
}
// Unit normal:
let n = plane.normal() / len;
// Adjusted offset = w / ||n||
let w = plane.offset() / len;
// Translate so the plane crosses the origin
// The plane’s offset vector from origin is (w * n).
let offset = n * w;
let t1 = Translation3::from(-offset).to_homogeneous(); // push the plane to origin
// Build the reflection matrix about a plane normal n at the origin
// R = I - 2 n n^T
let mut reflect_4 = Matrix4::identity();
let reflect_3 = Matrix3::identity() - 2.0 * n * n.transpose();
reflect_4.fixed_view_mut::<3, 3>(0, 0).copy_from(&reflect_3);
// Translate back
let t2 = Translation3::from(offset).to_homogeneous(); // pull the plane back out
// Combine into a single 4×4
let mirror_mat = t2 * reflect_4 * t1;
// Apply to all polygons
self.transform(&mirror_mat).inverse()
}
/// Distribute Self `count` times around an arc (in XY plane) of radius,
/// from `start_angle_deg` to `end_angle_deg`.
/// Returns a new shape with all copies
fn distribute_arc(
&self,
count: usize,
radius: Real,
start_angle_deg: Real,
end_angle_deg: Real,
) -> Self {
if count < 1 {
return self.clone();
}
let start_rad = start_angle_deg.to_radians();
let end_rad = end_angle_deg.to_radians();
let sweep = end_rad - start_rad;
(0..count)
.map(|i| {
let t = if count == 1 {
0.5
} else {
i as Real / ((count - 1) as Real)
};
let angle = start_rad + t * sweep;
let rot =
nalgebra::Rotation3::from_axis_angle(&nalgebra::Vector3::z_axis(), angle)
.to_homogeneous();
// translate out to radius in x
let trans = nalgebra::Translation3::new(radius, 0.0, 0.0).to_homogeneous();
let mat = rot * trans;
self.transform(&mat)
})
.reduce(|acc, csg| acc.union(&csg))
.unwrap()
}
/// Distribute Self `count` times along a straight line (vector),
/// each copy spaced by `spacing`.
/// E.g. if `dir=(1.0,0.0,0.0)` and `spacing=2.0`, you get copies at
/// x=0, x=2, x=4, ... etc.
fn distribute_linear(
&self,
count: usize,
dir: nalgebra::Vector3<Real>,
spacing: Real,
) -> Self {
if count < 1 {
return self.clone();
}
let step = dir.normalize() * spacing;
(0..count)
.map(|i| {
let offset = step * (i as Real);
let trans = nalgebra::Translation3::from(offset).to_homogeneous();
self.transform(&trans)
})
.reduce(|acc, csg| acc.union(&csg))
.unwrap()
}
/// Distribute Self in a grid of `rows x cols`, with spacing dx, dy in XY plane.
/// top-left or bottom-left depends on your usage of row/col iteration.
fn distribute_grid(&self, rows: usize, cols: usize, dx: Real, dy: Real) -> Self {
if rows < 1 || cols < 1 {
return self.clone();
}
let step_x = nalgebra::Vector3::new(dx, 0.0, 0.0);
let step_y = nalgebra::Vector3::new(0.0, dy, 0.0);
(0..rows)
.flat_map(|r| {
(0..cols).map(move |c| {
let offset = step_x * (c as Real) + step_y * (r as Real);
let trans = nalgebra::Translation3::from(offset).to_homogeneous();
self.transform(&trans)
})
})
.reduce(|acc, csg| acc.union(&csg))
.unwrap()
}
}