use facet::Facet;
use facet::Partial;
use unlit_ecs::{ArchetypeBuilder, Entity, World};
use crate::components::{
Camera, GpuMaterial, GpuMesh, GpuRenderPipeline, MorphBinding, MorphWeights, RenderLoadOps,
SkinBinding, SkinPose, Transform, ZSortedDrawing,
};
use crate::input::InputHandle;
use crate::unlit::{InstanceColor, InstanceCutoff, UnlitPipelineKey};
#[derive(Debug)]
pub struct ComponentEntry {
name: &'static str,
module_path: Option<&'static str>,
encode: fn(&World, Entity) -> Result<String, String>,
set: fn(&World, Entity, &str) -> Result<(), String>,
push: fn(&mut ArchetypeBuilder, &str) -> Result<(), String>,
}
impl ComponentEntry {
pub const fn new<C: Facet<'static> + Clone>() -> Self {
Self {
name: C::SHAPE.type_identifier,
module_path: C::SHAPE.module_path,
encode: encode_component::<C>,
set: set_component::<C>,
push: push_component::<C>,
}
}
pub const fn default<C: Facet<'static> + Default + Clone>() -> Self {
Self {
name: C::SHAPE.type_identifier,
module_path: C::SHAPE.module_path,
encode: encode_component::<C>,
set: set_component::<C>,
push: push_default_component::<C>,
}
}
pub const fn name(&self) -> &'static str {
self.name
}
}
inventory::collect!(ComponentEntry);
inventory::submit! { ComponentEntry::default::<Transform>() }
inventory::submit! { ComponentEntry::new::<Camera>() }
inventory::submit! { ComponentEntry::new::<RenderLoadOps>() }
inventory::submit! { ComponentEntry::new::<GpuMesh>() }
inventory::submit! { ComponentEntry::new::<GpuMaterial>() }
inventory::submit! { ComponentEntry::new::<GpuRenderPipeline<UnlitPipelineKey>>() }
inventory::submit! { ComponentEntry::default::<ZSortedDrawing>() }
inventory::submit! { ComponentEntry::default::<SkinPose>() }
inventory::submit! { ComponentEntry::default::<MorphWeights>() }
inventory::submit! { ComponentEntry::new::<SkinBinding>() }
inventory::submit! { ComponentEntry::new::<MorphBinding>() }
inventory::submit! { ComponentEntry::default::<InstanceColor>() }
inventory::submit! { ComponentEntry::default::<InstanceCutoff>() }
inventory::submit! { ComponentEntry::new::<InputHandle>() }
pub fn entries() -> impl Iterator<Item = &'static ComponentEntry> {
inventory::iter::<ComponentEntry>.into_iter()
}
pub fn names() -> impl Iterator<Item = &'static str> {
entries().map(ComponentEntry::name)
}
pub fn contains(name: &str) -> bool {
find(name).is_some()
}
pub fn entry(name: &str) -> Result<&'static ComponentEntry, String> {
find(name).ok_or_else(|| {
format!(
"unknown component {name:?}; known components: {}",
names().collect::<Vec<_>>().join(", ")
)
})
}
fn find(name: &str) -> Option<&'static ComponentEntry> {
let (module, bare) = match name.rsplit_once("::") {
Some((module, bare)) => (Some(module), bare),
None => (None, name),
};
entries().find(|entry| {
entry.name == bare && module.is_none_or(|module| entry.module_path == Some(module))
})
}
pub fn encode(world: &World, entity: Entity, name: &str) -> Result<String, String> {
(entry(name)?.encode)(world, entity)
}
pub fn set(world: &World, entity: Entity, name: &str, patch: &str) -> Result<(), String> {
(entry(name)?.set)(world, entity, patch)
}
pub fn push(builder: &mut ArchetypeBuilder, name: &str, patch: &str) -> Result<(), String> {
(entry(name)?.push)(builder, patch)
}
fn to_json<C: Facet<'static>>(value: &C) -> Result<String, String> {
facet_json::to_string(value).map_err(|error| error.to_string())
}
fn encode_component<C: Facet<'static>>(world: &World, entity: Entity) -> Result<String, String> {
let component = world.get::<C>(entity).ok_or_else(|| missing::<C>(entity))?;
to_json(&*component)
}
fn set_component<C: Facet<'static> + Clone>(
world: &World,
entity: Entity,
patch: &str,
) -> Result<(), String> {
let current = world
.get::<C>(entity)
.map(|component| (*component).clone())
.ok_or_else(|| missing::<C>(entity))?;
let value: C = apply(current, patch)?;
world
.with_mut::<C, _>(entity, |component| *component = value)
.ok_or_else(|| missing::<C>(entity))
}
fn apply<C: Facet<'static> + Clone>(base: C, patch: &str) -> Result<C, String> {
let partial = Partial::alloc_owned::<C>().map_err(|error| error.to_string())?;
let partial = partial.set(base).map_err(|error| error.to_string())?;
let partial = facet_json::from_str_into(patch, partial).map_err(|error| error.to_string())?;
materialize(partial)
}
fn push_component<C: Facet<'static> + Clone>(
builder: &mut ArchetypeBuilder,
patch: &str,
) -> Result<(), String> {
let partial = Partial::alloc_owned::<C>().map_err(|error| error.to_string())?;
let partial = facet_json::from_str_into(patch, partial).map_err(|error| error.to_string())?;
builder.push(materialize::<C>(partial)?);
Ok(())
}
fn push_default_component<C: Facet<'static> + Default + Clone>(
builder: &mut ArchetypeBuilder,
patch: &str,
) -> Result<(), String> {
builder.push(apply(C::default(), patch)?);
Ok(())
}
fn materialize<C: Facet<'static>>(partial: Partial<'static, false>) -> Result<C, String> {
let heap = partial.build().map_err(|error| error.to_string())?;
heap.materialize::<C>().map_err(|error| error.to_string())
}
fn missing<C: Facet<'static>>(entity: Entity) -> String {
format!(
"entity {entity:?} has no {} component",
C::SHAPE.type_identifier
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reflected_component_round_trips_through_its_own_fields() {
let mut world = World::new();
let entity = world.spawn((Transform::default(),));
set(
&world,
entity,
"Transform",
r#"{"translation":[1.0,2.0,3.0]}"#,
)
.expect("a partial transform is accepted");
let encoded = encode(&world, entity, "Transform").expect("the transform encodes");
let value: Transform = facet_json::from_str(&encoded).expect("the encoding is JSON");
assert_eq!(value.translation, glam::Vec3::new(1.0, 2.0, 3.0));
assert_eq!(value.scale, glam::Vec3::ONE);
assert_eq!(value.rotation, glam::Quat::IDENTITY);
}
#[test]
fn a_qualified_name_addresses_the_same_component() {
assert!(contains("Transform"));
assert!(contains("unlit3d::components::Transform"));
assert!(!contains("unlit3d::other::Transform"));
let mut world = World::new();
let entity = world.spawn((Transform::default(),));
assert!(
encode(&world, entity, "unlit3d::components::Transform").is_ok(),
"the qualified name reaches the entry"
);
}
#[test]
fn spawning_merges_over_the_default() {
let mut builder = ArchetypeBuilder::new();
push(
&mut builder,
"Transform",
r#"{"translation":[4.0,5.0,6.0]}"#,
)
.expect("a partial transform is accepted");
let mut world = World::new();
let entity = world.spawn(builder);
let encoded = encode(&world, entity, "Transform").expect("the spawned transform encodes");
let value: Transform = facet_json::from_str(&encoded).expect("the encoding is JSON");
assert_eq!(value.translation, glam::Vec3::new(4.0, 5.0, 6.0));
assert_eq!(value.scale, glam::Vec3::ONE);
}
#[test]
fn an_unknown_component_names_the_known_ones() {
let error = entry("Nope").expect_err("an unknown component is refused");
assert!(error.contains("Nope"), "{error}");
assert!(error.contains("Transform"), "{error}");
}
#[test]
fn a_read_only_component_reports_its_own_reason() {
use arrayvec::ArrayVec;
use std::rc::Rc;
use unlit_wgpu::resources::{ResourceGraph, Virtual};
use unlit_wgpu::specialize::VertexLayout;
use crate::bounds::Aabb;
use crate::components::MeshParts;
let mut world = World::new();
let mut graph = ResourceGraph::new();
let root = graph.insert(Virtual, None);
let mesh = GpuMesh {
parts: Rc::new(MeshParts {
root,
vertex_buffers: ArrayVec::new(),
index_buffer: None,
bind_group_id: None,
vertex_allocation: None,
index_allocation: None,
morph_deltas_allocation: None,
metadata_index: 0,
}),
vertex_layout: VertexLayout::default(),
count: 3,
first: 0,
base_vertex: 0,
indexed: false,
aabb: Aabb::new(glam::Vec3::ZERO, glam::Vec3::splat(0.5)),
morph_targets: 0,
skinned: false,
};
let entity = world.spawn((mesh,));
let error = set(&world, entity, "GpuMesh", "{}").expect_err("GpuMesh is read-only");
assert!(error.contains("read-only"), "{error}");
let encoded = encode(&world, entity, "GpuMesh").expect("GpuMesh still encodes");
assert!(encoded.contains("count"), "{encoded}");
}
#[test]
fn a_component_the_entity_lacks_is_reported() {
let world = World::new();
let entity = Entity::from_raw(0, 0);
let error = encode(&world, entity, "Transform").expect_err("the entity has nothing");
assert!(error.contains("has no Transform"), "{error}");
}
#[test]
fn a_proxied_field_is_set_without_disturbing_the_others() {
let mut world = World::new();
let entity = world.spawn((RenderLoadOps::default(),));
set(
&world,
entity,
"RenderLoadOps",
r#"{"depth":{"clear":0.5}}"#,
)
.expect("a partial load op is accepted");
let ops = world
.get::<RenderLoadOps>(entity)
.expect("the load ops are there");
assert_eq!(ops.depth, wgpu::LoadOp::Clear(0.5));
assert_eq!(ops.color, RenderLoadOps::default().color);
}
#[test]
fn every_submitted_name_is_distinct() {
let mut names: Vec<&str> = names().collect();
names.sort_unstable();
let count = names.len();
names.dedup();
assert_eq!(names.len(), count, "component names are unique");
assert!(
count >= 14,
"the built-in components are submitted: {names:?}"
);
}
}