Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 33 additions & 25 deletions main/opengeometry/src/tessellation/loop_minimum.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,59 @@
use crate::brep::{BrepEnvelope, CurveGeometry, EdgeGeometry, GeometryError, SurfaceGeometry};

pub(super) fn raise_short_planar_loops(
const PLANAR_LOOP_POINTS: usize = 3;
const CURVED_TRIM_SAMPLES: usize = 4;

pub(super) fn raise_short_loops(
brep: &BrepEnvelope,
counts: &mut [usize],
) -> Result<(), GeometryError> {
for minimum in [2, 3] {
let points = planar_loop_points(brep, counts)?;
for minimum in [2, 3, 4] {
let points = loop_points(brep, counts)?;
for h in &brep.topology.halfedges {
let short = h
.loop_ref
.and_then(|id| points[id as usize])
.is_some_and(|n| n < 3);
if short && conic(brep, h.edge) {
let Some((total, planar)) = h.loop_ref.map(|id| points[id as usize]) else {
continue;
};
let short = if planar {
minimum <= PLANAR_LOOP_POINTS && total < PLANAR_LOOP_POINTS
} else {
total < CURVED_TRIM_SAMPLES
};
if short && raisable(brep, h.edge, planar) {
counts[h.edge as usize] = counts[h.edge as usize].max(minimum);
}
}
}
Ok(())
}

fn planar_loop_points(
brep: &BrepEnvelope,
counts: &[usize],
) -> Result<Vec<Option<usize>>, GeometryError> {
fn loop_points(brep: &BrepEnvelope, counts: &[usize]) -> Result<Vec<(usize, bool)>, GeometryError> {
let mut points = Vec::with_capacity(brep.topology.loops.len());
for face_loop in &brep.topology.loops {
let face = &brep.topology.faces[face_loop.face_ref as usize];
let surface = brep.geometry.surface(face.surface)?;
points.push(matches!(surface, SurfaceGeometry::Plane { .. }).then_some(0));
points.push((0, matches!(surface, SurfaceGeometry::Plane { .. })));
}
for h in &brep.topology.halfedges {
let edge = &brep.topology.edges[h.edge as usize];
if let (Some(id), EdgeGeometry::Curve { .. }) = (h.loop_ref, &edge.geometry) {
if let Some(total) = &mut points[id as usize] {
*total += counts[h.edge as usize];
}
let Some(id) = h.loop_ref else {
continue;
};
let (total, planar) = &mut points[id as usize];
match brep.topology.edges[h.edge as usize].geometry {
EdgeGeometry::Curve { .. } => *total += counts[h.edge as usize],
EdgeGeometry::Collapsed { .. } if !*planar => *total += 1,
EdgeGeometry::Collapsed { .. } => {}
}
}
Ok(points)
}

fn conic(brep: &BrepEnvelope, edge: u32) -> bool {
match brep.topology.edges[edge as usize].geometry {
EdgeGeometry::Curve { curve, .. } => matches!(
brep.geometry.curves[curve as usize],
CurveGeometry::Circle { .. } | CurveGeometry::Ellipse { .. }
),
EdgeGeometry::Collapsed { .. } => false,
fn raisable(brep: &BrepEnvelope, edge: u32, planar: bool) -> bool {
let EdgeGeometry::Curve { curve, .. } = brep.topology.edges[edge as usize].geometry else {
return false;
};
match brep.geometry.curves[curve as usize] {
CurveGeometry::Circle { .. } | CurveGeometry::Ellipse { .. } => true,
CurveGeometry::Intersection { .. } => !planar,
CurveGeometry::Line { .. } => false,
}
}
6 changes: 3 additions & 3 deletions main/opengeometry/src/tessellation/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ impl Tessellation {
}
let orientation = dot(direction, normal);
if !orientation.is_finite() || orientation == 0.0 {
return Err(GeometryError::UnresolvedTessellation(
"triangle orientation could not be resolved".into(),
));
return Err(GeometryError::UnresolvedTessellation(format!(
"triangle orientation could not be resolved on face {face}"
)));
}
if orientation < 0.0 {
ids.swap(1, 2);
Expand Down
4 changes: 2 additions & 2 deletions main/opengeometry/src/tessellation/tessellate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::curved_face::curved_face;
use super::deviation::edge_count;
use super::edges::{sample_edge, synchronize_edge_samples};
use super::grid::{grid_size, rectangle, Rect};
use super::loop_minimum::raise_short_planar_loops;
use super::loop_minimum::raise_short_loops;
use super::mesh::Tessellation;
use super::planar_face::planar_face;
use super::trimmed_face::{curved_trimmed_face, FaceGrid};
Expand Down Expand Up @@ -39,7 +39,7 @@ pub fn tessellate(
for edge in &brep.topology.edges {
counts.push(edge_count(brep, edge.id, error, max_triangles)?);
}
raise_short_planar_loops(brep, &mut counts)?;
raise_short_loops(brep, &mut counts)?;
let mut grids = Vec::with_capacity(brep.topology.faces.len());
for face in &brep.topology.faces {
let surface = brep.geometry.surface(face.surface)?;
Expand Down
136 changes: 136 additions & 0 deletions main/opengeometry/src/tessellation/tests/curved_trims.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use crate::brep::{fine_accuracy, BrepEnvelope, Frame3, PcurveGeometry, SurfaceGeometry};
use crate::math::cross;
use crate::operations::modifying::boolean::{boolean_brep, BooleanOp};
use crate::primitives::{self, ProfileEdge};
use crate::tessellation::tessellate;
use std::f64::consts::FRAC_PI_2;

const WIDTH: f64 = 100.0;
const OUTER: f64 = 3.0;

fn quarter_tube() -> BrepEnvelope {
let inner = 2.0;
primitives::arc_edged_extrusion(
"quarter-tube".into(),
Frame3::IDENTITY,
vec![
ProfileEdge::Line {
from: [inner, 0.0],
to: [OUTER, 0.0],
},
ProfileEdge::Arc {
center: [0.0, 0.0],
radius: OUTER,
start_angle: 0.0,
sweep_angle: FRAC_PI_2,
},
ProfileEdge::Line {
from: [0.0, OUTER],
to: [0.0, inner],
},
ProfileEdge::Arc {
center: [0.0, 0.0],
radius: inner,
start_angle: FRAC_PI_2,
sweep_angle: -FRAC_PI_2,
},
],
WIDTH,
fine_accuracy(),
)
.unwrap()
}

fn holed_quarter_tube() -> BrepEnvelope {
let angle = 0.7_f64;
let axis = [angle.cos(), angle.sin(), 0.0];
let x = [0.0, 0.0, 1.0];
let hole = primitives::cylinder(
"hole".into(),
Frame3 {
origin: [0.0, 0.0, WIDTH / 2.0],
x,
y: cross(axis, x),
z: axis,
},
0.4,
10.0,
fine_accuracy(),
)
.unwrap();
let cut = boolean_brep(
&quarter_tube(),
&hole,
BooleanOp::Subtraction,
"holed".into(),
)
.unwrap()
.brep;
let holed = cut
.topology
.faces
.iter()
.filter(|face| {
!face.trim.holes.is_empty()
&& matches!(
cut.geometry.surfaces[face.surface as usize],
SurfaceGeometry::Cylinder { radius, .. } if radius >= 2.0
)
})
.count();
assert_eq!(holed, 2, "the hole goes through both cylinder walls");
cut
}

#[test]
fn a_small_hole_in_a_curved_face_tessellates_at_a_coarse_deflection() {
let body = holed_quarter_tube();
for deflection in [2.0, 5.0] {
let mesh = tessellate(&body, deflection, 1_000_000).unwrap();
for face in &body.topology.faces {
assert!(
mesh.triangle_face_ids.contains(&face.id),
"face {} has no triangles at deflection {deflection}",
face.key
);
}
}
}

#[test]
fn a_trim_run_off_its_corner_vertex_by_rounding_tessellates() {
let body = holed_quarter_tube();
let face = body
.topology
.faces
.iter()
.find(|face| {
!face.trim.holes.is_empty()
&& matches!(
body.geometry.surfaces[face.surface as usize],
SurfaceGeometry::Cylinder { radius, .. } if radius == OUTER
)
})
.unwrap();
let start = body.topology.loops[face.trim.outer as usize].start_halfedge;
for steps in [1.0, 2.0, 4.0] {
let mut noisy = body.clone();
let mut current = start;
loop {
let halfedge = &body.topology.halfedges[current as usize];
let pcurve = halfedge.geometry_use.pcurve.unwrap() as usize;
if let PcurveGeometry::Line2 { origin, direction } = &mut noisy.geometry.pcurves[pcurve]
{
if direction[1] == 0.0 && origin[1] == WIDTH {
origin[1] = WIDTH + steps * WIDTH * f64::EPSILON;
}
}
current = halfedge.next.unwrap();
if current == start {
break;
}
}
assert_ne!(noisy.geometry.pcurves, body.geometry.pcurves);
tessellate(&noisy, 0.01, 1_000_000).unwrap();
}
}
1 change: 1 addition & 0 deletions main/opengeometry/src/tessellation/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod curved_trims;
mod display;
mod seams;
mod short_loops;
Expand Down
26 changes: 23 additions & 3 deletions main/opengeometry/src/tessellation/trimmed_face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ fn general_curved_trimmed_face(
uv.push(coordinate);
points.push(point);
}
snap_rounding_noise(&mut uv[start..]);
for offset in 0..ring.len() {
let a = start + offset;
let b = start + (offset + 1) % ring.len();
Expand All @@ -235,9 +236,10 @@ fn general_curved_trimmed_face(
.checked_mul(4)
.is_none_or(|count| count > max_triangles)
{
return Err(GeometryError::LimitExceeded(
"curved trim refinement triangles".into(),
));
return Err(GeometryError::LimitExceeded(format!(
"curved trim refinement triangles on face {} ('{}')",
face.id, face.key
)));
}
let midpoints =
add_edge_midpoints(&split_edges, surface, &mut uv, &mut points, boundary_count)?;
Expand Down Expand Up @@ -267,6 +269,24 @@ fn general_curved_trimmed_face(
Ok(())
}

fn snap_rounding_noise(ring: &mut [[f64; 2]]) {
let magnitude = ring
.iter()
.flatten()
.fold(1.0_f64, |maximum, value| maximum.max(value.abs()));
let tolerance = 64.0 * f64::EPSILON * magnitude;
let count = ring.len();
for index in 0..count {
let previous = ring[(index + count - 1) % count];
for axis in 0..2 {
let difference = (ring[index][axis] - previous[axis]).abs();
if difference != 0.0 && difference <= tolerance {
ring[index][axis] = previous[axis];
}
}
}
}

fn earcut_triangles(uv: &[[f64; 2]], holes: &[usize]) -> Result<Vec<[usize; 3]>, GeometryError> {
let flattened = uv
.iter()
Expand Down