Add CSG tree API surface
This commit is contained in:
parent
cc4bbaa635
commit
e0cbecf539
4 changed files with 367 additions and 1 deletions
|
|
@ -247,6 +247,20 @@ Process brush:
|
|||
emit visible pieces into render mesh
|
||||
```
|
||||
|
||||
`vg_csg` now exposes the first version of that public tree surface:
|
||||
|
||||
- `CsgOperationType::{Additive, Subtractive, Intersecting}` with parity-tested
|
||||
ordinals matching the public API
|
||||
- `CsgBranchOp::{Addition, Subtraction, Common}` matching the public demo
|
||||
- `CsgTreeArena`, `CsgTree`, `CsgTreeBranch`, `CsgTreeBrush`, and `CsgNodeId`
|
||||
- branch child replacement and operation mutation
|
||||
- brush operation mutation
|
||||
- compilation from tree intent into the current ordered assembler backend
|
||||
|
||||
This is still not the final classifier kernel. It is the API skeleton that lets
|
||||
grammar and editor code speak in RealtimeCSG-shaped boolean trees while the
|
||||
backend grows into the category router.
|
||||
|
||||
Algorithm migration steps:
|
||||
|
||||
1. Add polygon categories and visible/reversed flags to `vg_csg`.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod convex;
|
|||
mod dsl;
|
||||
mod mesh;
|
||||
mod primitives;
|
||||
mod tree;
|
||||
|
||||
pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning};
|
||||
pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive};
|
||||
|
|
@ -21,3 +22,7 @@ pub use mesh::TriangleMesh;
|
|||
pub use primitives::{
|
||||
DomeCapZSpec, FloretArmSpec, append_cylinder_z, append_dome_cap_z, append_floret_arm,
|
||||
};
|
||||
pub use tree::{
|
||||
CsgBranchOp, CsgNode, CsgNodeId, CsgOperationType, CsgTree, CsgTreeArena, CsgTreeBranch,
|
||||
CsgTreeBrush,
|
||||
};
|
||||
|
|
|
|||
306
src/tree.rs
Normal file
306
src/tree.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
use crate::{Assembler, Brush, BrushId, BrushOp, MaterialId, Primitive};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgNodeId(pub u32);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTreeBrush {
|
||||
pub node: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTreeBranch {
|
||||
pub node: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTree {
|
||||
pub root: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CsgOperationType {
|
||||
Additive = 0,
|
||||
Subtractive = 1,
|
||||
Intersecting = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CsgBranchOp {
|
||||
Addition,
|
||||
Subtraction,
|
||||
Common,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CsgNode {
|
||||
Brush(Brush),
|
||||
Branch {
|
||||
name: String,
|
||||
op: CsgBranchOp,
|
||||
children: Vec<CsgNodeId>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CsgTreeArena {
|
||||
nodes: Vec<CsgNode>,
|
||||
}
|
||||
|
||||
impl CsgTreeArena {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn nodes(&self) -> &[CsgNode] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
pub fn node(&self, id: CsgNodeId) -> Option<&CsgNode> {
|
||||
self.nodes.get(id.0 as usize)
|
||||
}
|
||||
|
||||
pub fn node_mut(&mut self, id: CsgNodeId) -> Option<&mut CsgNode> {
|
||||
self.nodes.get_mut(id.0 as usize)
|
||||
}
|
||||
|
||||
pub fn brush_count(&self) -> usize {
|
||||
self.nodes
|
||||
.iter()
|
||||
.filter(|node| matches!(node, CsgNode::Brush(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn branch_count(&self) -> usize {
|
||||
self.nodes
|
||||
.iter()
|
||||
.filter(|node| matches!(node, CsgNode::Branch { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn generate_brush(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
operation: CsgOperationType,
|
||||
primitive: Primitive,
|
||||
material: MaterialId,
|
||||
) -> CsgTreeBrush {
|
||||
let id = CsgNodeId(self.nodes.len() as u32);
|
||||
let brush = Brush::new(
|
||||
BrushId(id.0),
|
||||
name,
|
||||
operation.into_brush_op(),
|
||||
primitive,
|
||||
material,
|
||||
);
|
||||
self.nodes.push(CsgNode::Brush(brush));
|
||||
CsgTreeBrush { node: id }
|
||||
}
|
||||
|
||||
pub fn generate_branch(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
op: CsgBranchOp,
|
||||
children: impl IntoIterator<Item = CsgNodeId>,
|
||||
) -> CsgTreeBranch {
|
||||
let id = CsgNodeId(self.nodes.len() as u32);
|
||||
self.nodes.push(CsgNode::Branch {
|
||||
name: name.into(),
|
||||
op,
|
||||
children: children.into_iter().collect(),
|
||||
});
|
||||
CsgTreeBranch { node: id }
|
||||
}
|
||||
|
||||
pub fn generate_tree(&self, root: CsgNodeId) -> CsgTree {
|
||||
CsgTree { root }
|
||||
}
|
||||
|
||||
pub fn set_brush_operation_type(
|
||||
&mut self,
|
||||
brush: CsgTreeBrush,
|
||||
operation: CsgOperationType,
|
||||
) -> bool {
|
||||
let Some(CsgNode::Brush(node)) = self.node_mut(brush.node) else {
|
||||
return false;
|
||||
};
|
||||
node.op = operation.into_brush_op();
|
||||
node.set_dirty();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_branch_operation_type(&mut self, branch: CsgTreeBranch, op: CsgBranchOp) -> bool {
|
||||
let Some(CsgNode::Branch { op: current_op, .. }) = self.node_mut(branch.node) else {
|
||||
return false;
|
||||
};
|
||||
*current_op = op;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_child_nodes(
|
||||
&mut self,
|
||||
branch: CsgTreeBranch,
|
||||
children: impl IntoIterator<Item = CsgNodeId>,
|
||||
) -> bool {
|
||||
let Some(CsgNode::Branch {
|
||||
children: current_children,
|
||||
..
|
||||
}) = self.node_mut(branch.node)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
*current_children = children.into_iter().collect();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn child_nodes(&self, branch: CsgTreeBranch) -> Option<&[CsgNodeId]> {
|
||||
let Some(CsgNode::Branch { children, .. }) = self.node(branch.node) else {
|
||||
return None;
|
||||
};
|
||||
Some(children)
|
||||
}
|
||||
|
||||
pub fn compile_tree_to_assembler(&self, tree: CsgTree) -> Assembler {
|
||||
let mut assembler = Assembler::new();
|
||||
self.compile_node(tree.root, &mut assembler, None);
|
||||
assembler
|
||||
}
|
||||
|
||||
fn compile_node(
|
||||
&self,
|
||||
id: CsgNodeId,
|
||||
assembler: &mut Assembler,
|
||||
operation_override: Option<CsgOperationType>,
|
||||
) {
|
||||
let Some(node) = self.node(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match node {
|
||||
CsgNode::Brush(brush) => {
|
||||
let op = operation_override
|
||||
.map(CsgOperationType::into_brush_op)
|
||||
.unwrap_or(brush.op);
|
||||
assembler.add_brush(
|
||||
brush.name.clone(),
|
||||
op,
|
||||
brush.primitive.clone(),
|
||||
brush.material,
|
||||
);
|
||||
}
|
||||
CsgNode::Branch { op, children, .. } => match op {
|
||||
CsgBranchOp::Addition => {
|
||||
for child in children {
|
||||
self.compile_node(*child, assembler, operation_override);
|
||||
}
|
||||
}
|
||||
CsgBranchOp::Subtraction => {
|
||||
if let Some((first, rest)) = children.split_first() {
|
||||
self.compile_node(*first, assembler, operation_override);
|
||||
for child in rest {
|
||||
self.compile_node(
|
||||
*child,
|
||||
assembler,
|
||||
Some(CsgOperationType::Subtractive),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
CsgBranchOp::Common => {
|
||||
for child in children {
|
||||
self.compile_node(*child, assembler, Some(CsgOperationType::Intersecting));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CsgOperationType {
|
||||
pub fn into_brush_op(self) -> BrushOp {
|
||||
match self {
|
||||
Self::Additive => BrushOp::Add,
|
||||
Self::Subtractive => BrushOp::Subtract,
|
||||
Self::Intersecting => BrushOp::Intersect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrushOp> for CsgOperationType {
|
||||
fn from(value: BrushOp) -> Self {
|
||||
match value {
|
||||
BrushOp::Add => Self::Additive,
|
||||
BrushOp::Subtract => Self::Subtractive,
|
||||
BrushOp::Intersect => Self::Intersecting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_math::Vec3;
|
||||
|
||||
use super::*;
|
||||
use crate::{Aabb, Primitive};
|
||||
|
||||
#[test]
|
||||
fn tree_subtraction_compiles_to_ordered_brush_stream() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let solid = arena.generate_brush(
|
||||
"solid",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let void = arena.generate_brush(
|
||||
"void",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
},
|
||||
MaterialId(0),
|
||||
);
|
||||
let branch = arena.generate_branch(
|
||||
"solid minus void",
|
||||
CsgBranchOp::Subtraction,
|
||||
[solid.node, void.node],
|
||||
);
|
||||
let assembler = arena.compile_tree_to_assembler(arena.generate_tree(branch.node));
|
||||
|
||||
assert_eq!(assembler.brushes().len(), 2);
|
||||
assert_eq!(assembler.brushes()[0].op, BrushOp::Add);
|
||||
assert_eq!(assembler.brushes()[1].op, BrushOp::Subtract);
|
||||
assert_eq!(assembler.build().report.emitted_convex_fragments, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_edit_api_replaces_children_and_operations() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let a = arena.generate_brush(
|
||||
"a",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let b = arena.generate_brush(
|
||||
"b",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::X, Vec3::splat(1.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let branch = arena.generate_branch("branch", CsgBranchOp::Addition, [a.node]);
|
||||
|
||||
assert!(arena.set_child_nodes(branch, [a.node, b.node]));
|
||||
assert_eq!(arena.child_nodes(branch).expect("branch").len(), 2);
|
||||
assert!(arena.set_branch_operation_type(branch, CsgBranchOp::Subtraction));
|
||||
assert!(arena.set_brush_operation_type(b, CsgOperationType::Subtractive));
|
||||
assert_eq!(arena.brush_count(), 2);
|
||||
assert_eq!(arena.branch_count(), 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
use bevy_math::{Quat, Vec3};
|
||||
use vg_csg::{Aabb, Assembler, ConvexSolid, MaterialId, PolygonCategory};
|
||||
use vg_csg::{
|
||||
Aabb, Assembler, ConvexSolid, CsgBranchOp, CsgOperationType, CsgTreeArena, MaterialId,
|
||||
PolygonCategory, Primitive,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn parity_polygon_category_vocabulary_matches_public_demo() {
|
||||
|
|
@ -13,6 +16,44 @@ fn parity_polygon_category_vocabulary_matches_public_demo() {
|
|||
assert_eq!(categories.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_operation_type_ordinals_match_public_api() {
|
||||
assert_eq!(CsgOperationType::Additive as u8, 0);
|
||||
assert_eq!(CsgOperationType::Subtractive as u8, 1);
|
||||
assert_eq!(CsgOperationType::Intersecting as u8, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_demo_branch_vocabulary_matches_public_demo() {
|
||||
let branches = [
|
||||
CsgBranchOp::Addition,
|
||||
CsgBranchOp::Subtraction,
|
||||
CsgBranchOp::Common,
|
||||
];
|
||||
|
||||
assert_eq!(branches.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_tree_api_exposes_brush_branch_tree_handles() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let brush = arena.generate_brush(
|
||||
"brush",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::ONE),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let branch = arena.generate_branch("branch", CsgBranchOp::Addition, [brush.node]);
|
||||
let tree = arena.generate_tree(branch.node);
|
||||
|
||||
assert_eq!(arena.brush_count(), 1);
|
||||
assert_eq!(arena.branch_count(), 1);
|
||||
assert_eq!(arena.child_nodes(branch).expect("branch"), &[brush.node]);
|
||||
assert_eq!(tree.root, branch.node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_box_brush_observable_counts_match_realtime_csg_demo() {
|
||||
let solid = ConvexSolid::box_from_center_size(
|
||||
|
|
|
|||
Loading…
Reference in a new issue