Add transportable chunk artifacts

This commit is contained in:
Meta Vens 2026-05-29 16:40:05 +01:00
commit 82d341be5f
3 changed files with 201 additions and 6 deletions

View file

@ -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<TriangleMeshDocument>,
}
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<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(source: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(source)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectedCutManifest {
pub id: String,
pub selected_nodes: Vec<String>,
pub deferred_child_requests: Vec<String>,
pub parent_fallback_nodes: Vec<String>,
pub diagnostics: Vec<ContributionManifest>,
pub chunks: Vec<TriangleChunkManifest>,
}
#[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();

View file

@ -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,
};

View file

@ -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<u32>,
pub triangle_materials: Vec<u32>,
}
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(),
}
}
}