Emit CultCache geometry artifacts
This commit is contained in:
parent
4289100d65
commit
1c5e6c9144
5 changed files with 592 additions and 297 deletions
|
|
@ -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
570
src/cult_geometry.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
235
src/domain.rs
235
src/domain.rs
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
20
src/lib.rs
20
src/lib.rs
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
61
src/mesh.rs
61
src/mesh.rs
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue