From a466b8ddc3d7ce33277bb3b5791c80cea8cd74d9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 29 May 2026 15:55:45 +0100 Subject: [PATCH] Extend domain CSG chunk runtime --- examples/csg_perf_fixture.rs | 46 ++-- src/domain.rs | 469 ++++++++++++++++++++++++++++++++--- src/lib.rs | 7 +- 3 files changed, 466 insertions(+), 56 deletions(-) diff --git a/examples/csg_perf_fixture.rs b/examples/csg_perf_fixture.rs index c64d26b..7485f94 100644 --- a/examples/csg_perf_fixture.rs +++ b/examples/csg_perf_fixture.rs @@ -5,7 +5,7 @@ use std::{ use bevy_math::{Quat, Vec3}; use vg_csg::{ - Aabb, Assembler, BrushOp, DomainQuery, MaterialId, Primitive, lower_selected_cut, + Aabb, Assembler, BrushOp, DomainQuery, MaterialId, Primitive, lower_selected_cut_chunks, ragnarok_column_fixture, select_domain_cut, }; @@ -240,13 +240,23 @@ fn percentile_ns(values: &[Duration], percentile: usize) -> u128 { fn run_lod_case(name: &'static str, query: DomainQuery) { let fixture = ragnarok_column_fixture(); let warmup_cut = select_domain_cut(&fixture, &query); - let warmup_chunk = lower_selected_cut(&warmup_cut); - black_box(warmup_chunk.mesh.triangle_count()); + let warmup_chunks = lower_selected_cut_chunks(&warmup_cut); + black_box( + warmup_chunks + .iter() + .map(|chunk| chunk.mesh.triangle_count()) + .sum::(), + ); for _ in 0..WARMUP_ITERS { let cut = select_domain_cut(&fixture, &query); - let chunk = lower_selected_cut(&cut); - black_box(chunk.mesh.triangle_count()); + let chunks = lower_selected_cut_chunks(&cut); + black_box( + chunks + .iter() + .map(|chunk| chunk.mesh.triangle_count()) + .sum::(), + ); } let mut timings = Vec::with_capacity(MEASURE_ITERS); @@ -261,18 +271,26 @@ fn run_lod_case(name: &'static str, query: DomainQuery) { for _ in 0..MEASURE_ITERS { let start = Instant::now(); let cut = select_domain_cut(&fixture, &query); - let chunk = lower_selected_cut(&cut); + let chunks = lower_selected_cut_chunks(&cut); let elapsed = start.elapsed(); selected_nodes = cut.selected_nodes.len(); emitted_claims = cut.emitted_claims.len(); - triangles = chunk.mesh.triangle_count(); - collider_triangles = chunk - .collider_mesh - .as_ref() - .map_or(0, |mesh| mesh.triangle_count()); - brush_count = chunk.report.input_brushes; - candidate_pairs = chunk.report.candidate_pairs; - rejected_pairs = chunk.report.rejected_pairs; + triangles = chunks.iter().map(|chunk| chunk.mesh.triangle_count()).sum(); + collider_triangles = chunks + .iter() + .map(|chunk| { + chunk + .collider_mesh + .as_ref() + .map_or(0, |mesh| mesh.triangle_count()) + }) + .sum(); + brush_count = chunks.iter().map(|chunk| chunk.report.input_brushes).sum(); + candidate_pairs = chunks + .iter() + .map(|chunk| chunk.report.candidate_pairs) + .sum(); + rejected_pairs = chunks.iter().map(|chunk| chunk.report.rejected_pairs).sum(); black_box(( selected_nodes, emitted_claims, diff --git a/src/domain.rs b/src/domain.rs index e1645e9..df472d1 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,6 +1,9 @@ use bevy_math::{Quat, Vec3}; -use crate::{Aabb, Assembler, BrushOp, BuildReport, MaterialId, Primitive, TriangleMesh}; +use crate::{ + Aabb, BrushOp, BuildReport, CsgBranchOp, CsgNodeId, CsgOperationType, CsgTreeArena, MaterialId, + Primitive, TriangleMesh, +}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct DomainKey(pub String); @@ -122,18 +125,18 @@ impl FeatureClaimKind { } } - fn brush_op(self) -> BrushOp { + pub fn brush_op(self) -> BrushOp { match self { Self::VoidBrush | Self::ClearanceVolume => BrushOp::Subtract, _ => BrushOp::Add, } } - fn emits_render(self) -> bool { + pub fn emits_render(self) -> bool { !matches!(self, Self::ColliderProxy | Self::ClearanceVolume) } - fn emits_collider(self) -> bool { + pub fn emits_collider(self) -> bool { matches!( self, Self::SolidBrush | Self::RoadSurfaceSlab | Self::SupportShell | Self::ColliderProxy @@ -156,7 +159,7 @@ impl FeatureClaim { self.frame.transform_bounds(self.support) } - fn primitive(&self) -> Primitive { + pub fn primitive(&self) -> Primitive { Primitive::OrientedBox { center: self.frame.transform_point(self.support.center()), size: self.support.size(), @@ -278,8 +281,12 @@ pub struct ContributionRow { pub kind: DomainKind, pub contribution: f32, pub estimated_triangle_cost: usize, + pub child_cost: usize, + pub remaining_triangle_budget: usize, + pub requested: bool, pub selected: bool, pub used_fallback: bool, + pub deferred_by_budget: bool, } #[derive(Clone, Debug, PartialEq)] @@ -304,6 +311,21 @@ pub struct TriangleChunk { pub report: BuildReport, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ClaimLoweringTarget { + Render, + Collider, +} + +#[derive(Clone, Debug)] +pub struct CsgClaimLowering { + pub arena: CsgTreeArena, + pub root: Option, + pub bounds: Aabb, + pub source_domain_keys: Vec, + pub source_claim_keys: Vec, +} + pub fn select_domain_cut(root: &DomainNode, query: &DomainQuery) -> SelectedCut { let mut state = CutState { query, @@ -313,6 +335,7 @@ pub fn select_domain_cut(root: &DomainNode, query: &DomainQuery) -> SelectedCut fallback: Vec::new(), diagnostics: Vec::new(), remaining_triangles: query.triangle_budget, + remaining_colliders: query.collider_budget, }; state.visit(root, true); SelectedCut { @@ -326,44 +349,103 @@ pub fn select_domain_cut(root: &DomainNode, query: &DomainQuery) -> SelectedCut } pub fn lower_selected_cut(cut: &SelectedCut) -> TriangleChunk { - let mut render_assembler = Assembler::new(); - let mut collider_assembler = Assembler::new(); + lower_claims_to_chunk( + DomainKey::new(format!("chunk/{}", cut.id)), + &cut.id, + &cut.emitted_claims, + ) +} + +pub fn lower_selected_cut_chunks(cut: &SelectedCut) -> Vec { + let mut chunks = Vec::new(); + for domain_key in &cut.selected_nodes { + let claims = cut + .emitted_claims + .iter() + .filter(|claim| &claim.domain_key == domain_key) + .cloned() + .collect::>(); + if claims.is_empty() { + continue; + } + chunks.push(lower_claims_to_chunk( + DomainKey::new(format!("chunk/{}/{}", cut.id, domain_key.0)), + &cut.id, + &claims, + )); + } + chunks +} + +pub fn lower_feature_claims_to_csg_tree( + claims: &[FeatureClaim], + target: ClaimLoweringTarget, +) -> CsgClaimLowering { + let mut arena = CsgTreeArena::new(); + let mut additive_nodes = Vec::new(); + let mut subtractive_nodes = Vec::new(); let mut source_domain_keys = Vec::::new(); let mut source_claim_keys = Vec::::new(); let mut bounds = Aabb::empty(); - for claim in &cut.emitted_claims { + for claim in claims.iter().filter(|claim| claim_in_target(claim, target)) { source_claim_keys.push(claim.key.clone()); if !source_domain_keys.contains(&claim.domain_key) { source_domain_keys.push(claim.domain_key.clone()); } bounds = bounds.union(claim.world_bounds()); - if claim.kind.emits_render() || matches!(claim.kind, FeatureClaimKind::VoidBrush) { - render_assembler.add_brush( - claim.key.clone(), - claim.kind.brush_op(), - claim.primitive(), - claim.material, - ); - } - if claim.kind.emits_collider() || matches!(claim.kind, FeatureClaimKind::VoidBrush) { - collider_assembler.add_brush( - claim.key.clone(), - claim.kind.brush_op(), - claim.primitive(), - claim.material, - ); + let brush = arena.generate_brush( + claim.key.clone(), + CsgOperationType::Additive, + claim.primitive(), + claim.material, + ); + match claim.kind.brush_op() { + BrushOp::Add => additive_nodes.push(brush.node), + BrushOp::Subtract => subtractive_nodes.push(brush.node), + BrushOp::Intersect => additive_nodes.push(brush.node), } } - let output = render_assembler.build(); - let collider_output = - (!collider_assembler.brushes().is_empty()).then(|| collider_assembler.build()); + let root = build_claim_tree(&mut arena, additive_nodes, subtractive_nodes); + CsgClaimLowering { + arena, + root, + bounds, + source_domain_keys, + source_claim_keys, + } +} + +fn lower_claims_to_chunk( + key: DomainKey, + selected_cut_id: &str, + claims: &[FeatureClaim], +) -> TriangleChunk { + let render = lower_feature_claims_to_csg_tree(claims, ClaimLoweringTarget::Render); + let collider = lower_feature_claims_to_csg_tree(claims, ClaimLoweringTarget::Collider); + let (source_domain_keys, source_claim_keys) = claim_metadata(claims); + + let output = render + .root + .map(|root| { + render + .arena + .compile_tree_to_assembler(render.arena.generate_tree(root)) + .build() + }) + .unwrap_or_else(|| crate::Assembler::new().build()); + let collider_output = collider.root.map(|root| { + collider + .arena + .compile_tree_to_assembler(collider.arena.generate_tree(root)) + .build() + }); TriangleChunk { - key: DomainKey::new(format!("chunk/{}", cut.id)), - selected_cut_id: cut.id.clone(), - bounds, + key, + selected_cut_id: selected_cut_id.to_owned(), + bounds: render.bounds.union(collider.bounds), mesh: output.mesh, collider_mesh: collider_output.map(|output| output.mesh), source_domain_keys, @@ -372,6 +454,85 @@ pub fn lower_selected_cut(cut: &SelectedCut) -> TriangleChunk { } } +fn claim_metadata(claims: &[FeatureClaim]) -> (Vec, Vec) { + let mut source_domain_keys = Vec::::new(); + let mut source_claim_keys = Vec::::new(); + for claim in claims { + source_claim_keys.push(claim.key.clone()); + if !source_domain_keys.contains(&claim.domain_key) { + source_domain_keys.push(claim.domain_key.clone()); + } + } + (source_domain_keys, source_claim_keys) +} + +fn claim_in_target(claim: &FeatureClaim, target: ClaimLoweringTarget) -> bool { + match target { + ClaimLoweringTarget::Render => { + claim.kind.emits_render() + || matches!( + claim.kind, + FeatureClaimKind::VoidBrush | FeatureClaimKind::ClearanceVolume + ) + } + ClaimLoweringTarget::Collider => { + claim.kind.emits_collider() + || matches!( + claim.kind, + FeatureClaimKind::VoidBrush | FeatureClaimKind::ClearanceVolume + ) + } + } +} + +fn build_claim_tree( + arena: &mut CsgTreeArena, + additive_nodes: Vec, + subtractive_nodes: Vec, +) -> Option { + let additive_root = match additive_nodes.len() { + 0 => None, + 1 => additive_nodes.first().copied(), + _ => Some( + arena + .generate_branch( + "domain-additive-claims", + CsgBranchOp::Addition, + additive_nodes, + ) + .node, + ), + }?; + + if subtractive_nodes.is_empty() { + return Some(additive_root); + } + + let subtractive_root = match subtractive_nodes.len() { + 0 => unreachable!(), + 1 => subtractive_nodes[0], + _ => { + arena + .generate_branch( + "domain-subtractive-claims", + CsgBranchOp::Addition, + subtractive_nodes, + ) + .node + } + }; + + Some( + arena + .generate_branch( + "domain-claims", + CsgBranchOp::Subtraction, + [additive_root, subtractive_root], + ) + .node, + ) +} + pub fn ragnarok_column_fixture() -> DomainNode { let root_key = DomainKey::new("ragnarok-column"); let column_key = root_key.child("stellarator-column-00"); @@ -467,14 +628,26 @@ fn ragnarok_branch(parent: &DomainKey, band: usize, lane: usize) -> DomainNode { FeatureClaimKind::ClearanceVolume, MaterialId(0), ); + let rib = claim( + &key, + "coarse-support-rib", + frame, + Aabb::from_center_size(Vec3::new(7.0, 0.0, -1.0), Vec3::new(18.5, 1.0, 1.5)), + FeatureClaimKind::SupportShell, + MaterialId(12), + ); + let mut children = Vec::new(); + for segment in 0..3 { + children.push(ragnarok_road_chunk(&key, band, lane, segment, frame)); + } DomainNode::new( key, Some(parent.clone()), DomainKind::BranchRoad, frame, 0xA11E_0000 + (band * 10 + lane) as u64, - vec![road, void], - Vec::new(), + vec![road, void, rib], + children, ) } @@ -497,6 +670,10 @@ fn ragnarok_roundabout(parent: &DomainKey, band: usize) -> DomainNode { FeatureClaimKind::RoadSurfaceSlab, MaterialId(60 + band as u32), ); + let mut children = Vec::new(); + for quadrant in 0..4 { + children.push(ragnarok_roundabout_chunk(&key, band, quadrant)); + } DomainNode::new( key, Some(parent.clone()), @@ -504,6 +681,97 @@ fn ragnarok_roundabout(parent: &DomainKey, band: usize) -> DomainNode { frame, 0xF00D_0000 + band as u64, vec![crossing_a, crossing_b], + children, + ) +} + +fn ragnarok_road_chunk( + parent: &DomainKey, + band: usize, + lane: usize, + segment: usize, + frame: DomainFrame, +) -> DomainNode { + let key = parent.child(format!("chunk-{segment}")); + let x = 1.5 + segment as f32 * 5.5; + let road = claim( + &key, + "road-slab", + frame, + Aabb::from_center_size(Vec3::new(x, 0.0, 0.0), Vec3::new(6.5, 4.0, 0.75)), + FeatureClaimKind::RoadSurfaceSlab, + MaterialId(80 + band as u32), + ); + let clearance = claim( + &key, + "hover-clearance", + frame, + Aabb::from_center_size(Vec3::new(x, 0.0, 2.1), Vec3::new(6.0, 3.0, 1.9)), + FeatureClaimKind::ClearanceVolume, + MaterialId(0), + ); + let support = claim( + &key, + "support-rib", + frame, + Aabb::from_center_size(Vec3::new(x, 0.0, -1.05), Vec3::new(6.8, 0.9, 1.4)), + FeatureClaimKind::SupportShell, + MaterialId(90 + lane as u32), + ); + let collider = claim( + &key, + "collider-proxy", + frame, + Aabb::from_center_size(Vec3::new(x, 0.0, 0.35), Vec3::new(6.8, 4.2, 0.35)), + FeatureClaimKind::ColliderProxy, + MaterialId(0), + ); + DomainNode::new( + key, + Some(parent.clone()), + DomainKind::Chunk, + frame, + 0xC40B_0000 + (band * 100 + lane * 10 + segment) as u64, + vec![road, clearance, support, collider], + Vec::new(), + ) +} + +fn ragnarok_roundabout_chunk(parent: &DomainKey, band: usize, quadrant: usize) -> DomainNode { + let key = parent.child(format!("chunk-{quadrant}")); + let angle = quadrant as f32 * std::f32::consts::FRAC_PI_2; + let frame = DomainFrame::rotated_z(Vec3::ZERO, angle); + let road = claim( + &key, + "arc-road-slab", + frame, + Aabb::from_center_size(Vec3::new(5.0, 5.0, 0.0), Vec3::new(12.0, 4.0, 0.8)), + FeatureClaimKind::RoadSurfaceSlab, + MaterialId(100 + band as u32), + ); + let clearance = claim( + &key, + "arc-clearance", + frame, + Aabb::from_center_size(Vec3::new(5.0, 5.0, 2.1), Vec3::new(11.0, 3.0, 1.8)), + FeatureClaimKind::ClearanceVolume, + MaterialId(0), + ); + let support = claim( + &key, + "arc-support", + frame, + Aabb::from_center_size(Vec3::new(5.0, 5.0, -1.0), Vec3::new(12.5, 0.8, 1.2)), + FeatureClaimKind::SupportShell, + MaterialId(90), + ); + DomainNode::new( + key, + Some(parent.clone()), + DomainKind::Chunk, + frame, + 0xC10C_0000 + (band * 10 + quadrant) as u64, + vec![road, clearance, support], Vec::new(), ) } @@ -589,36 +857,64 @@ struct CutState<'a> { fallback: Vec, diagnostics: Vec, remaining_triangles: usize, + remaining_colliders: usize, } impl CutState<'_> { fn visit(&mut self, node: &DomainNode, force_visit: bool) { - if !force_visit && !node.summary.bounds.intersects(self.query.frustum) { + let requested_self = self.query.requested_chunk_keys.contains(&node.key); + let requested_descendant = has_requested_descendant(node, &self.query.requested_chunk_keys); + if !force_visit + && !requested_self + && !requested_descendant + && !node.summary.bounds.intersects(self.query.frustum) + { return; } let score = contribution(node, self.query); let child_cost = node .children .iter() - .map(|child| child.summary.estimated_triangle_cost) + .map(selection_triangle_cost) .sum::(); - let requested = self.query.requested_chunk_keys.contains(&node.key); + let child_collider_cost = node + .children + .iter() + .map(selection_collider_cost) + .sum::(); + let budget_allows_children = child_cost <= self.remaining_triangles + && child_collider_cost <= self.remaining_colliders; + let descend_for_detail = score >= self.query.target_error && budget_allows_children; + let descend_for_request = requested_descendant && !requested_self; let should_descend = node.summary.has_children - && (requested || score >= self.query.target_error) - && child_cost <= self.remaining_triangles; + && !requested_self + && (descend_for_request || descend_for_detail); self.diagnostics.push(ContributionRow { domain_key: node.key.clone(), kind: node.kind, contribution: score, estimated_triangle_cost: node.summary.estimated_triangle_cost, + child_cost, + remaining_triangle_budget: self.remaining_triangles, + requested: requested_self, selected: !should_descend && self.query.accepts_kind(node.kind), used_fallback: !should_descend && node.summary.has_children, + deferred_by_budget: node.summary.has_children + && !requested_self + && score >= self.query.target_error + && !budget_allows_children, }); if should_descend { - self.remaining_triangles = self.remaining_triangles.saturating_sub(child_cost); + if budget_allows_children { + self.remaining_triangles = self.remaining_triangles.saturating_sub(child_cost); + self.remaining_colliders = + self.remaining_colliders.saturating_sub(child_collider_cost); + } for child in &node.children { - self.visit(child, false); + let child_requested = self.query.requested_chunk_keys.contains(&child.key) + || has_requested_descendant(child, &self.query.requested_chunk_keys); + self.visit(child, child_requested); } return; } @@ -639,6 +935,41 @@ impl CutState<'_> { } } +fn selection_triangle_cost(node: &DomainNode) -> usize { + let claim_count = if node.summary.has_children { + node.summary.fallback_claims.len() + } else { + node.claims.len() + }; + claim_count.max(1) * 12 +} + +fn selection_collider_cost(node: &DomainNode) -> usize { + let claims = if node.summary.has_children { + &node.summary.fallback_claims + } else { + &node.claims + }; + claims + .iter() + .filter(|claim| { + claim.kind.emits_collider() + || matches!( + claim.kind, + FeatureClaimKind::VoidBrush | FeatureClaimKind::ClearanceVolume + ) + }) + .count() + .max(1) + * 12 +} + +fn has_requested_descendant(node: &DomainNode, requested: &[DomainKey]) -> bool { + node.children + .iter() + .any(|child| requested.contains(&child.key) || has_requested_descendant(child, requested)) +} + #[cfg(test)] mod tests { use super::*; @@ -697,6 +1028,16 @@ mod tests { ); } + #[test] + fn selected_cut_honors_collider_budget() { + let fixture = ragnarok_column_fixture(); + let mut collider_tight = query(0.01, 10_000); + collider_tight.collider_budget = 1; + let cut = select_domain_cut(&fixture, &collider_tight); + assert_eq!(cut.selected_nodes, vec![fixture.key.clone()]); + assert!(cut.diagnostics[0].deferred_by_budget); + } + #[test] fn missing_children_degrade_to_parent_fallback_claims() { let fixture = ragnarok_column_fixture(); @@ -726,6 +1067,49 @@ mod tests { assert!(chunk.collider_mesh.is_some()); } + #[test] + fn feature_claims_lower_through_csg_tree_branches() { + let fixture = ragnarok_column_fixture(); + let cut = select_domain_cut(&fixture, &query(0.01, 10_000)); + let lowering = + lower_feature_claims_to_csg_tree(&cut.emitted_claims, ClaimLoweringTarget::Render); + assert!(lowering.root.is_some()); + assert!(lowering.arena.brush_count() > 0); + assert!(lowering.arena.branch_count() > 0); + } + + #[test] + fn selected_cut_chunks_emit_per_selected_domain() { + let fixture = ragnarok_column_fixture(); + let cut = select_domain_cut(&fixture, &query(0.01, 10_000)); + let chunks = lower_selected_cut_chunks(&cut); + assert_eq!(chunks.len(), cut.selected_nodes.len()); + for chunk in chunks { + assert_eq!(chunk.source_domain_keys.len(), 1); + assert!(cut.selected_nodes.contains(&chunk.source_domain_keys[0])); + assert!(chunk.mesh.triangle_count() > 0); + } + } + + #[test] + fn requested_chunk_bypasses_frustum_and_budget() { + let fixture = ragnarok_column_fixture(); + let requested = fixture.children[0].children[2].children[0].children[1] + .key + .clone(); + let mut request = query(0.01, 1); + request.frustum = + Aabb::from_center_size(Vec3::new(10_000.0, 10_000.0, 10_000.0), Vec3::splat(1.0)); + request.requested_chunk_keys.push(requested.clone()); + let cut = select_domain_cut(&fixture, &request); + assert_eq!(cut.selected_nodes, vec![requested.clone()]); + assert!( + cut.diagnostics + .iter() + .any(|row| row.domain_key == requested && row.requested) + ); + } + #[test] fn ragnarok_fixture_emits_lod_chunks_and_preserves_parent_for_transition() { let fixture = ragnarok_column_fixture(); @@ -738,9 +1122,16 @@ mod tests { assert!(tight.mesh.triangle_count() > 0); assert!(medium.mesh.triangle_count() > tight.mesh.triangle_count()); assert!(relaxed.mesh.triangle_count() > medium.mesh.triangle_count()); - assert_eq!(medium_cut.selected_nodes.len(), 3); + assert!(medium_cut.selected_nodes.len() > tight_cut.selected_nodes.len()); + assert!(relaxed_cut.selected_nodes.len() > medium_cut.selected_nodes.len()); assert!(!tight_cut.fallback_nodes.is_empty()); assert!(!relaxed_cut.selected_nodes.is_empty()); + assert!( + relaxed_cut + .selected_nodes + .iter() + .any(|key| key.0.contains("/chunk-")) + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 23f1298..aab99fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,9 +21,10 @@ pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning}; pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive}; pub use convex::{CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane, PolygonRouteScratch}; pub use domain::{ - ContributionRow, DomainFrame, DomainKey, DomainKind, DomainNode, DomainQuery, DomainSummary, - FeatureClaim, FeatureClaimKind, FieldEncoding, FieldLayer, SelectedCut, TriangleChunk, - lower_selected_cut, ragnarok_column_fixture, select_domain_cut, + ClaimLoweringTarget, ContributionRow, CsgClaimLowering, DomainFrame, DomainKey, DomainKind, + DomainNode, DomainQuery, DomainSummary, FeatureClaim, FeatureClaimKind, FieldEncoding, + FieldLayer, SelectedCut, TriangleChunk, lower_feature_claims_to_csg_tree, lower_selected_cut, + lower_selected_cut_chunks, ragnarok_column_fixture, select_domain_cut, }; pub use dsl::LevelDsl; pub use frontier::{DemandFrontier, DemandPair, DirtyDemandFrontier};