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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! Deriving an attribute's own corner table from the position corner table.
//!
//! An interior seam is a base-connectivity edge where the attribute's value
//! differs on its two sides -- the encoder finds these by comparing attribute
//! values around each edge, the decoder by reading the seam bits it just
//! decoded. Either way, turning "these edges are seams" into "here is the
//! per-attribute corner table, with its own vertex ids split at every seam" is
//! one computation, and [`cut_seam_edges_and_recompute_vertices`] is the only
//! place it happens: `mesh_encoder.rs` and `mesh_decoder.rs` both call it
//! rather than each recomputing vertices its own way. Upstream keeps the same
//! logic in one class, `MeshAttributeCornerTable`; this keeps it in one
//! function instead of porting the class.
use crate::corner_table::CornerTable;
use crate::geometry_indices::{
CornerIndex, VertexIndex, INVALID_CORNER_INDEX, INVALID_VERTEX_INDEX,
};
use crate::status::DracoError;
/// Cuts every marked seam edge out of a clone of `base_ct` and gives each
/// resulting fan piece its own vertex id.
///
/// `is_edge_on_seam` is indexed by corner and must mark both sides of a cut
/// edge: a seam corner and, when it has one, its opposite. The caller decides
/// how it learns that -- from attribute-value comparisons on the encode side,
/// from bitstream bits on the decode side -- this function only needs the
/// result.
///
/// Returns the attribute's corner table together with, per *base* vertex,
/// whether it sits on a seam (so more than one attribute-vertex can answer for
/// it). A malformed seam pattern that made a fan cycle without ever reaching a
/// cut is refused rather than looped on forever: only the decoder can be
/// handed such a pattern, since the encoder's own seam detection cannot
/// produce one, but the guard costs nothing on that side either.
pub fn cut_seam_edges_and_recompute_vertices(
base_ct: &CornerTable,
is_edge_on_seam: &[bool],
) -> Result<(CornerTable, Vec<bool>), DracoError> {
let mut ct = base_ct.clone();
for c_idx in 0..base_ct.num_corners() {
if is_edge_on_seam[c_idx] {
ct.set_opposite(CornerIndex(c_idx as u32), INVALID_CORNER_INDEX);
}
}
let mut is_vertex_on_seam = vec![false; base_ct.num_vertices()];
for c_idx in 0..base_ct.num_corners() {
if !is_edge_on_seam[c_idx] {
continue;
}
let c = CornerIndex(c_idx as u32);
let next_vertex = base_ct.vertex_after(c);
if next_vertex != INVALID_VERTEX_INDEX {
is_vertex_on_seam[next_vertex.0 as usize] = true;
}
let previous_vertex = base_ct.vertex_before(c);
if previous_vertex != INVALID_VERTEX_INDEX {
is_vertex_on_seam[previous_vertex.0 as usize] = true;
}
}
// A seam-aware swing, taken over the *uncut* base table: it walks past a
// seam like any other edge, and the caller renumbers at the crossing
// instead of stopping there. One continuous pass then covers a vertex
// with any number of seam edges around it, not just the common one-cut
// case -- `ct.swing_left`, which stops dead at the first cut, cannot.
let seam_swing_left = |corner: CornerIndex| -> CornerIndex {
let opposite = base_ct.next(corner);
let opposite = if is_edge_on_seam
.get(opposite.0 as usize)
.copied()
.unwrap_or(false)
{
INVALID_CORNER_INDEX
} else {
base_ct.opposite(opposite)
};
base_ct.next(opposite)
};
ct.corner_to_vertex_map.fill(INVALID_VERTEX_INDEX);
ct.vertex_corners.clear();
let max_swing_steps = base_ct.num_corners().saturating_add(1);
let mut num_new_vertices = 0usize;
for v in 0..base_ct.num_vertices() {
let c = base_ct.left_most_corner(VertexIndex(v as u32));
if c == INVALID_CORNER_INDEX {
continue;
}
let mut first_vertex_id = VertexIndex(num_new_vertices as u32);
num_new_vertices += 1;
let mut first_c = c;
if is_vertex_on_seam[v] {
let mut act_c = seam_swing_left(first_c);
let mut swing_steps = 0usize;
while act_c != INVALID_CORNER_INDEX {
swing_steps += 1;
if swing_steps > max_swing_steps {
return Err(DracoError::general(
"Attribute seam left-swing traversal did not terminate".to_string(),
));
}
first_c = act_c;
act_c = seam_swing_left(act_c);
}
}
ct.corner_to_vertex_map[first_c.0 as usize] = first_vertex_id;
ct.vertex_corners.push(first_c);
let mut act_c = base_ct.swing_right(first_c);
let mut swing_steps = 0usize;
while act_c != INVALID_CORNER_INDEX && act_c != first_c {
swing_steps += 1;
if swing_steps > max_swing_steps {
return Err(DracoError::general(
"Attribute seam right-swing traversal did not terminate".to_string(),
));
}
if is_edge_on_seam[base_ct.next(act_c).0 as usize] {
first_vertex_id = VertexIndex(num_new_vertices as u32);
num_new_vertices += 1;
ct.vertex_corners.push(act_c);
}
ct.corner_to_vertex_map[act_c.0 as usize] = first_vertex_id;
act_c = base_ct.swing_right(act_c);
}
}
ct.num_original_vertices = ct.vertex_corners.len();
ct.num_isolated_vertices = 0;
ct.num_degenerated_faces = base_ct.num_degenerated_faces;
Ok((ct, is_vertex_on_seam))
}
#[cfg(test)]
mod tests {
use super::*;
/// Six triangles fanned around a shared centre vertex 0, ring vertices
/// 1..=6 in order. `spoke(j)` is the edge `(0, 1+j)`, shared by `face(j-1)`
/// and `face(j)` (indices mod 6) -- the only edge those two faces have in
/// common, so cutting it also splits the ring vertex `1+j`, not only the
/// centre.
fn hexagon_fan() -> CornerTable {
let faces: Vec<[VertexIndex; 3]> = (0..6u32)
.map(|j| {
let a = 1 + j;
let b = 1 + (j + 1) % 6;
[VertexIndex(0), VertexIndex(a), VertexIndex(b)]
})
.collect();
let mut ct = CornerTable::new(faces.len());
assert!(ct.init(&faces));
ct
}
/// The corner in `face(j)` that is *not* on `spoke(j)` -- marking it seam
/// cuts that spoke from `face(j)`'s side.
fn spoke_opposite_corner(j: u32) -> CornerIndex {
CornerIndex(3 * j + 2)
}
/// Cutting three of the six spokes, spaced one apart, must split the
/// centre vertex into exactly the three arcs the cuts leave behind -- not
/// two (an algorithm that only handles a single cut correctly) and not
/// six (one that always isolates every corner it touches).
///
/// This is the case `ct.swing_left`/`ct.swing_right` on a once-cut clone
/// cannot answer directly: a vertex can have any number of seam edges
/// around it, and [`cut_seam_edges_and_recompute_vertices`] handles that
/// by walking the uncut fan once and renumbering at every crossing, rather
/// than cutting and re-swinging per piece.
#[test]
fn a_vertex_with_three_seam_edges_splits_into_three_groups() {
let base_ct = hexagon_fan();
let mut is_edge_on_seam = vec![false; base_ct.num_corners()];
for &j in &[0u32, 2, 4] {
let c = spoke_opposite_corner(j);
is_edge_on_seam[c.0 as usize] = true;
let opp = base_ct.opposite(c);
assert_ne!(opp, INVALID_CORNER_INDEX, "spoke {j} should be interior");
is_edge_on_seam[opp.0 as usize] = true;
}
let (attr_ct, is_vertex_on_seam) =
cut_seam_edges_and_recompute_vertices(&base_ct, &is_edge_on_seam)
.expect("a well-formed cut must not fail");
// The centre corner of each face, grouped by the uncut spokes between
// them: (face0,face1), (face2,face3), (face4,face5).
let centre = |face: u32| attr_ct.vertex(CornerIndex(3 * face));
let (g01, g23, g45) = (centre(0), centre(2), centre(4));
assert_eq!(centre(0), centre(1), "face0/face1 share an uncut spoke");
assert_eq!(centre(2), centre(3), "face2/face3 share an uncut spoke");
assert_eq!(centre(4), centre(5), "face4/face5 share an uncut spoke");
assert_ne!(g01, g23, "the cut at spoke2 must separate these arcs");
assert_ne!(g23, g45, "the cut at spoke4 must separate these arcs");
assert_ne!(g01, g45, "the cut at spoke0 must separate these arcs");
// A ring vertex touches exactly two faces through exactly one edge
// (its spoke), so it splits in two precisely when that spoke is cut.
let ring_pair = |j: u32| {
let prev_face = (j + 5) % 6; // j - 1, mod 6
(
attr_ct.vertex(CornerIndex(3 * prev_face + 2)),
attr_ct.vertex(CornerIndex(3 * j + 1)),
)
};
for &j in &[0u32, 2, 4] {
let (a, b) = ring_pair(j);
assert_ne!(a, b, "ring vertex {} sits on the cut spoke {j}", 1 + j);
}
for &j in &[1u32, 3, 5] {
let (a, b) = ring_pair(j);
assert_eq!(a, b, "ring vertex {} sits on an uncut spoke {j}", 1 + j);
}
assert!(is_vertex_on_seam[0], "the centre sits on three cuts");
for j in [0u32, 2, 4] {
assert!(
is_vertex_on_seam[(1 + j) as usize],
"ring vertex {} sits on cut spoke {j}",
1 + j
);
}
for j in [1u32, 3, 5] {
assert!(
!is_vertex_on_seam[(1 + j) as usize],
"ring vertex {} touches only uncut spokes",
1 + j
);
}
}
/// A one-sided seam mark -- the kind a corrupted or adversarial bitstream
/// can produce, since the decoder trusts the seam corners it reads rather
/// than deriving them from real attribute values -- can flag a vertex as
/// seam-bound without actually cutting anything on its fan. Two
/// disconnected faces share vertex id 0 here (a non-manifold corner table,
/// which the position corner table can itself produce): face A's own
/// opposite pointers are corrupted into a closed 3-cycle that never
/// reaches a real cut or an invalid corner, while the seam mark lives on
/// face B, unreachable from face A's fan walk. Left unguarded, the
/// left-swing loops forever; this checks it is refused instead.
#[test]
fn a_seam_mark_unreachable_from_a_corrupted_fan_is_refused_not_looped_on() {
let base_ct = CornerTable {
// Face A: corners 0,1,2 at vertices 0,1,2. Face B: corners 3,4,5
// at vertices 0,3,4 -- sharing vertex 0 with face A, but not
// linked to it by any opposite pointer.
corner_to_vertex_map: [0, 1, 2, 0, 3, 4].map(VertexIndex).to_vec(),
// Face A's pointers form a closed cycle among themselves instead
// of the real (all-INVALID) topology of two disjoint triangles --
// `next` never sees an invalid corner and never revisits corner 0
// by construction. Face B's are irrelevant to the walk and left
// invalid.
opposite_corners: vec![
CornerIndex(1),
CornerIndex(2),
CornerIndex(0),
INVALID_CORNER_INDEX,
INVALID_CORNER_INDEX,
INVALID_CORNER_INDEX,
],
vertex_corners: vec![
CornerIndex(0),
CornerIndex(1),
CornerIndex(2),
CornerIndex(4),
CornerIndex(5),
],
num_original_vertices: 5,
num_degenerated_faces: 0,
num_isolated_vertices: 0,
};
// Corner 5 (face B, vertex 4) is opposite the edge (vertex 0, vertex
// 3) -- marking it seam flags vertex 0 as seam-bound without cutting
// anything face A's own fan walk will ever reach.
let mut is_edge_on_seam = vec![false; base_ct.num_corners()];
is_edge_on_seam[5] = true;
let result = cut_seam_edges_and_recompute_vertices(&base_ct, &is_edge_on_seam);
assert!(
result.is_err(),
"a fan that never reaches a cut or an invalid corner must be refused"
);
}
}