khora_sdk/
vessel.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Vessel abstraction for the Khora SDK.
16//!
17//! A Vessel is a high-level wrapper around an ECS entity that provides
18//! a convenient API for common game development tasks. It allows you to
19//! create and manipulate entities without dealing directly with the ECS.
20//!
21//! Every Vessel has both a Transform (local) and GlobalTransform (world)
22//! which are kept in sync automatically.
23//!
24//! # Example
25//!
26//! ```rust,ignore
27//! // Create a cube at a specific position
28//! let cube = world.spawn_at(Vec3::new(0.0, 0.5, -5.0))
29//!     .as_cube(1.0)
30//!     .build();
31//!
32//! // Create a camera
33//! let camera = world.spawn_at(Vec3::new(0.0, 2.0, 10.0))
34//!     .as_camera_perspective(45.0, 16.0/9.0, 0.1, 1000.0)
35//!     .build();
36//! ```
37
38use khora_core::ecs::entity::EntityId;
39use khora_core::math::{Aabb, Vec2, Vec3};
40use khora_core::renderer::api::{
41    pipeline::{PrimitiveTopology, VertexAttributeDescriptor, VertexFormat},
42    scene::Mesh,
43};
44use khora_data::ecs::{GlobalTransform, Transform};
45
46use crate::GameWorld;
47
48/// A high-level wrapper around an ECS entity.
49///
50/// Vessel provides a builder-pattern API for creating and configuring
51/// game entities. It automatically handles the underlying ECS components
52/// and synchronization between Transform and GlobalTransform.
53///
54/// Every Vessel is guaranteed to have:
55/// - Transform (local position/rotation/scale)
56/// - GlobalTransform (world-space transform for rendering)
57pub struct Vessel<'a> {
58    world: &'a mut GameWorld,
59    entity: EntityId,
60    transform: Transform,
61}
62
63impl<'a> Vessel<'a> {
64    /// Creates a new Vessel at the origin.
65    ///
66    /// The entity is spawned immediately with Transform and GlobalTransform.
67    pub fn new(world: &'a mut GameWorld) -> Self {
68        let transform = Transform::identity();
69        let global = GlobalTransform::new(transform.to_mat4());
70        let entity = world.spawn((transform, global));
71
72        Self {
73            world,
74            entity,
75            transform,
76        }
77    }
78
79    /// Creates a new Vessel at the specified position.
80    pub fn at(world: &'a mut GameWorld, position: Vec3) -> Self {
81        let transform = Transform::from_translation(position);
82        let global = GlobalTransform::new(transform.to_mat4());
83        let entity = world.spawn((transform, global));
84
85        Self {
86            world,
87            entity,
88            transform,
89        }
90    }
91
92    /// Sets the transform (position, rotation, scale).
93    pub fn with_transform(mut self, transform: Transform) -> Self {
94        self.transform = transform;
95        self
96    }
97
98    /// Sets the position.
99    pub fn at_position(mut self, position: Vec3) -> Self {
100        self.transform.translation = position;
101        self
102    }
103
104    /// Sets the rotation of the transform.
105    pub fn with_rotation(mut self, rotation: khora_core::math::Quaternion) -> Self {
106        self.transform.rotation = rotation;
107        self
108    }
109
110    /// Sets the scale of the transform.
111    pub fn with_scale(mut self, scale: Vec3) -> Self {
112        self.transform.scale = scale;
113        self
114    }
115
116    /// Adds a generic component to the entity immediately.
117    ///
118    /// Since the entity is already spawned when the `Vessel` is created,
119    /// we can add the component right away. This avoids needing a field
120    /// on `Vessel` for every possible component type.
121    pub fn with_component<C: khora_data::ecs::Component>(self, component: C) -> Self {
122        self.world.add_component(self.entity, component);
123        self
124    }
125
126    /// Returns the entity ID.
127    pub fn entity(&self) -> EntityId {
128        self.entity
129    }
130
131    /// Builds the Vessel, updating the final transforms.
132    ///
133    /// This finalizes the Vessel creation and returns the entity ID.
134    pub fn build(self) -> EntityId {
135        // Update transform (entity was spawned with one, so we need to update it)
136        if let Some(existing_transform) = self.world.get_component_mut::<Transform>(self.entity) {
137            *existing_transform = self.transform;
138        }
139
140        // Sync GlobalTransform
141        let global = GlobalTransform::new(self.transform.to_mat4());
142        if let Some(existing_global) = self.world.get_component_mut::<GlobalTransform>(self.entity)
143        {
144            *existing_global = global;
145        }
146
147        self.entity
148    }
149}
150
151/// Creates a Vessel with a plane mesh at the origin.
152pub fn spawn_plane<'a>(world: &'a mut GameWorld, size: f32, y: f32) -> Vessel<'a> {
153    let mesh = create_plane(size, y);
154    let handle = world.add_mesh(mesh);
155    Vessel::new(world).with_component(handle)
156}
157
158/// Creates a Vessel with a cube mesh at a specific position.
159pub fn spawn_cube_at<'a>(world: &'a mut GameWorld, position: Vec3, size: f32) -> Vessel<'a> {
160    let mesh = create_cube(size);
161    let handle = world.add_mesh(mesh);
162    Vessel::at(world, position).with_component(handle)
163}
164
165/// Creates a Vessel with a sphere mesh at the origin.
166pub fn spawn_sphere<'a>(
167    world: &'a mut GameWorld,
168    radius: f32,
169    segments: u32,
170    rings: u32,
171) -> Vessel<'a> {
172    let mesh = create_sphere(radius, segments, rings);
173    let handle = world.add_mesh(mesh);
174    Vessel::new(world).with_component(handle)
175}
176
177// =============================================================================
178// Primitive Mesh Generation (internal)
179// =============================================================================
180
181/// Creates a plane mesh on the XZ plane.
182fn create_plane(size: f32, y: f32) -> Mesh {
183    let half = size / 2.0;
184
185    let positions = vec![
186        Vec3::new(-half, y, -half),
187        Vec3::new(half, y, -half),
188        Vec3::new(half, y, half),
189        Vec3::new(-half, y, half),
190    ];
191
192    let normals = vec![
193        Vec3::new(0.0, 1.0, 0.0),
194        Vec3::new(0.0, 1.0, 0.0),
195        Vec3::new(0.0, 1.0, 0.0),
196        Vec3::new(0.0, 1.0, 0.0),
197    ];
198
199    let tex_coords = vec![
200        Vec2::new(0.0, 0.0),
201        Vec2::new(1.0, 0.0),
202        Vec2::new(1.0, 1.0),
203        Vec2::new(0.0, 1.0),
204    ];
205
206    let indices = vec![0u32, 1, 2, 0, 2, 3];
207
208    // Layout: Position (0), Normal (1), UV (2)
209    let vertex_layout = vec![
210        VertexAttributeDescriptor {
211            shader_location: 0,
212            format: VertexFormat::Float32x3,
213            offset: 0,
214        },
215        VertexAttributeDescriptor {
216            shader_location: 1,
217            format: VertexFormat::Float32x3,
218            offset: 12,
219        },
220        VertexAttributeDescriptor {
221            shader_location: 2,
222            format: VertexFormat::Float32x2,
223            offset: 24,
224        },
225    ];
226
227    Mesh {
228        positions,
229        normals: Some(normals),
230        tex_coords: Some(tex_coords),
231        tangents: None,
232        colors: None,
233        indices: Some(indices),
234        primitive_type: PrimitiveTopology::TriangleList,
235        bounding_box: Aabb::from_min_max(Vec3::new(-half, y, -half), Vec3::new(half, y, half)),
236        vertex_layout,
237    }
238}
239
240/// Creates a cube mesh centered at origin.
241fn create_cube(size: f32) -> Mesh {
242    let half = size / 2.0;
243
244    // 24 vertices (4 per face, 6 faces)
245    let positions = vec![
246        // Front face (+Z)
247        Vec3::new(-half, -half, half),
248        Vec3::new(half, -half, half),
249        Vec3::new(half, half, half),
250        Vec3::new(-half, half, half),
251        // Back face (-Z)
252        Vec3::new(half, -half, -half),
253        Vec3::new(-half, -half, -half),
254        Vec3::new(-half, half, -half),
255        Vec3::new(half, half, -half),
256        // Right face (+X)
257        Vec3::new(half, -half, half),
258        Vec3::new(half, -half, -half),
259        Vec3::new(half, half, -half),
260        Vec3::new(half, half, half),
261        // Left face (-X)
262        Vec3::new(-half, -half, -half),
263        Vec3::new(-half, -half, half),
264        Vec3::new(-half, half, half),
265        Vec3::new(-half, half, -half),
266        // Top face (+Y)
267        Vec3::new(-half, half, half),
268        Vec3::new(half, half, half),
269        Vec3::new(half, half, -half),
270        Vec3::new(-half, half, -half),
271        // Bottom face (-Y)
272        Vec3::new(-half, -half, -half),
273        Vec3::new(half, -half, -half),
274        Vec3::new(half, -half, half),
275        Vec3::new(-half, -half, half),
276    ];
277
278    let normals = vec![
279        // Front
280        Vec3::new(0.0, 0.0, 1.0),
281        Vec3::new(0.0, 0.0, 1.0),
282        Vec3::new(0.0, 0.0, 1.0),
283        Vec3::new(0.0, 0.0, 1.0),
284        // Back
285        Vec3::new(0.0, 0.0, -1.0),
286        Vec3::new(0.0, 0.0, -1.0),
287        Vec3::new(0.0, 0.0, -1.0),
288        Vec3::new(0.0, 0.0, -1.0),
289        // Right
290        Vec3::new(1.0, 0.0, 0.0),
291        Vec3::new(1.0, 0.0, 0.0),
292        Vec3::new(1.0, 0.0, 0.0),
293        Vec3::new(1.0, 0.0, 0.0),
294        // Left
295        Vec3::new(-1.0, 0.0, 0.0),
296        Vec3::new(-1.0, 0.0, 0.0),
297        Vec3::new(-1.0, 0.0, 0.0),
298        Vec3::new(-1.0, 0.0, 0.0),
299        // Top
300        Vec3::new(0.0, 1.0, 0.0),
301        Vec3::new(0.0, 1.0, 0.0),
302        Vec3::new(0.0, 1.0, 0.0),
303        Vec3::new(0.0, 1.0, 0.0),
304        // Bottom
305        Vec3::new(0.0, -1.0, 0.0),
306        Vec3::new(0.0, -1.0, 0.0),
307        Vec3::new(0.0, -1.0, 0.0),
308        Vec3::new(0.0, -1.0, 0.0),
309    ];
310
311    let tex_coords = vec![
312        [0.0, 0.0],
313        [1.0, 0.0],
314        [1.0, 1.0],
315        [0.0, 1.0],
316        [0.0, 0.0],
317        [1.0, 0.0],
318        [1.0, 1.0],
319        [0.0, 1.0],
320        [0.0, 0.0],
321        [1.0, 0.0],
322        [1.0, 1.0],
323        [0.0, 1.0],
324        [0.0, 0.0],
325        [1.0, 0.0],
326        [1.0, 1.0],
327        [0.0, 1.0],
328        [0.0, 0.0],
329        [1.0, 0.0],
330        [1.0, 1.0],
331        [0.0, 1.0],
332        [0.0, 0.0],
333        [1.0, 0.0],
334        [1.0, 1.0],
335        [0.0, 1.0],
336    ]
337    .into_iter()
338    .map(|uv| Vec2::new(uv[0], uv[1]))
339    .collect();
340
341    // Indices for all 6 faces (2 triangles per face)
342    let indices = vec![
343        // Front
344        0u32, 1, 2, 0, 2, 3, // Back
345        4, 5, 6, 4, 6, 7, // Right
346        8, 9, 10, 8, 10, 11, // Left
347        12, 13, 14, 12, 14, 15, // Top
348        16, 17, 18, 16, 18, 19, // Bottom
349        20, 21, 22, 20, 22, 23,
350    ];
351
352    // Layout: Position (0), Normal (1), UV (2)
353    let vertex_layout = vec![
354        VertexAttributeDescriptor {
355            shader_location: 0,
356            format: VertexFormat::Float32x3,
357            offset: 0,
358        },
359        VertexAttributeDescriptor {
360            shader_location: 1,
361            format: VertexFormat::Float32x3,
362            offset: 12,
363        },
364        VertexAttributeDescriptor {
365            shader_location: 2,
366            format: VertexFormat::Float32x2,
367            offset: 24,
368        },
369    ];
370
371    Mesh {
372        positions,
373        normals: Some(normals),
374        tex_coords: Some(tex_coords),
375        tangents: None,
376        colors: None,
377        indices: Some(indices),
378        primitive_type: PrimitiveTopology::TriangleList,
379        bounding_box: Aabb::from_min_max(
380            Vec3::new(-half, -half, -half),
381            Vec3::new(half, half, half),
382        ),
383        vertex_layout,
384    }
385}
386
387/// Creates a sphere mesh.
388fn create_sphere(radius: f32, segments: u32, rings: u32) -> Mesh {
389    let mut positions = Vec::new();
390    let mut normals = Vec::new();
391    let mut tex_coords = Vec::new();
392
393    // Generate vertices
394    for ring in 0..=rings {
395        let phi = std::f32::consts::PI * (ring as f32 / rings as f32);
396        let y = radius * phi.cos();
397        let ring_radius = radius * phi.sin();
398
399        for segment in 0..=segments {
400            let theta = 2.0 * std::f32::consts::PI * (segment as f32 / segments as f32);
401            let x = ring_radius * theta.cos();
402            let z = ring_radius * theta.sin();
403
404            positions.push(Vec3::new(x, y, z));
405            normals.push(Vec3::new(x / radius, y / radius, z / radius));
406            tex_coords.push(Vec2::new(
407                segment as f32 / segments as f32,
408                ring as f32 / rings as f32,
409            ));
410        }
411    }
412
413    // Generate indices
414    let mut indices = Vec::new();
415    for ring in 0..rings {
416        for segment in 0..segments {
417            let current = ring * (segments + 1) + segment;
418            let next = current + segments + 1;
419
420            // Two triangles per quad
421            indices.push(current);
422            indices.push(next);
423            indices.push(current + 1);
424
425            indices.push(current + 1);
426            indices.push(next);
427            indices.push(next + 1);
428        }
429    }
430
431    // Layout: Position (0), Normal (1), UV (2)
432    let vertex_layout = vec![
433        VertexAttributeDescriptor {
434            shader_location: 0,
435            format: VertexFormat::Float32x3,
436            offset: 0,
437        },
438        VertexAttributeDescriptor {
439            shader_location: 1,
440            format: VertexFormat::Float32x3,
441            offset: 12,
442        },
443        VertexAttributeDescriptor {
444            shader_location: 2,
445            format: VertexFormat::Float32x2,
446            offset: 24,
447        },
448    ];
449
450    Mesh {
451        positions,
452        normals: Some(normals),
453        tex_coords: Some(tex_coords),
454        tangents: None,
455        colors: None,
456        indices: Some(indices),
457        primitive_type: PrimitiveTopology::TriangleList,
458        bounding_box: Aabb::from_min_max(
459            Vec3::new(-radius, -radius, -radius),
460            Vec3::new(radius, radius, radius),
461        ),
462        vertex_layout,
463    }
464}