diff --git a/crates/vg_csg/src/brush.rs b/crates/vg_csg/src/brush.rs index 2f04216..1004932 100644 --- a/crates/vg_csg/src/brush.rs +++ b/crates/vg_csg/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/crates/vg_csg/src/convex.rs b/crates/vg_csg/src/convex.rs index a00d810..f19235f 100644 --- a/crates/vg_csg/src/convex.rs +++ b/crates/vg_csg/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/crates/vg_csg/src/lib.rs b/crates/vg_csg/src/lib.rs index dae2d44..c47e809 100644 --- a/crates/vg_csg/src/lib.rs +++ b/crates/vg_csg/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/crates/vg_csg/tests/realtime_csg_parity.rs b/crates/vg_csg/tests/realtime_csg_parity.rs new file mode 100644 index 0000000..9116e42 --- /dev/null +++ b/crates/vg_csg/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); +} diff --git a/crates/vg_grammar/examples/dream_room.rs b/crates/vg_grammar/examples/dream_room.rs index 8c549f5..9ee19e3 100644 --- a/crates/vg_grammar/examples/dream_room.rs +++ b/crates/vg_grammar/examples/dream_room.rs @@ -1,18 +1,16 @@ use bevy_math::Vec3; -use vg_grammar::{ClaimTree, CorridorSpec, DoorSpec, GrammarContext, RoomSpec, Rule}; +use vg_grammar::{ClaimTree, Frame, GalleryChainSpec, GrammarContext, Rule}; fn main() { let ctx = GrammarContext::new(7); let mut tree = ClaimTree::new(); - RoomSpec::new("dream gallery", Vec3::ZERO, Vec3::new(6.0, 5.0, 3.0)).emit(&ctx, &mut tree); - CorridorSpec::along_x("east artery", Vec3::new(5.0, 0.0, 0.0), 5.0).emit(&ctx, &mut tree); - DoorSpec::new( - "gallery to artery", - Vec3::new(3.0, 0.0, 1.1), - Vec3::new(0.7, 1.4, 2.2), - ) - .emit(&ctx, &mut tree); + GalleryChainSpec::new("dream gallery", 4) + .in_frame(Frame::new( + Vec3::ZERO, + bevy_math::Quat::from_rotation_z(0.2), + )) + .emit(&ctx, &mut tree); let compiled = tree.compile(); let output = compiled.assembler.build(); diff --git a/crates/vg_grammar/src/claim.rs b/crates/vg_grammar/src/claim.rs index f7a87bf..ec54123 100644 --- a/crates/vg_grammar/src/claim.rs +++ b/crates/vg_grammar/src/claim.rs @@ -1,6 +1,8 @@ -use bevy_math::Vec3; +use bevy_math::{Quat, Vec3}; use vg_csg::{Assembler, BrushId, MaterialId}; +use crate::Frame; + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ClaimTag(pub String); @@ -22,6 +24,7 @@ pub struct Claim { pub kind: ClaimKind, pub center: Vec3, pub size: Vec3, + pub rotation: Quat, pub material: MaterialId, pub tags: Vec, } @@ -38,6 +41,25 @@ impl Claim { kind: ClaimKind::Solid, center, size, + rotation: Quat::IDENTITY, + material, + tags: Vec::new(), + } + } + + pub fn solid_oriented_box( + name: impl Into, + center: Vec3, + size: Vec3, + rotation: Quat, + material: MaterialId, + ) -> Self { + Self { + name: name.into(), + kind: ClaimKind::Solid, + center, + size, + rotation, material, tags: Vec::new(), } @@ -49,6 +71,24 @@ impl Claim { kind: ClaimKind::Void, center, size, + rotation: Quat::IDENTITY, + material: MaterialId::default(), + tags: Vec::new(), + } + } + + pub fn void_oriented_box( + name: impl Into, + center: Vec3, + size: Vec3, + rotation: Quat, + ) -> Self { + Self { + name: name.into(), + kind: ClaimKind::Void, + center, + size, + rotation, material: MaterialId::default(), tags: Vec::new(), } @@ -58,6 +98,16 @@ impl Claim { self.tags.push(ClaimTag::new(tag)); self } + + pub fn in_frame(mut self, frame: Frame) -> Self { + self.center = frame.point(self.center); + self.rotation = frame.rotation * self.rotation; + self + } + + pub fn is_axis_aligned(&self) -> bool { + self.rotation.abs_diff_eq(Quat::IDENTITY, 1.0e-6) + } } #[derive(Clone, Debug, Default)] @@ -94,15 +144,28 @@ impl ClaimTree { let mut brush_ids = Vec::with_capacity(self.claims.len()); for claim in &self.claims { let brush_id = match claim.kind { - ClaimKind::Solid => assembler.solid_box( + ClaimKind::Solid if claim.is_axis_aligned() => assembler.solid_box( &claim.name, vg_csg::Aabb::from_center_size(claim.center, claim.size), claim.material, ), - ClaimKind::Void => assembler.cut_box( + ClaimKind::Solid => assembler.solid_oriented_box( + &claim.name, + claim.center, + claim.size, + claim.rotation, + claim.material, + ), + ClaimKind::Void if claim.is_axis_aligned() => assembler.cut_box( &claim.name, vg_csg::Aabb::from_center_size(claim.center, claim.size), ), + ClaimKind::Void => assembler.cut_oriented_box( + &claim.name, + claim.center, + claim.size, + claim.rotation, + ), }; brush_ids.push(brush_id); } @@ -151,4 +214,25 @@ mod tests { assert_eq!(report.brush_ids.len(), 2); assert_eq!(report.assembler.brushes().len(), 2); } + + #[test] + fn oriented_claims_compile_to_oriented_brushes() { + let mut tree = ClaimTree::new(); + tree.push(Claim::solid_oriented_box( + "angled wall", + Vec3::ZERO, + Vec3::new(4.0, 0.25, 3.0), + Quat::from_rotation_z(0.25), + MaterialId(1), + )); + tree.push(Claim::void_oriented_box( + "angled door", + Vec3::ZERO, + Vec3::new(1.0, 0.5, 1.5), + Quat::from_rotation_z(0.25), + )); + + let report = tree.compile(); + assert_eq!(report.assembler.brushes().len(), 2); + } } diff --git a/crates/vg_grammar/src/context.rs b/crates/vg_grammar/src/context.rs index 6343832..73aa137 100644 --- a/crates/vg_grammar/src/context.rs +++ b/crates/vg_grammar/src/context.rs @@ -34,6 +34,21 @@ impl GrammarContext { world_scale: 1.0, } } + + pub fn unit(&self, salt: impl AsRef) -> f32 { + let mut hash = self.seed ^ 0x9E37_79B9_7F4A_7C15; + for byte in salt.as_ref().bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x1000_0000_01B3); + hash ^= hash >> 32; + } + let bits = (hash >> 40) as u32; + bits as f32 / 0x00FF_FFFFu32 as f32 + } + + pub fn range(&self, salt: impl AsRef, min: f32, max: f32) -> f32 { + min + (max - min) * self.unit(salt) + } } impl Default for GrammarContext { diff --git a/crates/vg_grammar/src/frame.rs b/crates/vg_grammar/src/frame.rs new file mode 100644 index 0000000..bbfa383 --- /dev/null +++ b/crates/vg_grammar/src/frame.rs @@ -0,0 +1,40 @@ +use bevy_math::{Quat, Vec3}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Frame { + pub origin: Vec3, + pub rotation: Quat, +} + +impl Frame { + pub const IDENTITY: Self = Self { + origin: Vec3::ZERO, + rotation: Quat::IDENTITY, + }; + + pub fn new(origin: Vec3, rotation: Quat) -> Self { + Self { origin, rotation } + } + + pub fn translated(self, local_offset: Vec3) -> Self { + Self { + origin: self.point(local_offset), + rotation: self.rotation, + } + } + + pub fn rotated(self, local_rotation: Quat) -> Self { + Self { + origin: self.origin, + rotation: self.rotation * local_rotation, + } + } + + pub fn point(self, local: Vec3) -> Vec3 { + self.origin + self.rotation * local + } + + pub fn vector(self, local: Vec3) -> Vec3 { + self.rotation * local + } +} diff --git a/crates/vg_grammar/src/lib.rs b/crates/vg_grammar/src/lib.rs index 3c08305..94eba56 100644 --- a/crates/vg_grammar/src/lib.rs +++ b/crates/vg_grammar/src/lib.rs @@ -5,8 +5,10 @@ mod claim; mod context; +mod frame; mod rules; pub use claim::{Claim, ClaimKind, ClaimTag, ClaimTree, CompileReport}; pub use context::{GrammarContext, Materials}; -pub use rules::{CorridorSpec, DoorSpec, RoomSpec, Rule}; +pub use frame::Frame; +pub use rules::{CorridorSpec, DoorSpec, GalleryChainSpec, RoomSpec, Rule, RuleSet}; diff --git a/crates/vg_grammar/src/rules.rs b/crates/vg_grammar/src/rules.rs index dca68b1..92e4379 100644 --- a/crates/vg_grammar/src/rules.rs +++ b/crates/vg_grammar/src/rules.rs @@ -1,16 +1,84 @@ use bevy_math::Vec3; -use crate::{Claim, ClaimTree, GrammarContext}; +use crate::{Claim, ClaimTree, Frame, GrammarContext}; pub trait Rule { fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree); } +#[derive(Clone, Debug, Default)] +pub struct RuleSet { + rules: Vec, +} + +impl RuleSet { + pub fn new() -> Self { + Self::default() + } + + pub fn with(mut self, rule: impl Into) -> Self { + self.rules.push(rule.into()); + self + } +} + +impl Rule for RuleSet { + fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) { + for rule in &self.rules { + rule.emit(ctx, tree); + } + } +} + +#[derive(Clone, Debug)] +pub enum RuleNode { + Room(RoomSpec), + Corridor(CorridorSpec), + Door(DoorSpec), + GalleryChain(GalleryChainSpec), +} + +impl Rule for RuleNode { + fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) { + match self { + Self::Room(rule) => rule.emit(ctx, tree), + Self::Corridor(rule) => rule.emit(ctx, tree), + Self::Door(rule) => rule.emit(ctx, tree), + Self::GalleryChain(rule) => rule.emit(ctx, tree), + } + } +} + +impl From for RuleNode { + fn from(value: RoomSpec) -> Self { + Self::Room(value) + } +} + +impl From for RuleNode { + fn from(value: CorridorSpec) -> Self { + Self::Corridor(value) + } +} + +impl From for RuleNode { + fn from(value: DoorSpec) -> Self { + Self::Door(value) + } +} + +impl From for RuleNode { + fn from(value: GalleryChainSpec) -> Self { + Self::GalleryChain(value) + } +} + #[derive(Clone, Debug)] pub struct RoomSpec { pub name: String, pub center: Vec3, pub size: Vec3, + pub frame: Frame, pub wall_thickness: f32, } @@ -20,9 +88,15 @@ impl RoomSpec { name: name.into(), center, size, + frame: Frame::IDENTITY, wall_thickness: 0.25, } } + + pub fn in_frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } } impl Rule for RoomSpec { @@ -31,6 +105,7 @@ impl Rule for RoomSpec { let t = self.wall_thickness; let wall_height = self.size.z; let wall_z = self.center.z + wall_height * 0.5; + let frame = self.frame; tree.push( Claim::solid_box( @@ -39,6 +114,7 @@ impl Rule for RoomSpec { Vec3::new(self.size.x, self.size.y, t), ctx.materials.floor, ) + .in_frame(frame) .tagged("room") .tagged("floor"), ); @@ -49,6 +125,7 @@ impl Rule for RoomSpec { Vec3::new(self.size.x, self.size.y, t), ctx.materials.ceiling, ) + .in_frame(frame) .tagged("room") .tagged("ceiling"), ); @@ -59,6 +136,7 @@ impl Rule for RoomSpec { Vec3::new(self.size.x, t, wall_height), ctx.materials.wall, ) + .in_frame(frame) .tagged("room") .tagged("wall"), ); @@ -69,6 +147,7 @@ impl Rule for RoomSpec { Vec3::new(self.size.x, t, wall_height), ctx.materials.wall, ) + .in_frame(frame) .tagged("room") .tagged("wall"), ); @@ -79,6 +158,7 @@ impl Rule for RoomSpec { Vec3::new(t, self.size.y, wall_height), ctx.materials.wall, ) + .in_frame(frame) .tagged("room") .tagged("wall"), ); @@ -89,6 +169,7 @@ impl Rule for RoomSpec { Vec3::new(t, self.size.y, wall_height), ctx.materials.wall, ) + .in_frame(frame) .tagged("room") .tagged("wall"), ); @@ -102,6 +183,7 @@ pub struct CorridorSpec { pub length: f32, pub width: f32, pub height: f32, + pub frame: Frame, pub wall_thickness: f32, } @@ -113,15 +195,22 @@ impl CorridorSpec { length, width: 2.0, height: 2.6, + frame: Frame::IDENTITY, wall_thickness: 0.2, } } + + pub fn in_frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } } impl Rule for CorridorSpec { fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) { let t = self.wall_thickness; let z = self.center.z + self.height * 0.5; + let frame = self.frame; tree.push( Claim::solid_box( format!("{} floor", self.name), @@ -129,6 +218,7 @@ impl Rule for CorridorSpec { Vec3::new(self.length, self.width, t), ctx.materials.floor, ) + .in_frame(frame) .tagged("corridor") .tagged("floor"), ); @@ -139,6 +229,7 @@ impl Rule for CorridorSpec { Vec3::new(self.length, t, self.height), ctx.materials.wall, ) + .in_frame(frame) .tagged("corridor") .tagged("wall"), ); @@ -149,6 +240,7 @@ impl Rule for CorridorSpec { Vec3::new(self.length, t, self.height), ctx.materials.wall, ) + .in_frame(frame) .tagged("corridor") .tagged("wall"), ); @@ -159,6 +251,7 @@ impl Rule for CorridorSpec { Vec3::new(self.length, self.width, t), ctx.materials.ceiling, ) + .in_frame(frame) .tagged("corridor") .tagged("ceiling"), ); @@ -170,6 +263,7 @@ pub struct DoorSpec { pub name: String, pub center: Vec3, pub size: Vec3, + pub frame: Frame, } impl DoorSpec { @@ -178,22 +272,112 @@ impl DoorSpec { name: name.into(), center, size, + frame: Frame::IDENTITY, } } + + pub fn in_frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } } impl Rule for DoorSpec { fn emit(&self, _ctx: &GrammarContext, tree: &mut ClaimTree) { tree.push( Claim::void_box(&self.name, self.center, self.size) + .in_frame(self.frame) .tagged("door") .tagged("void"), ); } } +#[derive(Clone, Debug)] +pub struct GalleryChainSpec { + pub name: String, + pub frame: Frame, + pub rooms: usize, + pub base_room_size: Vec3, + pub corridor_length: f32, +} + +impl GalleryChainSpec { + pub fn new(name: impl Into, rooms: usize) -> Self { + Self { + name: name.into(), + frame: Frame::IDENTITY, + rooms: rooms.max(1), + base_room_size: Vec3::new(5.0, 4.0, 3.0), + corridor_length: 3.0, + } + } + + pub fn in_frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } +} + +impl Rule for GalleryChainSpec { + fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) { + let mut cursor_x = 0.0; + for index in 0..self.rooms { + let width_jitter = ctx.range(format!("{}:{index}:width", self.name), -0.4, 0.6); + let depth_jitter = ctx.range(format!("{}:{index}:depth", self.name), -0.25, 0.5); + let room_size = self.base_room_size + Vec3::new(width_jitter, depth_jitter, 0.0); + let room_center = Vec3::new(cursor_x, 0.0, 0.0); + + RoomSpec::new( + format!("{} room {index}", self.name), + room_center, + room_size, + ) + .in_frame(self.frame) + .emit(ctx, tree); + + if index + 1 < self.rooms { + let room_half = room_size.x * 0.5; + let next_width = self.base_room_size.x + + ctx.range(format!("{}:{}:width", self.name, index + 1), -0.4, 0.6); + let next_half = next_width * 0.5; + let corridor_center_x = cursor_x + room_half + self.corridor_length * 0.5; + let door_x = cursor_x + room_half; + + DoorSpec::new( + format!("{} doorway {index}", self.name), + Vec3::new(door_x, 0.0, 1.1), + Vec3::new(0.8, 1.5, 2.2), + ) + .in_frame(self.frame) + .emit(ctx, tree); + + CorridorSpec::along_x( + format!("{} corridor {index}", self.name), + Vec3::new(corridor_center_x, 0.0, 0.0), + self.corridor_length, + ) + .in_frame(self.frame) + .emit(ctx, tree); + + let next_door_x = cursor_x + room_half + self.corridor_length; + DoorSpec::new( + format!("{} doorway {}b", self.name, index), + Vec3::new(next_door_x, 0.0, 1.1), + Vec3::new(0.8, 1.5, 2.2), + ) + .in_frame(self.frame) + .emit(ctx, tree); + + cursor_x += room_half + self.corridor_length + next_half; + } + } + } +} + #[cfg(test)] mod tests { + use bevy_math::Quat; use vg_csg::Aabb; use super::*; @@ -234,4 +418,40 @@ mod tests { assert!(!door_void.contains_point_strict(center, 1.0e-4)); } } + + #[test] + fn gallery_chain_uses_seeded_variation_and_frames() { + let ctx = GrammarContext::new(99); + let mut tree = ClaimTree::new(); + let frame = Frame::new(Vec3::new(10.0, 0.0, 0.0), Quat::from_rotation_z(0.4)); + GalleryChainSpec::new("chain", 3) + .in_frame(frame) + .emit(&ctx, &mut tree); + + assert_eq!(tree.count_kind(ClaimKind::Solid), 26); + assert_eq!(tree.count_kind(ClaimKind::Void), 4); + assert!(tree.claims().iter().any(|claim| !claim.is_axis_aligned())); + + let report = tree.compile(); + let output = report.assembler.build(); + assert!(output.mesh.triangle_count() > 150); + assert!(output.report.warnings.is_empty()); + } + + #[test] + fn ruleset_composes_rules_without_losing_claims() { + let ctx = GrammarContext::new(5); + let mut tree = ClaimTree::new(); + RuleSet::new() + .with(RoomSpec::new("a", Vec3::ZERO, Vec3::new(4.0, 4.0, 3.0))) + .with(DoorSpec::new( + "a door", + Vec3::new(2.0, 0.0, 1.0), + Vec3::new(0.7, 1.2, 2.0), + )) + .emit(&ctx, &mut tree); + + assert_eq!(tree.count_kind(ClaimKind::Solid), 6); + assert_eq!(tree.count_kind(ClaimKind::Void), 1); + } } diff --git a/docs/research/dream-machine-grammar.md b/docs/research/dream-machine-grammar.md index a24152b..abe4070 100644 --- a/docs/research/dream-machine-grammar.md +++ b/docs/research/dream-machine-grammar.md @@ -25,11 +25,21 @@ The first rules are small on purpose: - `RoomSpec` - `CorridorSpec` - `DoorSpec` +- `GalleryChainSpec` They prove the shape of the pipeline: grammar emits semantic claims, claims compile into `vg_csg` brushes, and the CSG assembler produces the mesh verdict. +The first expansion adds local `Frame`s, deterministic seeded variation, and +`RuleSet`/`GalleryChainSpec` composition. This is the rule: every richer grammar +feature must sharpen spatial intent before it increases mesh complexity. + The rule is simple: future procedural generators should feed CSG with authored space claims instead of bypassing the tree with final triangles. Cities, habitats, corridors, and districts can grow from fields and grammars, but hard architectural legality belongs in the CSG tree. + +Where a grammar feature compiles to a CSG behavior that overlaps public +RealtimeCSG semantics, tests should preserve exact parity at that seam: claim +counts, brush counts, category vocabulary, split counts, and mesh invariants. +Novel grammar can sing. Shared API behavior must stay in tune. diff --git a/docs/research/realtime-csg-doctrine.md b/docs/research/realtime-csg-doctrine.md index 4815fa0..e6953d8 100644 --- a/docs/research/realtime-csg-doctrine.md +++ b/docs/research/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/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 022abf0..6fcca5b 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -48,10 +48,14 @@ brushes, exact AABB and oriented-box subtraction via convex plane splitting, additive `CylinderZ`, `DomeCapZ`, and `FloretArm` primitives, Bevy-math mesh buffers, `LevelDsl`, tests, clippy, and an example room. Research note: `docs/research/realtime-csg-bevy-assembler.md`. +Where `vg_csg` overlaps public RealtimeCSG/demo behavior, add exact observable +parity fixtures instead of approximate plausibility tests. -`crates/vg_grammar` is the first Dream Machine grammar layer above CSG. Rules -emit semantic solid/void claims with tags, then compile those claims into -`vg_csg` brushes. Current rules: `RoomSpec`, `CorridorSpec`, `DoorSpec`. +`crates/vg_grammar` is the Dream Machine grammar layer above CSG. Rules emit +semantic solid/void claims with tags, then compile those claims into `vg_csg` +brushes. Current features: local `Frame`s, oriented claims, seeded variation, +`RuleSet` composition, `RoomSpec`, `CorridorSpec`, `DoorSpec`, and +`GalleryChainSpec`. Example: `cargo run -p vg_grammar --example dream_room`. Research note: `docs/research/dream-machine-grammar.md`. @@ -60,11 +64,10 @@ Repo-local Geometry Script clone notes live in `.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is missing. -The current next action is to grow `vg_grammar` from room/corridor/door claims -into a real procedural level grammar: add frames, seeded variation, rule -composition, semantic materials/tags, and generated CSG tree structure, while -moving `vg_csg` toward explicit polygon categories and boolean-tree -classification. +The current next action is to add generated CSG tree structure to `vg_grammar` +and explicit polygon categories to `vg_csg`: grammar should emit nested boolean +intent instead of only a flat brush list, while the kernel should classify +source polygons through that tree. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0d87bcf..14923bc 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -52,3 +52,5 @@ {"ts": "2026-05-06T23:20:20+00:00", "type": "implementation", "status": "accepted", "note": "Upgraded vg_csg from AABB-only subtraction to a convex-brush stepping-stone kernel: oriented-box brushes, plane splitting, convex fragment emission, angled-cut tests, and updated RealtimeCSG doctrine. Public LogicalError Realtime-CSG-demo and Sander van Rossen blog/article material are now preferred clean research inputs for the faster target model: polygon classification through boolean trees, not decompiled native internals."} {"ts": "2026-05-06T23:31:47+00:00", "type": "research", "status": "accepted", "note": "Distilled public Realtime CSG ecosystem doctrine from Sander van Rossen blog posts, LogicalError/Realtime-CSG-demo, BSO3, GDC talk listing, and downstream engine discussions: target vg_csg should migrate from fragment carving toward polygon classification through boolean trees with explicit categories, half-edge/topology working data, bounds early-outs, dirty affected-brush rebuilds, separate mesh optimization, and CSG as one procedural grammar operator."} {"ts": "2026-05-07T00:12:44+00:00", "type": "implementation", "status": "accepted", "note": "Built the first Dream Machine grammar layer: new vg_grammar crate with semantic solid/void ClaimTree, GrammarContext materials, RoomSpec/CorridorSpec/DoorSpec rules, a dream_room example, and tests proving grammar emits claims that compile into vg_csg brushes and leave doorway voids empty. The authoring split is now grammar writes spatial claims, CSG adjudicates matter, mesh is cache."} +{"ts": "2026-05-07T00:19:35+00:00", "type": "implementation", "status": "accepted", "note": "Extended the Dream Machine grammar with local frames, oriented claims, deterministic seeded variation, RuleSet composition, and GalleryChainSpec. The dream_room example now emits a rotated four-room chain as 42 semantic claims into vg_csg; workspace tests and clippy prove the grammar still preserves void claims through CSG assembly."} +{"ts": "2026-05-07T00:21:11+00:00", "type": "implementation", "status": "accepted", "note": "Added exact parity testing doctrine and first vg_csg parity fixtures for public RealtimeCSG-overlapping seams: polygon category vocabulary, six-plane/six-polygon box brush counts, and six surviving regions for centered box subtraction. Future matching API surface should get countable parity fixtures, not approximate plausibility checks."} diff --git a/state/map.yaml b/state/map.yaml index 342cb90..edbe907 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -29,6 +29,7 @@ hard_boundaries: - Prefer established Blender node idioms and current API guidance over bespoke graph inventions. - Keep failed experiments only when they change future belief. - Do not let proxy checks replace visual review. + - Where VibeGeometry exposes behavior matching public RealtimeCSG/demo seams, tests must maintain exact observable parity rather than approximate plausibility. live_architecture: pipeline: @@ -109,8 +110,8 @@ data_flow: - If state grows faster than understanding, cut it back. current_status: - summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. The Rust crate `vg_csg` now has a convex-brush stepping-stone kernel: RealtimeCSG-inspired ordered brushes, operation types, generation tracking, exact AABB and oriented-box subtraction via convex plane splitting, additive cylinder/dome/floret primitives, mesh-array output, DSL coverage, tests, clippy, and a sample room. The new `vg_grammar` crate is the first Dream Machine authoring layer: room/corridor/door rules emit semantic solid/void claims with tags into `vg_csg`, preserving intent before CSG resolves matter. Public RealtimeCSG demo/source research points the target away from fragment carving and toward faster polygon classification through boolean trees. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. - next_action: Grow `vg_grammar` from room/corridor/door claims into a real procedural level grammar: add frames, seeded variation, rule composition, semantic materials/tags, and generated CSG tree structure, while moving `vg_csg` toward explicit polygon categories and boolean-tree classification. + summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. The Rust crate `vg_csg` now has a convex-brush stepping-stone kernel: RealtimeCSG-inspired ordered brushes, operation types, generation tracking, exact AABB and oriented-box subtraction via convex plane splitting, additive cylinder/dome/floret primitives, mesh-array output, DSL coverage, tests, clippy, and a sample room. Exact parity fixtures now guard public RealtimeCSG-overlapping seams such as category vocabulary, box brush counts, and centered box subtraction regions. The `vg_grammar` crate is the Dream Machine authoring layer: semantic solid/void claims with tags compile into `vg_csg`; it now supports local frames, oriented claims, deterministic seeded variation, rule composition, and generated gallery chains. Public RealtimeCSG demo/source research points the target away from fragment carving and toward faster polygon classification through boolean trees. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + next_action: Add generated CSG tree structure to `vg_grammar` and explicit polygon categories to `vg_csg`: grammar should emit nested boolean intent instead of only a flat brush list, while the kernel should classify source polygons through that tree. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry geometry_script_fork: https://github.com/GameCult/geometry-script