From bfcb8a52b0bf216b02fb57302a40cb5a8df8e492 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 29 May 2026 16:27:53 +0100 Subject: [PATCH] Add domain spec wire documents --- Cargo.toml | 2 + src/domain.rs | 267 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 5 +- 3 files changed, 262 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 520843c..e3bc4e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ readme = "README.md" [dependencies] bevy_math = "0.17" +serde = { version = "1", features = ["derive"] } +serde_json = "1" [dev-dependencies] approx = "0.5" diff --git a/src/domain.rs b/src/domain.rs index 424f1c6..7200d16 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,4 +1,5 @@ use bevy_math::{Quat, Vec3}; +use serde::{Deserialize, Serialize}; use crate::{ Aabb, BrushOp, BuildReport, CsgBranchOp, CsgNodeId, CsgOperationType, CsgTreeArena, MaterialId, @@ -66,7 +67,7 @@ impl DomainFrame { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DomainKind { Root, Column, @@ -79,14 +80,14 @@ pub enum DomainKind { Chunk, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FieldLayer { Form, Appearance, Transport, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FieldEncoding { Mesh, Sdf3d, @@ -95,7 +96,7 @@ pub enum FieldEncoding { Confidence, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FeatureClaimKind { SolidBrush, VoidBrush, @@ -105,7 +106,7 @@ pub enum FeatureClaimKind { ColliderProxy, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FeatureLoweringPolicy { RenderOnly, ColliderOnly, @@ -279,6 +280,138 @@ pub struct DomainNodeSpec { pub children: Vec, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct DomainSpecDocument { + pub schema_version: u32, + pub root: DomainNodeDocument, +} + +impl DomainSpecDocument { + pub const CURRENT_SCHEMA_VERSION: u32 = 1; + + pub fn from_spec(spec: &DomainNodeSpec) -> Self { + Self { + schema_version: Self::CURRENT_SCHEMA_VERSION, + root: DomainNodeDocument::from_spec(spec), + } + } + + pub fn to_spec(&self) -> DomainNodeSpec { + self.root.to_spec() + } + + pub fn compile_root(&self) -> DomainNode { + self.to_spec().compile_root() + } + + pub fn to_json_pretty(&self) -> Result { + serde_json::to_string_pretty(self) + } + + pub fn from_json(source: &str) -> Result { + serde_json::from_str(source) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct DomainNodeDocument { + pub name: String, + pub kind: DomainKind, + pub translation: [f32; 3], + pub rotation_xyzw: [f32; 4], + pub seed: u64, + pub claims: Vec, + pub children: Vec, +} + +impl DomainNodeDocument { + pub fn from_spec(spec: &DomainNodeSpec) -> Self { + Self { + name: spec.name.clone(), + kind: spec.kind, + translation: vec3_to_array(spec.frame.translation), + rotation_xyzw: quat_to_array(spec.frame.rotation), + seed: spec.seed, + claims: spec + .claims + .iter() + .map(FeatureClaimDocument::from_spec) + .collect(), + children: spec + .children + .iter() + .map(DomainNodeDocument::from_spec) + .collect(), + } + } + + pub fn to_spec(&self) -> DomainNodeSpec { + DomainNodeSpec { + name: self.name.clone(), + kind: self.kind, + frame: DomainFrame { + translation: array_to_vec3(self.translation), + rotation: array_to_quat(self.rotation_xyzw), + }, + seed: self.seed, + claims: self + .claims + .iter() + .map(FeatureClaimDocument::to_spec) + .collect(), + children: self + .children + .iter() + .map(DomainNodeDocument::to_spec) + .collect(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct FeatureClaimDocument { + pub name: String, + pub translation: [f32; 3], + pub rotation_xyzw: [f32; 4], + pub support_center: [f32; 3], + pub support_size: [f32; 3], + pub kind: FeatureClaimKind, + pub material: u32, + pub lowering: FeatureLoweringPolicy, +} + +impl FeatureClaimDocument { + pub fn from_spec(spec: &FeatureClaimSpec) -> Self { + Self { + name: spec.name.clone(), + translation: vec3_to_array(spec.frame.translation), + rotation_xyzw: quat_to_array(spec.frame.rotation), + support_center: vec3_to_array(spec.support.center()), + support_size: vec3_to_array(spec.support.size()), + kind: spec.kind, + material: spec.material.0, + lowering: spec.lowering, + } + } + + pub fn to_spec(&self) -> FeatureClaimSpec { + FeatureClaimSpec { + name: self.name.clone(), + frame: DomainFrame { + translation: array_to_vec3(self.translation), + rotation: array_to_quat(self.rotation_xyzw), + }, + support: Aabb::from_center_size( + array_to_vec3(self.support_center), + array_to_vec3(self.support_size), + ), + kind: self.kind, + material: MaterialId(self.material), + lowering: self.lowering, + } + } +} + impl DomainNodeSpec { pub fn new(name: impl Into, kind: DomainKind, frame: DomainFrame, seed: u64) -> Self { Self { @@ -441,6 +574,59 @@ pub struct TriangleChunk { pub report: BuildReport, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TriangleChunkManifest { + pub key: String, + pub selected_cut_id: String, + pub bounds_min: [f32; 3], + pub bounds_max: [f32; 3], + pub render_vertices: usize, + pub render_triangles: usize, + pub collider_triangles: usize, + pub source_domain_keys: Vec, + pub source_claim_keys: Vec, + pub input_brushes: usize, + pub candidate_pairs: usize, + pub rejected_pairs: usize, + pub transition_hint: ChunkTransitionHint, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChunkTransitionHint { + pub stable_clip_seed: u64, + pub supports_parent_child_coexistence: bool, +} + +impl TriangleChunk { + pub fn manifest(&self) -> TriangleChunkManifest { + TriangleChunkManifest { + key: self.key.0.clone(), + selected_cut_id: self.selected_cut_id.clone(), + bounds_min: vec3_to_array(self.bounds.min), + bounds_max: vec3_to_array(self.bounds.max), + render_vertices: self.mesh.vertex_count(), + render_triangles: self.mesh.triangle_count(), + collider_triangles: self + .collider_mesh + .as_ref() + .map_or(0, TriangleMesh::triangle_count), + source_domain_keys: self + .source_domain_keys + .iter() + .map(|key| key.0.clone()) + .collect(), + source_claim_keys: self.source_claim_keys.clone(), + input_brushes: self.report.input_brushes, + candidate_pairs: self.report.candidate_pairs, + rejected_pairs: self.report.rejected_pairs, + transition_hint: ChunkTransitionHint { + stable_clip_seed: stable_str_hash(&self.key.0), + supports_parent_child_coexistence: true, + }, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ClaimLoweringTarget { Render, @@ -861,6 +1047,22 @@ fn claim_spec( FeatureClaimSpec::new(name, frame, support, kind, material) } +fn vec3_to_array(value: Vec3) -> [f32; 3] { + [value.x, value.y, value.z] +} + +fn array_to_vec3(value: [f32; 3]) -> Vec3 { + Vec3::new(value[0], value[1], value[2]) +} + +fn quat_to_array(value: Quat) -> [f32; 4] { + [value.x, value.y, value.z, value.w] +} + +fn array_to_quat(value: [f32; 4]) -> Quat { + Quat::from_xyzw(value[0], value[1], value[2], value[3]) +} + fn fallback_box_claim( key: &DomainKey, kind: DomainKind, @@ -908,16 +1110,28 @@ fn contribution(node: &DomainNode, query: &DomainQuery) -> f32 { } fn stable_cut_id(keys: &[DomainKey]) -> String { - let mut hash = 0xcbf2_9ce4_8422_2325u64; + let mut hash = FNV_OFFSET_BASIS; for key in keys { - for byte in key.0.as_bytes() { - hash ^= u64::from(*byte); - hash = hash.wrapping_mul(0x1000_0000_01b3); - } + hash = stable_hash_bytes(hash, key.0.as_bytes()); } format!("cut-{hash:016x}") } +const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325; +const FNV_PRIME: u64 = 0x1000_0000_01b3; + +fn stable_str_hash(value: &str) -> u64 { + stable_hash_bytes(FNV_OFFSET_BASIS, value.as_bytes()) +} + +fn stable_hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 { + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(FNV_PRIME); + } + hash +} + struct CutState<'a> { query: &'a DomainQuery, selected: Vec, @@ -1082,6 +1296,26 @@ mod tests { ); } + #[test] + fn domain_spec_document_round_trips_ragnarok_fixture() { + let spec = ragnarok_column_spec(); + let document = DomainSpecDocument::from_spec(&spec); + let json = document.to_json_pretty().unwrap(); + let decoded = DomainSpecDocument::from_json(&json).unwrap(); + let original = spec.compile_root(); + let round_trip = decoded.compile_root(); + assert_eq!( + decoded.schema_version, + DomainSpecDocument::CURRENT_SCHEMA_VERSION + ); + assert_eq!(original.key, round_trip.key); + assert_eq!( + original.children[0].children[1].children[2].children[0].claims[0].key, + round_trip.children[0].children[1].children[2].children[0].claims[0].key + ); + assert!(json.contains("\"RoadSurfaceSlab\"")); + } + #[test] fn domain_summary_contains_child_bounds() { let fixture = ragnarok_column_fixture(); @@ -1207,6 +1441,19 @@ mod tests { } } + #[test] + fn triangle_chunk_manifest_preserves_streaming_metadata() { + let fixture = ragnarok_column_fixture(); + let cut = select_domain_cut(&fixture, &query(0.01, 10_000)); + let chunks = lower_selected_cut_chunks(&cut); + let manifest = chunks[0].manifest(); + assert_eq!(manifest.selected_cut_id, cut.id); + assert!(manifest.render_triangles > 0); + assert!(!manifest.source_domain_keys.is_empty()); + assert!(manifest.transition_hint.supports_parent_child_coexistence); + assert_ne!(manifest.transition_hint.stable_clip_seed, 0); + } + #[test] fn requested_chunk_bypasses_frustum_and_budget() { let fixture = ragnarok_column_fixture(); diff --git a/src/lib.rs b/src/lib.rs index 2f65b49..f477042 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,8 +22,9 @@ pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Prim pub use convex::{CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane, PolygonRouteScratch}; pub use domain::{ ClaimLoweringTarget, ContributionRow, CsgClaimLowering, DomainFrame, DomainKey, DomainKind, - DomainNode, DomainNodeSpec, DomainQuery, DomainSummary, FeatureClaim, FeatureClaimKind, - FeatureClaimSpec, FeatureLoweringPolicy, FieldEncoding, FieldLayer, SelectedCut, TriangleChunk, + DomainNode, DomainNodeDocument, DomainNodeSpec, DomainQuery, DomainSpecDocument, DomainSummary, + FeatureClaim, FeatureClaimDocument, FeatureClaimKind, FeatureClaimSpec, FeatureLoweringPolicy, + FieldEncoding, FieldLayer, SelectedCut, TriangleChunk, TriangleChunkManifest, lower_feature_claims_to_csg_tree, lower_selected_cut, lower_selected_cut_chunks, ragnarok_column_fixture, ragnarok_column_spec, select_domain_cut, };