Skip to main content

kvas_engine/
components.rs

1//! Built-in components. Every one is plain serde data plus an optional `fields()` listing for the inspector.
2
3use crate::choice_enum;
4use crate::reflect::{AssetKind, Field};
5use glam::{EulerRot, Mat4, Quat, Vec3};
6use kvas_ecs::{Entity, World};
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9
10/// Editor-visible component contract: identity, serde persistence and editable fields.
11/// Built-ins are registered in both [`register_all`] and [`crate::registry::Registry`].
12/// Script libraries may access built-ins but must not register custom unloadable storage.
13pub trait Component: kvas_ecs::Component + Serialize + DeserializeOwned + Default + Clone {
14    /// Whether the user may remove it in the inspector.
15    const REMOVABLE: bool = true;
16    /// Mutable field descriptors used by the Inspector and script property serialization.
17    fn fields(&mut self) -> Vec<Field<'_>> {
18        Vec::new()
19    }
20}
21
22/// Runtime link to the parent entity. Saved in scenes as an index, not as a component.
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub struct Parent(pub Entity);
25
26/// World-space matrix, recomputed every frame from `Transform` and `Parent`.
27#[derive(Clone, Copy, Debug)]
28pub struct GlobalTransform(pub Mat4);
29
30/// Human-readable entity label. Names are not required to be unique.
31#[derive(Clone, Default, Serialize, Deserialize)]
32pub struct Name(pub String);
33
34impl Component for Name {
35    const REMOVABLE: bool = false;
36}
37
38/// Local translation, Euler rotation and scale relative to the parent.
39///
40/// Units: metres, degrees and dimensionless scale. Rotations use Y-X-Z order,
41/// +Y is up and -Z is forward. Scale defaults to one. Shear is not representable.
42///
43/// ```
44/// use kvas_engine::components::Transform;
45/// use glam::Vec3;
46/// let mut pose = Transform::at(Vec3::new(0.0, 1.0, 0.0));
47/// assert!(pose.look_at(Vec3::new(0.0, 1.0, -1.0), Vec3::Y));
48/// assert!((pose.forward() - Vec3::NEG_Z).length() < 1e-5);
49/// ```
50#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
51#[serde(default)]
52pub struct Transform {
53    /// Translation in parent-local metres.
54    pub position: Vec3,
55    /// Euler angles in degrees.
56    pub rotation: Vec3,
57    /// Per-axis local scale, one for an unscaled object.
58    pub scale: Vec3,
59}
60
61impl Default for Transform {
62    fn default() -> Self {
63        Self { position: Vec3::ZERO, rotation: Vec3::ZERO, scale: Vec3::ONE }
64    }
65}
66
67impl Transform {
68    /// Identity orientation/scale positioned at a parent-local point.
69    pub fn at(position: Vec3) -> Self {
70        Self { position, ..Default::default() }
71    }
72
73    /// Converts stored Euler degrees to a quaternion using Y-X-Z rotation order.
74    pub fn quat(&self) -> Quat {
75        let r = self.rotation * (std::f32::consts::PI / 180.0);
76        Quat::from_euler(EulerRot::YXZ, r.y, r.x, r.z)
77    }
78
79    /// Stores a unit quaternion as Euler degrees; equivalent angles may wrap.
80    /// The caller must supply a finite normalized quaternion.
81    pub fn set_quat(&mut self, q: Quat) {
82        let (y, x, z) = q.to_euler(EulerRot::YXZ);
83        self.rotation = Vec3::new(x, y, z) * (180.0 / std::f32::consts::PI);
84    }
85
86    /// Local TRS matrix for column-vector multiplication (`translation * rotation * scale`).
87    pub fn matrix(&self) -> Mat4 {
88        Mat4::from_scale_rotation_translation(self.scale, self.quat(), self.position)
89    }
90
91    /// Decomposes a finite, non-singular TRS matrix.
92    /// Shear cannot be retained. Use [`Self::try_from_matrix`] for unchecked input.
93    pub fn from_matrix(m: Mat4) -> Self {
94        let (scale, rot, position) = m.to_scale_rotation_translation();
95        let mut t = Self { position, rotation: Vec3::ZERO, scale };
96        t.set_quat(rot);
97        t
98    }
99
100    /// Local -Z rotated into parent space, ignoring scale.
101    pub fn forward(&self) -> Vec3 {
102        self.quat() * Vec3::NEG_Z
103    }
104    /// Local +X rotated into parent space, ignoring scale.
105    pub fn right(&self) -> Vec3 {
106        self.quat() * Vec3::X
107    }
108    /// Local +Y rotated into parent space, ignoring scale.
109    pub fn up(&self) -> Vec3 {
110        self.quat() * Vec3::Y
111    }
112
113    /// Checks that every stored scalar is finite; zero scale remains valid data.
114    pub fn is_finite(&self) -> bool {
115        self.position.is_finite() && self.rotation.is_finite() && self.scale.is_finite()
116    }
117
118    /// Decomposes a finite non-singular matrix, returning `None` for invalid data.
119    /// Perspective and shear matrices are rejected by checking the reconstructed TRS.
120    pub fn try_from_matrix(matrix: Mat4) -> Option<Self> {
121        if !matrix.is_finite() || matrix.determinant() == 0.0 {
122            return None;
123        }
124        let pose = Self::from_matrix(matrix);
125        if !pose.is_finite() || !pose.matrix().abs_diff_eq(matrix, 1e-4) {
126            return None;
127        }
128        Some(pose)
129    }
130
131    /// Transforms a local point into parent space, including translation and scale.
132    pub fn transform_point(&self, point: Vec3) -> Vec3 {
133        self.matrix().transform_point3(point)
134    }
135
136    /// Rotates a direction into parent space, ignoring translation and scale.
137    pub fn transform_direction(&self, direction: Vec3) -> Vec3 {
138        self.quat() * direction
139    }
140
141    /// Converts a parent-space point to local space, or `None` for a singular/invalid pose.
142    pub fn inverse_transform_point(&self, point: Vec3) -> Option<Vec3> {
143        let matrix = self.matrix();
144        if !point.is_finite() || !matrix.is_finite() || matrix.determinant() == 0.0 {
145            return None;
146        }
147        let local = matrix.inverse().transform_point3(point);
148        local.is_finite().then_some(local)
149    }
150
151    /// Orients local -Z toward a parent-space target while keeping position/scale.
152    /// Returns `false` without mutation for coincident points, non-finite data,
153    /// a zero up vector or an up vector parallel to the viewing direction.
154    pub fn look_at(&mut self, target: Vec3, up: Vec3) -> bool {
155        if !self.position.is_finite() || !target.is_finite() || !up.is_finite() {
156            return false;
157        }
158        let Some(forward) = (target - self.position).try_normalize() else {
159            return false;
160        };
161        let Some(right) = forward.cross(up).try_normalize() else {
162            return false;
163        };
164        let up = right.cross(forward);
165        self.set_quat(Quat::from_mat3(&glam::Mat3::from_cols(right, up, -forward)).normalize());
166        true
167    }
168
169    /// Interpolates translation/scale linearly and orientation along the shortest
170    /// quaternion arc. `t` is clamped to `[0, 1]`; non-finite `t` selects `self`.
171    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
172        let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
173        let mut pose =
174            Self { position: self.position.lerp(other.position, t), scale: self.scale.lerp(other.scale, t), rotation: Vec3::ZERO };
175        pose.set_quat(self.quat().slerp(other.quat(), t));
176        pose
177    }
178}
179
180impl Component for Transform {
181    const REMOVABLE: bool = false;
182    fn fields(&mut self) -> Vec<Field<'_>> {
183        vec![
184            Field::vec3("position", &mut self.position),
185            Field::vec3("rotation", &mut self.rotation),
186            Field::vec3("scale", &mut self.scale),
187        ]
188    }
189}
190
191/// Static/procedural mesh appearance consumed by the renderer.
192/// Texture/shader paths are asset-relative or built-in procedural keys.
193#[derive(Clone, Serialize, Deserialize)]
194#[serde(default)]
195pub struct MeshRenderer {
196    /// `primitive:<name>` or a model path (.obj / .gltf / .glb).
197    pub mesh: String,
198    /// Base RGBA tint. Alpha below one enables the renderer's transparency path.
199    pub color: [f32; 4],
200    /// Surface roughness; zero is smooth and one is fully rough.
201    pub roughness: f32,
202    /// Emission multiplier added independently of incoming light.
203    pub emissive: f32,
204    /// 0 = plastic-like, 1 = metal (reflections take the surface colour, no diffuse).
205    pub metallic: f32,
206    /// Mirror strength; fades in at grazing angles (Fresnel). 1 with a low colour alpha gives glass.
207    pub reflectivity: f32,
208    /// Whether this mesh contributes to the directional shadow map.
209    pub cast_shadows: bool,
210    /// Free parameters passed to custom shaders as `object.user0` / `object.user1`.
211    pub user: [f32; 8],
212    /// How many times the texture repeats across the mesh UVs.
213    pub tiling: f32,
214    /// Base-colour texture asset or procedural key; empty uses a white fallback.
215    pub texture: String,
216    /// Tangent-space normal map (OpenGL convention: green up).
217    pub normal_map: String,
218    /// Packed occlusion (R) / roughness (G) / metallic (B) texture, or an `orm:` key composed from
219    /// separate maps. Any normal or ORM map switches the material to PBR shading.
220    pub orm_map: String,
221    /// Tangent-space normal-map intensity; one uses the map unchanged.
222    pub normal_strength: f32,
223    /// Triplanar mapping scale in metres per texture repeat (0 = use the mesh UVs). Good for rocks.
224    pub triplanar: f32,
225    /// Custom WGSL file; empty means the built-in lit shader.
226    pub shader: String,
227}
228
229impl Default for MeshRenderer {
230    fn default() -> Self {
231        Self {
232            mesh: "primitive:cube".into(),
233            color: [0.8, 0.8, 0.8, 1.0],
234            roughness: 0.5,
235            emissive: 0.0,
236            metallic: 0.0,
237            reflectivity: 0.0,
238            cast_shadows: true,
239            user: [0.0; 8],
240            tiling: 1.0,
241            texture: String::new(),
242            normal_map: String::new(),
243            orm_map: String::new(),
244            normal_strength: 1.0,
245            triplanar: 0.0,
246            shader: String::new(),
247        }
248    }
249}
250
251impl Component for MeshRenderer {
252    fn fields(&mut self) -> Vec<Field<'_>> {
253        let [p1, p2, p3, p4, ..] = &mut self.user;
254        vec![
255            Field::asset("mesh", &mut self.mesh, AssetKind::Mesh),
256            Field::color("color", &mut self.color),
257            Field::f32("roughness", &mut self.roughness),
258            Field::f32("emissive", &mut self.emissive),
259            Field::f32("metallic", &mut self.metallic),
260            Field::f32("reflectivity", &mut self.reflectivity),
261            Field::bool("cast_shadows", &mut self.cast_shadows),
262            Field::f32("tiling", &mut self.tiling),
263            Field::f32("shader param 1", p1),
264            Field::f32("shader param 2", p2),
265            Field::f32("shader param 3", p3),
266            Field::f32("shader param 4", p4),
267            Field::asset("texture", &mut self.texture, AssetKind::Texture),
268            Field::asset("normal_map", &mut self.normal_map, AssetKind::Texture),
269            Field::asset("orm_map", &mut self.orm_map, AssetKind::Texture),
270            Field::f32("normal_strength", &mut self.normal_strength),
271            Field::f32("triplanar", &mut self.triplanar),
272            Field::asset("shader", &mut self.shader, AssetKind::Shader),
273        ]
274    }
275}
276
277choice_enum!(
278    /// Procedural mesh families. Cube/plane use size, round shapes use radii, and Rock uses seed.
279    ShapeType { Cube, Sphere, Plane, Cylinder, Capsule, Cone, Torus, Rock } default Cube);
280
281/// Parametric mesh. When an entity has both `Shape` and `MeshRenderer`, the shape is what is drawn.
282#[derive(Clone, Serialize, Deserialize)]
283#[serde(default)]
284pub struct Shape {
285    /// Procedural shape family.
286    pub kind: ShapeType,
287    /// Cube: width/height/depth. Plane: width (x) and depth (z).
288    pub size: Vec3,
289    /// Sphere/cylinder/cone/capsule radius; torus: distance from the centre to the tube.
290    pub radius: f32,
291    /// Cylinder: top radius (0 = cone). Torus: tube radius.
292    pub radius2: f32,
293    /// Full shape height in local metres.
294    pub height: f32,
295    /// Segments around the axis (more = smoother).
296    pub segments: i32,
297    /// Rings along the surface (sphere/capsule/torus tube) or plane subdivisions.
298    pub rings: i32,
299    /// Random seed (rocks).
300    pub seed: i32,
301}
302
303impl Default for Shape {
304    fn default() -> Self {
305        Self { kind: ShapeType::Cube, size: Vec3::ONE, radius: 0.5, radius2: 0.5, height: 1.0, segments: 32, rings: 16, seed: 1 }
306    }
307}
308
309impl Shape {
310    /// Creates default parameters for a shape family (including a zero top radius for cones).
311    pub fn of(kind: ShapeType) -> Self {
312        let base = Self { kind, ..Default::default() };
313        match kind {
314            ShapeType::Capsule => Self { height: 2.0, ..base },
315            ShapeType::Torus => Self { radius: 0.5, radius2: 0.18, segments: 48, rings: 24, ..base },
316            ShapeType::Plane => Self { rings: 16, ..base },
317            ShapeType::Rock => Self { size: Vec3::new(1.6, 1.0, 1.3), radius2: 1.0, segments: 5, ..base },
318            _ => base,
319        }
320    }
321
322    /// Encodes sanitized geometry parameters as a stable procedural mesh cache key.
323    pub fn key(&self) -> String {
324        use crate::reflect::Choice;
325        crate::mesh::ProcParams {
326            kind: ShapeType::OPTIONS[self.kind.to_index()].to_lowercase(),
327            size: self.size,
328            radius: self.radius,
329            radius2: self.radius2,
330            height: self.height,
331            segments: self.segments.max(3) as u32,
332            rings: self.rings.max(2) as u32,
333            seed: self.seed as u32,
334        }
335        .key()
336    }
337}
338
339impl Component for Shape {
340    fn fields(&mut self) -> Vec<Field<'_>> {
341        let kind = self.kind;
342        let mut f = vec![Field::choice("kind", &mut self.kind)];
343        match kind {
344            ShapeType::Cube => f.push(Field::vec3("size", &mut self.size)),
345            ShapeType::Plane => {
346                f.push(Field::vec3("size", &mut self.size));
347                f.push(Field::i32("subdivisions", &mut self.rings));
348            }
349            ShapeType::Sphere => {
350                f.push(Field::f32("radius", &mut self.radius));
351                f.push(Field::i32("segments", &mut self.segments));
352                f.push(Field::i32("rings", &mut self.rings));
353            }
354            ShapeType::Capsule => {
355                f.push(Field::f32("radius", &mut self.radius));
356                f.push(Field::f32("height", &mut self.height));
357                f.push(Field::i32("segments", &mut self.segments));
358                f.push(Field::i32("rings", &mut self.rings));
359            }
360            ShapeType::Cylinder => {
361                f.push(Field::f32("radius", &mut self.radius));
362                f.push(Field::f32("top radius", &mut self.radius2));
363                f.push(Field::f32("height", &mut self.height));
364                f.push(Field::i32("segments", &mut self.segments));
365            }
366            ShapeType::Cone => {
367                f.push(Field::f32("radius", &mut self.radius));
368                f.push(Field::f32("height", &mut self.height));
369                f.push(Field::i32("segments", &mut self.segments));
370            }
371            ShapeType::Rock => {
372                f.push(Field::vec3("size", &mut self.size));
373                f.push(Field::f32("ruggedness", &mut self.radius2));
374                f.push(Field::i32("detail", &mut self.segments));
375                f.push(Field::i32("seed", &mut self.seed));
376            }
377            ShapeType::Torus => {
378                f.push(Field::f32("radius", &mut self.radius));
379                f.push(Field::f32("tube radius", &mut self.radius2));
380                f.push(Field::i32("segments", &mut self.segments));
381                f.push(Field::i32("tube rings", &mut self.rings));
382            }
383        }
384        f
385    }
386}
387
388/// Marks entities created by systems (e.g. grass chunks). They are hidden from the hierarchy,
389/// never saved in scenes and not pickable.
390#[derive(Clone, Copy, Default)]
391pub struct Transient;
392
393/// Marks a chunk entity created for a grass patch.
394#[derive(Clone, Copy, Default)]
395pub struct GrassChunk;
396
397/// Geometry produced at run time by a system (cloth, smoke, grass...). Not saved in scenes.
398/// Bump `version` whenever `data` changes so the renderer re-uploads it.
399#[derive(Clone, Default)]
400pub struct DynamicMesh {
401    /// Shared immutable CPU geometry; clone the Arc to inspect without copying vertices.
402    pub data: std::sync::Arc<crate::mesh::MeshData>,
403    /// Change counter used to invalidate GPU buffers when geometry is rebuilt.
404    pub version: u64,
405}
406
407/// The mesh key an entity is drawn with: dynamic geometry, else its `Shape`, else `MeshRenderer.mesh`.
408pub fn mesh_key(world: &World, e: Entity) -> Option<String> {
409    let mr = world.get::<MeshRenderer>(e)?;
410    if world.has::<DynamicMesh>(e) {
411        return Some(format!("dyn:{}", e.to_bits()));
412    }
413    Some(world.get::<Shape>(e).map_or_else(|| mr.mesh.clone(), Shape::key))
414}
415
416/// Perspective or orthographic scene camera. The first primary camera is selected for gameplay.
417#[derive(Clone, Serialize, Deserialize)]
418#[serde(default)]
419pub struct Camera {
420    /// Vertical perspective field of view in degrees. Ignored for orthographic cameras.
421    pub fov: f32,
422    /// Near clipping distance in metres; must be positive for perspective projection.
423    pub near: f32,
424    /// Far clipping distance in metres; must exceed near.
425    pub far: f32,
426    /// The first primary camera is used to render the game.
427    pub primary: bool,
428    /// Orthographic projection (2D games). `ortho_size` is the visible height in world units.
429    pub orthographic: bool,
430    /// Full vertical orthographic view height in metres.
431    pub ortho_size: f32,
432    /// Lens focal length in millimetres on a 24 mm tall sensor; above 0 it replaces `fov`
433    /// (24 mm is a wide shot, 50 mm natural, 135 mm a telephoto).
434    pub focal_length: f32,
435    /// Depth of field: blur what is not at the focus distance.
436    pub dof: bool,
437    /// Aperture as an f-number: 1.4 gives strong blur, 8 a deep focus, 22 almost none.
438    pub aperture: f32,
439    /// Distance in metres that is in focus (set live by autofocus).
440    pub focus_distance: f32,
441    /// Focus on whatever is in the middle of the screen (needs colliders to hit).
442    pub autofocus: bool,
443    /// How fast autofocus racks to a new target.
444    pub focus_speed: f32,
445    /// Exposure compensation in stops (+1 doubles the light).
446    pub exposure_comp: f32,
447    /// Cinematic bars: target picture ratio such as 2.39 or 1.85 (0 = full screen).
448    pub letterbox: f32,
449    /// Camera shaders of this camera only, applied after the scene's own effects while it is the
450    /// active camera (security cam static, a character's blurred view...).
451    pub effects: Vec<crate::fx::PostEffect>,
452}
453
454impl Default for Camera {
455    fn default() -> Self {
456        Self { fov: 60.0, near: 0.1, far: 500.0, primary: true, orthographic: false, ortho_size: 10.0, focal_length: 0.0, dof: false, aperture: 2.8, focus_distance: 8.0, autofocus: true, focus_speed: 6.0, exposure_comp: 0.0, letterbox: 0.0, effects: Vec::new() }
457    }
458}
459
460/// The camera the game looks through: the first `primary` camera, else the first camera at all.
461pub fn active_camera(world: &World) -> Option<Entity> {
462    world.iter::<Camera>().find(|(_, c)| c.primary).or_else(|| world.iter::<Camera>().next()).map(|(e, _)| e)
463}
464
465/// Makes `camera` the only primary camera (the one the game looks through).
466pub fn set_active_camera(world: &mut World, camera: Entity) -> bool {
467    if !world.has::<Camera>(camera) {
468        return false;
469    }
470    let all: Vec<Entity> = world.iter::<Camera>().map(|(e, _)| e).collect();
471    for e in all {
472        if let Some(c) = world.get_mut::<Camera>(e) {
473            c.primary = e == camera;
474        }
475    }
476    true
477}
478
479impl Camera {
480    /// Vertical field of view in degrees: from the focal length when one is set, else `fov`.
481    pub fn vertical_fov(&self) -> f32 {
482        if self.focal_length > 1.0 { (2.0 * (12.0 / self.focal_length).atan()).to_degrees().clamp(2.0, 170.0) } else { self.fov }
483    }
484}
485
486impl Component for Camera {
487    fn fields(&mut self) -> Vec<Field<'_>> {
488        vec![
489            Field::f32("fov", &mut self.fov),
490            Field::f32("near", &mut self.near),
491            Field::f32("far", &mut self.far),
492            Field::bool("primary", &mut self.primary),
493            Field::bool("orthographic", &mut self.orthographic),
494            Field::f32("ortho_size", &mut self.ortho_size),
495            Field::f32("focal_length", &mut self.focal_length),
496            Field::bool("dof", &mut self.dof),
497            Field::f32("aperture", &mut self.aperture),
498            Field::f32("focus_distance", &mut self.focus_distance),
499            Field::bool("autofocus", &mut self.autofocus),
500            Field::f32("focus_speed", &mut self.focus_speed),
501            Field::f32("exposure_comp", &mut self.exposure_comp),
502            Field::f32("letterbox", &mut self.letterbox),
503        ]
504    }
505}
506
507choice_enum!(
508    /// Directional lights illuminate globally; point/spot lights have a finite range.
509    LightKind { Directional, Point, Spot } default Directional);
510
511/// Directional, point or spot light with optional directional shadow casting.
512#[derive(Clone, Serialize, Deserialize)]
513#[serde(default)]
514pub struct Light {
515    /// Directional sun, spherical point source or spot cone.
516    pub kind: LightKind,
517    /// RGBA light tint; RGB channels affect illumination.
518    pub color: [f32; 4],
519    /// Linear light strength multiplier.
520    pub intensity: f32,
521    /// Local light influence radius in metres (point and spot).
522    pub range: f32,
523    /// Spot only: outer cone angle in degrees.
524    pub angle: f32,
525    /// Spot only: 0 = hard cone edge, 1 = fully soft.
526    pub softness: f32,
527    /// Directional only: cast shadows.
528    pub shadows: bool,
529}
530
531impl Default for Light {
532    fn default() -> Self {
533        Self {
534            kind: LightKind::Directional,
535            color: [1.0, 0.96, 0.9, 1.0],
536            intensity: 1.0,
537            range: 10.0,
538            angle: 35.0,
539            softness: 0.25,
540            shadows: true,
541        }
542    }
543}
544
545impl Component for Light {
546    fn fields(&mut self) -> Vec<Field<'_>> {
547        vec![
548            Field::choice("kind", &mut self.kind),
549            Field::color("color", &mut self.color),
550            Field::f32("intensity", &mut self.intensity),
551            Field::f32("range", &mut self.range),
552            Field::f32("angle", &mut self.angle),
553            Field::f32("softness", &mut self.softness),
554            Field::bool("shadows", &mut self.shadows),
555        ]
556    }
557}
558
559choice_enum!(
560    /// Dynamic bodies respond to forces, Fixed bodies stay still, Kinematic bodies follow transforms.
561    BodyKind { Dynamic, Fixed, Kinematic } default Dynamic);
562
563/// Physics body configuration and per-step motion requests.
564/// Dynamic results are written back into Transform and velocity by Physics::step.
565#[derive(Clone, Serialize, Deserialize)]
566#[serde(default)]
567pub struct RigidBody {
568    /// Dynamic, fixed or position-driven kinematic body.
569    pub kind: BodyKind,
570    /// Body mass in kilograms; physics clamps it to at least 0.001.
571    pub mass: f32,
572    /// Multiplier for scene gravity; zero disables gravity for this body.
573    pub gravity_scale: f32,
574    /// Linear velocity damping coefficient.
575    pub linear_damping: f32,
576    /// Angular velocity damping coefficient.
577    pub angular_damping: f32,
578    /// Locks all rotational axes, useful for upright player capsules.
579    pub lock_rotation: bool,
580    /// Continuous collision detection for fast dynamic bodies (helps prevent tunnelling).
581    pub ccd: bool,
582    /// Current linear velocity (read-only, mirrored from the simulation).
583    #[serde(skip)]
584    pub velocity: Vec3,
585    /// Scripts write these; the physics step consumes them.
586    #[serde(skip)]
587    pub impulse: Vec3,
588    #[serde(skip)]
589    /// One-shot world-space linear velocity request, consumed on the next dynamic step.
590    pub set_velocity: Option<Vec3>,
591    #[serde(skip)]
592    /// One-shot world-space angular velocity request in radians/second.
593    pub set_angular_velocity: Option<Vec3>,
594    /// One-shot request to move the simulated body to the current Transform pose.
595    /// Use after editing a dynamic body's transform; ordinary physics normally owns its pose.
596    #[serde(skip)]
597    pub teleport: bool,
598}
599
600impl Default for RigidBody {
601    fn default() -> Self {
602        Self {
603            kind: BodyKind::Dynamic,
604            mass: 1.0,
605            gravity_scale: 1.0,
606            linear_damping: 0.05,
607            angular_damping: 0.05,
608            lock_rotation: false,
609            ccd: false,
610            velocity: Vec3::ZERO,
611            impulse: Vec3::ZERO,
612            set_velocity: None,
613            set_angular_velocity: None,
614            teleport: false,
615        }
616    }
617}
618
619impl Component for RigidBody {
620    fn fields(&mut self) -> Vec<Field<'_>> {
621        vec![
622            Field::choice("kind", &mut self.kind),
623            Field::f32("mass", &mut self.mass),
624            Field::f32("gravity_scale", &mut self.gravity_scale),
625            Field::f32("linear_damping", &mut self.linear_damping),
626            Field::f32("angular_damping", &mut self.angular_damping),
627            Field::bool("lock_rotation", &mut self.lock_rotation),
628            Field::bool("ccd", &mut self.ccd),
629        ]
630    }
631}
632
633choice_enum!(
634    /// Collider shapes. Primitives use full extents; `Mesh` is the exact triangles of the entity's
635    /// model (for static level geometry), `Hull` its convex hull (for moving props).
636    ShapeKind { Box, Sphere, Capsule, Cylinder, Mesh, Hull } default Box);
637
638/// Collision shape. `size` is the full extent: box = (w, h, d); sphere = diameter in x;
639/// capsule/cylinder = diameter in x and total height in y; mesh/hull = a multiplier on the model
640/// (`MeshRenderer.mesh`, a box of `size` if it has none). Scaled by the entity's world scale.
641/// Without a RigidBody on the entity or an ancestor it is static geometry.
642#[derive(Clone, Serialize, Deserialize)]
643#[serde(default)]
644pub struct Collider {
645    /// Primitive collision shape family.
646    pub shape: ShapeKind,
647    /// Full local extents/diameters in metres, multiplied by world scale.
648    pub size: Vec3,
649    /// Surface friction coefficient; typical values lie in 0..1.
650    pub friction: f32,
651    /// Bounciness; zero is inelastic, one is ideally elastic.
652    pub restitution: f32,
653    /// Sensor-only volume: reports overlap without applying collision forces.
654    pub is_trigger: bool,
655    /// Collision-layer membership bits. Defaults to every layer (`u32::MAX`).
656    pub memberships: u32,
657    /// Layers this collider accepts. Interaction requires both colliders' masks to agree.
658    pub filter: u32,
659}
660
661impl Default for Collider {
662    fn default() -> Self {
663        Self {
664            shape: ShapeKind::Box,
665            size: Vec3::ONE,
666            friction: 0.6,
667            restitution: 0.1,
668            is_trigger: false,
669            memberships: u32::MAX,
670            filter: u32::MAX,
671        }
672    }
673}
674
675impl Component for Collider {
676    fn fields(&mut self) -> Vec<Field<'_>> {
677        vec![
678            Field::choice("shape", &mut self.shape),
679            Field::vec3("size", &mut self.size),
680            Field::f32("friction", &mut self.friction),
681            Field::f32("restitution", &mut self.restitution),
682            Field::bool("is_trigger", &mut self.is_trigger),
683            Field::u32("memberships", &mut self.memberships),
684            Field::u32("filter", &mut self.filter),
685        ]
686    }
687}
688
689choice_enum!(
690    /// Fixed weld, freely rotating ball, limited hinge, or damped spring constraint.
691    JointKind { Fixed, Ball, Hinge, Spring } default Fixed);
692
693/// Connects this entity's RigidBody to another body (by entity name) or to the world if `target` is empty.
694/// The joint is attached where the bodies currently are, so place objects first.
695/// Fixed: weld at the current pose. Ball/Hinge: pivot at `anchor` (local to this entity).
696/// Spring: `anchor` is where the other end is attached, relative to this entity.
697#[derive(Clone, Serialize, Deserialize)]
698#[serde(default)]
699pub struct Joint {
700    /// Constraint family connecting this body to a named target or world anchor.
701    pub kind: JointKind,
702    /// Target body entity name; empty connects to a fixed world anchor.
703    pub target: String,
704    /// Pivot (Ball/Hinge) or far end (Spring), in this entity's local space.
705    pub anchor: Vec3,
706    /// Hinge axis in local space.
707    pub axis: Vec3,
708    /// Whether hinge angle bounds are applied.
709    pub use_limits: bool,
710    /// Minimum hinge angle in degrees.
711    pub limit_min: f32,
712    /// Maximum hinge angle in degrees.
713    pub limit_max: f32,
714    /// Spring equilibrium length in metres.
715    pub rest_length: f32,
716    /// Spring stiffness coefficient.
717    pub stiffness: f32,
718    /// Spring damping coefficient.
719    pub damping: f32,
720}
721
722impl Default for Joint {
723    fn default() -> Self {
724        Self {
725            kind: JointKind::Fixed,
726            target: String::new(),
727            anchor: Vec3::ZERO,
728            axis: Vec3::Y,
729            use_limits: false,
730            limit_min: -45.0,
731            limit_max: 45.0,
732            rest_length: 1.0,
733            stiffness: 50.0,
734            damping: 5.0,
735        }
736    }
737}
738
739impl Component for Joint {
740    fn fields(&mut self) -> Vec<Field<'_>> {
741        vec![
742            Field::choice("kind", &mut self.kind),
743            Field::entity("target", &mut self.target),
744            Field::vec3("anchor", &mut self.anchor),
745            Field::vec3("axis", &mut self.axis),
746            Field::bool("use_limits", &mut self.use_limits),
747            Field::f32("limit_min", &mut self.limit_min),
748            Field::f32("limit_max", &mut self.limit_max),
749            Field::f32("rest_length", &mut self.rest_length),
750            Field::f32("stiffness", &mut self.stiffness),
751            Field::f32("damping", &mut self.damping),
752        ]
753    }
754}
755
756/// One attached native script type and its saved editable properties.
757#[derive(Clone, Default, Serialize, Deserialize)]
758pub struct ScriptSlot {
759    /// Exact exported Rust script type name.
760    pub name: String,
761    /// Saved field values keyed by the names returned from Script::fields.
762    #[serde(default)]
763    pub props: serde_json::Map<String, serde_json::Value>,
764}
765
766/// Rust scripts attached to an entity (several allowed). Property values live in `props`.
767#[derive(Clone, Default, Serialize, Deserialize)]
768pub struct Scripts(pub Vec<ScriptSlot>);
769
770impl Component for Scripts {}
771
772macro_rules! name_components {
773    ($($t:ident),+ $(,)?) => {
774        $(impl kvas_ecs::Component for $t { const NAME: &'static str = stringify!($t); })+
775
776        /// Creates the storage of every built-in component. Must run on every new `World`.
777        pub fn register_all(world: &mut World) {
778            $(world.register::<$t>();)+
779        }
780    };
781}
782
783pub use crate::audio::AudioSource;
784pub use crate::gui::{UiElement, UiKind};
785pub use crate::mesh_edit::EditableMesh;
786pub use crate::neural::NeuralAgent;
787pub use crate::animation::TransformAnimation;
788pub use crate::cloth::Cloth;
789pub use crate::gameplay::{Lifetime, Tags, Visibility};
790pub use crate::grass::Grass;
791pub use crate::tree::{Tree, TreePart};
792pub use crate::decal::Decal;
793pub use crate::interaction::{Slider, Switch};
794pub use crate::meshmod::{MeshModifiers, ModifierStateCell};
795pub use crate::paint::TexturePaint;
796pub use crate::particles::ParticleEmitter;
797pub use crate::skeletal::{Animator, AnimatorState};
798pub use crate::ik::CharacterIk;
799pub use crate::ragdoll::Ragdoll;
800pub use crate::modifiers::{Bob, ColorCycle, Pulse, Spin, Wobble};
801pub use crate::tween::MoveTween;
802pub use crate::voxel::{VoxelClouds, VoxelSmoke};
803pub use crate::water::VoxelWater;
804
805name_components!(
806    UiElement,
807    EditableMesh,
808    NeuralAgent,
809    TransformAnimation,
810    ParticleEmitter,
811    Animator,
812    AnimatorState,
813    CharacterIk,
814    Ragdoll,
815    TexturePaint,
816    MeshModifiers,
817    ModifierStateCell,
818    AudioSource,
819    MoveTween,
820    Tags,
821    Lifetime,
822    Visibility,
823    Switch,
824    Slider,
825    Transient,
826    GrassChunk,
827    Tree,
828    TreePart,
829    Decal,
830    DynamicMesh,
831    Cloth,
832    Grass,
833    VoxelClouds,
834    VoxelSmoke,
835    VoxelWater,
836    Bob,
837    Spin,
838    Pulse,
839    Wobble,
840    ColorCycle,
841    Parent,
842    GlobalTransform,
843    Name,
844    Transform,
845    MeshRenderer,
846    Shape,
847    Camera,
848    Light,
849    RigidBody,
850    Collider,
851    Joint,
852    Scripts
853);