diff --git a/src/domain.rs b/src/domain.rs index c6dfcf7..af374f9 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ Aabb, BrushOp, BuildReport, CsgBranchOp, CsgNodeId, CsgOperationType, CsgTreeArena, MaterialId, - Primitive, TriangleMesh, + Primitive, TriangleMesh, TriangleMeshDocument, }; #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -597,6 +597,110 @@ pub struct TriangleChunkManifest { pub transition_hint: ChunkTransitionHint, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TriangleChunkDocument { + pub manifest: TriangleChunkManifest, + pub mesh: TriangleMeshDocument, + pub collider_mesh: Option, +} + +impl TriangleChunkDocument { + pub fn from_chunk(chunk: &TriangleChunk) -> Self { + Self { + manifest: chunk.manifest(), + mesh: TriangleMeshDocument::from_mesh(&chunk.mesh), + collider_mesh: chunk + .collider_mesh + .as_ref() + .map(TriangleMeshDocument::from_mesh), + } + } + + pub fn to_json(&self) -> Result { + serde_json::to_string(self) + } + + pub fn from_json(source: &str) -> Result { + serde_json::from_str(source) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SelectedCutManifest { + pub id: String, + pub selected_nodes: Vec, + pub deferred_child_requests: Vec, + pub parent_fallback_nodes: Vec, + pub diagnostics: Vec, + pub chunks: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ContributionManifest { + pub domain_key: String, + pub kind: DomainKind, + pub contribution: f32, + pub projected_screen_error: f32, + pub semantic_priority: f32, + pub estimated_triangle_cost: usize, + pub child_cost: usize, + pub remaining_triangle_budget: usize, + pub requested: bool, + pub dirty: bool, + pub selected: bool, + pub used_fallback: bool, + pub deferred_by_budget: bool, +} + +impl SelectedCut { + pub fn manifest(&self, chunks: &[TriangleChunk]) -> SelectedCutManifest { + SelectedCutManifest { + id: self.id.clone(), + selected_nodes: self + .selected_nodes + .iter() + .map(|key| key.0.clone()) + .collect(), + deferred_child_requests: self + .deferred_children + .iter() + .map(|key| key.0.clone()) + .collect(), + parent_fallback_nodes: self + .fallback_nodes + .iter() + .map(|key| key.0.clone()) + .collect(), + diagnostics: self + .diagnostics + .iter() + .map(ContributionManifest::from_row) + .collect(), + chunks: chunks.iter().map(TriangleChunk::manifest).collect(), + } + } +} + +impl ContributionManifest { + pub fn from_row(row: &ContributionRow) -> Self { + Self { + domain_key: row.domain_key.0.clone(), + kind: row.kind, + contribution: row.contribution, + projected_screen_error: row.projected_screen_error, + semantic_priority: row.semantic_priority, + estimated_triangle_cost: row.estimated_triangle_cost, + child_cost: row.child_cost, + remaining_triangle_budget: row.remaining_triangle_budget, + requested: row.requested, + dirty: row.dirty, + selected: row.selected, + used_fallback: row.used_fallback, + deferred_by_budget: row.deferred_by_budget, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ChunkTransitionHint { pub stable_clip_seed: u64, @@ -1521,6 +1625,35 @@ mod tests { assert_ne!(manifest.transition_hint.stable_clip_seed, 0); } + #[test] + fn triangle_chunk_document_round_trips_transport_mesh() { + let fixture = ragnarok_column_fixture(); + let cut = select_domain_cut(&fixture, &query(0.01, 10_000)); + let chunks = lower_selected_cut_chunks(&cut); + let document = TriangleChunkDocument::from_chunk(&chunks[0]); + let json = document.to_json().unwrap(); + let decoded = TriangleChunkDocument::from_json(&json).unwrap(); + let mesh = decoded.mesh.to_mesh(); + assert_eq!(decoded.manifest.key, chunks[0].key.0); + assert_eq!(mesh.indices, chunks[0].mesh.indices); + assert_eq!( + decoded.collider_mesh.unwrap().to_mesh().triangle_count(), + chunks[0].collider_mesh.as_ref().unwrap().triangle_count() + ); + } + + #[test] + fn selected_cut_manifest_carries_worker_artifact_context() { + 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 = cut.manifest(&chunks); + assert_eq!(manifest.id, cut.id); + assert_eq!(manifest.chunks.len(), chunks.len()); + assert_eq!(manifest.selected_nodes.len(), cut.selected_nodes.len()); + assert!(!manifest.diagnostics.is_empty()); + } + #[test] fn requested_chunk_bypasses_frustum_and_budget() { let fixture = ragnarok_column_fixture(); diff --git a/src/lib.rs b/src/lib.rs index f477042..861466d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,16 +21,17 @@ 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::{ - ClaimLoweringTarget, ContributionRow, CsgClaimLowering, DomainFrame, DomainKey, DomainKind, - DomainNode, DomainNodeDocument, DomainNodeSpec, DomainQuery, DomainSpecDocument, DomainSummary, - FeatureClaim, FeatureClaimDocument, FeatureClaimKind, FeatureClaimSpec, FeatureLoweringPolicy, - FieldEncoding, FieldLayer, SelectedCut, TriangleChunk, TriangleChunkManifest, + ClaimLoweringTarget, ContributionManifest, ContributionRow, CsgClaimLowering, DomainFrame, + DomainKey, DomainKind, DomainNode, DomainNodeDocument, DomainNodeSpec, DomainQuery, + DomainSpecDocument, DomainSummary, FeatureClaim, FeatureClaimDocument, FeatureClaimKind, + FeatureClaimSpec, FeatureLoweringPolicy, FieldEncoding, FieldLayer, SelectedCut, + SelectedCutManifest, TriangleChunk, TriangleChunkDocument, TriangleChunkManifest, lower_feature_claims_to_csg_tree, lower_selected_cut, lower_selected_cut_chunks, ragnarok_column_fixture, ragnarok_column_spec, select_domain_cut, }; pub use dsl::LevelDsl; pub use frontier::{DemandFrontier, DemandPair, DirtyDemandFrontier}; -pub use mesh::TriangleMesh; +pub use mesh::{TriangleMesh, TriangleMeshDocument}; pub use primitives::{ DomeCapZSpec, FloretArmSpec, append_cylinder_z, append_dome_cap_z, append_floret_arm, }; diff --git a/src/mesh.rs b/src/mesh.rs index 94cfb3d..ebcc274 100644 --- a/src/mesh.rs +++ b/src/mesh.rs @@ -1,4 +1,5 @@ use bevy_math::{Vec2, Vec3}; +use serde::{Deserialize, Serialize}; use crate::{Aabb, MaterialId}; @@ -100,3 +101,63 @@ impl TriangleMesh { self.triangle_materials.push(material); } } + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct TriangleMeshDocument { + pub positions: Vec<[f32; 3]>, + pub normals: Vec<[f32; 3]>, + pub uvs: Vec<[f32; 2]>, + pub indices: Vec, + pub triangle_materials: Vec, +} + +impl TriangleMeshDocument { + pub fn from_mesh(mesh: &TriangleMesh) -> Self { + Self { + positions: mesh + .positions + .iter() + .map(|value| [value.x, value.y, value.z]) + .collect(), + normals: mesh + .normals + .iter() + .map(|value| [value.x, value.y, value.z]) + .collect(), + uvs: mesh.uvs.iter().map(|value| [value.x, value.y]).collect(), + indices: mesh.indices.clone(), + triangle_materials: mesh + .triangle_materials + .iter() + .map(|material| material.0) + .collect(), + } + } + + pub fn to_mesh(&self) -> TriangleMesh { + TriangleMesh { + positions: self + .positions + .iter() + .map(|value| Vec3::new(value[0], value[1], value[2])) + .collect(), + normals: self + .normals + .iter() + .map(|value| Vec3::new(value[0], value[1], value[2])) + .collect(), + uvs: self + .uvs + .iter() + .map(|value| Vec2::new(value[0], value[1])) + .collect(), + indices: self.indices.clone(), + triangle_materials: self + .triangle_materials + .iter() + .copied() + .map(MaterialId) + .collect(), + } + } +}