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}