Add routed CSG scratch guardrails

This commit is contained in:
Meta Vens 2026-05-07 08:38:20 +01:00
commit 8c61d2fbf7
4 changed files with 108 additions and 48 deletions

View file

@ -110,6 +110,11 @@ triangles instead of the fragment carver's 72 closed-fragment triangles. That is
cleaner surface output, not yet faster output. The implementation still moves
owned polygon vectors around, so it is a topology proof before it is a hot path.
Dense-kernel regression guardrail: `rotated_cut_stack_64` is pinned by tests at
280 fragments, 3404 triangles, 804 candidate pairs, and 9417 rejected pairs.
This protects against "optimizations" that alter ordered CSG semantics while
looking faster. Output-contract changes need to be explicit.
## Latest Local Baseline
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`:

View file

@ -3,8 +3,8 @@ use std::cell::RefCell;
use bevy_math::Vec3;
use crate::{
Aabb, Brush, BrushId, BrushOp, ConvexPolygon, ConvexSolid, MaterialId, Primitive, TriangleMesh,
append_cylinder_z, append_dome_cap_z, append_floret_arm,
Aabb, Brush, BrushId, BrushOp, ConvexPolygon, ConvexSolid, MaterialId, PolygonRouteScratch,
Primitive, TriangleMesh, append_cylinder_z, append_dome_cap_z, append_floret_arm,
primitives::{DomeCapZSpec, FloretArmSpec},
};
@ -204,6 +204,7 @@ impl Assembler {
let mut source = None::<ConvexSolid>;
let mut surfaces = Vec::<ConvexPolygon>::new();
let mut previous_cutters = Vec::<ConvexSolid>::new();
let mut scratch = PolygonRouteScratch::default();
for brush in &self.compiled {
match brush.op {
@ -224,14 +225,11 @@ impl Assembler {
}
let cutter = brush.convex_cutter()?;
report.candidate_pairs += 1;
surfaces = ConvexSolid::route_polygons_outside_of(surfaces, &cutter);
surfaces = scratch.route_outside_of(surfaces, &cutter);
let mut caps = ConvexSolid::route_polygons_inside_of(
cutter.polygons.clone(),
source_solid,
);
let mut caps = scratch.route_inside_of(cutter.polygons.clone(), source_solid);
for previous in &previous_cutters {
caps = ConvexSolid::route_polygons_outside_of(caps, previous);
caps = scratch.route_outside_of(caps, previous);
}
for cap in &mut caps {
cap.material = source_solid.material;
@ -910,4 +908,33 @@ mod tests {
assert_eq!(routed.mesh.positions, stable.mesh.positions);
assert_eq!(routed.mesh.indices, stable.mesh.indices);
}
#[test]
fn rotated_cut_stack_regression_preserves_ordered_output_shape() {
let mut asm = Assembler::new();
asm.solid_box(
"slab",
Aabb::from_center_size(Vec3::ZERO, Vec3::new(32.0, 32.0, 4.0)),
MaterialId(1),
);
for index in 0..64 {
let angle = index as f32 * 0.173;
let radius = 11.0 + (index % 7) as f32 * 0.35;
let center = Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
asm.cut_oriented_box(
format!("rotated_void_{index}"),
center,
Vec3::new(1.0 + (index % 3) as f32 * 0.3, 8.0, 5.0),
bevy_math::Quat::from_rotation_z(angle),
);
}
let output = asm.rebuild();
assert_eq!(output.report.emitted_convex_fragments, 280);
assert_eq!(output.mesh.triangle_count(), 3404);
assert_eq!(output.report.candidate_pairs, 804);
assert_eq!(output.report.rejected_pairs, 9417);
}
}

View file

@ -89,6 +89,13 @@ pub struct CategorizedPolygons {
pub reverse_aligned: Vec<ConvexPolygon>,
}
#[derive(Default)]
pub struct PolygonRouteScratch {
pieces: Vec<CategorizationPiece>,
inside_pieces: Vec<CategorizationPiece>,
cap_points: Vec<Vec3>,
}
impl ConvexSolid {
pub fn from_aabb(bounds: Aabb, material: MaterialId) -> Self {
Self::box_from_center_size(bounds.center(), bounds.size(), Quat::IDENTITY, material)
@ -256,14 +263,14 @@ impl ConvexSolid {
}
pub fn categorize_polygons_against(&self, cutter: &Self) -> CategorizedPolygons {
categorize_polygon_list_against(&self.polygons, cutter)
PolygonRouteScratch::default().categorize_against(&self.polygons, cutter)
}
pub fn route_polygons_outside_of(
polygons: Vec<ConvexPolygon>,
cutter: &Self,
) -> Vec<ConvexPolygon> {
let categorized = categorize_polygon_list_against(&polygons, cutter);
let categorized = PolygonRouteScratch::default().categorize_against(&polygons, cutter);
categorized.outside
}
@ -271,7 +278,7 @@ impl ConvexSolid {
polygons: Vec<ConvexPolygon>,
cutter: &Self,
) -> Vec<ConvexPolygon> {
let categorized = categorize_polygon_list_against(&polygons, cutter);
let categorized = PolygonRouteScratch::default().categorize_against(&polygons, cutter);
let mut inside = categorized.inside;
inside.extend(categorized.aligned);
inside.extend(categorized.reverse_aligned);
@ -384,38 +391,68 @@ impl ConvexSolid {
}
}
fn categorize_polygon_list_against(
polygons: &[ConvexPolygon],
cutter: &ConvexSolid,
) -> CategorizedPolygons {
let mut categorized = CategorizedPolygons::default();
impl PolygonRouteScratch {
pub fn categorize_against(
&mut self,
polygons: &[ConvexPolygon],
cutter: &ConvexSolid,
) -> CategorizedPolygons {
let mut categorized = CategorizedPolygons::default();
for polygon in polygons {
let mut pieces = vec![CategorizationPiece {
polygon: polygon.clone(),
aligned_category: None,
}];
for polygon in polygons {
self.pieces.clear();
self.pieces.push(CategorizationPiece {
polygon: polygon.clone(),
aligned_category: None,
});
for plane in &cutter.clip_planes {
let mut inside_pieces = Vec::new();
for piece in pieces {
classify_piece_against_plane(piece, *plane, &mut inside_pieces, &mut categorized);
}
pieces = inside_pieces;
}
for piece in pieces {
match piece.aligned_category {
Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon),
Some(PolygonCategory::ReverseAligned) => {
categorized.reverse_aligned.push(piece.polygon);
for plane in &cutter.clip_planes {
self.inside_pieces.clear();
for piece in self.pieces.drain(..) {
classify_piece_against_plane(
piece,
*plane,
&mut self.inside_pieces,
&mut categorized,
&mut self.cap_points,
);
}
std::mem::swap(&mut self.pieces, &mut self.inside_pieces);
}
for piece in self.pieces.drain(..) {
match piece.aligned_category {
Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon),
Some(PolygonCategory::ReverseAligned) => {
categorized.reverse_aligned.push(piece.polygon);
}
_ => categorized.inside.push(piece.polygon),
}
_ => categorized.inside.push(piece.polygon),
}
}
categorized
}
categorized
pub fn route_outside_of(
&mut self,
polygons: Vec<ConvexPolygon>,
cutter: &ConvexSolid,
) -> Vec<ConvexPolygon> {
self.categorize_against(&polygons, cutter).outside
}
pub fn route_inside_of(
&mut self,
polygons: Vec<ConvexPolygon>,
cutter: &ConvexSolid,
) -> Vec<ConvexPolygon> {
let categorized = self.categorize_against(&polygons, cutter);
let mut inside = categorized.inside;
inside.extend(categorized.aligned);
inside.extend(categorized.reverse_aligned);
inside
}
}
enum SplitResult {
@ -444,6 +481,7 @@ fn classify_piece_against_plane(
plane: Plane,
inside_pieces: &mut Vec<CategorizationPiece>,
categorized: &mut CategorizedPolygons,
cap_points: &mut Vec<Vec3>,
) {
let piece_plane = piece
.polygon
@ -477,25 +515,15 @@ fn classify_piece_against_plane(
}
(false, true) | (false, false) => inside_pieces.push(piece),
(true, true) => {
let mut cap_points = Vec::new();
let outside = clip_polygon(
&piece.polygon.vertices,
plane,
KeepSide::Front,
&mut cap_points,
);
cap_points.clear();
let outside = clip_polygon(&piece.polygon.vertices, plane, KeepSide::Front, cap_points);
if outside.len() >= 3 {
let mut outside_polygon = piece.polygon.with_vertices(outside);
outside_polygon.category = PolygonCategory::Outside;
categorized.outside.push(outside_polygon);
}
let inside = clip_polygon(
&piece.polygon.vertices,
plane,
KeepSide::Back,
&mut cap_points,
);
let inside = clip_polygon(&piece.polygon.vertices, plane, KeepSide::Back, cap_points);
if inside.len() >= 3 {
inside_pieces.push(CategorizationPiece {
polygon: piece.polygon.with_vertices(inside),

View file

@ -17,7 +17,7 @@ mod tree;
pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning};
pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive};
pub use convex::{CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane};
pub use convex::{CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane, PolygonRouteScratch};
pub use dsl::LevelDsl;
pub use frontier::{DemandFrontier, DemandPair};
pub use mesh::TriangleMesh;