Skip to main content

fast_mesh_scene/
join.rs

1//! Joining two meshes: append one's geometry into the other and bridge a
2//! loop on each, as one undo step, then remove the emptied mesh.
3
4use fast_mesh_ops::append::{AppendSource, append_mesh_with_uvs};
5use fast_mesh_ops::bridge::{BridgeOptions, bridge_loops};
6use fast_mesh_ops::{LoopId, OpsError, boundary_loops};
7use fast_mesh_store::{Mesh, StoreError, VertexId};
8
9use crate::placement::relative_affine;
10use crate::{MeshId, Result, Scene, SceneError};
11
12/// What to join: a loop on the target and a loop on the source.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct JoinOptions {
15    /// The loop on the target mesh, from its [`boundary_loops`].
16    pub loop_a: LoopId,
17    /// The loop on the source mesh, from its [`boundary_loops`].
18    pub loop_b: LoopId,
19    /// Maximum vertices per loop; `None` means the bridge default (128).
20    pub max_edges: Option<usize>,
21}
22
23/// What [`Scene::join`] did.
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct JoinReport {
26    /// Source vertices copied into the target.
27    pub vertices_added: usize,
28    /// Source triangles copied into the target.
29    pub triangles_added: usize,
30    /// Bridge triangles between the two loops.
31    pub bridge_triangles_added: usize,
32    /// Only one mesh had UVs, so the result has none; undo restores the
33    /// target's.
34    pub uvs_dropped: bool,
35}
36
37fn oom() -> SceneError {
38    SceneError::Store(StoreError::OutOfMemory)
39}
40
41/// The source mesh's alive geometry in the target's local frame, with the
42/// source loop's vertices in loop order.
43struct Source {
44    source: AppendSource,
45    /// The source's UVs, per source position, when it has them.
46    uvs: Option<Vec<f32>>,
47    /// Source position index per source-mesh vertex slot (`u32::MAX` for a
48    /// dead slot).
49    index_of: Vec<u32>,
50    /// The source loop's vertices, in loop order.
51    loop_vertices: Vec<VertexId>,
52}
53
54/// Copies the source mesh's alive positions (mapped into the target's local
55/// frame) and triangles into an [`AppendSource`], resolving the source loop
56/// on the way. Reads only; the scene is untouched.
57fn prepare_source(
58    target: &Mesh,
59    target_placement: &crate::Placement,
60    source: &Mesh,
61    source_placement: &crate::Placement,
62    loop_b: LoopId,
63) -> Result<Source> {
64    let rigid = relative_affine(
65        target_placement,
66        target.origin(),
67        source_placement,
68        source.origin(),
69    );
70    let loops = boundary_loops(source)?;
71    let loop_vertices = loops
72        .iter()
73        .find(|l| l.id == loop_b)
74        .map(|l| l.vertices.clone())
75        .ok_or(OpsError::StaleLoops)?;
76    let mut index_of: Vec<u32> = Vec::new();
77    index_of
78        .try_reserve(source.vertex_slots())
79        .map_err(|_| oom())?;
80    index_of.resize(source.vertex_slots(), u32::MAX);
81    let mut positions: Vec<[f32; 3]> = Vec::new();
82    positions
83        .try_reserve(source.vertex_count())
84        .map_err(|_| oom())?;
85    for (v, slot) in index_of.iter_mut().enumerate() {
86        let Some(p) = source.position(v as VertexId) else {
87            continue;
88        };
89        let w = rigid.apply(p.map(f64::from));
90        let q = [w[0] as f32, w[1] as f32, w[2] as f32];
91        if !q.iter().all(|c| c.is_finite()) {
92            return Err(SceneError::InvalidInput("joined positions must be finite"));
93        }
94        let idx = u32::try_from(positions.len()).map_err(|_| oom())?;
95        *slot = idx;
96        positions.push(q);
97    }
98    let mut uvs = None;
99    if let Some(from) = source.uvs() {
100        let mut to: Vec<f32> = Vec::new();
101        to.try_reserve_exact(2 * positions.len())
102            .map_err(|_| oom())?;
103        for (v, &slot) in index_of.iter().enumerate() {
104            if slot != u32::MAX {
105                to.extend_from_slice(&from[2 * v..2 * v + 2]);
106            }
107        }
108        uvs = Some(to);
109    }
110    let mut triangles: Vec<[u32; 3]> = Vec::new();
111    triangles
112        .try_reserve(source.triangle_count())
113        .map_err(|_| oom())?;
114    for t in 0..source.triangle_slots() {
115        let Some(c) = source.triangle(t as u32) else {
116            continue;
117        };
118        let mapped = c.map(|v| index_of[v as usize]);
119        debug_assert!(
120            mapped.iter().all(|&m| m != u32::MAX),
121            "live triangle on a dead vertex"
122        );
123        triangles.push(mapped);
124    }
125    Ok(Source {
126        source: AppendSource {
127            positions,
128            triangles,
129        },
130        uvs,
131        index_of,
132        loop_vertices,
133    })
134}
135
136impl Scene {
137    /// Joins the source mesh into the target: copies the source's alive
138    /// positions (mapped into the target's local frame, so the world shape
139    /// is kept) and triangles into the target, bridges `loop_a` to the
140    /// copied `loop_b`, and removes the source mesh. The copy and the bridge
141    /// run in one edit, so they undo together; the removal is permanent,
142    /// like [`Scene::remove_mesh`].
143    ///
144    /// Only positions and triangles cross over: the source's channels are
145    /// dropped, and the target's channels take their defaults on the new
146    /// vertices. UVs are the exception: when both meshes have them they carry
147    /// over, and when only one has them the result has none
148    /// ([`JoinReport::uvs_dropped`]).
149    ///
150    /// # Errors
151    /// [`SceneError::UnknownMesh`] for a stale id;
152    /// [`SceneError::InvalidInput`] when the two ids are one mesh;
153    /// [`SceneError::Ops`] with [`OpsError::StaleLoops`] for a loop that is
154    /// not there, or [`OpsError::InvalidInput`] when the loops cannot bridge
155    /// (unclean, over `max_edges`, winding the same way). A failed join
156    /// changes nothing: both meshes are left as they were. If removing the
157    /// source fails (out of memory), the joined geometry stays recorded and
158    /// the source mesh with it.
159    pub fn join(&mut self, a: MeshId, b: MeshId, options: &JoinOptions) -> Result<JoinReport> {
160        if a == b {
161            return Err(SceneError::InvalidInput("cannot join a mesh to itself"));
162        }
163        let prepared = {
164            let target = self.entry(a)?;
165            let source = self.entry(b)?;
166            prepare_source(
167                &target.mesh,
168                &target.placement,
169                &source.mesh,
170                &source.placement,
171                options.loop_b,
172            )?
173        };
174        let Source {
175            source,
176            uvs,
177            index_of,
178            loop_vertices,
179        } = prepared;
180        let report = self.edit(a, |tx| -> std::result::Result<JoinReport, OpsError> {
181            let appended = append_mesh_with_uvs(tx, &source, uvs.as_deref())?;
182            // The copied loop is the one loop whose vertices are exactly the
183            // copied source loop's: appended vertices are new ids, referenced
184            // only by the copied triangles, so no other loop can hold them.
185            let mapped = |v: VertexId| appended.vertex_map[index_of[v as usize] as usize];
186            let loops = boundary_loops(tx.mesh())?;
187            let copied = loops
188                .iter()
189                .find(|l| {
190                    l.vertices.len() == loop_vertices.len()
191                        && loop_vertices
192                            .iter()
193                            .all(|&v| l.vertices.contains(&mapped(v)))
194                })
195                .map(|l| l.id)
196                .ok_or(OpsError::StaleLoops)?;
197            let bridged = bridge_loops(
198                tx,
199                &BridgeOptions {
200                    loops: (options.loop_a, copied),
201                    max_edges: options.max_edges,
202                },
203            )?;
204            Ok(JoinReport {
205                vertices_added: appended.vertices_added,
206                triangles_added: appended.triangles_added,
207                bridge_triangles_added: bridged.triangles_added,
208                uvs_dropped: appended.uvs_dropped,
209            })
210        })?;
211        self.remove_mesh(b)?;
212        Ok(report)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use fast_mesh_ops::{LoopId, OpsError};
219    use fast_mesh_store::Mesh;
220
221    use crate::{MeshOptions, Placement, Scene, SceneError, SceneOptions};
222
223    fn quad_a() -> Mesh {
224        Mesh::from_buffers(
225            &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0],
226            &[0, 2, 1, 0, 3, 2],
227        )
228        .unwrap()
229    }
230
231    fn quad_b() -> Mesh {
232        Mesh::from_buffers(
233            &[0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0],
234            &[0, 1, 2, 0, 2, 3],
235        )
236        .unwrap()
237    }
238
239    fn loop_of(mesh: &Mesh, min_vertex: u32) -> LoopId {
240        fast_mesh_ops::boundary_loops(mesh)
241            .unwrap()
242            .iter()
243            .find(|l| l.id.min_vertex == min_vertex)
244            .unwrap()
245            .id
246    }
247
248    fn scene_of(a: Mesh, b: Mesh) -> Scene {
249        let mut scene = Scene::new(SceneOptions::default());
250        scene.add_mesh(a, MeshOptions::default()).unwrap();
251        scene.add_mesh(b, MeshOptions::default()).unwrap();
252        scene
253    }
254
255    #[test]
256    fn join_merges_source_into_target_and_removes_it() {
257        let a_mesh = quad_a();
258        let b_mesh = quad_b();
259        let loop_a = loop_of(&a_mesh, 0);
260        let loop_b = loop_of(&b_mesh, 0);
261        let mut scene = scene_of(a_mesh, b_mesh);
262        let ids: Vec<_> = scene.ids().collect();
263        let (a, b) = (ids[0], ids[1]);
264
265        let report = scene
266            .join(
267                a,
268                b,
269                &crate::JoinOptions {
270                    loop_a,
271                    loop_b,
272                    max_edges: None,
273                },
274            )
275            .unwrap();
276        assert_eq!(report.vertices_added, 4);
277        assert_eq!(report.triangles_added, 2);
278        assert_eq!(report.bridge_triangles_added, 8);
279        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2 + 2 + 8);
280        assert_eq!(scene.mesh(b).unwrap_err(), SceneError::UnknownMesh);
281
282        // One undo step restores the target; the source stays removed.
283        assert!(scene.undo().unwrap());
284        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2);
285        assert_eq!(scene.mesh(b).unwrap_err(), SceneError::UnknownMesh);
286        assert!(scene.redo().unwrap());
287        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2 + 2 + 8);
288    }
289
290    fn join_options(mesh_a: &Mesh, mesh_b: &Mesh) -> crate::JoinOptions {
291        crate::JoinOptions {
292            loop_a: loop_of(mesh_a, 0),
293            loop_b: loop_of(mesh_b, 0),
294            max_edges: None,
295        }
296    }
297
298    #[test]
299    fn join_rejects_same_mesh() {
300        let a_mesh = quad_a();
301        let b_mesh = quad_b();
302        let options = join_options(&a_mesh, &b_mesh);
303        let mut scene = scene_of(a_mesh, b_mesh);
304        let ids: Vec<_> = scene.ids().collect();
305        let (a, b) = (ids[0], ids[1]);
306
307        assert_eq!(
308            scene.join(a, a, &options),
309            Err(SceneError::InvalidInput("cannot join a mesh to itself"))
310        );
311        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2);
312        assert_eq!(scene.mesh(b).unwrap().triangle_count(), 2);
313        assert!(!scene.can_undo());
314    }
315
316    #[test]
317    fn join_rejects_stale_loops_and_changes_nothing() {
318        let a_mesh = quad_a();
319        let b_mesh = quad_b();
320        let fake = LoopId {
321            min_vertex: 99,
322            len: 4,
323            hash: 7,
324        };
325        let mut scene = scene_of(a_mesh, b_mesh);
326        let ids: Vec<_> = scene.ids().collect();
327        let (a, b) = (ids[0], ids[1]);
328        let options = join_options(scene.mesh(a).unwrap(), scene.mesh(b).unwrap());
329
330        // Unknown source loop: rejected before anything is touched.
331        let bad_b = crate::JoinOptions {
332            loop_b: fake,
333            ..options
334        };
335        assert_eq!(
336            scene.join(a, b, &bad_b),
337            Err(SceneError::Ops(OpsError::StaleLoops))
338        );
339        // Unknown target loop: the bridge finds nothing; the edit rolls back.
340        let bad_a = crate::JoinOptions {
341            loop_a: fake,
342            ..options
343        };
344        assert_eq!(
345            scene.join(a, b, &bad_a),
346            Err(SceneError::Ops(OpsError::StaleLoops))
347        );
348        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2);
349        assert_eq!(scene.mesh(b).unwrap().triangle_count(), 2);
350        assert!(!scene.can_undo());
351    }
352
353    #[test]
354    fn join_rejects_same_winding_and_changes_nothing() {
355        let a_mesh = quad_a();
356        // Quad B facing -z like A: both rims wind the same way, so no
357        // outward tube exists.
358        let b_mesh = Mesh::from_buffers(
359            &[0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0],
360            &[0, 2, 1, 0, 3, 2],
361        )
362        .unwrap();
363        let options = join_options(&a_mesh, &b_mesh);
364        let mut scene = scene_of(a_mesh, b_mesh);
365        let ids: Vec<_> = scene.ids().collect();
366        let (a, b) = (ids[0], ids[1]);
367
368        assert!(matches!(
369            scene.join(a, b, &options),
370            Err(SceneError::Ops(OpsError::InvalidInput(_)))
371        ));
372        assert_eq!(scene.mesh(a).unwrap().triangle_count(), 2);
373        assert_eq!(scene.mesh(b).unwrap().triangle_count(), 2);
374        assert!(!scene.can_undo());
375    }
376
377    #[test]
378    fn join_maps_source_into_target_frame() {
379        let a_mesh = quad_a();
380        // Quad B in its own local frame at z=0 facing +z, placed at z=1.
381        let b_mesh = Mesh::from_buffers(
382            &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0],
383            &[0, 1, 2, 0, 2, 3],
384        )
385        .unwrap();
386        let options = join_options(&a_mesh, &b_mesh);
387        let mut scene = Scene::new(SceneOptions::default());
388        let a = scene.add_mesh(a_mesh, MeshOptions::default()).unwrap();
389        let b = scene
390            .add_mesh(
391                b_mesh,
392                MeshOptions {
393                    placement: Placement {
394                        translation: [0.0, 0.0, 1.0],
395                        ..Placement::IDENTITY
396                    },
397                    ..MeshOptions::default()
398                },
399            )
400            .unwrap();
401
402        let report = scene.join(a, b, &options).unwrap();
403        assert_eq!((report.vertices_added, report.triangles_added), (4, 2));
404        assert_eq!(report.bridge_triangles_added, 8);
405        // The copied vertices sit at z=1 in the target's local frame: the
406        // world shape is kept.
407        let mesh = scene.mesh(a).unwrap();
408        let mut low = 0;
409        let mut high = 0;
410        for v in 0..mesh.vertex_slots() {
411            let Some(p) = mesh.position(v as u32) else {
412                continue;
413            };
414            if p[2] < 0.5 {
415                low += 1;
416            } else {
417                assert!((p[2] - 1.0).abs() < 1e-6);
418                high += 1;
419            }
420        }
421        assert_eq!((low, high), (4, 4));
422    }
423}