Expand grammar and add CSG parity fixtures
This commit is contained in:
parent
3cb2bd56bc
commit
3c8b37b53f
15 changed files with 494 additions and 26 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
50
crates/vg_csg/tests/realtime_csg_parity.rs
Normal file
50
crates/vg_csg/tests/realtime_csg_parity.rs
Normal file
|
|
@ -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::<usize>(),
|
||||
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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<ClaimTag>,
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
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<String>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,21 @@ impl GrammarContext {
|
|||
world_scale: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unit(&self, salt: impl AsRef<str>) -> 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<str>, min: f32, max: f32) -> f32 {
|
||||
min + (max - min) * self.unit(salt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GrammarContext {
|
||||
|
|
|
|||
40
crates/vg_grammar/src/frame.rs
Normal file
40
crates/vg_grammar/src/frame.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<RuleNode>,
|
||||
}
|
||||
|
||||
impl RuleSet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with(mut self, rule: impl Into<RuleNode>) -> 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<RoomSpec> for RuleNode {
|
||||
fn from(value: RoomSpec) -> Self {
|
||||
Self::Room(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CorridorSpec> for RuleNode {
|
||||
fn from(value: CorridorSpec) -> Self {
|
||||
Self::Corridor(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DoorSpec> for RuleNode {
|
||||
fn from(value: DoorSpec) -> Self {
|
||||
Self::Door(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GalleryChainSpec> 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<String>, 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue