Add routed CSG scratch guardrails
This commit is contained in:
parent
8f639d715b
commit
8c61d2fbf7
4 changed files with 108 additions and 48 deletions
|
|
@ -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`:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
110
src/convex.rs
110
src/convex.rs
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue