1use crate::ops::{CutOptions as OpsCutOptions, cut_by_plane as ops_cut_by_plane};
15use crate::placement::world_to_local;
16use crate::{MeshId, MeshOptions, Plane, Result, Scene};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct CutOptions {
21 pub plane: Plane,
24 pub cap: bool,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct CutReport {
33 pub added: MeshId,
35 pub kept_triangles: usize,
37 pub removed_triangles: usize,
39 pub cap_triangles: usize,
41}
42
43fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
44 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
45}
46
47fn unit(a: [f64; 3]) -> [f64; 3] {
48 let n = dot(a, a).sqrt();
49 a.map(|c| c / n)
50}
51
52fn local_plane(
58 placement: &crate::Placement,
59 mesh_origin: [f64; 3],
60 plane: &Plane,
61) -> Result<Plane> {
62 let origin = world_to_local(placement, mesh_origin, plane.origin());
63 let u = unit(placement.inverse_direction(plane.u()));
64 let v = placement.inverse_direction(plane.v());
65 let d = dot(v, u);
66 let v = unit(std::array::from_fn(|k| v[k] - d * u[k]));
67 Ok(Plane::with_axes(origin, u, v)?)
68}
69
70impl Scene {
71 pub fn cut_by_plane(&mut self, id: MeshId, options: &CutOptions) -> Result<CutReport> {
97 self.drop_field_unit();
98 let local = {
99 let entry = self.entry(id)?;
100 local_plane(&entry.placement, entry.mesh.origin(), &options.plane)?
101 };
102 let output = self.edit(id, |tx| {
103 ops_cut_by_plane(tx, &local, &OpsCutOptions { cap: options.cap })
104 })?;
105 let (placement, mode) = {
106 let entry = self.entry(id)?;
107 (entry.placement, entry.mode)
108 };
109 let removed_triangles = output.report.removed_triangles;
110 let cap_triangles = output.report.cap_triangles;
111 let added = self.add_mesh(
112 output.removed,
113 MeshOptions {
114 placement,
115 mode,
116 ..MeshOptions::default()
117 },
118 )?;
119 Ok(CutReport {
120 added,
121 kept_triangles: output.report.kept_triangles,
122 removed_triangles,
123 cap_triangles,
124 })
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use fast_mesh_store::Mesh;
131
132 use crate::ops::OpsError;
133 use crate::{CutOptions, MeshOptions, Placement, Plane, Scene, SceneError, SceneOptions};
134
135 fn cube() -> Mesh {
137 Mesh::from_buffers(
138 &[
139 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
147 ],
148 &[
149 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 2, 3, 7, 2, 7, 6, 1, 2, 6, 1, 6, 5, 3, 0, 4, 3, 4, 7, ],
156 )
157 .unwrap()
158 }
159
160 fn plane(origin: [f64; 3], normal: [f64; 3]) -> Plane {
161 Plane::new(origin, normal).unwrap()
162 }
163
164 #[test]
165 fn cut_keeps_placement_and_undoes_in_one_step() {
166 let mut scene = Scene::new(SceneOptions::default());
167 let placement = Placement {
170 translation: [10.0, 0.0, 0.0],
171 ..Placement::IDENTITY
172 };
173 let id = scene
174 .add_mesh(
175 cube(),
176 MeshOptions {
177 placement,
178 ..MeshOptions::default()
179 },
180 )
181 .unwrap();
182 scene.take_changes();
183 let before = scene.mesh(id).unwrap().triangle_count();
184
185 let report = scene
186 .cut_by_plane(
187 id,
188 &CutOptions {
189 plane: plane([10.5, 0.0, 0.0], [1.0, 0.0, 0.0]),
190 cap: true,
191 },
192 )
193 .unwrap();
194 assert!(
196 crate::ops::boundary_loops(scene.mesh(id).unwrap())
197 .unwrap()
198 .is_empty()
199 );
200 assert!(
201 crate::ops::boundary_loops(scene.mesh(report.added).unwrap())
202 .unwrap()
203 .is_empty()
204 );
205 assert_eq!(scene.placement(report.added), Ok(placement));
206 assert_eq!(scene.mode(report.added), Ok(scene.mode(id).unwrap()));
207 assert_ne!(report.added, id);
208 let kept_world: Vec<[f64; 3]> = (0..scene.mesh(id).unwrap().vertex_slots() as u32)
210 .filter(|&v| scene.mesh(id).unwrap().is_vertex_alive(v))
211 .map(|v| {
212 placement.apply([
213 scene.mesh(id).unwrap().origin()[0]
214 + f64::from(scene.mesh(id).unwrap().position(v).unwrap()[0]),
215 scene.mesh(id).unwrap().origin()[1]
216 + f64::from(scene.mesh(id).unwrap().position(v).unwrap()[1]),
217 scene.mesh(id).unwrap().origin()[2]
218 + f64::from(scene.mesh(id).unwrap().position(v).unwrap()[2]),
219 ])
220 })
221 .collect();
222 assert!(kept_world.iter().all(|p| p[0] >= 10.5 - 1e-6));
223 assert_eq!(
224 report.kept_triangles,
225 scene.mesh(id).unwrap().triangle_count()
226 );
227 assert_eq!(
228 report.removed_triangles,
229 scene.mesh(report.added).unwrap().triangle_count()
230 );
231
232 assert!(scene.can_undo());
234 assert!(scene.undo().unwrap());
235 assert_eq!(scene.mesh(id).unwrap().triangle_count(), before);
236 assert!(scene.mesh(report.added).is_ok());
237 assert!(!scene.can_undo());
238 assert!(scene.redo().unwrap());
239 assert_ne!(scene.mesh(id).unwrap().triangle_count(), before);
240 }
241
242 #[test]
243 fn scaled_cut_follows_the_scale() {
244 let mut scene = Scene::new(SceneOptions::default());
247 let placement = Placement {
248 translation: [0.0, 3.0, 0.0],
249 scale: [2.0, 1.0, 1.0],
250 ..Placement::IDENTITY
251 };
252 let id = scene
253 .add_mesh(
254 cube(),
255 MeshOptions {
256 placement,
257 ..MeshOptions::default()
258 },
259 )
260 .unwrap();
261 scene.take_changes();
262 let report = scene
263 .cut_by_plane(
264 id,
265 &CutOptions {
266 plane: plane([1.0, 3.0, 0.0], [1.0, 0.0, 0.0]),
267 cap: true,
268 },
269 )
270 .unwrap();
271 assert!(
272 crate::ops::boundary_loops(scene.mesh(id).unwrap())
273 .unwrap()
274 .is_empty()
275 );
276 assert!(
277 crate::ops::boundary_loops(scene.mesh(report.added).unwrap())
278 .unwrap()
279 .is_empty()
280 );
281 assert_eq!(scene.placement(report.added), Ok(placement));
282 let mut lo = [f64::INFINITY; 3];
284 let mut hi = [f64::NEG_INFINITY; 3];
285 let mesh = scene.mesh(id).unwrap();
286 for v in 0..mesh.vertex_slots() as u32 {
287 let Some(p) = mesh.position(v) else {
288 continue;
289 };
290 let w = placement.apply([
291 mesh.origin()[0] + f64::from(p[0]),
292 mesh.origin()[1] + f64::from(p[1]),
293 mesh.origin()[2] + f64::from(p[2]),
294 ]);
295 for k in 0..3 {
296 lo[k] = lo[k].min(w[k]);
297 hi[k] = hi[k].max(w[k]);
298 }
299 }
300 assert!((lo[0] - 1.0).abs() < 1e-4, "{lo:?}");
301 assert!((hi[0] - 2.0).abs() < 1e-4, "{hi:?}");
302 assert!((lo[1] - 3.0).abs() < 1e-4, "{lo:?}");
303 assert!((hi[1] - 4.0).abs() < 1e-4, "{hi:?}");
304 }
305
306 #[test]
307 fn missing_and_uncapped_cuts() {
308 let mut scene = Scene::new(SceneOptions::default());
309 let id = scene.add_mesh(cube(), MeshOptions::default()).unwrap();
310 scene.take_changes();
311 assert_eq!(
313 scene.cut_by_plane(
314 id,
315 &CutOptions {
316 plane: plane([5.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
317 cap: true,
318 },
319 ),
320 Err(SceneError::Ops(OpsError::InvalidInput(
321 "plane does not cut the mesh",
322 )))
323 );
324 assert_eq!(scene.mesh(id).unwrap().triangle_count(), 12);
325 assert!(!scene.can_undo());
326 let report = scene
328 .cut_by_plane(
329 id,
330 &CutOptions {
331 plane: plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]),
332 cap: false,
333 },
334 )
335 .unwrap();
336 assert_eq!(report.cap_triangles, 0);
337 assert!(
338 !crate::ops::boundary_loops(scene.mesh(id).unwrap())
339 .unwrap()
340 .is_empty()
341 );
342 assert!(
343 !crate::ops::boundary_loops(scene.mesh(report.added).unwrap())
344 .unwrap()
345 .is_empty()
346 );
347 }
348}