Skip to content

Commit 5c4435c

Browse files
committed
Add extrusion adversarial tests and rendering helpers
1 parent 0b63912 commit 5c4435c

11 files changed

Lines changed: 941 additions & 37 deletions

File tree

scripts/adversarial-medium.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ run() {
1111

1212
run cargo test --test adversarial
1313
run cargo test --test adversarial_deep
14+
run cargo test --test adversarial_extrusions
1415
run cargo test --test adversarial_stress
1516
run cargo test --test adversarial_fixtures
1617
run cargo test --doc

scripts/adversarial.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ expect_fail() {
1919

2020
run cargo test --test adversarial
2121
run cargo test --test adversarial_deep
22+
run cargo test --test adversarial_extrusions
2223
run cargo test --test adversarial_stress
2324
run cargo test --test adversarial_fixtures
2425

src/mesh/flatten_slice.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ impl<S: Clone + Debug + Send + Sync> Mesh<S> {
6969
geometry: new_gc,
7070
bounding_box: OnceLock::new(),
7171
metadata: self.metadata.clone(),
72+
origin: Vertex::default(),
73+
origin_transform: Sketch::<S>::prepare_origin_transform(Vertex::default()),
7274
}
7375
}
7476

@@ -154,6 +156,8 @@ impl<S: Clone + Debug + Send + Sync> Mesh<S> {
154156
geometry: new_gc,
155157
bounding_box: OnceLock::new(),
156158
metadata: self.metadata.clone(),
159+
origin: Vertex::default(),
160+
origin_transform: Sketch::<S>::prepare_origin_transform(Vertex::default()),
157161
}
158162
}
159163
}

src/mesh/mod.rs

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ use crate::sketch::Sketch;
2626
use crate::csg::CSG;
2727
#[cfg(feature = "sketch")]
2828
use geo::{CoordsIter, Geometry, Polygon as GeoPolygon};
29+
use hashbrown::HashMap;
2930
use nalgebra::{
3031
Isometry3, Matrix4, Point3, Quaternion, Unit, Vector3, partial_max, partial_min,
3132
};
3233
use std::{
33-
cmp::PartialEq,
34+
cmp::{Ordering, PartialEq},
3435
fmt::Debug,
3536
num::NonZeroU32,
3637
sync::OnceLock,
@@ -64,6 +65,16 @@ pub mod smoothing;
6465
pub mod tpms;
6566
pub mod triangulated;
6667

68+
/// Stored as `(position: [f32; 3], normal: [f32; 3])`.
69+
pub type GraphicsMeshVertex = ([f32; 3], [f32; 3]);
70+
71+
/// Mesh data laid out for renderers that want packed f32 vertices plus u32 indices.
72+
#[derive(Debug, Clone)]
73+
pub struct GraphicsMesh {
74+
pub vertices: Vec<GraphicsMeshVertex>,
75+
pub indices: Vec<u32>,
76+
}
77+
6778
#[derive(Clone, Debug)]
6879
pub struct Mesh<S: Clone + Send + Sync + Debug> {
6980
/// 3D polygons for volumetric shapes
@@ -144,9 +155,114 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
144155
.collect()
145156
}
146157

158+
/// Pre-pass to remove T-junctions between polygons by inserting missing
159+
/// vertices on shared or colinear overlapping edges before triangulation.
160+
fn fix_t_junctions_on_shared_edges(polygons: &mut [Polygon<S>]) {
161+
let eps = tolerance();
162+
let eps2 = eps * eps;
163+
164+
if polygons.len() < 2 {
165+
return;
166+
}
167+
168+
let poly_count = polygons.len();
169+
let mut edge_splits: Vec<HashMap<usize, Vec<(Real, Vertex)>>> =
170+
vec![HashMap::new(); poly_count];
171+
172+
for (i, poly_i) in polygons.iter().enumerate() {
173+
if poly_i.vertices.len() < 2 {
174+
continue;
175+
}
176+
177+
for vert in &poly_i.vertices {
178+
for (j, poly_j) in polygons.iter().enumerate() {
179+
if i == j || poly_j.vertices.len() < 2 {
180+
continue;
181+
}
182+
183+
let verts_j = &poly_j.vertices;
184+
let n_j = verts_j.len();
185+
for edge_start in 0..n_j {
186+
let a = &verts_j[edge_start];
187+
let b = &verts_j[(edge_start + 1) % n_j];
188+
let ab = b.position - a.position;
189+
let ab_len_sq = ab.norm_squared();
190+
if ab_len_sq < eps2 {
191+
continue;
192+
}
193+
194+
let av = vert.position - a.position;
195+
let bv = vert.position - b.position;
196+
if av.norm_squared() < eps2 || bv.norm_squared() < eps2 {
197+
continue;
198+
}
199+
200+
let t = ab.dot(&av) / ab_len_sq;
201+
if t <= eps || t >= 1.0 - eps {
202+
continue;
203+
}
204+
205+
let projected = a.position + ab * t;
206+
if (vert.position - projected).norm_squared() > eps2 {
207+
continue;
208+
}
209+
210+
let new_vertex = Vertex::new(vert.position, poly_j.plane.normal());
211+
let entry = edge_splits[j].entry(edge_start).or_default();
212+
let already_present = entry.iter().any(|(existing_t, existing_v)| {
213+
(existing_t - t).abs() < eps
214+
|| (existing_v.position - new_vertex.position).norm_squared()
215+
< eps2
216+
});
217+
218+
if !already_present {
219+
entry.push((t, new_vertex));
220+
}
221+
}
222+
}
223+
}
224+
}
225+
226+
for (poly_index, poly) in polygons.iter_mut().enumerate() {
227+
let splits_map = &edge_splits[poly_index];
228+
if splits_map.is_empty() {
229+
continue;
230+
}
231+
232+
let original = poly.vertices.clone();
233+
let n = original.len();
234+
if n < 2 {
235+
continue;
236+
}
237+
238+
let extra_vertices: usize = splits_map.values().map(Vec::len).sum();
239+
let mut new_vertices = Vec::with_capacity(n + extra_vertices);
240+
241+
for edge_start in 0..n {
242+
new_vertices.push(original[edge_start]);
243+
244+
if let Some(splits) = splits_map.get(&edge_start) {
245+
let mut splits_sorted = splits.clone();
246+
splits_sorted.sort_by(|(t_a, _), (t_b, _)| {
247+
t_a.partial_cmp(t_b).unwrap_or(Ordering::Equal)
248+
});
249+
250+
for (_, vertex) in splits_sorted {
251+
new_vertices.push(vertex);
252+
}
253+
}
254+
}
255+
256+
poly.vertices = new_vertices;
257+
}
258+
}
259+
147260
/// Triangulate each polygon in the Mesh returning a Mesh containing triangles
148261
pub fn triangulate(&self) -> Mesh<S> {
149-
let triangles = self.polygons
262+
let mut polygons = self.polygons.clone();
263+
Self::fix_t_junctions_on_shared_edges(&mut polygons);
264+
265+
let triangles = polygons
150266
.iter()
151267
.flat_map(|poly| {
152268
poly.triangulate().into_iter().map(move |triangle| {
@@ -258,7 +374,48 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
258374
dot.acos()
259375
}
260376

377+
/// Converts this mesh into f32 vertex/index buffers suitable for rendering.
378+
///
379+
/// Vertices are deduplicated by exact f32 position and normal bit pattern.
380+
pub fn build_graphics_mesh(&self) -> GraphicsMesh {
381+
let triangles = self.triangulate().polygons;
382+
let triangle_count = triangles.len();
383+
384+
let mut indices = Vec::with_capacity(triangle_count * 3);
385+
let mut vertices = Vec::with_capacity(triangle_count * 3);
386+
let mut vertices_hash: HashMap<([u32; 3], [u32; 3]), u32> =
387+
HashMap::with_capacity(triangle_count * 3);
388+
389+
for triangle in triangles {
390+
for vertex in triangle.vertices {
391+
let position = [
392+
vertex.position.x as f32,
393+
vertex.position.y as f32,
394+
vertex.position.z as f32,
395+
];
396+
let normal = [
397+
vertex.normal.x as f32,
398+
vertex.normal.y as f32,
399+
vertex.normal.z as f32,
400+
];
401+
let key = (position.map(f32::to_bits), normal.map(f32::to_bits));
402+
403+
let index = *vertices_hash.entry(key).or_insert_with(|| {
404+
let new_index = vertices.len() as u32;
405+
vertices.push((position, normal));
406+
new_index
407+
});
408+
indices.push(index);
409+
}
410+
}
411+
412+
vertices.shrink_to_fit();
413+
GraphicsMesh { vertices, indices }
414+
}
415+
261416
/// Extracts vertices and indices from the Mesh's tessellated polygons.
417+
///
418+
/// This intentionally does not remove duplicate vertices.
262419
pub fn get_vertices_and_indices(&self) -> (Vec<Point3<Real>>, Vec<[u32; 3]>) {
263420
let tri_csg = self.triangulate();
264421
let vertices = tri_csg

0 commit comments

Comments
 (0)