diff --git a/crates/pm64/src/model.rs b/crates/pm64/src/model.rs new file mode 100644 index 0000000..feecf4b --- /dev/null +++ b/crates/pm64/src/model.rs @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2026 Alex Bates +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! CRDT-backed model data for PM64 map geometry. +//! +//! These structs are backed by Loro via the [`loroscope`] macro, so every +//! field change is a CRDT operation suitable for collaborative editing and +//! undo/redo. + +use loroscope::loroscope; + +/// An RGBA color with integer channels (0–255 range by convention). +#[loroscope] +#[derive(Debug)] +pub struct ColorRgba { + /// Red channel. + pub r: i64, + /// Green channel. + pub g: i64, + /// Blue channel. + pub b: i64, + /// Alpha channel. + pub a: i64, +} + +/// A vertex with 3D position and vertex color. +#[loroscope] +#[derive(Debug)] +pub struct Vertex { + /// X position. + pub x: f64, + /// Y position. + pub y: f64, + /// Z position. + pub z: f64, + /// Vertex color. + pub color: ColorRgba, +} + +/// A triangle defined by three vertices. +#[loroscope] +#[derive(Debug)] +pub struct Triangle { + /// First vertex. + pub v0: Vertex, + /// Second vertex. + pub v1: Vertex, + /// Third vertex. + pub v2: Vertex, +} + +/// A named node in the model tree, containing a list of triangles. +#[loroscope] +#[derive(Debug)] +pub struct ModelNode { + /// Display name of this model node. + pub name: String, + /// The triangles that make up this node's geometry. + pub triangles: List, +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::float_cmp)] + + use loroscope::loroscope; + + use super::*; + + #[loroscope] + struct TestRoot { + pub map_model: Tree, + } + + #[test] + fn create_model_node_with_triangle() { + let root = TestRoot::new(); + let tree = root.map_model(); + + let (node_id, node) = tree.create_root(); + node.set_name("test_node"); + assert_eq!(node.name(), "test_node"); + + let tri = node.triangles().push_new(); + tri.v0().set_x(0.0); + tri.v0().set_y(0.0); + tri.v0().set_z(0.0); + tri.v0().color().set_r(255); + tri.v0().color().set_g(0); + tri.v0().color().set_b(0); + tri.v0().color().set_a(255); + + tri.v1().set_x(100.0); + tri.v1().set_y(0.0); + tri.v1().set_z(0.0); + + tri.v2().set_x(50.0); + tri.v2().set_y(100.0); + tri.v2().set_z(0.0); + + // Read back + let read_node = tree.get(node_id).unwrap(); + assert_eq!(read_node.name(), "test_node"); + assert_eq!(read_node.triangles().len(), 1); + + let read_tri = read_node.triangles().get(0).unwrap(); + assert_eq!(read_tri.v0().x(), 0.0); + assert_eq!(read_tri.v0().color().r(), 255); + assert_eq!(read_tri.v1().x(), 100.0); + assert_eq!(read_tri.v2().y(), 100.0); + } + + #[test] + fn multiple_triangles_in_node() { + let root = TestRoot::new(); + let tree = root.map_model(); + + let (_id, node) = tree.create_root(); + node.set_name("multi"); + + for i in 0..5 { + let tri = node.triangles().push_new(); + let val = f64::from(i) * 10.0; + tri.v0().set_x(val); + tri.v1().set_y(val); + tri.v2().set_z(val); + } + + assert_eq!(node.triangles().len(), 5); + assert_eq!(node.triangles().get(2).unwrap().v0().x(), 20.0); + assert_eq!(node.triangles().get(4).unwrap().v2().z(), 40.0); + } + + #[test] + fn tree_hierarchy() { + let root = TestRoot::new(); + let tree = root.map_model(); + + let (parent_id, parent) = tree.create_root(); + parent.set_name("parent"); + + let (child_id, child) = tree.create_child(parent_id); + child.set_name("child"); + + assert_eq!(tree.children(parent_id).unwrap().len(), 1); + assert_eq!(tree.children(parent_id).unwrap()[0], child_id); + assert_eq!(tree.get(child_id).unwrap().name(), "child"); + } +}