Skip to main content

galeon_engine/
render.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use galeon_engine_macros::Component;
4
5use crate::entity::Entity;
6
7/// 3D transform: position, rotation (quaternion), scale.
8///
9/// Flat array layout for efficient extraction into typed buffers.
10#[derive(Component, Debug, Clone, Copy, PartialEq)]
11pub struct Transform {
12    pub position: [f32; 3],
13    pub rotation: [f32; 4],
14    pub scale: [f32; 3],
15}
16
17impl Transform {
18    /// Identity transform: origin, no rotation, unit scale.
19    pub fn identity() -> Self {
20        Self {
21            position: [0.0, 0.0, 0.0],
22            rotation: [0.0, 0.0, 0.0, 1.0],
23            scale: [1.0, 1.0, 1.0],
24        }
25    }
26
27    /// Create a transform with only position set.
28    pub fn from_position(x: f32, y: f32, z: f32) -> Self {
29        Self {
30            position: [x, y, z],
31            ..Self::identity()
32        }
33    }
34}
35
36impl Default for Transform {
37    fn default() -> Self {
38        Self::identity()
39    }
40}
41
42/// Whether an entity is visible to the renderer.
43#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Visibility {
45    pub visible: bool,
46}
47
48impl Default for Visibility {
49    fn default() -> Self {
50        Self { visible: true }
51    }
52}
53
54/// Handle to a mesh asset. The renderer maps this ID to a Three.js geometry.
55#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct MeshHandle {
57    pub id: u32,
58}
59
60/// Handle to a material asset. The renderer maps this ID to a Three.js material.
61#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct MaterialHandle {
63    pub id: u32,
64}
65
66/// Marks an entity as a member of a GPU-instanced mesh batch.
67///
68/// When present, the renderer routes the entity's transform into a shared
69/// `THREE.InstancedMesh` keyed by the wrapped [`MeshHandle`], instead of
70/// creating a standalone `Object3D` per entity. Used for crowd-scale
71/// rendering (1000+ entities sharing one geometry).
72///
73/// The wrapped `MeshHandle` is the instance-group identifier — entities that
74/// share the same `InstanceOf(handle)` share the same `InstancedMesh`.
75#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct InstanceOf(pub MeshHandle);
77
78/// Per-instance color tint, written to `THREE.InstancedMesh.instanceColor`.
79///
80/// `[r, g, b]` in linear sRGB, each component in `[0.0, 1.0]`. The renderer
81/// multiplies the base material color by this value, so `[1.0, 1.0, 1.0]`
82/// (white) is the no-op identity. Entities without this component render at
83/// the batch's default white tint.
84///
85/// Only meaningful for entities also tagged with [`InstanceOf`] — the
86/// standalone-`Object3D` render path ignores it.
87#[derive(Component, Debug, Clone, Copy, PartialEq)]
88pub struct Tint(pub [f32; 3]);
89
90/// Parent entity for scene-graph hierarchy.
91///
92/// Attaching this component to an entity makes it a child of the referenced
93/// entity in the render scene graph. The renderer uses this to build
94/// Three.js parent-child relationships so transforms inherit correctly.
95///
96/// Entities without this component are children of the scene root.
97#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub struct ParentEntity(pub Entity);
99
100/// What kind of Three.js object to create for this entity.
101///
102/// Extracted as a `u8` in the FramePacket. The TS renderer uses this
103/// to pick the correct constructor (Mesh, PointLight, etc.).
104#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
105#[repr(u8)]
106pub enum ObjectType {
107    /// `THREE.Mesh` — the default for renderable entities.
108    #[default]
109    Mesh = 0,
110    /// `THREE.PointLight` — omni-directional light source.
111    PointLight = 1,
112    /// `THREE.DirectionalLight` — sun-like parallel light.
113    DirectionalLight = 2,
114    /// `THREE.LineSegments` — debug line rendering.
115    LineSegments = 3,
116    /// `THREE.Group` — container for hierarchy.
117    Group = 4,
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn transform_identity() {
126        let t = Transform::identity();
127        assert_eq!(t.position, [0.0, 0.0, 0.0]);
128        assert_eq!(t.rotation, [0.0, 0.0, 0.0, 1.0]);
129        assert_eq!(t.scale, [1.0, 1.0, 1.0]);
130    }
131
132    #[test]
133    fn transform_from_position() {
134        let t = Transform::from_position(1.0, 2.0, 3.0);
135        assert_eq!(t.position, [1.0, 2.0, 3.0]);
136        assert_eq!(t.scale, [1.0, 1.0, 1.0]);
137    }
138
139    #[test]
140    fn visibility_default_is_visible() {
141        assert!(Visibility::default().visible);
142    }
143
144    #[test]
145    fn parent_entity_stores_entity() {
146        let entity = crate::entity::Entity::from_raw(42, 0);
147        let parent = ParentEntity(entity);
148        assert_eq!(parent.0, entity);
149    }
150
151    #[test]
152    fn object_type_default_is_mesh() {
153        assert_eq!(ObjectType::default(), ObjectType::Mesh);
154    }
155
156    #[test]
157    fn instance_of_wraps_mesh_handle() {
158        let handle = MeshHandle { id: 42 };
159        let tag = InstanceOf(handle);
160        assert_eq!(tag.0, handle);
161        assert_eq!(tag.0.id, 42);
162    }
163
164    #[test]
165    fn instance_of_equality_is_by_mesh_handle() {
166        assert_eq!(
167            InstanceOf(MeshHandle { id: 7 }),
168            InstanceOf(MeshHandle { id: 7 })
169        );
170        assert_ne!(
171            InstanceOf(MeshHandle { id: 7 }),
172            InstanceOf(MeshHandle { id: 8 })
173        );
174    }
175
176    #[test]
177    fn tint_stores_rgb_triple() {
178        let t = Tint([0.25, 0.5, 1.0]);
179        assert_eq!(t.0[0], 0.25);
180        assert_eq!(t.0[1], 0.5);
181        assert_eq!(t.0[2], 1.0);
182    }
183
184    #[test]
185    fn tint_equality() {
186        assert_eq!(Tint([1.0, 0.0, 0.0]), Tint([1.0, 0.0, 0.0]));
187        assert_ne!(Tint([1.0, 0.0, 0.0]), Tint([0.0, 1.0, 0.0]));
188    }
189
190    #[test]
191    fn object_type_as_u8() {
192        assert_eq!(ObjectType::Mesh as u8, 0);
193        assert_eq!(ObjectType::PointLight as u8, 1);
194        assert_eq!(ObjectType::DirectionalLight as u8, 2);
195        assert_eq!(ObjectType::LineSegments as u8, 3);
196        assert_eq!(ObjectType::Group as u8, 4);
197    }
198}