Skip to main content

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
27//! use khora_sdk::{GameWorld, Vessel};
28//! use khora_sdk::prelude::ecs::Camera;
29//! use khora_sdk::prelude::math::{Quaternion, Vec3};
30//!
31//! let mut world = GameWorld::new();
32//!
33//! // Spawn a camera at a position, rotated to face the scene.
34//! let camera = Camera::new_perspective(
35//!     std::f32::consts::FRAC_PI_4, 16.0 / 9.0, 0.1, 1000.0,
36//! );
37//! let _entity = Vessel::at(&mut world, Vec3::new(0.0, 2.0, 10.0))
38//!     .with_component(camera)
39//!     .with_rotation(Quaternion::from_axis_angle(Vec3::Y, std::f32::consts::PI))
40//!     .build();
41//! ```
42
43use khora_core::ecs::entity::EntityId;
44use khora_core::math::Vec3;
45use khora_data::ecs::{GlobalTransform, MeshRef, ProceduralMeshKind, Transform};
46
47use crate::GameWorld;
48
49/// A high-level wrapper around an ECS entity.
50///
51/// Vessel provides a builder-pattern API for creating and configuring
52/// game entities. It automatically handles the underlying ECS components
53/// and synchronization between Transform and GlobalTransform.
54///
55/// Every Vessel is guaranteed to have:
56/// - Transform (local position/rotation/scale)
57/// - GlobalTransform (world-space transform for rendering)
58///
59/// # Examples
60///
61/// ```rust
62/// use khora_sdk::{GameWorld, Vessel};
63/// use khora_sdk::prelude::ecs::Name;
64/// use khora_sdk::prelude::math::Vec3;
65///
66/// let mut world = GameWorld::new();
67///
68/// // `at(..)` spawns the entity; chained calls configure it; `build()` finalizes.
69/// let entity = Vessel::at(&mut world, Vec3::new(1.0, 0.0, -3.0))
70///     .with_scale(Vec3::ONE * 2.0)
71///     .with_component(Name::new("crate"))
72///     .build();
73///
74/// // The entity exists and carries the position we set.
75/// let transform = world.get_transform(entity).unwrap();
76/// assert_eq!(transform.translation, Vec3::new(1.0, 0.0, -3.0));
77/// ```
78pub struct Vessel<'a> {
79    world: &'a mut GameWorld,
80    entity: EntityId,
81    transform: Transform,
82}
83
84impl<'a> Vessel<'a> {
85    /// Creates a new Vessel at the origin.
86    ///
87    /// The entity is spawned immediately with Transform and GlobalTransform.
88    pub fn new(world: &'a mut GameWorld) -> Self {
89        let transform = Transform::identity();
90        let global = GlobalTransform::new(transform.to_mat4());
91        let entity = world.spawn((transform, global));
92
93        Self {
94            world,
95            entity,
96            transform,
97        }
98    }
99
100    /// Creates a new Vessel at the specified position.
101    pub fn at(world: &'a mut GameWorld, position: Vec3) -> Self {
102        let transform = Transform::from_translation(position);
103        let global = GlobalTransform::new(transform.to_mat4());
104        let entity = world.spawn((transform, global));
105
106        Self {
107            world,
108            entity,
109            transform,
110        }
111    }
112
113    /// Sets the transform (position, rotation, scale).
114    pub fn with_transform(mut self, transform: Transform) -> Self {
115        self.transform = transform;
116        self
117    }
118
119    /// Sets the position.
120    pub fn at_position(mut self, position: Vec3) -> Self {
121        self.transform.translation = position;
122        self
123    }
124
125    /// Sets the rotation of the transform.
126    pub fn with_rotation(mut self, rotation: khora_core::math::Quaternion) -> Self {
127        self.transform.rotation = rotation;
128        self
129    }
130
131    /// Sets the scale of the transform.
132    pub fn with_scale(mut self, scale: Vec3) -> Self {
133        self.transform.scale = scale;
134        self
135    }
136
137    /// Adds a generic component to the entity immediately.
138    ///
139    /// Since the entity is already spawned when the `Vessel` is created,
140    /// we can add the component right away. This avoids needing a field
141    /// on `Vessel` for every possible component type.
142    pub fn with_component<C: khora_data::ecs::Component>(self, component: C) -> Self {
143        self.world.add_component(self.entity, component);
144        self
145    }
146
147    /// Returns the entity ID.
148    pub fn entity(&self) -> EntityId {
149        self.entity
150    }
151
152    /// Builds the Vessel, updating the final transforms.
153    ///
154    /// This finalizes the Vessel creation and returns the entity ID.
155    pub fn build(self) -> EntityId {
156        // Update transform (entity was spawned with one, so we need to update it)
157        if let Some(existing_transform) = self.world.get_component_mut::<Transform>(self.entity) {
158            *existing_transform = self.transform;
159        }
160
161        // Sync GlobalTransform
162        let global = GlobalTransform::new(self.transform.to_mat4());
163        if let Some(existing_global) = self.world.get_component_mut::<GlobalTransform>(self.entity)
164        {
165            *existing_global = global;
166        }
167
168        self.entity
169    }
170}
171
172/// Creates a Vessel with a plane mesh at the origin.
173///
174/// Attaches an authored [`MeshRef::Procedural`]; the asset resolver rebuilds
175/// the plane geometry and mints the runtime `HandleComponent<Mesh>` before the
176/// GPU mesh projection runs.
177pub fn spawn_plane<'a>(world: &'a mut GameWorld, size: f32, y: f32) -> Vessel<'a> {
178    let mesh_ref = MeshRef::procedural(ProceduralMeshKind::Plane, [size, y, 0.0, 0.0]);
179    Vessel::new(world).with_component(mesh_ref)
180}
181
182/// Creates a Vessel with a cube mesh at a specific position.
183///
184/// Attaches an authored [`MeshRef::Procedural`]; see [`spawn_plane`] for the
185/// resolution flow.
186pub fn spawn_cube_at<'a>(world: &'a mut GameWorld, position: Vec3, size: f32) -> Vessel<'a> {
187    let mesh_ref = MeshRef::procedural(ProceduralMeshKind::Cube, [size, 0.0, 0.0, 0.0]);
188    Vessel::at(world, position).with_component(mesh_ref)
189}
190
191/// Creates a Vessel with a sphere mesh at the origin.
192///
193/// Attaches an authored [`MeshRef::Procedural`]; see [`spawn_plane`] for the
194/// resolution flow.
195pub fn spawn_sphere<'a>(
196    world: &'a mut GameWorld,
197    radius: f32,
198    segments: u32,
199    rings: u32,
200) -> Vessel<'a> {
201    let mesh_ref = MeshRef::procedural(
202        ProceduralMeshKind::Sphere,
203        [radius, segments as f32, rings as f32, 0.0],
204    );
205    Vessel::new(world).with_component(mesh_ref)
206}