Emit CultCache geometry artifacts

This commit is contained in:
Meta Vens 2026-05-29 23:24:27 +01:00
commit 1c5e6c9144
5 changed files with 592 additions and 297 deletions

View file

@ -9,8 +9,9 @@ readme = "README.md"
[dependencies]
bevy_math = "0.17"
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
[dev-dependencies]
approx = "0.5"

570
src/cult_geometry.rs Normal file
View file

@ -0,0 +1,570 @@
use std::fmt::Write;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
DomainKind, DomainNodeSpec, DomainQuery, FeatureClaimKind, FeatureLoweringPolicy, SelectedCut,
TriangleChunk, TriangleMesh,
};
pub const GEOMETRY_DOMAIN_SCHEMA: &str = "gamecult.geometry.domain.v1";
pub const GEOMETRY_BUILD_REQUEST_SCHEMA: &str = "gamecult.geometry.build_request.v1";
pub const GEOMETRY_SELECTED_CUT_SCHEMA: &str = "gamecult.geometry.selected_cut.v1";
pub const GEOMETRY_CHUNK_ARTIFACT_SCHEMA: &str = "gamecult.geometry.chunk_artifact.v1";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryDomainDocument(
pub String,
pub String,
pub String,
pub CultGeometryDomainNode,
pub String,
);
impl CultGeometryDomainDocument {
pub fn from_spec(
spec: &DomainNodeSpec,
source_runtime: impl Into<String>,
created_at: impl Into<String>,
) -> Self {
Self(
spec.name.clone(),
spec.name.clone(),
source_runtime.into(),
CultGeometryDomainNode::from_spec(spec),
created_at.into(),
)
}
pub fn record_key(&self) -> String {
format!(
"geometry:domain:{}",
stable_hash(&[&self.1, &self.2, &self.3.stable_fingerprint()])
)
}
pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
pub fn from_msgpack(payload: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
rmp_serde::from_slice(payload)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryDomainNode(
pub String,
pub String,
pub Vec<f32>,
pub Vec<f32>,
pub u64,
pub Vec<CultGeometryFeatureClaim>,
pub Vec<CultGeometryDomainNode>,
);
impl CultGeometryDomainNode {
pub fn from_spec(spec: &DomainNodeSpec) -> Self {
Self(
spec.name.clone(),
domain_kind_name(spec.kind).to_owned(),
vec3(spec.frame.translation),
quat(spec.frame.rotation),
spec.seed,
spec.claims
.iter()
.map(CultGeometryFeatureClaim::from_spec)
.collect(),
spec.children
.iter()
.map(CultGeometryDomainNode::from_spec)
.collect(),
)
}
pub fn stable_fingerprint(&self) -> String {
let mut parts = vec![
self.0.clone(),
self.1.clone(),
stable_f32_array(&self.2),
stable_f32_array(&self.3),
self.4.to_string(),
];
parts.extend(
self.5
.iter()
.map(CultGeometryFeatureClaim::stable_fingerprint),
);
parts.extend(
self.6
.iter()
.map(CultGeometryDomainNode::stable_fingerprint),
);
stable_join(&parts)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryFeatureClaim(
pub String,
pub Vec<f32>,
pub Vec<f32>,
pub Vec<f32>,
pub Vec<f32>,
pub String,
pub u32,
pub String,
);
impl CultGeometryFeatureClaim {
pub fn from_spec(spec: &crate::FeatureClaimSpec) -> Self {
Self(
spec.name.clone(),
vec3(spec.frame.translation),
quat(spec.frame.rotation),
vec3(spec.support.center()),
vec3(spec.support.size()),
feature_kind_name(spec.kind).to_owned(),
spec.material.0,
lowering_policy_name(spec.lowering).to_owned(),
)
}
pub fn stable_fingerprint(&self) -> String {
stable_join(&[
self.0.clone(),
stable_f32_array(&self.1),
stable_f32_array(&self.2),
stable_f32_array(&self.3),
stable_f32_array(&self.4),
self.5.clone(),
self.6.to_string(),
self.7.clone(),
])
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryBuildRequest(
pub String,
pub String,
pub String,
pub Vec<f32>,
pub Vec<f32>,
pub Vec<f32>,
pub f32,
pub f32,
pub f32,
pub i32,
pub i32,
pub Vec<String>,
pub Vec<String>,
pub Vec<String>,
pub String,
);
impl CultGeometryBuildRequest {
pub fn from_query(
request_id: impl Into<String>,
domain_key: impl Into<String>,
worker_group: impl Into<String>,
query: &DomainQuery,
created_at: impl Into<String>,
) -> Self {
Self(
request_id.into(),
domain_key.into(),
worker_group.into(),
vec3(query.camera_position),
vec3(query.frustum.min),
vec3(query.frustum.max),
query.viewport_height_px,
query.vertical_fov_radians,
query.target_error,
query.triangle_budget as i32,
query.collider_budget as i32,
query
.semantic_filter
.iter()
.map(|kind| domain_kind_name(*kind).to_owned())
.collect(),
query
.requested_chunk_keys
.iter()
.map(|key| key.0.clone())
.collect(),
query
.dirty_domain_keys
.iter()
.map(|key| key.0.clone())
.collect(),
created_at.into(),
)
}
pub fn record_key(&self) -> String {
format!(
"geometry:request:{}",
stable_hash(&[
&self.1,
&self.2,
&stable_f32_array(&self.3),
&stable_f32_array(&self.4),
&stable_f32_array(&self.5),
&stable_f32(self.6),
&stable_f32(self.7),
&stable_f32(self.8),
&self.9.to_string(),
&self.10.to_string(),
&stable_join(&self.11),
&stable_join(&self.12),
&stable_join(&self.13),
])
)
}
pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometrySelectedCutManifest(
pub String,
pub String,
pub Vec<String>,
pub Vec<String>,
pub Vec<String>,
pub Vec<CultGeometryContributionRow>,
);
impl CultGeometrySelectedCutManifest {
pub fn from_cut(cut: &SelectedCut, request_key: impl Into<String>) -> Self {
Self(
cut.id.clone(),
request_key.into(),
cut.selected_nodes.iter().map(|key| key.0.clone()).collect(),
cut.deferred_children
.iter()
.map(|key| key.0.clone())
.collect(),
cut.fallback_nodes.iter().map(|key| key.0.clone()).collect(),
cut.diagnostics
.iter()
.map(CultGeometryContributionRow::from_row)
.collect(),
)
}
pub fn record_key(&self) -> String {
format!(
"geometry:cut:{}",
stable_hash(&[
&self.1,
&self.0,
&stable_join(&self.2),
&stable_join(&self.3),
&stable_join(&self.4),
])
)
}
pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryContributionRow(
pub String,
pub String,
pub f32,
pub f32,
pub f32,
pub i32,
pub i32,
pub i32,
pub bool,
pub bool,
pub bool,
pub bool,
pub bool,
);
impl CultGeometryContributionRow {
pub fn from_row(row: &crate::ContributionRow) -> Self {
Self(
row.domain_key.0.clone(),
domain_kind_name(row.kind).to_owned(),
row.contribution,
row.projected_screen_error,
row.semantic_priority,
row.estimated_triangle_cost as i32,
row.child_cost as i32,
row.remaining_triangle_budget as i32,
row.requested,
row.dirty,
row.selected,
row.used_fallback,
row.deferred_by_budget,
)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryChunkArtifact(
pub String,
pub String,
pub String,
pub Vec<f32>,
pub Vec<f32>,
pub Vec<String>,
pub Vec<String>,
pub CultGeometryTriangleMesh,
pub Option<CultGeometryTriangleMesh>,
pub i32,
pub i32,
pub i32,
pub u64,
pub bool,
);
impl CultGeometryChunkArtifact {
pub fn from_chunk(chunk: &TriangleChunk, cut_key: impl Into<String>) -> Self {
let manifest = chunk.manifest();
Self(
chunk.key.0.clone(),
cut_key.into(),
chunk.selected_cut_id.clone(),
vec3(chunk.bounds.min),
vec3(chunk.bounds.max),
chunk
.source_domain_keys
.iter()
.map(|key| key.0.clone())
.collect(),
chunk.source_claim_keys.clone(),
CultGeometryTriangleMesh::from_mesh(&chunk.mesh),
chunk
.collider_mesh
.as_ref()
.map(CultGeometryTriangleMesh::from_mesh),
chunk.report.input_brushes as i32,
chunk.report.candidate_pairs as i32,
chunk.report.rejected_pairs as i32,
manifest.transition_hint.stable_clip_seed,
manifest.transition_hint.supports_parent_child_coexistence,
)
}
pub fn record_key(&self) -> String {
format!(
"geometry:chunk:{}",
stable_hash(&[
&self.1,
&self.0,
&self.2,
&stable_join(&self.5),
&stable_join(&self.6),
&self.7.stable_fingerprint(),
self.8
.as_ref()
.map(CultGeometryTriangleMesh::stable_fingerprint)
.as_deref()
.unwrap_or(""),
])
)
}
pub fn to_msgpack(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
pub fn from_msgpack(payload: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
rmp_serde::from_slice(payload)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CultGeometryTriangleMesh(
pub Vec<f32>,
pub Vec<f32>,
pub Vec<f32>,
pub Vec<u32>,
pub Vec<u32>,
);
impl CultGeometryTriangleMesh {
pub fn from_mesh(mesh: &TriangleMesh) -> Self {
Self(
mesh.positions
.iter()
.flat_map(|value| [value.x, value.y, value.z])
.collect(),
mesh.normals
.iter()
.flat_map(|value| [value.x, value.y, value.z])
.collect(),
mesh.uvs
.iter()
.flat_map(|value| [value.x, value.y])
.collect(),
mesh.indices.clone(),
mesh.triangle_materials
.iter()
.map(|material| material.0)
.collect(),
)
}
pub fn triangle_count(&self) -> usize {
self.3.len() / 3
}
pub fn stable_fingerprint(&self) -> String {
stable_hash(&[
&stable_f32_array(&self.0),
&stable_f32_array(&self.1),
&stable_f32_array(&self.2),
&stable_u32_array(&self.3),
&stable_u32_array(&self.4),
])
}
}
fn vec3(value: bevy_math::Vec3) -> Vec<f32> {
vec![value.x, value.y, value.z]
}
fn quat(value: bevy_math::Quat) -> Vec<f32> {
vec![value.x, value.y, value.z, value.w]
}
fn domain_kind_name(kind: DomainKind) -> &'static str {
match kind {
DomainKind::Root => "Root",
DomainKind::Column => "Column",
DomainKind::AltitudeBand => "AltitudeBand",
DomainKind::RoadSpine => "RoadSpine",
DomainKind::BranchRoad => "BranchRoad",
DomainKind::Roundabout => "Roundabout",
DomainKind::SupportMass => "SupportMass",
DomainKind::ClearanceVolume => "ClearanceVolume",
DomainKind::Chunk => "Chunk",
}
}
fn feature_kind_name(kind: FeatureClaimKind) -> &'static str {
match kind {
FeatureClaimKind::SolidBrush => "SolidBrush",
FeatureClaimKind::VoidBrush => "VoidBrush",
FeatureClaimKind::RoadSurfaceSlab => "RoadSurfaceSlab",
FeatureClaimKind::ClearanceVolume => "ClearanceVolume",
FeatureClaimKind::SupportShell => "SupportShell",
FeatureClaimKind::ColliderProxy => "ColliderProxy",
}
}
fn lowering_policy_name(policy: FeatureLoweringPolicy) -> &'static str {
match policy {
FeatureLoweringPolicy::RenderOnly => "RenderOnly",
FeatureLoweringPolicy::ColliderOnly => "ColliderOnly",
FeatureLoweringPolicy::RenderAndCollider => "RenderAndCollider",
FeatureLoweringPolicy::BooleanOperator => "BooleanOperator",
}
}
fn stable_hash(parts: &[&str]) -> String {
let canonical = parts.join("\u{1f}");
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let digest = hasher.finalize();
let mut output = String::with_capacity(digest.len() * 2);
for byte in digest {
write!(&mut output, "{byte:02x}").expect("write to string cannot fail");
}
output
}
fn stable_join(parts: &[String]) -> String {
parts.join("\u{1e}")
}
fn stable_f32_array(values: &[f32]) -> String {
values
.iter()
.map(|value| stable_f32(*value))
.collect::<Vec<_>>()
.join(",")
}
fn stable_u32_array(values: &[u32]) -> String {
values
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",")
}
fn stable_f32(value: f32) -> String {
if value.fract() == 0.0 {
format!("{value:.1}")
} else {
value.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Aabb, DomainQuery, build_domain_chunks, ragnarok_column_spec};
use bevy_math::Vec3;
fn query() -> DomainQuery {
DomainQuery {
camera_position: Vec3::new(36.0, -42.0, 30.0),
frustum: Aabb::from_center_size(Vec3::new(0.0, 0.0, 45.0), Vec3::splat(150.0)),
viewport_height_px: 1080.0,
vertical_fov_radians: std::f32::consts::FRAC_PI_3,
target_error: 0.01,
triangle_budget: 10_000,
collider_budget: 10_000,
semantic_filter: Vec::new(),
requested_chunk_keys: Vec::new(),
dirty_domain_keys: Vec::new(),
}
}
#[test]
fn domain_document_emits_msgpack_payload_and_stable_key() {
let spec = ragnarok_column_spec();
let document =
CultGeometryDomainDocument::from_spec(&spec, "vg-csg", "2026-05-29T00:00:00Z");
let payload = document.to_msgpack().unwrap();
let decoded = CultGeometryDomainDocument::from_msgpack(&payload).unwrap();
assert_eq!(document.record_key(), decoded.record_key());
assert!(document.record_key().starts_with("geometry:domain:"));
assert!(!payload.is_empty());
}
#[test]
fn chunk_artifact_emits_msgpack_payload_and_stable_key() {
let spec = ragnarok_column_spec();
let build = build_domain_chunks(&spec.compile_root(), &query());
let request = CultGeometryBuildRequest::from_query(
"request-0",
"geometry:domain:test",
"ragnarok-workers",
&query(),
"2026-05-29T00:00:00Z",
);
let cut = CultGeometrySelectedCutManifest::from_cut(&build.cut, request.record_key());
let artifact = CultGeometryChunkArtifact::from_chunk(&build.chunks[0], cut.record_key());
let payload = artifact.to_msgpack().unwrap();
let decoded = CultGeometryChunkArtifact::from_msgpack(&payload).unwrap();
assert_eq!(artifact.record_key(), decoded.record_key());
assert!(artifact.record_key().starts_with("geometry:chunk:"));
assert!(decoded.7.triangle_count() > 0);
}
}

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::{
Aabb, BrushOp, BuildReport, CsgBranchOp, CsgNodeId, CsgOperationType, CsgTreeArena, MaterialId,
Primitive, TriangleMesh, TriangleMeshDocument,
Primitive, TriangleMesh,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@ -286,100 +286,6 @@ pub struct DomainSpecDocument {
pub root: DomainNodeDocument,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DomainBuildRequestDocument {
pub schema_version: u32,
pub domain: DomainSpecDocument,
pub query: DomainQueryDocument,
}
impl DomainBuildRequestDocument {
pub const CURRENT_SCHEMA_VERSION: u32 = 1;
pub fn build(&self) -> DomainChunkBuild {
build_domain_chunks(&self.domain.compile_root(), &self.query.to_query())
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(source: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(source)
}
pub fn stable_key(&self) -> Result<String, serde_json::Error> {
stable_document_key("domain-build-request", self)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DomainQueryDocument {
pub camera_position: [f32; 3],
pub frustum_min: [f32; 3],
pub frustum_max: [f32; 3],
pub viewport_height_px: f32,
pub vertical_fov_radians: f32,
pub target_error: f32,
pub triangle_budget: usize,
pub collider_budget: usize,
pub semantic_filter: Vec<DomainKind>,
pub requested_chunk_keys: Vec<String>,
pub dirty_domain_keys: Vec<String>,
}
impl DomainQueryDocument {
pub fn from_query(query: &DomainQuery) -> Self {
Self {
camera_position: vec3_to_array(query.camera_position),
frustum_min: vec3_to_array(query.frustum.min),
frustum_max: vec3_to_array(query.frustum.max),
viewport_height_px: query.viewport_height_px,
vertical_fov_radians: query.vertical_fov_radians,
target_error: query.target_error,
triangle_budget: query.triangle_budget,
collider_budget: query.collider_budget,
semantic_filter: query.semantic_filter.clone(),
requested_chunk_keys: query
.requested_chunk_keys
.iter()
.map(|key| key.0.clone())
.collect(),
dirty_domain_keys: query
.dirty_domain_keys
.iter()
.map(|key| key.0.clone())
.collect(),
}
}
pub fn to_query(&self) -> DomainQuery {
DomainQuery {
camera_position: array_to_vec3(self.camera_position),
frustum: Aabb::new(
array_to_vec3(self.frustum_min),
array_to_vec3(self.frustum_max),
),
viewport_height_px: self.viewport_height_px,
vertical_fov_radians: self.vertical_fov_radians,
target_error: self.target_error,
triangle_budget: self.triangle_budget,
collider_budget: self.collider_budget,
semantic_filter: self.semantic_filter.clone(),
requested_chunk_keys: self
.requested_chunk_keys
.iter()
.map(|key| DomainKey::new(key.clone()))
.collect(),
dirty_domain_keys: self
.dirty_domain_keys
.iter()
.map(|key| DomainKey::new(key.clone()))
.collect(),
}
}
}
impl DomainSpecDocument {
pub const CURRENT_SCHEMA_VERSION: u32 = 1;
@ -397,14 +303,6 @@ impl DomainSpecDocument {
pub fn compile_root(&self) -> DomainNode {
self.to_spec().compile_root()
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(source: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(source)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -669,14 +567,7 @@ pub struct DomainChunkBuild {
pub manifest: SelectedCutManifest,
}
impl DomainChunkBuild {
pub fn chunk_documents(&self) -> Vec<TriangleChunkDocument> {
self.chunks
.iter()
.map(TriangleChunkDocument::from_chunk)
.collect()
}
}
impl DomainChunkBuild {}
#[derive(Clone, Debug)]
pub struct TriangleChunk {
@ -707,38 +598,6 @@ 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)
}
pub fn stable_key(&self) -> Result<String, serde_json::Error> {
stable_document_key("triangle-chunk", self)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectedCutManifest {
pub id: String,
@ -795,12 +654,6 @@ impl SelectedCut {
}
}
impl SelectedCutManifest {
pub fn stable_key(&self) -> Result<String, serde_json::Error> {
stable_document_key("selected-cut", self)
}
}
impl ContributionManifest {
pub fn from_row(row: &ContributionRow) -> Self {
Self {
@ -1378,14 +1231,6 @@ fn stable_str_hash(value: &str) -> u64 {
stable_hash_bytes(FNV_OFFSET_BASIS, value.as_bytes())
}
fn stable_document_key(
prefix: &str,
document: &impl Serialize,
) -> Result<String, serde_json::Error> {
let json = serde_json::to_string(document)?;
Ok(format!("{prefix}-{:016x}", stable_str_hash(&json)))
}
fn stable_hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
for byte in bytes {
hash ^= u64::from(*byte);
@ -1576,12 +1421,10 @@ mod tests {
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();
let round_trip = document.compile_root();
assert_eq!(
decoded.schema_version,
document.schema_version,
DomainSpecDocument::CURRENT_SCHEMA_VERSION
);
assert_eq!(original.key, round_trip.key);
@ -1589,7 +1432,6 @@ mod tests {
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]
@ -1764,23 +1606,6 @@ 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();
@ -1797,60 +1622,14 @@ mod tests {
fn build_domain_chunks_runs_full_runtime_pipeline() {
let fixture = ragnarok_column_fixture();
let build = build_domain_chunks(&fixture, &query(0.01, 10_000));
let documents = build.chunk_documents();
assert_eq!(build.manifest.id, build.cut.id);
assert_eq!(build.manifest.chunks.len(), build.chunks.len());
assert_eq!(documents.len(), build.chunks.len());
assert!(
documents
.iter()
.all(|document| !document.mesh.indices.is_empty())
);
}
#[test]
fn domain_build_request_document_round_trips_worker_input() {
let request = DomainBuildRequestDocument {
schema_version: DomainBuildRequestDocument::CURRENT_SCHEMA_VERSION,
domain: DomainSpecDocument::from_spec(&ragnarok_column_spec()),
query: DomainQueryDocument::from_query(&query(0.01, 10_000)),
};
let json = request.to_json_pretty().unwrap();
let decoded = DomainBuildRequestDocument::from_json(&json).unwrap();
let build = decoded.build();
assert_eq!(
decoded.schema_version,
DomainBuildRequestDocument::CURRENT_SCHEMA_VERSION
);
assert!(!build.chunks.is_empty());
assert_eq!(build.manifest.chunks.len(), build.chunks.len());
}
#[test]
fn transport_documents_have_stable_cache_keys() {
let request = DomainBuildRequestDocument {
schema_version: DomainBuildRequestDocument::CURRENT_SCHEMA_VERSION,
domain: DomainSpecDocument::from_spec(&ragnarok_column_spec()),
query: DomainQueryDocument::from_query(&query(0.01, 10_000)),
};
let build = request.build();
let chunk_document = TriangleChunkDocument::from_chunk(&build.chunks[0]);
let mut changed = request.clone();
changed.query.triangle_budget += 1;
assert_eq!(request.stable_key().unwrap(), request.stable_key().unwrap());
assert_ne!(request.stable_key().unwrap(), changed.stable_key().unwrap());
assert!(
build
.manifest
.stable_key()
.unwrap()
.starts_with("selected-cut-")
);
assert!(
chunk_document
.stable_key()
.unwrap()
.starts_with("triangle-chunk-")
.chunks
.iter()
.all(|chunk| !chunk.mesh.indices.is_empty())
);
}

View file

@ -10,6 +10,7 @@
mod assembler;
mod brush;
mod convex;
mod cult_geometry;
mod domain;
mod dsl;
mod frontier;
@ -20,19 +21,24 @@ mod tree;
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 cult_geometry::{
CultGeometryBuildRequest, CultGeometryChunkArtifact, CultGeometryContributionRow,
CultGeometryDomainDocument, CultGeometryDomainNode, CultGeometryFeatureClaim,
CultGeometrySelectedCutManifest, CultGeometryTriangleMesh, GEOMETRY_BUILD_REQUEST_SCHEMA,
GEOMETRY_CHUNK_ARTIFACT_SCHEMA, GEOMETRY_DOMAIN_SCHEMA, GEOMETRY_SELECTED_CUT_SCHEMA,
};
pub use domain::{
ClaimLoweringTarget, ContributionManifest, ContributionRow, CsgClaimLowering,
DomainBuildRequestDocument, DomainChunkBuild, DomainFrame, DomainKey, DomainKind, DomainNode,
DomainNodeDocument, DomainNodeSpec, DomainQuery, DomainQueryDocument, DomainSpecDocument,
DomainSummary, FeatureClaim, FeatureClaimDocument, FeatureClaimKind, FeatureClaimSpec,
FeatureLoweringPolicy, FieldEncoding, FieldLayer, SelectedCut, SelectedCutManifest,
TriangleChunk, TriangleChunkDocument, TriangleChunkManifest, build_domain_chunks,
ClaimLoweringTarget, ContributionManifest, ContributionRow, CsgClaimLowering, DomainChunkBuild,
DomainFrame, DomainKey, DomainKind, DomainNode, DomainNodeDocument, DomainNodeSpec,
DomainQuery, DomainSpecDocument, DomainSummary, FeatureClaim, FeatureClaimDocument,
FeatureClaimKind, FeatureClaimSpec, FeatureLoweringPolicy, FieldEncoding, FieldLayer,
SelectedCut, SelectedCutManifest, TriangleChunk, TriangleChunkManifest, build_domain_chunks,
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, TriangleMeshDocument};
pub use mesh::TriangleMesh;
pub use primitives::{
DomeCapZSpec, FloretArmSpec, append_cylinder_z, append_dome_cap_z, append_floret_arm,
};

View file

@ -1,5 +1,4 @@
use bevy_math::{Vec2, Vec3};
use serde::{Deserialize, Serialize};
use crate::{Aabb, MaterialId};
@ -101,63 +100,3 @@ 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(),
}
}
}