gizmo_core/component.rs
1//! What makes a type a component, and where its rows are stored.
2//!
3//! [`Component`] is the trait every stored type implements — usually through the
4//! `impl_component!` macro rather than by hand — and [`StorageType`] chooses between the
5//! archetype table (dense, fast to iterate, the default) and a sparse set (cheap to add and
6//! remove on a small fraction of entities).
7//!
8//! The storage choice is not cosmetic: it decides which query operands are legal. Table
9//! storage backs the chunked/contiguous iteration paths; sparse-set components cannot be
10//! served as slices and are rejected — or panic — there.
11use std::any::Any;
12
13/// Which of the world's two backing stores holds the data of a component type.
14///
15/// The choice is a property of the *type*, not of an entity: it comes from
16/// [`Component::storage_type`], which must return the same answer on every call — see there for
17/// what an inconsistent impl breaks. The two stores are disjoint: a component lives in one of
18/// them, never both.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum StorageType {
21 /// Inside the archetype, as a contiguous column with one row per entity.
22 ///
23 /// Iteration is a linear scan over that column, and this is the only storage a chunked
24 /// query can hand out as a `&[T]` slice. The price is structural churn: adding or removing
25 /// a table component migrates the entity into a different archetype, which copies all of
26 /// its *other* components into a fresh row and swap-removes the old one (so an unrelated
27 /// entity changes row as a side effect).
28 ///
29 /// The default, and the right answer unless the component is added and removed far more
30 /// often than it is read.
31 Table,
32 /// Outside the archetype, in one per-type sparse set keyed by raw entity id.
33 ///
34 /// Adding or removing costs no archetype migration, which is what makes this suitable for
35 /// churny short-lived tags. In exchange: every access is an indirection through the
36 /// id → row table; archetype-level query matching cannot narrow anything, so
37 /// a `With`/`Without` on a sparse component matches *every* archetype and degrades into a
38 /// per-row presence test (unlike the table case, where the archetype test alone settles it);
39 /// and asking a query for a contiguous slice (`iter_chunks`) panics rather than degrading.
40 ///
41 /// Not every code path supports it — the bundle fast path writes archetype columns only,
42 /// so a bundle containing a sparse component is routed component-by-component instead
43 /// (see [`Bundle::apply`]).
44 SparseSet,
45}
46
47/// Data that can be attached to an entity.
48///
49/// There is no derive macro in this workspace; write the impl by hand or use
50/// [`impl_component!`](crate::impl_component). Implementing it is the only registration a type
51/// needs — the world records a component's runtime metadata the first time it sees the type.
52///
53/// The supertraits are load-bearing rather than decorative. `'static` because a component's
54/// identity everywhere in the ECS is its `TypeId`, so two distinct components can never share
55/// a type and a component can never borrow. `Send + Sync` because component storage is shared
56/// across worker threads by parallel query iteration. `Clone` because the storage layer records
57/// a clone thunk for every component type at registration; that thunk is what entity cloning
58/// (prefab splicing) uses, and a type that cannot clone cannot be a component.
59///
60/// Zero-sized components are legal and cost no allocation; they are the usual shape for
61/// markers such as [`IsHidden`].
62pub trait Component: 'static + Any + Send + Sync + Clone {
63 /// Where instances of this type are stored; see [`StorageType`].
64 ///
65 /// Answered by the type, not by an instance, so it must be a constant — the value is
66 /// captured into the world's component metadata the first time the type is registered, and
67 /// an impl that returned different values on different calls would leave the storage and
68 /// the metadata disagreeing about where the data lives.
69 ///
70 /// Defaults to [`StorageType::Table`].
71 fn storage_type() -> StorageType {
72 StorageType::Table
73 }
74}
75
76/// Writes an empty [`Component`] impl for one or more types, optionally choosing their
77/// [`StorageType`].
78///
79/// ```
80/// # #[derive(Clone)] struct Position; #[derive(Clone)] struct Velocity;
81/// # #[derive(Clone)] struct Frozen; #[derive(Clone)] struct Stunned;
82/// // Neither is in the prelude: `StorageType` has to be nameable in *your* scope for the
83/// // `; $storage` argument below, and `Component` for `storage_type()`.
84/// use gizmo_core::component::{Component, StorageType};
85/// use gizmo_core::impl_component;
86///
87/// impl_component!(Position, Velocity); // default: Table storage
88/// impl_component!(Frozen, Stunned; StorageType::SparseSet); // explicit storage
89///
90/// assert_eq!(Position::storage_type(), StorageType::Table);
91/// assert_eq!(Frozen::storage_type(), StorageType::SparseSet);
92/// ```
93///
94/// The macro only writes the impl: the types must already satisfy `Component`'s supertraits
95/// (`'static + Send + Sync + Clone`), and the expansion is an ordinary trait impl, so the usual
96/// orphan rule applies — outside gizmo-core itself, only types local to the invoking crate can
97/// be passed.
98///
99/// The `; $storage` argument is an expression expanded in the *caller's* scope, so whatever
100/// path it names (`StorageType::SparseSet`, `gizmo_core::component::StorageType::SparseSet`, …)
101/// has to resolve there. Only the storage-less form accepts a trailing comma after the last
102/// type; `impl_component!(A, B,; …)` does not parse.
103///
104/// `#[macro_export]` puts it at the crate root: `gizmo_core::impl_component!`.
105#[macro_export]
106macro_rules! impl_component {
107 ($($t:ty),+ $(,)?) => {
108 $(
109 impl $crate::Component for $t {}
110 )+
111 };
112 ($($t:ty),+ ; $storage:expr) => {
113 $(
114 impl $crate::Component for $t {
115 fn storage_type() -> $crate::component::StorageType {
116 $storage
117 }
118 }
119 )+
120 };
121}
122
123// --- Hiyerarşi (Scene Graph) Bileşenleri ---
124/// Up-link from a child to its parent, carrying the parent's raw
125/// [`Entity::id`](crate::Entity::id) — a slot index with the generation stripped off.
126///
127/// Because the generation is gone the value cannot distinguish a live parent from a recycled
128/// id, and nothing revalidates it when the parent is despawned. Resolve it through
129/// [`World::entity`](crate::World::entity), which returns `None` for an id that is dead or has
130/// no storage, instead of trusting the number.
131///
132/// This is only half of a link: [`Children`] on the parent is the other half. Nothing
133/// synchronises the two halves automatically, so adding or editing this component directly
134/// leaves the parent's `Children` list stale.
135#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
136pub struct Parent(pub u32);
137
138/// Down-link from a parent to its children: raw entity ids (no generations), in the order they
139/// were attached.
140///
141/// The order is stable and meaningful. [`HierarchyExt::add_child`](crate::HierarchyExt::add_child)
142/// appends and skips ids already in the list, and `remove_child` retains, so iteration over a
143/// `Children` list is deterministic across a run — which is what lets hierarchy walks be
144/// replayed. Presence of this component is also what marks an archetype as a candidate for
145/// [`World::sort_archetype_hierarchy`](crate::World::sort_archetype_hierarchy), which permutes
146/// rows to put parents and children next to each other.
147///
148/// Entries are not validated: a child despawned outside `HierarchyExt` leaves a dangling id.
149/// Duplicates and cycles are impossible through `HierarchyExt` (it rejects self-parenting and
150/// any reparent that would close a loop) but perfectly possible via direct component writes or
151/// a hand-edited scene file, so `despawn_recursive` carries a visited set rather than assuming
152/// the graph is acyclic.
153#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
154pub struct Children(pub Vec<u32>);
155
156/// Human-readable label for an entity — for editor lists, logs and scene files.
157///
158/// Purely descriptive: nothing enforces uniqueness, nothing indexes it, and gizmo-core itself
159/// never reads it. Two entities may hold the same name, and an absent `EntityName` is normal
160/// (most entities never get one).
161#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
162pub struct EntityName(pub String);
163
164impl EntityName {
165 /// Allocates an owned copy of `name`. The field is public, so an already-owned `String` can
166 /// be moved in as `EntityName(s)` instead of paying for a second allocation here.
167 ///
168 /// No validation and no normalisation: empty, blank and already-used names are all accepted.
169 /// That matters because the derived `PartialEq` compares the stored text byte for byte —
170 /// `"Cube"` and `"cube "` are different labels to anything that searches by name.
171 pub fn new(name: &str) -> Self {
172 Self(name.to_string())
173 }
174}
175
176/// Zero-sized marker meaning "do not display this entity".
177///
178/// It carries no data, so visibility is binary and expressed structurally: hide with
179/// `add_component(e, IsHidden)`, show with `remove_component::<IsHidden>(e)`. gizmo-core
180/// attaches no behaviour to it whatsoever. A hidden entity is still alive and still visited by
181/// every query that does not explicitly exclude it.
182#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
183pub struct IsHidden;
184
185/// Zero-sized marker meaning "this entity is the editor's own tooling, not scene content":
186/// grids, light icons, selection boxes, handles.
187///
188/// # Why this is in the ECS floor and not in the editor
189///
190/// Because four unrelated layers need the answer and only this one is below all of them: the
191/// hierarchy panel hides these rows, the studio's game view refuses to draw them, the windowed
192/// app's editor runtime skips them, and — the one that matters most — [`crate`]'s sibling
193/// `gizmo-scene` leaves them out of a saved scene. That last consumer cannot see a renderer
194/// component: scene sits beside the renderer in the graph, not above it.
195///
196/// Until this existed, all four asked the same question by **string prefix on the entity name**
197/// (`"Editor "` / `"Highlight Box"`), written out four times. That works only by convention and
198/// fails in a way nobody would debug quickly: an office scene with a desk named "Editor Desk"
199/// loses it from the hierarchy and from every save.
200///
201/// The name rule is still honoured — see [`is_editor_only`] — because scenes saved before this
202/// component existed carry names and not markers.
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
204pub struct EditorOnly;
205
206/// Is this entity the editor's own tooling rather than scene content?
207///
208/// The single place that decides. Callers pass whatever they have — a marker lookup, a name, or
209/// both — because the four call sites reach the world differently and a shared signature that
210/// takes `&World` would force three of them to look up something they already hold.
211///
212/// The legacy half (the name rule) is a **transition**, not a design: it exists so that scenes
213/// written before [`EditorOnly`] still round-trip. New tooling entities should carry the marker
214/// and need no particular name.
215#[inline]
216pub fn is_editor_only(has_marker: bool, name: Option<&str>) -> bool {
217 has_marker || name.is_some_and(|n| n.starts_with("Editor ") || n == "Highlight Box")
218}
219
220/// Zero-sized marker meaning "soft-deleted": the entity is still alive with its id, handles and
221/// components intact, but is meant to be skipped by processing.
222///
223/// gizmo-core never acts on it. It is a convention for the layers above, which exclude it with
224/// `Without<IsDeleted>` — the rigid-body physics systems do exactly that — and later despawn
225/// the marked entities for real. Because nothing is destroyed when the marker is added,
226/// removing it again restores the entity exactly; that reversibility is the point of the flag.
227#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
228pub struct IsDeleted;
229
230/// A request to populate this entity from a named prefab, left on the entity until something
231/// fulfils it.
232///
233/// The string is an opaque key: gizmo-core neither resolves nor validates it. No system in this
234/// workspace consumes the component — the Lua scripting bridge is its only in-tree producer —
235/// so it does nothing unless the application runs its own resolver, which must also remove the
236/// component afterwards, since nothing clears it automatically.
237#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
238pub struct PrefabRequest(pub String);
239
240impl PrefabRequest {
241 /// Stores an owned copy of `name` as the request key.
242 ///
243 /// Infallible and unvalidated: gizmo-core holds no prefab catalogue, so an empty or
244 /// misspelled key is indistinguishable here from a good one and can only fail later,
245 /// wherever the application resolves it.
246 pub fn new(name: &str) -> Self {
247 Self(name.to_string())
248 }
249 /// The request key, borrowed from the component — no allocation, no copy.
250 ///
251 /// May be empty, and is never checked against anything (see
252 /// [`new`](PrefabRequest::new)). The tuple field is public, so this is a reading
253 /// convenience rather than encapsulation: `req.0` reaches the same `String` and can
254 /// replace it.
255 pub fn name(&self) -> &str {
256 &self.0
257 }
258}
259
260impl std::fmt::Display for EntityName {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 write!(f, "{}", self.0)
263 }
264}
265
266/// Renderer-independent request for a mesh, as an asset key that the renderer's asset-loading
267/// pass turns into a GPU `Mesh` component.
268///
269/// The key is not simply a file path. The loader recognises the built-in primitives
270/// `"standard_cube"`, `"inverted_cube"`, `"plane"`, `"sphere"` and `"sprite_quad"`, the
271/// `"gltf_mesh_<file>.glb…"` / `"gltf_mesh_<file>.gltf…"` form for a mesh inside a glTF scene,
272/// and `"obj:<path>"`; anything else is treated as an OBJ path. Nothing validates the key when
273/// the component is attached, and a key that fails to load does not fail the frame: the entity
274/// is given a stand-in mesh and the failure is only logged.
275///
276/// Upload happens only while the entity has *no* `Mesh` yet, so this is a one-shot request:
277/// editing the string afterwards changes nothing until the `Mesh` component is removed.
278/// Keeping the key on the entity is also what lets a scene be saved back out.
279#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
280pub struct MeshSource(pub String);
281
282/// Renderer-independent material description, converted into a GPU `Material` by the renderer's
283/// asset-loading pass.
284///
285/// Like [`MeshSource`] this is consumed once — the conversion runs only while the entity has no
286/// `Material` component, so later edits to these fields do not reach the GPU material.
287///
288/// The numeric fields are carried through verbatim: nothing here clamps, normalises or
289/// colour-converts them, so the ranges named below are conventions rather than guarantees.
290#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
291pub struct MaterialSource {
292 /// Base colour and opacity as `[r, g, b, a]`, conventionally `0.0..=1.0` per channel.
293 ///
294 /// The fourth channel is the only opacity control on this struct — there is no separate
295 /// alpha, blend-mode or cutoff field. It is also the one channel group that has a texture
296 /// counterpart: [`texture_source`](Self::texture_source) is an *albedo* map, and no other
297 /// field here can be driven by a texture.
298 pub albedo: [f32; 4],
299 /// Microfacet roughness: `0.0` is a perfect mirror, `1.0` fully diffuse.
300 ///
301 /// A shading parameter, not a shading mode — which mode runs is decided solely by
302 /// [`unlit`](Self::unlit), and writing a roughness never changes it.
303 pub roughness: f32,
304 /// Metalness: `0.0` for a dielectric, `1.0` for a metal.
305 ///
306 /// Unlike [`roughness`](Self::roughness) this is conceptually two-valued rather than a
307 /// continuum — the two ends are the physical materials, and an intermediate number describes
308 /// a blend between them, which is normally only wanted where a texture crosses from one to
309 /// the other.
310 pub metallic: f32,
311 /// Shading mode selector, despite the name and the type: it is a *tri-state* float, not a
312 /// boolean.
313 ///
314 /// The renderer thresholds it — above `1.5` the material becomes a skybox, above `0.5` it
315 /// becomes unlit, and anything else (including `0.0`) is lit PBR. Values in between behave
316 /// like the lower bucket, and negative values are lit like zero.
317 ///
318 /// `MaterialSource` derives no `Default`, so there is no `..Default::default()` shorthand:
319 /// every literal must state this field, and `0.0` is the lit-PBR choice.
320 pub unlit: f32,
321 /// Path of the albedo texture to load, or `None` for an untextured material.
322 ///
323 /// `None` and a failed load are handled the same way — the material falls back to the
324 /// shared default white texture — except that a failure also logs a warning, so a
325 /// mistyped path renders as plain white rather than failing loudly.
326 pub texture_source: Option<String>,
327}
328
329impl_component!(Parent, Children, EntityName, IsHidden, EditorOnly, PrefabRequest, IsDeleted, MeshSource, MaterialSource);
330
331// ============================================================
332// Bundle Trait
333// ============================================================
334
335/// A group of components that can be attached to an entity as one unit.
336///
337/// Implemented blanket-wise for every [`Component`] (a lone component is a one-element bundle)
338/// and for tuples of bundles up to 16 elements, which nest freely — `(A, (B, C))` is a bundle.
339///
340/// There are two ways into the world and an implementor owns both:
341/// [`apply`](Bundle::apply) inserts the components one at a time through the world, and
342/// [`write_to_archetype`](Bundle::write_to_archetype) blits them directly into archetype
343/// columns. They are not interchangeable — the second cannot store `SparseSet` components at
344/// all — and the caller picks: `World::spawn_bundle` always goes through `apply`;
345/// `World::add_bundle` writes the archetype directly; `World::spawn_batch` spawns its first
346/// entity through `apply` to discover the archetype and appends the rest directly. Whenever
347/// [`get_infos`](Bundle::get_infos) declares a `SparseSet` member, the direct paths give up and
348/// fall back to `apply`.
349///
350/// Since `apply` has a do-nothing default, a hand-written bundle that implements only
351/// `write_to_archetype` compiles and then silently attaches nothing wherever the `apply` path
352/// is used. Implement both.
353pub trait Bundle {
354 /// Runtime metadata for every component this bundle will write, in the order it writes
355 /// them.
356 ///
357 /// A property of the type — it takes no `self` and is called before any data moves — so it
358 /// must describe exactly what `apply`/`write_to_archetype` produce. The world relies on it
359 /// twice: to work out the destination archetype, and to spot `SparseSet` members that make
360 /// the fast path unusable. Nested bundles simply concatenate their infos, and repeated
361 /// component types are *not* deduplicated here; the world folds duplicates away when it
362 /// builds the archetype's sorted type set, but a duplicate still means the same column is
363 /// written twice.
364 fn get_infos() -> Vec<crate::archetype::ComponentInfo>;
365 /// Moves the bundle's components straight into `arch`'s columns at `row`, bypassing the
366 /// world.
367 ///
368 /// The fast path, and a narrow one: it can only reach `Table`-storage components, it
369 /// updates no entity location and fires no hooks, and it takes `arch` as given rather than
370 /// choosing it — the caller is responsible for the row belonging to the right entity in the
371 /// right archetype.
372 ///
373 /// Each component is appended when its column is still short of `row`, and otherwise raw-
374 /// written into slot `row` — treating that slot as *uninitialised*, so an already-live value
375 /// there is overwritten without being dropped (a leak for anything owning an allocation).
376 /// Either way the row's ticks are reset to `tick` in both fields, i.e. it reads as freshly
377 /// added, not merely changed.
378 ///
379 /// # Safety
380 /// `arch` must contain the component columns that `Self::get_infos()` returns, and `_row`
381 /// must be a valid row reserved in this archetype. The data is copied raw; ownership is
382 /// transferred to the archetype.
383 unsafe fn write_to_archetype(self, arch: &mut crate::archetype::Archetype, _row: usize, tick: u32);
384 /// Attaches the bundle to an entity that already exists, component by component, through
385 /// `World::add_component`.
386 ///
387 /// The storage-agnostic route: each component reaches whichever store its
388 /// [`StorageType`] says, and the normal `on_add`/`on_set` hooks fire. It is the only route
389 /// that can place a `SparseSet` component, and the slow one — every table component the
390 /// entity does not already have migrates it to another archetype, so an n-component bundle
391 /// can pay n migrations where the direct path pays one.
392 ///
393 /// The default body does nothing at all. It exists so that a bundle which only supports the
394 /// archetype fast path still compiles; an implementor who forgets to override it gets an
395 /// entity with none of the components and no error.
396 fn apply(self, _world: &mut crate::world::World, _entity: crate::entity::Entity) where Self: Sized {}
397}
398
399/// A bundle with one extra component appended, as produced by [`BundleExt::with`].
400///
401/// Composition is purely type-level: `get_infos` lists `B`'s components followed by `C`, and
402/// `write_to_archetype` writes `B` first and `C` second. If `C` also occurs in `B` the later
403/// write wins by overwriting the slot in place — without dropping what was there, which leaks
404/// whatever that value owned. An archetype with no column for `C` (a `SparseSet` `C`, or simply
405/// the wrong archetype) is a bare `unwrap` panic, without the explanation the single-component
406/// path prints.
407///
408/// **Known limitation.** This type does not override [`Bundle::apply`], so it inherits the
409/// no-op default: passing a `DynamicBundle` to `World::spawn_bundle` — or to `World::add_bundle`
410/// when it contains a `SparseSet` component — produces an entity with *none* of the components,
411/// silently. Only the direct archetype path (`World::add_bundle` with all-table components)
412/// stores anything. Nothing in this workspace constructs a `DynamicBundle`; treat it as
413/// experimental until `apply` is implemented.
414pub struct DynamicBundle<B: Bundle, C: Component> {
415 /// The base bundle, written before `component` and listed first in `get_infos`.
416 pub bundle: B,
417 /// The appended component, written last — so on a type collision with `bundle` this is the
418 /// value that survives.
419 pub component: C,
420}
421
422impl<B: Bundle, C: Component> Bundle for DynamicBundle<B, C> {
423 fn get_infos() -> Vec<crate::archetype::ComponentInfo> {
424 let mut infos = B::get_infos();
425 infos.push(crate::archetype::ComponentInfo::of::<C>());
426 infos
427 }
428
429 unsafe fn write_to_archetype(self, arch: &mut crate::archetype::Archetype, row: usize, tick: u32) {
430 self.bundle.write_to_archetype(arch, row, tick);
431 let col = arch.get_column_mut(std::any::TypeId::of::<C>()).unwrap();
432 if col.len() <= row {
433 col.push_raw(&self.component as *const _ as *const u8, tick);
434 std::mem::forget(self.component);
435 } else {
436 let ptr = col.get_mut_ptr(row) as *mut C;
437 std::ptr::write(ptr, self.component);
438 *col.ticks_ptr_mut().add(row) = crate::archetype::ComponentTicks::new(tick);
439 }
440 }
441}
442
443/// Chaining sugar for growing a bundle one component at a time.
444///
445/// Blanket-implemented for every [`Bundle`], so `a.with(b).with(c)` type-checks for any
446/// components; the result is a nest of [`DynamicBundle`]s, whose limitations apply — read them
447/// before using this.
448pub trait BundleExt: Bundle + Sized {
449 /// Appends `component` to this bundle and returns the combined value.
450 ///
451 /// Pure value construction: nothing touches a world until the result is spawned or added,
452 /// and both operands are moved. It is additive, never a replacement — appending a component
453 /// type the bundle already carries produces a bundle listing that type twice rather than
454 /// substituting the earlier one.
455 fn with<C: Component>(self, component: C) -> DynamicBundle<Self, C> {
456 DynamicBundle { bundle: self, component }
457 }
458}
459
460impl<T: Bundle> BundleExt for T {}
461
462impl<T: Component> Bundle for T {
463 fn get_infos() -> Vec<crate::archetype::ComponentInfo> {
464 vec![crate::archetype::ComponentInfo::of::<T>()]
465 }
466
467 fn apply(self, world: &mut crate::world::World, entity: crate::entity::Entity) {
468 world.add_component(entity, self);
469 }
470
471 unsafe fn write_to_archetype(self, arch: &mut crate::archetype::Archetype, row: usize, tick: u32) {
472 let col = arch.get_column_mut(std::any::TypeId::of::<T>()).unwrap_or_else(|| {
473 panic!(
474 "Component column for `{}` missing in Archetype. The bundle fast-path \
475 (write_to_archetype) only handles Table-storage components; SparseSet \
476 components must be routed via World::add_component. spawn_batch already \
477 falls back for sparse bundles — reaching here means another bundle path \
478 wrote a sparse component into the archetype.",
479 std::any::type_name::<T>()
480 )
481 });
482 if col.len() <= row {
483 col.push_raw(&self as *const _ as *const u8, tick);
484 std::mem::forget(self);
485 } else {
486 let ptr = col.get_mut_ptr(row) as *mut T;
487 std::ptr::write(ptr, self);
488 *col.ticks_ptr_mut().add(row) = crate::archetype::ComponentTicks::new(tick);
489 }
490 }
491}
492
493macro_rules! impl_bundle_tuple {
494 ($($name:ident),*) => {
495 #[allow(non_snake_case)]
496 impl<$($name: crate::component::Bundle),*> Bundle for ($($name,)*) {
497 fn get_infos() -> Vec<crate::archetype::ComponentInfo> {
498 let mut infos = Vec::new();
499 $(
500 infos.extend(<$name as crate::component::Bundle>::get_infos());
501 )*
502 infos
503 }
504
505 fn apply(self, world: &mut crate::world::World, entity: crate::entity::Entity) {
506 let ($($name,)*) = self;
507 $(
508 $name.apply(world, entity);
509 )*
510 }
511
512 unsafe fn write_to_archetype(self, arch: &mut crate::archetype::Archetype, row: usize, tick: u32) {
513 let ($($name,)*) = self;
514 $(
515 $name.write_to_archetype(arch, row, tick);
516 )*
517 }
518 }
519 };
520}
521
522impl_bundle_tuple!(A);
523impl_bundle_tuple!(A, B);
524impl_bundle_tuple!(A, B, C);
525impl_bundle_tuple!(A, B, C, D);
526impl_bundle_tuple!(A, B, C, D, E);
527impl_bundle_tuple!(A, B, C, D, E, F);
528impl_bundle_tuple!(A, B, C, D, E, F, G);
529impl_bundle_tuple!(A, B, C, D, E, F, G, H);
530impl_bundle_tuple!(A, B, C, D, E, F, G, H, I);
531impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J);
532impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K);
533impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
534impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
535impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
536impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
537impl_bundle_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
538
539
540#[cfg(test)]
541mod editor_only_tests {
542 use super::*;
543
544 /// The predicate itself, both halves.
545 #[test]
546 fn the_marker_wins_and_the_legacy_names_still_count() {
547 // The marker alone is enough — no name needed, which is the point of having it.
548 assert!(is_editor_only(true, None));
549 assert!(is_editor_only(true, Some("Enemy")));
550
551 // Legacy: scenes written before the marker existed carry only names.
552 assert!(is_editor_only(false, Some("Editor Grid")));
553 assert!(is_editor_only(false, Some("Editor Light Icon 1")));
554 assert!(is_editor_only(false, Some("Highlight Box")));
555
556 // Content stays content.
557 assert!(!is_editor_only(false, Some("Enemy")));
558 assert!(!is_editor_only(false, None));
559 assert!(
560 !is_editor_only(false, Some("Editor")),
561 "the legacy rule needs the trailing space; a scene object merely named \"Editor\" is \
562 not tooling"
563 );
564 assert!(
565 !is_editor_only(false, Some("My Editor Desk")),
566 "the prefix is anchored — only names that START with it"
567 );
568 }
569
570 /// The name rule's failure mode, stated so it is not mistaken for a design.
571 ///
572 /// A scene object legitimately named "Editor Desk" is invisible in the hierarchy and dropped
573 /// from every save. That is why the marker exists; the rule survives only for old scenes.
574 #[test]
575 fn the_legacy_name_rule_still_swallows_a_legitimately_named_object() {
576 assert!(
577 is_editor_only(false, Some("Editor Desk")),
578 "documented wart: this is what the marker is for"
579 );
580 }
581
582 /// Nothing may re-implement the rule.
583 ///
584 /// It lived in **eight** places — the hierarchy panel (twice), the windowed app's editor
585 /// runtime, two filters in `gizmo-scene`'s snapshot, one in its scene writer, the studio's
586 /// protected-entity set, its delete guard, its select-all shortcut and its play-mode hide.
587 /// Eight copies of one string comparison, and every one of them had to agree about the
588 /// trailing space.
589 #[test]
590 fn the_editor_only_rule_is_written_once() {
591 let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
592 .parent()
593 .and_then(|p| p.parent())
594 .expect("crates/gizmo-core sits two levels below the workspace root")
595 .to_path_buf();
596 if !workspace.join("crates/gizmo-studio").is_dir() {
597 return; // packaged crate
598 }
599
600 let mut sources = Vec::new();
601 collect_rs(&workspace.join("crates"), &mut sources);
602 collect_rs(&workspace.join("demo"), &mut sources);
603 assert!(sources.len() > 100, "source walk found only {} files", sources.len());
604
605 let this_file = std::path::Path::new(file!()).file_name().unwrap();
606 let mut offenders = Vec::new();
607 for path in &sources {
608 if path.file_name() == Some(this_file) {
609 continue;
610 }
611 let text = std::fs::read_to_string(path).unwrap_or_default();
612 // Production code only. A test that asserts "this got filtered out" names the strings
613 // on purpose and is checking the outcome, not re-deciding it — `gizmo-scene`'s save
614 // test does exactly that. Cutting at the first `#[cfg(test)]` is approximate and
615 // deliberately so: the cost of a miss is a test module that could re-implement the
616 // rule unnoticed, and a test module that did would be caught by its own assertions
617 // disagreeing with production.
618 let code = text.split("#[cfg(test)]").next().unwrap_or("");
619 for (i, line) in code.lines().enumerate() {
620 let t = line.trim_start();
621 if t.starts_with("//") || t.starts_with("///") {
622 continue;
623 }
624 if line.contains("starts_with(\"Editor \")") || line.contains("== \"Highlight Box\"") {
625 offenders.push(format!("{}:{}", path.display(), i + 1));
626 }
627 }
628 }
629 assert!(
630 offenders.is_empty(),
631 "the editor-only rule must be asked of `component::is_editor_only`, not re-written. \
632 Offenders:\n{}",
633 offenders.join("\n")
634 );
635 }
636
637 fn collect_rs(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
638 let Ok(entries) = std::fs::read_dir(dir) else { return };
639 for entry in entries.flatten() {
640 let path = entry.path();
641 if path.is_dir() {
642 if path.file_name().is_some_and(|n| n == "target") {
643 continue;
644 }
645 collect_rs(&path, out);
646 } else if path.extension().is_some_and(|e| e == "rs") {
647 out.push(path);
648 }
649 }
650 }
651}