diff --git a/docs/realtime-csg-doctrine.md b/docs/realtime-csg-doctrine.md index 4815fa0..e6953d8 100644 --- a/docs/realtime-csg-doctrine.md +++ b/docs/realtime-csg-doctrine.md @@ -265,6 +265,12 @@ Algorithm migration steps: ## Acceptance Tests To Add +- Where `vg_csg` exposes the same behavior as public RealtimeCSG/demo APIs, + maintain exact observable parity fixtures. These should compare public + behavior such as category vocabulary, brush plane/polygon counts, split + counts, visible/reversed semantics, bounds early-outs, and mesh output + invariants. Do not use vague "looks right" tests where a public seam has a + countable contract. - Two overlapping additive boxes produce no duplicate coplanar surface. - Subtraction is implemented as `!(!A || B)` and matches direct cases. - Intersection is implemented as `!(!A || !B)` and matches direct cases. diff --git a/src/brush.rs b/src/brush.rs index 2f04216..1004932 100644 --- a/src/brush.rs +++ b/src/brush.rs @@ -13,6 +13,14 @@ pub enum BrushOp { Intersect, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PolygonCategory { + Inside, + Outside, + Aligned, + ReverseAligned, +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct Aabb { pub min: Vec3, diff --git a/src/convex.rs b/src/convex.rs index a00d810..f19235f 100644 --- a/src/convex.rs +++ b/src/convex.rs @@ -330,6 +330,35 @@ fn polygon_normal(vertices: &[Vec3]) -> Vec3 { mod tests { use super::*; + fn assert_box_parity(solid: &ConvexSolid, material: MaterialId) { + assert_eq!(solid.polygons.len(), 6); + assert_eq!(solid.clip_planes.len(), 6); + assert!( + solid + .polygons + .iter() + .all(|polygon| polygon.vertices.len() == 4) + ); + assert!( + solid + .polygons + .iter() + .all(|polygon| polygon.material == material) + ); + } + + #[test] + fn parity_box_brush_from_planes_has_six_quad_polygons() { + let solid = ConvexSolid::box_from_center_size( + Vec3::ZERO, + Vec3::splat(2.0), + Quat::IDENTITY, + MaterialId(9), + ); + + assert_box_parity(&solid, MaterialId(9)); + } + #[test] fn subtract_center_box_emits_six_convex_fragments() { let source = ConvexSolid::box_from_center_size( diff --git a/src/lib.rs b/src/lib.rs index dae2d44..c47e809 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ mod mesh; mod primitives; pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning}; -pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, Primitive}; +pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive}; pub use convex::{ConvexPolygon, ConvexSolid, Plane}; pub use dsl::LevelDsl; pub use mesh::TriangleMesh; diff --git a/tests/realtime_csg_parity.rs b/tests/realtime_csg_parity.rs new file mode 100644 index 0000000..9116e42 --- /dev/null +++ b/tests/realtime_csg_parity.rs @@ -0,0 +1,50 @@ +use bevy_math::{Quat, Vec3}; +use vg_csg::{Aabb, Assembler, ConvexSolid, MaterialId, PolygonCategory}; + +#[test] +fn parity_polygon_category_vocabulary_matches_public_demo() { + let categories = [ + PolygonCategory::Inside, + PolygonCategory::Outside, + PolygonCategory::Aligned, + PolygonCategory::ReverseAligned, + ]; + + assert_eq!(categories.len(), 4); +} + +#[test] +fn parity_box_brush_observable_counts_match_realtime_csg_demo() { + let solid = ConvexSolid::box_from_center_size( + Vec3::ZERO, + Vec3::splat(2.0), + Quat::IDENTITY, + MaterialId(3), + ); + + assert_eq!(solid.clip_planes.len(), 6); + assert_eq!(solid.polygons.len(), 6); + assert_eq!( + solid + .polygons + .iter() + .map(|polygon| polygon.vertices.len()) + .sum::(), + 24 + ); +} + +#[test] +fn parity_center_box_subtraction_has_six_surviving_regions() { + let mut asm = Assembler::new(); + asm.solid_box( + "source", + Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)), + MaterialId(1), + ); + asm.cut_box("void", Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0))); + + let output = asm.build(); + assert_eq!(output.report.emitted_convex_fragments, 6); + assert_eq!(output.report.warnings.len(), 0); +}