Skip to main content

fast_mesh_ops/
append.rs

1//! Appending copied geometry to a mesh.
2//!
3//! [`append_mesh`] copies a position/triangle soup into a transaction and
4//! reports the new ids. Positions arrive in the target mesh's local frame;
5//! any world-frame transform is the caller's job. Only positions and
6//! triangles cross over: source channels take defaults, derived channels
7//! (like normals) are recomputed by the scene. Triangle indices are bounds
8//! checked; anything else about the soup is the caller's responsibility.
9//!
10//! UVs are the exception: a default would misalign them, so a target with UVs
11//! gets the source's from [`append_mesh_with_uvs`], or loses its own.
12
13use fast_mesh_store::{Transaction, UV_CHANNEL, VertexId};
14
15use crate::{OpsError, Result, reserve};
16
17/// Geometry to append, indexed from zero.
18#[derive(Debug, Clone, Default, PartialEq)]
19pub struct AppendSource {
20    /// Vertex positions in the target mesh's local frame.
21    pub positions: Vec<[f32; 3]>,
22    /// Triangles indexed into [`AppendSource::positions`].
23    pub triangles: Vec<[u32; 3]>,
24}
25
26/// Outcome of [`append_mesh`].
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub struct AppendReport {
29    /// New vertex id per source position, in source order.
30    pub vertex_map: Vec<VertexId>,
31    /// Vertices added.
32    pub vertices_added: usize,
33    /// Triangles added.
34    pub triangles_added: usize,
35    /// Only one side had UVs, so the result has none (undo restores a target's).
36    pub uvs_dropped: bool,
37}
38
39/// Copies `source` into `tx`, returning the new vertex ids in source order.
40///
41/// Triangle indices past the positions are rejected before anything is
42/// written. Either every vertex and triangle lands or nothing does.
43pub fn append_mesh(tx: &mut Transaction, source: &AppendSource) -> Result<AppendReport> {
44    append_mesh_with_uvs(tx, source, None)
45}
46
47/// [`append_mesh`] with the source's UVs: 2 values per source position, or
48/// `None` for a source without them. When both sides have UVs the result does
49/// too; otherwise it has none and [`AppendReport::uvs_dropped`] is set.
50///
51/// # Errors
52/// [`OpsError::InvalidInput`] for bad triangle indices or a wrong `uvs`
53/// length, before anything is written.
54pub fn append_mesh_with_uvs(
55    tx: &mut Transaction,
56    source: &AppendSource,
57    uvs: Option<&[f32]>,
58) -> Result<AppendReport> {
59    if uvs.is_some_and(|u| u.len() != 2 * source.positions.len()) {
60        return Err(OpsError::InvalidInput(
61            "append uvs need 2 values per position",
62        ));
63    }
64    for t in &source.triangles {
65        if t.iter().any(|&v| (v as usize) >= source.positions.len()) {
66            return Err(OpsError::InvalidInput(
67                "append triangle indexes no position",
68            ));
69        }
70    }
71    // Reserved before the first write; past it only infallible pushes into
72    // reserved capacity remain, so any allocation failure leaves an empty
73    // patch.
74    let mut mapped: Vec<[VertexId; 3]> = Vec::new();
75    reserve(&mut mapped, source.triangles.len())?;
76    let ids = tx.add_vertices(&source.positions)?;
77    for t in &source.triangles {
78        mapped.push([ids[t[0] as usize], ids[t[1] as usize], ids[t[2] as usize]]);
79    }
80    tx.add_triangles(&mapped)?;
81    let target_has_uvs = tx.mesh().uvs().is_some();
82    match uvs {
83        Some(u) if target_has_uvs => tx.write_channel(UV_CHANNEL, &ids, u)?,
84        None if target_has_uvs => tx.drop_channel(UV_CHANNEL)?,
85        _ => {}
86    }
87    Ok(AppendReport {
88        vertices_added: ids.len(),
89        triangles_added: mapped.len(),
90        vertex_map: ids,
91        uvs_dropped: target_has_uvs != uvs.is_some(),
92    })
93}
94
95#[cfg(test)]
96mod tests {
97    use fast_mesh_store::Transaction;
98
99    use super::*;
100    use crate::test_util::{edit, mesh_of, parts, sweep};
101
102    fn quad_source() -> AppendSource {
103        AppendSource {
104            positions: vec![
105                [0.0, 0.0, 1.0],
106                [1.0, 0.0, 1.0],
107                [1.0, 1.0, 1.0],
108                [0.0, 1.0, 1.0],
109            ],
110            triangles: vec![[0, 1, 2], [0, 2, 3]],
111        }
112    }
113
114    #[test]
115    fn append_copies_geometry_with_id_map() {
116        let mut mesh = mesh_of(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[[0, 1, 2]]);
117        let (before_pos, before_tris) = parts(&mesh);
118        let (report, _) = edit(&mut mesh, |tx| append_mesh(tx, &quad_source()));
119        assert_eq!(report.vertices_added, 4);
120        assert_eq!(report.triangles_added, 2);
121        assert_eq!(report.vertex_map.len(), 4);
122        let (pos_after, tris_after) = parts(&mesh);
123        assert_eq!(tris_after.len(), before_tris.len() + 2);
124        // Originals untouched; appended vertices carry exact positions.
125        assert_eq!(&pos_after[..before_pos.len()], &before_pos[..]);
126        for (k, &id) in report.vertex_map.iter().enumerate() {
127            let p = &pos_after[3 * id as usize..3 * id as usize + 3];
128            assert_eq!(p, &quad_source().positions[k][..]);
129        }
130        for t in &tris_after[before_tris.len()..] {
131            assert!(t.iter().all(|v| report.vertex_map.contains(v)));
132        }
133    }
134
135    #[test]
136    fn append_empty_source_is_noop() {
137        let mut mesh = mesh_of(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[[0, 1, 2]]);
138        let before = parts(&mesh);
139        let empty = AppendSource {
140            positions: Vec::new(),
141            triangles: Vec::new(),
142        };
143        let (report, patch_empty) = edit(&mut mesh, |tx| append_mesh(tx, &empty));
144        assert_eq!((report.vertices_added, report.triangles_added), (0, 0));
145        assert!(report.vertex_map.is_empty());
146        assert!(patch_empty);
147        assert_eq!(parts(&mesh), before);
148    }
149
150    #[test]
151    fn append_sweep_keeps_atomicity() {
152        let mesh = mesh_of(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[[0, 1, 2]]);
153        let source = quad_source();
154        let n = sweep(mesh, |tx: &mut Transaction| {
155            append_mesh(tx, &source)?;
156            Ok(())
157        });
158        assert!(n > 0);
159    }
160}