# Internals (/docs/advanced/internals)



Entities [#entities]

| Entity              | Location               | Role                                                                                                                                                                                                                                                                                                 |
| ------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ThreeStart`        | `ThreeStart.ts`        | Orchestrator. Owns the single `ThreeContext`. Owns lifecycle (mount/runLoop/stopLoop/unmount/dispose). Owns module registration and the `start()` bootstrap.                                                                                                                                         |
| `ThreeContext`      | `ThreeContext.ts`      | Per-scene runtime: renderer, scene, camera, timer, render pipeline, event bus (extends `TypedEmitter`), modules registry. Exposed to behaviours/modules via `this.ctx`. Lifecycle methods (`mount`/`unmount`/`runLoop`/`stopLoop`/`dispose`) are `@internal` — only called by `ThreeStart` wrappers. |
| `Object3DExtension` | `Object3DExtension.ts` | Per-`Object3D` sidecar: list of attached `Object3DBehaviour`s, cached `ThreeContext` pointer, own active flag (`activeSelf`), `activeInHierarchy` getter, cascade traversal helpers.                                                                                                                 |
| `Object3DBehaviour` | `Object3DBehaviour.ts` | User-defined component attached to an `Object3D`. Has full lifecycle.                                                                                                                                                                                                                                |
| `ContextModule`     | `ContextModule.ts`     | User-defined global singleton attached to the context. Has a subset of lifecycle.                                                                                                                                                                                                                    |
| `TypedEmitter`      | `TypedEmitter.ts`      | Tiny base providing `on`/`off`/`once` (public) and `emit` (protected). Wraps `eventemitter3` lazily — inner emitter is allocated on first subscription, so unused emitters cost only one nullable slot. Extended by `ThreeContext`, `ContextModule`, `Object3DBehaviour`.                            |
| Operations          | `methods.ts`           | Top-level imperative functions: `addComponent`, `getComponent`, `getComponents`, `setActive`, `getIsActive`, `getIsActiveSelf`, `destroy`. All delegate into `Object3DExtension`. (`attachContext` is re-exported from `Object3DExtension.ts` directly via `index.ts`.)                              |

Object3DExtension attachment [#object3dextension-attachment]

* Stored on the Three.js `Object3D` under the non-enumerable symbol `EXT = Symbol.for("three-start:ext")`.
* Created lazily by `ensureExtension(obj)`. Read by `getExtension(obj)` (returns `null` if missing).
* `attachContext(obj, ctx)` calls `ensureExtension(obj)` and assigns `ext.context = ctx`. In normal flow this is done exactly once on the scene root by `ThreeStart.start()`.

Context resolution [#context-resolution]

`Object3DExtension.resolveContext()` walks up the `.parent` chain looking for the first ancestor whose extension has `.context` set, caches the result on `this.context`, and returns it. A component inside the scene therefore shares the context of whatever ancestor has it attached — in practice the scene root, after `ThreeStart.start()` has run.

A component whose object is not yet in a hierarchy with an attached context has `resolveContext() === null`. It is stored on the extension but remains dormant (no `_ctx`, no `_activate` call) until the object joins such a hierarchy.

Component state flags [#component-state-flags]

| Field                | Meaning                                                                                                                                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_object`            | Back-pointer to the `Object3D`. `null` after `_destroy()`.                                                                                                                                                      |
| `_ext`               | Back-pointer to the owning `Object3DExtension`.                                                                                                                                                                 |
| `_ctx`               | `ThreeContext` once resolved; `null` before that.                                                                                                                                                               |
| `_awoken`            | `true` after `onAwake` has fired. Never reset.                                                                                                                                                                  |
| `_started`           | `true` after `onStart` has fired. Never reset.                                                                                                                                                                  |
| `_isActive`          | `true` while the component is currently subscribed to ctx events. Set by `_activate` / cleared by `_deactivate`. Tracks the effective lifecycle state (object's `activeInHierarchy` AND component's `enabled`). |
| `_enabled` (private) | User-facing `enabled` flag, controlled via `enable()`/`disable()`/`setEnabled()`.                                                                                                                               |

Extension state [#extension-state]

| Field                      | Meaning                                                                                                                                                                                                                   |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `components`               | Array of `Object3DBehaviour`s in insertion order.                                                                                                                                                                         |
| `context`                  | The `ThreeContext` this subtree is bound to, or `null` if unresolved.                                                                                                                                                     |
| `_active`                  | The object's own active flag. Default `true`. Toggled via `setActive(active)`. Whether components actually run depends on the full chain — see `activeInHierarchy` below and the [Cascade algorithm](#cascade-algorithm). |
| `activeSelf` getter        | Reads `_active`. Public accessor for the own flag.                                                                                                                                                                        |
| `activeInHierarchy` getter | `_active` AND every ancestor's `activeSelf` is true. Computed via `parentChainActive(this.object)` walk.                                                                                                                  |

Context state [#context-state]

`ThreeContext` keeps a few internal slots beyond the public surface:

* `_isBootstrapping: boolean` — `true` only during `ThreeStart.start()` Phase 0 (module bootstrap). Read by `Object3DExtension.addComponent` to decide whether to defer activation. `@internal`, stripped from `.d.ts` output.
* `_modules: Record<string, ContextModule>` — the actual mutable backing store for registered modules. Only `_registerModule(key, instance)` writes to it.
* `_renderFn / _srcRenderFn` — the current render function and the original. `overrideRender(fn)` swaps `_renderFn`; `resetRender()` restores from `_srcRenderFn`.
* `_timer: THREE.Timer` — drives `getDeltaTime` / `getTime` / `getTimescale` / `setTimescale`. Connected to `document` on `runLoop`, disconnected/reset on `stopLoop`.
* `_camera`, `_canvasContainer`, `_resizeObserver`, `_isMounted`, `_isLoopRunning`, `_requestAnimationFrame` — backing state for the corresponding getters / lifecycle methods.

Sealed runtime fields [#sealed-runtime-fields]

`ThreeContext` constructor uses `defineProps + readOnly` to make the following fields **non-writable** and **non-configurable** at runtime, in addition to the TS `readonly` modifier:

* `isThreeContext` (discriminator marker)
* `modules` (the Proxy view, see below)
* `renderer`, `scene`, `scenePass`, `renderPipeline`

Result: `ctx.scene = otherScene`, `ctx.renderer = fake`, `ctx.modules = {}` all throw `TypeError`. This is hard runtime protection against third-party module/component code rebinding core context state. The TS `readonly` modifier alone is bypassable via `as any`; `defineProperty(..., { writable: false, configurable: false })` is not.

Modules registry — read-only Proxy view [#modules-registry--read-only-proxy-view]

The public `ctx.modules` field is **not** the backing store directly. It is a `Proxy` over `this._modules` whose only allowed operation is reads:

```
get → forwards to target (default trap)
set / deleteProperty / defineProperty / setPrototypeOf → throw
```

The Proxy is built via `createReadonlyView(this._modules, "ctx.modules")` (in `utils/define-props.ts`), then sealed onto the instance via `defineProps(this, { modules: readOnly(view) })`.

The only legitimate way to add modules is `ctx._registerModule(key, instance)` — `@internal`, called by `ThreeStart.addModules()`. It writes to the Proxy's target (`_modules`) directly, bypassing the readonly view, and returns `false` if the key already exists.

Threat model: third-party plugin code might try `ctx.modules.foo = bar` (typo, malice, or to monkey-patch). All such mutations throw with a clear error. `addModules()` remains the only path.

`ThreeStart.start()` phases [#threestartstart-phases]

A single idempotent call, guarded by `this._started`.

```
attachContext(root, ctx)

ctx._isBootstrapping = true      ← gate ON
  for m of modules: m._ctx = ctx
  for m of modules: m.onAwake()
  for m of modules: m.onStart()
  for m of modules: m._subscribe()
ctx._isBootstrapping = false     ← gate OFF

Phase 1 (traverseActiveSelf): for every component on every active-in-hierarchy object
  ensure comp._ctx, call onAwake (once)

Phase 2 (traverseActiveSelf): for every component on every active-in-hierarchy object
  if enabled: comp._activate()

listenForChildren(root)          ← wire childadded for dynamic additions
```

Bootstrap gate [#bootstrap-gate]

While `ctx._isBootstrapping === true`, `Object3DExtension.addComponent` performs its usual work — instantiates the component, sets `_object`/`_ext`/`_ctx`, pushes onto `ext.components` — but **skips** the `_activate()` call. The component lies dormant until Phase 1/2 traversal awakens and activates it.

Why: a module's `onAwake`/`onStart` may freely add Three.js objects with components attached (directly or via helper methods). Without the gate, those components would auto-activate mid-bootstrap and subscribe to ctx events **before** modules finish their own subscription step. The gate guarantees modules are always earlier in the `eventemitter3` listener list than any component activated during or after bootstrap.

The gate is not exposed publicly; `addComponent` reads it via `this.context?._isBootstrapping`. `addComponent` never activates across contexts.

Module lifecycle [#module-lifecycle]

Modules have a strict subset of `Object3DBehaviour`'s lifecycle:

```
onAwake   — self-init; other modules may not yet have awoken
onStart   — cross-module setup; all modules have been onAwake'd
onUpdate
onBeforeRender
onAfterRender
```

No `onEnable`/`onDisable` (modules are always on once registered) and no `onDestroy` (the core does not dispose modules; the user tears down via `ctx.dispose()` manually).

Per-frame handlers are only subscribed if the subclass overrides the method. `ContextModule._subscribe` compares each handler against the prototype's no-op body (`this.onUpdate !== proto().onUpdate`) and calls `ctx.on(...)` only for overridden methods. `_unsubscribe` mirrors this with `ctx.off(...)`.

Component lifecycle [#component-lifecycle]

Full lifecycle, per instance:

```
onAwake   (once, ever)
onEnable  (every (re)activation)
onStart   (once, ever)
onUpdate / onBeforeRender / onAfterRender  (per frame while active)
onDisable (every deactivation)
onDestroy (once, at destroy)
```

Implemented in `Object3DBehaviour._activate()`:

```
if already active: return
if no ctx: return
_isActive = true
if !_awoken:  _awoken = true;  onAwake()
onEnable()
if !_started: _started = true; onStart()
_subscribe()
```

Subscription is last, so a component's per-frame method can never fire before its own `onStart`.

`_deactivate()` calls `_unsubscribe()` + `onDisable()` and flips `_isActive = false`. `_destroy()` calls `_deactivate()`, then `onDestroy()`, then `_removeAllListeners()` on the inner emitter (provided by `TypedEmitter`), then nulls out back-pointers.

Component construction and `addComponent` typing [#component-construction-and-addcomponent-typing]

`addComponent(obj, Klass, ...args)` forwards `...args` to `new Klass(...args)`. The constructor parameter types are inferred from the component class via the second type parameter on `Object3DBehaviourConstructor`:

```ts
type Object3DBehaviourConstructor<
  T extends Object3DBehaviour = Object3DBehaviour,
  TArgs extends any[] = any[],
> = new (...args: TArgs) => T;

function addComponent<T extends Object3DBehaviour, TArgs extends any[]>(
  obj: THREE.Object3D,
  klass: Object3DBehaviourConstructor<T, TArgs>,
  ...args: TArgs
): T;
```

Behaviour:

* A component without a custom constructor (`class A extends Object3DBehaviour {}`) infers `TArgs = []`, so `addComponent(obj, A)` requires no extra args.
* A component with a custom constructor (`class B extends Object3DBehaviour { constructor(x: number, y: string) { super(); ... } }`) infers `TArgs = [number, string]`, and `addComponent(obj, B, 1, "hi")` is type-checked end-to-end (arity, types, optionality).
* The default `TArgs = any[]` on `Object3DBehaviourConstructor` keeps callers that only need the instance type — `getComponent(obj, MyComp)`, `getComponents(obj, MyComp)` — free of an extra type parameter; their signatures don't need `TArgs` at all.

`Object3DExtension.addComponent` mirrors the same generic and instantiates with `new klass(...args)`. The lifecycle wiring (set `_object` / `_ext` / `_ctx`, push to `components`, conditional `_activate()`) is unchanged regardless of constructor shape — `_object`, `_ctx`, and `_ext` are still assigned **after** the constructor returns, so the existing rule "don't read `this.object` / `this.ctx` from constructors or field initializers" still holds.

When does a component activate? [#when-does-a-component-activate]

`Object3DExtension.addComponent` activates immediately iff **all** are true:

1. `component._ctx` resolved (object is in a hierarchy with attached ctx)
2. `ext.activeInHierarchy` (the extension's own flag is true AND no ancestor was `setActive(false)`'d)
3. `component.enabled`
4. `!ctx._isBootstrapping` (not inside `ThreeStart.start()` Phase 0)

If any condition fails, the component is still appended to `ext.components`. It will be activated later by:

* Phase 2 of `start()` — when bootstrap finishes.
* `childadded` flow — when the object joins a hierarchy with ctx (see below).
* `ext.setActive(true)` — if the extension was inactive (or an ancestor's cascade reaches the object).
* `comp.enable()` — if the component was disabled.

Dynamic additions after `start()` [#dynamic-additions-after-start]

`ThreeStart.listenForChildren(obj)` registers a `childadded` listener on `obj` and every existing descendant. On event, `onChildAdded` traverses the new subtree with `_callback(node)`:

```
_callback(node):
  ext = getExtension(node)
  if ext && !ext.context: ext.resolveContext()
  bootstrapNode(node)
  node.addEventListener("childadded", onChildAdded)  ← keep chain alive
```

`bootstrapNode(node)` resolves context if missing, then for each enabled component calls `_activate()`. Since bootstrap is already done (gate is off), activation is immediate and components subscribe in the natural order: after existing subscribers.

This means: dynamically spawning objects during gameplay (e.g. from a module's `onUpdate`) activates their components immediately, with subscription order equal to insertion order.

`setActive` and `visible` [#setactive-and-visible]

`setActive` is **lifecycle-only**: it controls whether components run, nothing more. It does NOT mutate `object.visible`. Hiding a deactivated subtree from rendering is the caller's responsibility — set `obj.visible = false` yourself if that's what you want; Three.js cascades the flag through descendants at render time.

This was a deliberate change from an earlier design that overrode `Object3D.prototype.visible` via `defineProperty`. The override coupled two unrelated concerns (lifecycle vs. rendering) and was surprising for users who expected `visible` to mean exactly what Three.js says it means. Decoupling them keeps the lib's mutation surface small and predictable.

Cascade algorithm [#cascade-algorithm]

`setActive(obj, ...)` exposes two notions of active: `activeSelf` (the own flag) and `activeInHierarchy` (own AND every ancestor active). Public surface in `methods.ts`:

| Function                 | Returns / does                                                                         |
| ------------------------ | -------------------------------------------------------------------------------------- |
| `setActive(obj, active)` | Sets `obj`'s `activeSelf`. Cascades through the subtree (see below).                   |
| `getIsActive(obj)`       | `activeInHierarchy` (own + chain). For objects without an extension, treats as `true`. |
| `getIsActiveSelf(obj)`   | `activeSelf` (own flag). For objects without an extension, returns `true` (default).   |

Helpers exported from `Object3DExtension.ts` (all `@internal`):

* `parentChainActive(obj)` — walks `obj.parent` up to the scene root; returns `false` if any ancestor's `activeSelf` is `false`. Ancestors with no extension count as active. `obj` itself is **not** checked.
* `isActiveInHierarchy(obj)` — combines `getExtension(obj)?.activeSelf ?? true` with `parentChainActive(obj)`.
* `traverseActiveSelf(root, cb)` — walks `root` and descendants, invokes `cb` on each node whose own `activeSelf` is true (or that has no extension), pruning at any inactive node. Caller must guarantee `root`'s parent chain is active.

`Object3DExtension.setActive(active)` algorithm:

```
if (this._active === active) return;             ← idempotence
this._active = active;

if (!parentChainActive(this.object)) return;     ← chain wasn't active, nothing to cascade

if (active) activateSubtree(this.object);
else        deactivateSubtree(this.object);
```

The chain check is the key invariant: if any ancestor was already inactive, this object's effective state didn't change (it was inactive before the flip, it's inactive after) — so no components need to be (de)activated. The flag is recorded on `_active`; the cascade fires only when crossing the effective-state boundary.

`activateSubtree(root)` and `deactivateSubtree(root)` (private to `Object3DExtension.ts`):

```
activateSubtree(root):
  // root.activeSelf is now true → traverseActiveSelf descends through it.
  traverseActiveSelf(root, activateNode)

deactivateSubtree(root):
  // root.activeSelf was just set to false; traverseActiveSelf would stop at root.
  // Process root explicitly, then descend into each child via traverseActiveSelf.
  deactivateNode(root)
  for c of root.children: traverseActiveSelf(c, deactivateNode)
```

`activateNode(node)` resolves context if missing, then for each component on the node assigns `comp._ctx` (if unset) and calls `_activate()` if `enabled`. `deactivateNode(node)` calls `_deactivate()` on every currently `_isActive` component.

Awake/start flags (`_awoken`, `_started`) are never reset. Subsequent activations re-fire only `onEnable` and render loop event methods — `onAwake` and `onStart` stay one-shot per instance.

The cascade prunes at any descendant whose own `activeSelf` is `false` — that subtree was already effectively inactive and stays so even after the ancestor flips on. To bring a pruned subtree back, call `setActive` on its own root.

Event subscription order [#event-subscription-order]

`eventemitter3` dispatches listeners in registration order. The core guarantees:

| Order of subscription within `ctx`                      | When                                |
| ------------------------------------------------------- | ----------------------------------- |
| Module handlers                                         | Last step of Phase 0 in `start()`   |
| Component handlers activated in Phase 2                 | After all modules subscribed        |
| Component handlers activated via `childadded`           | After Phase 2 finished, at add time |
| Component handlers from `enable()` or `setActive(true)` | At call time, appended to end       |

Per-frame handler order at emit time therefore reflects:

```
modules (in registration order) → components activated in Phase 2 (traversal order)
→ dynamically added components (chronological)
```

Ordering guarantees provided [#ordering-guarantees-provided]

1. `module.onAwake` and `module.onStart` always run before **any** `component.onAwake`/`onStart`.
2. A module's per-frame handlers always run before its corresponding `component` handlers within the same tick (for all components activated during or before the module's subscription).
3. A module's per-frame handlers cannot fire before the module's own `onStart`, even if user code manually emits `Update`/`RenderBefore`/`RenderAfter` during initialization — subscription is strictly the last step of module bootstrap.
4. A component's per-frame handlers cannot fire before its own `onStart` — subscription is the last step of `_activate()`.
5. `onAwake` fires exactly once per component instance. `onStart` fires exactly once per component instance. Toggling enabled/active re-fires only `onEnable`/`onDisable`.

Modules registry — registration flow [#modules-registry--registration-flow]

* `ThreeContext.modules` is a sealed read-only Proxy (see "Modules registry — read-only Proxy view" above), typed as `ThreeStartModules` (Register-pattern extensible). The backing `_modules` store is initialized to `{}` at construction.
* `ThreeStart.addModules(map)` is the **only** public registration entry. It iterates the map and calls `ctx._registerModule(key, instance)` per entry.
* `addModules` throws if `this._started` is `true` (cannot register modules after bootstrap). Duplicate keys log `console.warn` and skip — `_registerModule` returns `false` to signal the skip.
* Consumers read modules via `this.ctx.modules`, `this.modules` (behaviour), or `this.modules` (module). The latter two are getters that delegate to `this._ctx!.modules`.

Register pattern [#register-pattern]

`Register.ts` defines:

```ts
export interface ThreeStartRegister {}
export type RegisterField<K extends string, Fallback = unknown> =
  K extends keyof ThreeStartRegister ? ThreeStartRegister[K] : Fallback;
```

`ThreeStartModules = RegisterField<"modules", Record<string, ContextModule>>` resolves to the strongly-typed module map when the consumer augments `ThreeStartRegister["modules"]`, or falls back to the loose record type otherwise.

The same Register interface is the extension point for any future strongly-typed registries (e.g. typed events, typed components). Consumers augment via standard module augmentation:

```ts
declare module "three-start" {
  interface ThreeStartRegister {
    modules: { input: InputModule; physics: PhysicsModule };
  }
}
```

Beyond `ThreeStartRegister`, three-start's exported classes (`Object3DBehaviour`, `ContextModule`, `ThreeContext`) are also valid targets for declaration merging. A user can extend their instance interface with optional methods:

```ts
declare module "three-start" {
  interface Object3DBehaviour {
    onPointerDown?(e: PointerEvent): void;
  }
}
```

This adds the optional method to the instance type of every subclass — letting a custom dispatcher module call `comp.onPointerDown?.(event)` with full type safety, without library-side changes.

Cycles and module load order [#cycles-and-module-load-order]

The core has two latent type-level cycles, both kept harmless by `import type`:

1. **`ContextModule.ts` ↔ `ThreeContext.ts`**
   * `ContextModule.ts` imports `ThreeContextEvents` (enum — **value**) from `ThreeContext.ts`. Runtime dependency.
   * `ContextModule.ts` also imports `type ThreeContext` (type only). Erased.
   * `ThreeContext.ts` imports `type ContextModule, ThreeStartModules` from `ContextModule.ts`. Type-only — erased.
   * Actual load order: `ThreeContext.ts` first, `ContextModule.ts` second.

2. **`ThreeContext.ts` ↔ `ThreeStart.ts`**
   * `ThreeStart.ts` imports `ThreeContext` (class — **value**) from `ThreeContext.ts`. Runtime.
   * `ThreeContext.ts` imports `type ThreeStartOptions` from `ThreeStart.ts`. Type-only — erased.
   * Actual load order: `ThreeContext.ts` first, `ThreeStart.ts` second.

**Do not turn any of these type imports into value imports.** Doing so creates a real ESM cycle and at minimum spawns hard-to-debug undefined references at module init.

Lifecycle ownership: ThreeStart vs ThreeContext [#lifecycle-ownership-threestart-vs-threecontext]

`ThreeContext` exposes lifecycle methods `mount`, `unmount`, `runLoop`, `stopLoop`, `dispose` — but each is marked `@internal` and stripped from `.d.ts`. The public surface for runtime control lives on `ThreeStart`:

| Public on `ThreeStart` | Internal on `ThreeContext` | What it does                                                                        |
| ---------------------- | -------------------------- | ----------------------------------------------------------------------------------- |
| `mount(container)`     | `ctx.mount(container)`     | Append canvas, attach resize observer, optionally start loop.                       |
| `unmount()`            | `ctx.unmount()`            | Remove canvas, disconnect observer, optionally stop loop.                           |
| `runLoop()`            | `ctx.runLoop()`            | Connect timer to `document`, call `renderer.setAnimationLoop(...)`, emit `LoopRun`. |
| `stopLoop()`           | `ctx.stopLoop()`           | Disconnect timer, set animation loop `null`, emit `LoopStop`.                       |
| `dispose()`            | `ctx.dispose()`            | Tear down: unmount, stopLoop, dispose renderer + timer, removeAllListeners.         |

This split exists to enforce a clean access boundary: third-party components/modules holding `this.ctx` cannot call `mount`/`runLoop`/etc. by accident or design — the type system simply doesn't expose them. The ThreeStart wrappers are thin: each just forwards to the corresponding `ctx` method and returns `this` for chaining.

Camera swap mechanics [#camera-swap-mechanics]

Setting `ctx.camera = newCamera` triggers `cameraChanged(new, prev)` (private), which:

1. Reassigns `this.scenePass.camera = newCamera` — the render pipeline reads `scenePass.camera` per frame, so this is what actually swaps the rendered viewpoint.
2. If `newCamera.parent === null`, calls `this.scene.add(newCamera)` — matches the constructor behaviour for a floating default camera.
3. Updates `aspect` from current container dimensions and calls `updateProjectionMatrix()`.
4. Emits `CameraChanged` with `[newCamera, prevCamera]`.
5. Calls `requestRender()` to redraw at next frame.

`PassNode.camera` is a plain instance field read each frame — direct assignment is sufficient, no need to rebuild `scenePass` / `renderPipeline` (both are sealed via `defineProps + readOnly` so couldn't be replaced anyway).

Idempotence and invariants [#idempotence-and-invariants]

* `ThreeStart.start()` is idempotent: second call is a no-op (`this._started` guard).
* `ensureExtension(obj)` is idempotent: returns the existing extension if present.
* `attachContext(obj, ctx)` overwrites `ext.context` (no guard). Multiple attaches are a user error but not blocked.
* `ext.setActive(v)` with `v === ext._active` is a no-op.
* `comp.setEnabled(v)` with `v === this._enabled` is a no-op.
* `_subscribe` / `_unsubscribe` are safe to call even when the method is not overridden (no listener added → no-op `off`).

`destroy()` [#destroy]

`methods.ts::destroy`:

* Behaviour target: calls `ext.destroyComponent(comp)` (which splices from `components` + `_destroy`); falls back to `comp._destroy()` if no ext.
* Object target: traverses the subtree destroying all components on every extension, then `removeFromParent()`. Extensions themselves are not unset — they stay on the detached `Object3D` and can be re-used if the object is re-added.

TypedEmitter [#typedemitter]

`TypedEmitter<T extends EventMap = {}>` is the base class behind every emitter in the core (`ThreeContext`, `ContextModule`, `Object3DBehaviour`).

Surface:

* `on(event, fn, context?) → this` — public.
* `once(event, fn, context?) → this` — public.
* `off(event, fn?, context?, once?) → this` — public.
* `emit(event, ...args) → boolean&#x60; — &#x2A;*`protected`**. Only the owning class fires its own events; external code physically cannot.
* `_removeAllListeners()` — `@internal`, used by destroy paths.

Internals:

* One nullable field `_ee?: EventEmitter` (from `eventemitter3`). Allocated lazily on first `on`/`once`. Classes that extend `TypedEmitter` but never receive a subscriber pay only one reference slot per instance — important when there can be thousands of `Object3DBehaviour` instances.
* `emit` short-circuits via `this._ee?.emit(...) ?? false` — fire-and-forget without listeners is a no-op, no allocation.
* All public methods take a generic `K extends keyof T & (string | symbol)` so the event name and listener payload (`...args: T[K]`) are checked at compile time. Default `T = {}` makes both `keyof T = never` — i.e. a class without an explicit event map cannot legitimately call `on` / `emit` (compile error). This is the type-level opt-in pattern.

Combined with `protected emit` + lazy allocation, `TypedEmitter` gives strict encapsulation, type safety, and zero cost when unused — all without a wrapper class on the user side.

Source layout [#source-layout]

```
src/core/
├── index.ts                  re-exports the public surface
├── ThreeStart.ts             entry point + lifecycle wrappers
├── ThreeContext.ts           shared runtime, sealed fields, modules Proxy, ThreeContextEvents enum + ThreeContextEventMap
├── ContextModule.ts          base class for global systems
├── Object3DBehaviour.ts      base class for components (+ Object3DBehaviourConstructor<T, TArgs>)
├── Object3DExtension.ts      per-Object3D sidecar, ensureExtension/getExtension/attachContext
├── TypedEmitter.ts           lazy-allocating typed emitter base
├── Register.ts               ThreeStartRegister + RegisterField helper
├── methods.ts                top-level operations: addComponent / getComponent / setActive / destroy / ...
├── react/
│   ├── ThreeRendererMount.tsx  React component that mounts a ThreeStart instance into a div
│   └── index.ts
└── utils/
    ├── define-props.ts       defineProps, readOnly, notEnumer, createReadonlyView
    └── assert-defined.ts
```


# ContextModule (/docs/api/context-module)





`ContextModule` is the base class for global systems. Extend it to implement input handling, physics, asset loading, audio, or any other system that needs to live outside of individual objects but participate in the scene lifecycle.

<Callout type="info">
  Register modules via [`addModules()`](/docs/api/three-start) on a [`ThreeStart`](/docs/api/three-start) instance, **before** `start()`. Once registered, they're accessible from any [`Object3DBehaviour`](/docs/api/object3d-behaviour) or sibling module via `this.modules.<key>`.
</Callout>

```ts
import { ContextModule, ThreeStart } from "three-start";

class InputModule extends ContextModule {
  readonly keys = new Set<string>();

  onAwake() {
    window.addEventListener("keydown", (e) => this.keys.add(e.code));
    window.addEventListener("keyup", (e) => this.keys.delete(e.code));
  }

  isPressed(key: string) {
    return this.keys.has(key);
  }
}

const starter = new ThreeStart().addModules({ input: new InputModule() });
```

```ts
// Inside any component:
onUpdate() {
  if (this.modules.input.isPressed("Space")) { ... }
}
```

Lifecycle order during [`starter.start()`](/docs/api/three-start): `onAwake` (all modules) → `onStart` (all modules) → subscribe — **before any [`Object3DBehaviour`](/docs/api/object3d-behaviour) lifecycle fires**.

Properties [#properties]

| Name | Type | Description |
|---|---|---|
| `ctx` | `ThreeContext` | The shared [`ThreeContext`](/docs/api/three-context) runtime. Available from `onAwake` onwards. |
| `modules` | `ThreeStartModules` | Shortcut to sibling [`ContextModule`](/docs/api/context-module) instances: `this.modules.myModule`. |

Methods [#methods]



Override methods [#override-methods]

Lifecycle hooks you implement in your subclass.

<Callout type="info">
  Per-frame hooks (`onUpdate`, `onBeforeRender`, `onAfterRender`) are subscribed to the runtime loop **only if you override them**. An empty module pays nothing for dispatch.
</Callout>

| Name | Type | Description |
|---|---|---|
| `onAwake` | `() => void` | `@lifecycle` `@override` Called once during [`starter.start()`](/docs/api/three-start), before any component lifecycle. Use for one-time module initialization. |
| `onStart` | `() => void` | `@lifecycle` `@override` Called once after all modules have awakened. Use for cross-module wiring that requires other modules to be ready. |
| `onUpdate` | `() => void` | `@lifecycle` `@override` Called once per frame while the module is registered. Fires before any component `onUpdate`. |
| `onBeforeRender` | `() => void` | `@lifecycle` `@override` Called once per frame before the scene renders. Fires before any component `onBeforeRender`. |
| `onAfterRender` | `() => void` | `@lifecycle` `@override` Called once per frame after the scene renders. Fires before any component `onAfterRender`. |

Events [#events]

`ContextModule` extends [`TypedEmitter`](/docs/api/typed-emitter) — declare your own event map via the generic parameter to expose typed events to subscribers (other modules, UI, etc.).

**Emitter methods**

| Name | Type | Description |
|---|---|---|
| `on` | `<K extends keyof TEvents>(event: K, fn: (...args: TEvents[K]) => void, context?: any) => this` | Subscribe to an event declared via the class generic parameter. |
| `once` | `<K extends keyof TEvents>(event: K, fn: (...args: TEvents[K]) => void, context?: any) => this` | Subscribe to an event, automatically unsubscribing after it fires once. |
| `off` | `<K extends keyof TEvents>(event: K, fn?: (...args: TEvents[K]) => void, context?: any, once?: boolean) => this` | Remove a previously registered listener. Omit `fn` to remove all listeners for the event. |

Declaring events in your subclass [#declaring-events-in-your-subclass]

Pass a typed event map as the generic parameter. Keys become the `event` argument in `on` / `off` / `once`, and tuple values type the listener payload. `emit` is `protected` — only the module itself can fire its own events.

```ts
type GameEvents = {
  scoreChanged: [score: number];
  gameOver: [finalScore: number];
};

class GameModule extends ContextModule<GameEvents> {
  score = 0;

  addScore(points: number) {
    this.score += points;
    this.emit("scoreChanged", this.score);  // ✅ inside the class
  }

  endGame() {
    this.emit("gameOver", this.score);      // ✅
  }
}

// Subscribe from another module or component:
this.modules.game.on("scoreChanged", (score) => updateHud(score));
this.modules.game.emit("gameOver", 0);     // ❌ compile error: emit is protected
```

The default for the generic is `{}` — without declaring events, calling `on` / `emit` fails at compile time (`keyof {} = never`).

Type-safe modules [#type-safe-modules]

Use the Register pattern to get typed access to your modules:

```ts
declare module "three-start" {
  interface ThreeStartRegister {
    modules: {
      input: InputModule;
      physics: PhysicsModule;
    };
  }
}
```

After this, `this.modules.input` and `this.modules.physics` are fully typed everywhere.


# Object3DBehaviour (/docs/api/object3d-behaviour)





`Object3DBehaviour` is the base class for all components. Extend it, override the lifecycle methods you need, and attach it to any `Object3D` via [`addComponent`](/docs/api/operations). The component hooks itself into the loop and the lifecycle automatically — the shared [`ThreeContext`](/docs/api/three-context) is available as `this.ctx`, and any registered [`ContextModule`](/docs/api/context-module) via `this.modules`.

```ts
import { Object3DBehaviour, addComponent } from "three-start";

class Spin extends Object3DBehaviour {
  onAwake() {
    // initialize
  }
  onUpdate() {
    this.object.rotation.y += this.ctx.getDeltaTime();
  }
  onDestroy() {
    // clean up
  }
}

addComponent(mesh, Spin);
```

First activation order: `onAwake` → `onEnable` → `onStart`

Enable/disable cycle: `onEnable` ↔ `onDisable`

On removal: `onDisable` → `onDestroy`

Properties [#properties]

| Name | Type | Description |
|---|---|---|
| `object` | `THREE.Object3D` | The Three.js `Object3D` this component is attached to. Available from `onAwake` onwards. |
| `ctx` | `ThreeContext` | The shared [`ThreeContext`](/docs/api/three-context) runtime. Available from `onAwake` onwards. |
| `modules` | `ThreeStartModules` | Shortcut to registered [`ContextModule`](/docs/api/context-module) instances: `this.modules.myModule`. |
| `enabled` | `boolean` | `true` while the component is enabled. Toggle with `enable()` / `disable()` / `setEnabled()`. |

Methods [#methods]

| Name | Type | Description |
|---|---|---|
| `enable` | `() => void` | Enable the component. Fires `onEnable` if the owning object is active and the context is available. |
| `disable` | `() => void` | Disable the component. Fires `onDisable` if it was active and unsubscribes per-frame hooks. |
| `setEnabled` | `(enabled: boolean) => void` | Set the enabled flag explicitly. No-op if the value is unchanged. |

Override methods [#override-methods]

These are the lifecycle hooks you implement in your subclass.

<Callout type="info">
  Per-frame hooks (`onUpdate`, `onBeforeRender`, `onAfterRender`) are subscribed to the runtime loop **only if you override them**. An empty component pays nothing for dispatch.
</Callout>

| Name | Type | Description |
|---|---|---|
| `onAwake` | `() => void` | `@lifecycle` `@override` Called once when the component first becomes active. Use for one-time initialization. Runs before `onEnable` and `onStart`. |
| `onStart` | `() => void` | `@lifecycle` `@override` Called once after `onEnable`, on the first activation. Runs after all modules and sibling components have awakened. |
| `onEnable` | `() => void` | `@lifecycle` `@override` Called every time the component transitions from disabled → enabled. Also runs on the first activation (after `onAwake`). |
| `onDisable` | `() => void` | `@lifecycle` `@override` Called every time the component transitions from enabled → disabled, and before `onDestroy`. |
| `onUpdate` | `() => void` | `@lifecycle` `@override` Called once per frame while the component is active. Override to run per-frame logic. |
| `onBeforeRender` | `() => void` | `@lifecycle` `@override` Called once per frame, before the scene is rendered. Override to run pre-render logic. |
| `onAfterRender` | `() => void` | `@lifecycle` `@override` Called once per frame, after the scene is rendered. Override to run post-render logic. |
| `onDestroy` | `() => void` | `@lifecycle` `@override` Called when the component is destroyed. Use to dispose resources, cancel requests, or remove external listeners. |

Events [#events]

`Object3DBehaviour` extends [`TypedEmitter`](/docs/api/typed-emitter) — declare your own event map via the generic parameter to expose typed events on instances of your subclass.

**Emitter methods**

| Name | Type | Description |
|---|---|---|
| `on` | `<K extends keyof TEvents>(event: K, fn: (...args: TEvents[K]) => void, context?: any) => this` | Subscribe to an event declared via the class generic parameter. |
| `once` | `<K extends keyof TEvents>(event: K, fn: (...args: TEvents[K]) => void, context?: any) => this` | Subscribe to an event, automatically unsubscribing after it fires once. |
| `off` | `<K extends keyof TEvents>(event: K, fn?: (...args: TEvents[K]) => void, context?: any, once?: boolean) => this` | Remove a previously registered listener. Omit `fn` to remove all listeners for the event. |

Declaring events in your subclass [#declaring-events-in-your-subclass]

Pass a typed event map as the generic parameter. Keys become the `event` argument in `on` / `off` / `once`, and tuple values type the listener payload. `emit` is `protected` — only the component itself can fire its own events.

```ts
type HealthEvents = {
  damaged: [amount: number, source: string];
  died: [];
};

class Health extends Object3DBehaviour<HealthEvents> {
  hp = 100;

  takeDamage(n: number, by: string) {
    this.hp -= n;
    this.emit("damaged", n, by);          // ✅ inside the class
    if (this.hp <= 0) this.emit("died");  // ✅
  }
}

// Subscribe from outside:
const health = getComponent(player, Health)!;
health.on("damaged", (amount, by) => console.log(`-${amount} from ${by}`));
health.emit("damaged", 10, "x");          // ❌ compile error: emit is protected
```

The default for the generic is `{}` — without declaring events, calling `on` / `emit` fails at compile time (`keyof {} = never`).


# Operations (/docs/api/operations)



These are the top-level functions exported from `three-start` for working with [`Object3DBehaviour`](/docs/api/object3d-behaviour) components and the active state of `Object3D` instances at runtime.

```ts
import { addComponent, getComponent, getComponents, setActive, getIsActive, getIsActiveSelf, destroy } from "three-start";
```

addComponent [#addcomponent]

```ts
function addComponent<T extends Object3DBehaviour>(
  obj: THREE.Object3D,
  klass: new () => T
): T
```

Instantiate a component and attach it to `obj`. If the object is already in an active hierarchy with context, the full lifecycle fires immediately (`onAwake` → `onEnable` → `onStart`). Otherwise it fires when the object joins such a hierarchy.

Returns the new component instance.

```ts
const spin = addComponent(cube, Spin);
spin.disable(); // immediately control it
```

***

getComponent [#getcomponent]

```ts
function getComponent<T extends Object3DBehaviour>(
  obj: THREE.Object3D,
  klass: new () => T
): T | null
```

Returns the first component of the given class on `obj`, or `null` if none.

```ts
const spin = getComponent(cube, Spin);
spin?.enable();
```

***

getComponents [#getcomponents]

```ts
function getComponents(obj: THREE.Object3D): Object3DBehaviour[]
function getComponents<T extends Object3DBehaviour>(obj: THREE.Object3D, klass: new () => T): T[]
```

Returns all components on `obj`. Optionally filtered by class.

***

setActive [#setactive]

```ts
function setActive(obj: THREE.Object3D, active: boolean): void
```

Set `obj`'s own active flag (`activeSelf`). The change cascades down the subtree: every descendant whose own flag is `true` switches its effective state with `obj`, and components on each affected object receive `onEnable` / `onDisable`. Descendants whose own flag is already `false` are skipped — their subtree was already inactive.

`setActive` only drives the component lifecycle — it does **not** touch the object's `visible` property. If you want a deactivated subtree to also stop rendering, set `obj.visible = false` yourself (Three.js cascades visibility through the scene tree on its own). See the [setActive guide](/docs/core-guides/set-active) for the cascade rules and recipes.

```ts
setActive(player, false); // pauses player + all descendants whose own flag is true
setActive(player, true);  // resumes them
```

***

getIsActive [#getisactive]

```ts
function getIsActive(obj: THREE.Object3D): boolean
```

Returns `obj`'s **effective** active state — `activeInHierarchy`. `true` only when `obj`'s own flag is `true` AND every ancestor's own flag is also `true`. Objects (and ancestors) that have never been touched by three-start count as active by default.

```ts
if (getIsActive(player)) {
  // player is currently running its lifecycle
}
```

***

getIsActiveSelf [#getisactiveself]

```ts
function getIsActiveSelf(obj: THREE.Object3D): boolean
```

Returns `obj`'s **own** active flag — `activeSelf`. The value last passed to `setActive(obj, ...)`, regardless of any ancestor's state. `true` by default for objects never touched by three-start.

```ts
setActive(child, false);
getIsActiveSelf(child); // false
getIsActive(child);     // false (and would also be false if any ancestor were inactive)
```

***

destroy [#destroy]

```ts
function destroy(target: THREE.Object3D | Object3DBehaviour): void
```

Destroy a component or an entire object.

* **Component**: fires `onDisable` → `onDestroy`, removes from the object, cleans up all listeners.
* **Object3D**: traverses the hierarchy, fires `onDestroy` on all components, then removes the object from its parent.

```ts
destroy(getComponent(cube, Spin)); // remove just the component
destroy(cube);                      // remove the whole object + all components
```


# ThreeContext (/docs/api/three-context)





`ThreeContext` is the single runtime object for a scene. It holds the renderer, scene, camera, animation loop, timer, and event bus.

You don't construct `ThreeContext` directly — it's created automatically inside [`ThreeStart`](/docs/api/three-start) and exposed via its [`ctx`](/docs/api/three-start#properties) field.

<Callout type="info">
  Every [`Object3DBehaviour`](/docs/api/object3d-behaviour) and [`ContextModule`](/docs/api/context-module) instance has access to the same context via `this.ctx`.
</Callout>

Properties [#properties]

| Name | Type | Description |
|---|---|---|
| `camera` | `THREE.PerspectiveCamera` | The active camera. Reassigning it swaps the camera used by the scene pass and fires `CameraChanged`. |
| `canvasContainer` | `HTMLDivElement | null` | The HTML element the renderer canvas is currently mounted into, or `null` if not mounted. |
| `isMounted` | `boolean` | `true` while the renderer canvas is attached to a container (between `mount()` and `unmount()`). |
| `isLoopRunning` | `boolean` | `true` while the animation loop is running (between `runLoop()` and `stopLoop()`). |
| `renderer` | `THREE.Renderer` | The Three.js renderer owned by this context. Sealed at construction. |
| `scene` | `THREE.Scene` | The root `Scene` that components and the camera live in. Sealed at construction. |
| `renderPipeline` | `THREE.RenderPipeline` | The render pipeline used by the default render function. Sealed at construction. |
| `scenePass` | `THREE.PassNode` | The scene pass node fed into `renderPipeline`. Attach post-processing effects to it via TSL. |
| `modules` | `ThreeStartModules` | Registered [`ContextModule`](/docs/api/context-module) instances, keyed by name. Read-only at runtime — mutations throw. Populate via `starter.addModules()` before `start()`. |

Methods [#methods]

| Name | Type | Description |
|---|---|---|
| `render` | `() => void` | Render once using the current render function. Fires `RenderBefore` / `RenderAfter`. |
| `overrideRender` | `(fn: () => void) => this` | Replace the render function with a custom implementation. Restore via `resetRender()`. |
| `resetRender` | `() => this` | Restore the default render function (undoes `overrideRender`). |
| `getDeltaTime` | `() => number` | Seconds elapsed since the previous tick, scaled by `timescale`. Use inside per-frame methods. |
| `getTime` | `() => number` | Total elapsed time in seconds since the loop started, scaled by `timescale`. |
| `getTimescale` | `() => number` | Current time-scaling factor. `1` = realtime, `0` = paused, `2` = 2× speed. |
| `setTimescale` | `(value: number) => this` | Change the time-scaling factor applied to `getDeltaTime` / `getTime`. `1` = realtime, `0` = paused. |
| `requestRender` | `() => void` | Schedule a single render on the next animation frame. Coalesces multiple calls within the same frame. |

Events [#events]

`ThreeContext` extends [`TypedEmitter`](/docs/api/typed-emitter) and acts as the central **event bus** of the runtime — subscribe via `ctx.on(...)`, unsubscribe via `ctx.off(...)`. Events are typed by `ThreeContextEventMap` below.

**Emitter methods**

| Name | Type | Description |
|---|---|---|
| `on` | `<K extends ThreeContextEvents>(event: K, fn: (...args: ThreeContextEventMap[K]) => void, context?: any) => this` | Subscribe to an event. See the Events table for available keys and payloads. |
| `once` | `<K extends ThreeContextEvents>(event: K, fn: (...args: ThreeContextEventMap[K]) => void, context?: any) => this` | Subscribe to an event, automatically unsubscribing after it fires once. |
| `off` | `<K extends ThreeContextEvents>(event: K, fn?: (...args: ThreeContextEventMap[K]) => void, context?: any, once?: boolean) => this` | Remove a previously registered listener. Omit `fn` to remove all listeners for the event. |

**Events**

| Event | String key | Args | Description |
|---|---|---|---|
| `ThreeContextEvents.Update` | `"update"` | — | Fired once per animation frame, before `RenderBefore` and rendering. Driver of all per-frame logic. |
| `ThreeContextEvents.RenderBefore` | `"renderbefore"` | — | Fired once per animation frame, just before the scene is rendered. Use to mutate state right before draw. |
| `ThreeContextEvents.RenderAfter` | `"renderafter"` | — | Fired once per animation frame, immediately after the scene is rendered. |
| `ThreeContextEvents.Mount` | `"mount"` | `root: HTMLDivElement` | Fired when `starter.mount(container)` succeeds. Carries the container element. |
| `ThreeContextEvents.Unmount` | `"unmount"` | — | Fired when `starter.unmount()` runs. |
| `ThreeContextEvents.Resized` | `"resized"` | `width: number`, `height: number` | Fired when the canvas container resizes (and once on first mount). Carries the new pixel dimensions. |
| `ThreeContextEvents.CameraChanged` | `"camerachanged"` | `newCamera: THREE.PerspectiveCamera`, `prevCamera: THREE.PerspectiveCamera` | Fired when `ctx.camera` is reassigned. Carries the new and previous cameras. |
| `ThreeContextEvents.LoopRun` | `"looprun"` | — | Fired when `starter.runLoop()` starts the animation loop. |
| `ThreeContextEvents.LoopStop` | `"loopstop"` | — | Fired when `starter.stopLoop()` halts the animation loop. |


# ThreeStart (/docs/api/three-start)





`ThreeStart` is the entry point of every three-start project. It creates a [`ThreeContext`](/docs/api/three-context), accepts [`ContextModule`](/docs/api/context-module) instances, and bootstraps the entire scene in a defined order when `start()` is called.

```ts
import { ThreeStart } from "three-start";

const starter = new ThreeStart();
starter.addModules({ ... });
starter.start();

starter.mount(document.getElementById("three")!);
```

<Callout type="warn">
  `addModules()` must be called **before** `start()`. Modules participate in the bootstrap lifecycle (`onAwake` → `onStart`), so registering after `start()` throws.
</Callout>

<Callout type="info">
  `mount()` / `unmount()` can be called any number of times across the instance's lifetime — by default each pair starts and stops the render loop automatically. Pass `manageLoopManually: true` in [`ThreeStartOptions`](#threestartoptions) to disable this and control the loop yourself via `runLoop()` / `stopLoop()`.
</Callout>

Initialization [#initialization]

```ts
new ThreeStart(options?: ThreeStartOptions)
```

**`ThreeStartOptions`**

| Field | Type | Default | Description |
|---|---|---|---|
| `renderer?` | `THREE.Renderer` | — | The Three.js renderer to use. Defaults to a `WebGPURenderer` with antialiasing. |
| `camera?` | `THREE.PerspectiveCamera` | — | Override the default `PerspectiveCamera`. If not provided, one is created automatically and added to the scene. |
| `scene?` | `THREE.Scene` | — | Override the default `Scene`. If not provided, an empty scene is created. |
| `manageLoopManually?` | `boolean` | `false` | When `true`, the render loop is NOT started automatically on `mount()`. Call `starter.runLoop()` / `starter.stopLoop()` manually. |
| `autoInitRenderer?` | `boolean` | `true` | When `true`, calls `renderer.init()` inside the constructor. |

Properties [#properties]

| Name | Type | Description |
|---|---|---|
| `isStarted` | `boolean` | `true` once `start()` has been called. After this, modules can no longer be registered. |
| `ctx` | `ThreeContext` | The shared [`ThreeContext`](/docs/api/three-context) runtime (renderer, scene, camera, modules, events). Created in the constructor. |

Methods [#methods]

| Name | Type | Description |
|---|---|---|
| `start` | `() => this` | Bootstrap the scene: awaken registered [`ContextModule`](/docs/api/context-module)s, attach the context to the scene graph, and activate [`Object3DBehaviour`](/docs/api/object3d-behaviour) instances on every existing object. After `start()`, any objects added to the scene get bootstrapped automatically. No more modules can be registered after this call. |
| `addModules` | `(modules: Partial<ThreeStartModules>) => this` | Register [`ContextModule`](/docs/api/context-module) instances on the context. Can be called multiple times to register them incrementally — one at a time, in groups, or all at once. The same key cannot be registered twice (subsequent attempts are skipped with a warning). **Must be called before `start()`** — modules participate in the bootstrap lifecycle (`onAwake` → `onStart`), so registering after it throws. |
| `mount` | `(container: HTMLDivElement) => this` | Append the renderer canvas to a container and wire up resize + render loop (unless `manageLoopManually` is set). Fires `Mount`. |
| `unmount` | `() => this` | Remove the renderer canvas from DOM, disconnect resize observer. Fires `Unmount`. |
| `runLoop` | `() => this` | Start the render loop. Resumable after `stopLoop()`. |
| `stopLoop` | `() => this` | Stop the render loop. Resumable via `runLoop()`. |
| `dispose` | `() => this` | Tear everything down: unmount, stop loop, dispose renderer and timer. |


# TypedEmitter (/docs/api/typed-emitter)





`TypedEmitter` is the base class that gives every other class in three-start its event API. [`ThreeContext`](/docs/api/three-context), [`ContextModule`](/docs/api/context-module), and [`Object3DBehaviour`](/docs/api/object3d-behaviour) all extend it.

What it does [#what-it-does]

* Wraps an [`eventemitter3`](https://github.com/primus/eventemitter3) instance internally — the proven small/fast emitter, \~1KB minified.
* **Lazy initialization** — the inner emitter is only allocated on the first `on` / `once`. Classes that extend `TypedEmitter` but never have a subscriber pay only one reference slot per instance.
* **Type-safe events** via the `<T extends EventMap>` generic. Keys become valid `event` arguments, tuple values type the listener payload.
* **Encapsulation by design** — `on` / `off` / `once` are public so anyone can subscribe; `emit` is `protected` so only the owning class fires its own events. No external code can fake an event.

```ts
type HealthEvents = {
  damaged: [amount: number, source: string];
  died: [];
};

class Health extends Object3DBehaviour<HealthEvents> {
  hp = 100;
  hit(damage: number, by: string) {
    this.hp -= damage;
    this.emit("damaged", damage, by);     // ✅ inside the class
    if (this.hp <= 0) this.emit("died");  // ✅
  }
}

const health = getComponent(player, Health)!;
health.on("damaged", (amount, by) => console.log(`-${amount} from ${by}`));
health.emit("damaged", 10, "x");          // ❌ compile error: emit is protected
```

The default for `T` is `{}` — a class without an explicit event map can't legitimately call `on` / `emit` (compile error from `keyof {} = never`). This is the type-level opt-in pattern.

Methods [#methods]




# Components (/docs/core-guides/components)



Components are classes that extend [`Object3DBehaviour`](/docs/api/object3d-behaviour). They are one of the core entities in three-start. They live within the 3D object they are attached to, have their own lifecycle, have access to the context, and plug into the main event/render loop. Through components we give 3D objects functionality.

First component [#first-component]

```ts title="Spin.ts"
import { Object3DBehaviour } from "three-start";

class Spin extends Object3DBehaviour { // [!code highlight]
    speed = 2;
    private _initRotY = 0;

    onAwake() {
        this._initRotY = this.object.rotation.y;
    }

    onUpdate() {
        const dt = this.ctx.getDeltaTime();
        this.object.rotation.y += dt * this.speed;
    }

    onDestroy() {
        this.object.rotation.y = this._initRotY;
    }
}
```

`onAwake` fires at the start of the lifecycle. `onUpdate` fires every frame (before rendering), and `onDestroy` is called when the component is destroyed. So we just override the built-in event methods and don't have to manually track the lifecycle or organize our own animation/render loop. It's all already wired up!

<Callout type="info">
  Components are added strictly through the `addComponent` factory. It returns the instance of the created component.
</Callout>

```ts
import * as THREE from "three";
import { addComponent, ThreeStart } from "three-start";
import { Spin } from "./Spin.ts";

const startInstance = new ThreeStart();

const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshMatcapMaterial());
startInstance.ctx.add(cube);

const spin = addComponent(cube, Spin); // [!code ++]

startInstance.start();
```

<Callout type="warn">
  Don't forget that the component's lifecycle (first activation) starts on the condition that the component's object has been added to the context's scene **and** `start()` has been called on the `ThreeStart` instance. The order of these two actions doesn't matter. See [Lifecycle](/docs/core-guides/lifecycle) for details.
</Callout>

<Callout type="info">
  In the examples that follow we'll keep using the same `cube` variable as our example 3D object, implied to already be added to the context's scene. We also assume `start()` has already been called.
</Callout>

Component with constructor parameters [#component-with-constructor-parameters]

If you want, you can declare any parameters in your component's constructor. This makes the component more versatile and easier to reuse.

```ts title="Spin.ts"
import * as THREE from "three";
import { Object3DBehaviour } from "three-start";

type Axis = "x" | "y" | "z";

class Spin extends Object3DBehaviour {
    speed: number;
    axes: Axis[];
    private _initRot = new THREE.Euler();

    constructor(axes: Axis[], speed = 2) { // [!code highlight]
        super(); // [!code highlight]
        this.axes = axes;
        this.speed = speed;
    }

    onAwake() {
        this._initRot.copy(this.object.rotation);
    }

    onUpdate() {
        const dt = this.ctx.getDeltaTime();
        for (const axis of this.axes) {
            this.object.rotation[axis] += dt * this.speed;
        }
    }

    onDestroy() {
        this.object.rotation.copy(this._initRot);
    }
}
```

When adding such a component, you have to pass the required arguments:

```ts
const spin = addComponent(cube, Spin, ["y"]);
```

Or like this, since the second parameter is optional:

```ts
const spin = addComponent(cube, Spin, ["y"], 1.5);
```

<Callout type="success">
  The argument types are inferred from the component class — you get full TypeScript autocomplete and arity/type checking on the `...args` of `addComponent`.
</Callout>

Reading components off an object [#reading-components-off-an-object]

We can add any number of components to an object, including duplicates of the same class:

```ts
class Bob extends Object3DBehaviour {
    // ...
}

addComponent(cube, Spin, ["x", "z"], 0.5);
addComponent(cube, Spin, ["y"], 1.5);

addComponent(cube, Bob);
```

You get back an array in the order the components were added:

```ts
const components = getComponents(cube);
//      ^? [Spin, Spin, Bob]
```

Filter by a specific class:

```ts
const spins = getComponents(cube, Spin);
//      ^? [Spin, Spin]

const bob = getComponent(cube, Bob);
//    ^? Bob
```

Destroying a component [#destroying-a-component]

When `destroy` is called on the `Bob` component, its `onDisable` → `onDestroy` fire and it is removed from the object's component list.

```ts
const before = getComponents(cube);
//      ^? [Spin, Spin, Bob]

destroy(getComponent(cube, Bob)!);

const after = getComponents(cube);
//      ^? [Spin, Spin]
```

`destroy` can also be called on a 3D object. This destroys all components on it and on its descendants in scene-graph order (via `traverse`):

```ts
destroy(cube);

const components = getComponents(cube);
//      ^? []
```

<Callout type="error">
  Remember that `destroy` called on an object will destroy all components on every one of its descendants too.
</Callout>

<Callout type="warn">
  `destroy` is the final point of a component instance's life. If you need the functionality back, you have to add it again via `addComponent`, which creates a new instance.
</Callout>

Enabling and disabling components [#enabling-and-disabling-components]

If you need to switch a component's functionality on and off, the recommended approach is the built-in `enable()` / `disable()` mechanism. When enabled, `onEnable` fires; when disabled, `onDisable` fires. Also keep in mind that at the start of a component's lifecycle, `onEnable` fires too — in the sequence `onAwake` → `onEnable` → `onStart`.

<Callout type="info">
  `onAwake` and `onStart` fire only **once** during a component's lifetime, unlike `onEnable`, which fires every time the component is enabled.
</Callout>

<Callout type="success">
  Render-loop events (`onUpdate`, `onBeforeRender`, `onAfterRender`) stop firing on a component while it is disabled, and resume as soon as it is enabled again.
</Callout>

Example:

```ts title="OverheadLight.ts"
import * as THREE from "three";
import { Object3DBehaviour } from "three-start";

class OverheadLight extends Object3DBehaviour {
    private _light = new THREE.PointLight();

    onAwake() {
        this._light.intensity = 0;
        this.object.add(this._light);
    }

    onEnable() { // [!code highlight]
        this._light.intensity = 1;
    }

    onDisable() { // [!code highlight]
        this._light.intensity = 0;
    }

    onDestroy() {
        this._light.removeFromParent();
        this._light.dispose();
    }
}

const ideaLump = addComponent(cube, OverheadLight);
// -> onEnable -> intensity = 1

ideaLump.disable(); // -> onDisable -> intensity = 0

ideaLump.enable(); // -> onEnable -> intensity = 1

// etc.
```

Accessing modules [#accessing-modules]

Context modules are an important part of how components work. For example, imagine we've registered an `InputSystem` module under the key `input`, which exposes an arrow function `isPressed: (key: string) => boolean` — calling it tells you whether a given key is currently held down.

```ts
import { InputSystem } from "./InputSystem.ts";

new ThreeStart().addModules({ input: new InputSystem() });
```

You can read more about modules in [Writing modules](/docs/core-guides/writing-modules).

And then we write, say, a `PlayerControls` component:

```ts title="PlayerControls.ts"
class PlayerControls extends Object3DBehaviour {
    constructor(public speed = 3) {
        super();
    }

    onUpdate() {
        const dt = this.ctx.getDeltaTime();
        const objPos = this.object.position;
        const key = this.modules.input.isPressed; // [!code highlight]
        if (key("KeyD")) objPos.x += this.speed * dt;
        if (key("KeyA")) objPos.x -= this.speed * dt;
        if (key("KeyW")) objPos.z -= this.speed * dt;
        if (key("KeyS")) objPos.z += this.speed * dt;
    }
}
```

```ts
const player = new THREE.Group();
const controls = addComponent(player, PlayerControls);
```

<Callout type="success">
  The lifecycle is structured so that `onAwake` and `onStart` of modules always fire before those of components. So when you reach for a module from a component, you can rely on it being ready to use.
</Callout>

<Callout type="success">
  Likewise, every render-loop event fires first on modules, then on components.
</Callout>

Communication between components [#communication-between-components]

Emitting your own events [#emitting-your-own-events]

`Object3DBehaviour` extends [`TypedEmitter`](/docs/api/typed-emitter). Pass an event map as a generic, then `emit` / `on` / `off` / `once` are typed:

```ts title="Health.ts"
type HealthEvents = {
    damaged: [amount: number];
    died: [];
};

class Health extends Object3DBehaviour<HealthEvents> {
    constructor(private _hp = 100) {
        super();
    }

    damage(n: number) {
        this._hp -= n;
        this.emit("damaged", n); // [!code ++]
        if (this._hp <= 0) {
            this.emit("died"); // [!code ++]
        }
    }
}
```

And here's how it's used. For example, we want a barrel to explode when it's damaged:

```ts
const h = addComponent(barrelObj, Health, 20);
h.on("died", () => spawnExplosion(barrelObj.position));
```

<Callout type="success">
  `onDestroy` clears every listener registered on the component itself — you don't need to manually unsubscribe consumers from a destroyed component.
</Callout>

Reaching for other components [#reaching-for-other-components]

For example, say we have a `Shooting` component that does a raycast against targets. The raycast / shooting implementation details are deliberately omitted here, otherwise the example would get long and the focus on the library itself would be lost.

```ts title="Shooting.ts"
class Shooting extends Object3DBehaviour {
    damageAmount = 20;

    // ...

    private handleRaycast(obj: THREE.Object3D) {
        const h = getComponent(obj, Health); // [!code highlight]
        if (!h) return;
        h.damage(this.damageAmount);
    }

    // ...
}
```

Adding components from inside a component [#adding-components-from-inside-a-component]

A "composite" component that builds combined functionality out of separate components — or a component spawned dynamically at runtime:

```ts title="PlayerBehaviour.ts"
class PlayerBehaviour extends Object3DBehaviour {
    onAwake() {
        addComponent(this.object, PlayerControls);
        addComponent(this.object, CameraControls);
        const blood = addComponent(this.object, BloodParticles);
        const h = addComponent(this.object, Health);

        h.on("damaged", (value) => {
            blood.burst(value);
        });

        h.once("died", () => {
            addComponent(this.object, DiedEffect);
        });
    }
}
```

Recap: what you have inside a component [#recap-what-you-have-inside-a-component]

| Member         | What it is                                                                                                                                                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `this.object`  | The 3D object the component was attached to. Stays the same for the entire lifetime of the component.                                                                                                                          |
| `this.ctx`     | The [`ThreeContext`](/docs/api/three-context). Exposes the core three.js bits (`scene`, `camera`, `renderer`, `renderPipeline`, …) plus helpers like `getDeltaTime()` / `getTime()` that are handy for updates and animations. |
| `this.modules` | Access to the registered modules ([`ContextModule`](/docs/api/context-module)).                                                                                                                                                |
| `this.enabled` | Read-only flag indicating whether the component is enabled.                                                                                                                                                                    |

<Callout type="error">
  Don't read `this.object` from a **field initializer** or a **constructor**. Initialization happens inside `addComponent`, and `this.object` is only assigned after the constructor finishes.
</Callout>

<Callout type="error">
  Don't read `this.ctx` / `this.modules` from a **field initializer** or **constructor** either — and avoid using them before `onAwake` in general. The context and its modules become available only once the context is attached, which happens as soon as the component's object lands in the context's scene hierarchy.
</Callout>

Picking event methods [#picking-event-methods]

The full set, ordered by when they fire on the first activation:

| Method             | Fires                                                                                                                                                                             |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onAwake()`        | Once on first activation, before anything else. One-time setup that depends on `this.object` / `this.ctx`.                                                                        |
| `onEnable()`       | Each time the component goes from inactive → active (first activation, re-`enable()`, ancestor `setActive(..., true)` cascade).                                                   |
| `onStart()`        | Once after `onEnable` on the first activation. Sibling components and modules are ready by now — use for cross-references.                                                        |
| `onUpdate()`       | Each frame, before `onBeforeRender`. Game logic, input reads, animation step.                                                                                                     |
| `onBeforeRender()` | Each frame, just before the renderer draws. Uniform updates, custom matrix work that must happen post-input but pre-draw.                                                         |
| `onAfterRender()`  | Each frame, immediately after the draw. Post-effects, screenshot capture, frame-late state read-back.                                                                             |
| `onDisable()`      | Each time the component leaves active state. Pair with `onEnable` for symmetric resource management.                                                                              |
| `onDestroy()`      | Once when [`destroy(comp)`](/docs/api/operations) (or destroying the host object) runs. Pair with `onAwake` — release any external listeners, geometries, requests created there. |

The render-loop event methods (`onUpdate` / `onBeforeRender` / `onAfterRender`) only dispatch on **enabled, active-in-hierarchy** components. See [Lifecycle](/docs/core-guides/lifecycle) for full ordering rules and [setActive](/docs/core-guides/set-active) for hierarchy gating.

Things to remember [#things-to-remember]

* **Pair your event methods.** Anything you allocate in `onAwake` belongs in `onDestroy`. Anything you subscribe to in `onEnable` belongs in `onDisable`. Symmetry keeps state clean.
* **Always clean up in `onDestroy`.** Every resource the component allocated during its lifecycle — DOM listeners, `ctx` event subscriptions, raycasters, `THREE.BufferGeometry` / `Material` / `Texture` instances you created, timers, fetch controllers, RAF handles, audio nodes — must be released or unsubscribed in `onDestroy`. The library doesn't track or auto-dispose any of it for you (component-internal `TypedEmitter` listeners are the only thing cleared automatically). Leaks here grow silently with every spawn/destroy cycle.
* **`onUpdate` runs every single frame.** That's 60–120+ calls per second per active component. Keep it lean: precompute heavy math once in `onAwake` / `onStart`, cache references, allocate vectors/matrices as fields and reuse them. **Never** `new THREE.Vector3()` inside `onUpdate`, never load assets, never query the scene by name, never construct `RegExp` / `new Date()`. The same rule applies to `onBeforeRender` / `onAfterRender`.


# Context (/docs/core-guides/context)



[`ThreeContext`](/docs/api/three-context) is the single runtime object every scene gets. It bundles the renderer, scene, camera, animation loop, timer, render pipeline, and event bus into one place. Every [`ContextModule`](/docs/api/context-module) and [`Object3DBehaviour`](/docs/api/object3d-behaviour) sees the same instance.

You don't construct `ThreeContext` directly. [`ThreeStart`](/docs/api/three-start) creates one in its constructor and exposes it as `starter.ctx`. From any component or module, the same instance is `this.ctx`.

```ts
import * as THREE from "three";
import { ThreeStart } from "three-start";

const starter = new ThreeStart();
starter.ctx.scene.add(new THREE.Mesh(geo, mat));
starter.ctx.camera.position.set(0, 5, 10);
starter.mount(document.body);
starter.start();
```

What's on it [#whats-on-it]

| Field / method                                                                          | Purpose                                                                                                         |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `ctx.scene`                                                                             | The root `THREE.Scene`. Add objects directly.                                                                   |
| `ctx.camera`                                                                            | The active `PerspectiveCamera`. Reassigning swaps the camera the render pipeline draws — fires `CameraChanged`. |
| `ctx.renderer`                                                                          | The `THREE.Renderer` (default: `WebGPURenderer` with antialiasing).                                             |
| `ctx.renderPipeline`                                                                    | The `RenderPipeline` driving the default render call.                                                           |
| `ctx.scenePass`                                                                         | The `PassNode` for the current scene/camera. Attach post-processing via TSL.                                    |
| `ctx.modules`                                                                           | Registered [`ContextModule`](/docs/api/context-module) instances, keyed by name. Read-only at runtime.          |
| `ctx.getDeltaTime()` / `ctx.getTime()` / `ctx.getTimescale()`                           | Frame-time accessors. Time scaling applies to both.                                                             |
| `ctx.setTimescale(n)`                                                                   | Set time scale — `1` = realtime, `0` = freeze, `2` = 2× speed.                                                  |
| `ctx.render()` / `ctx.requestRender()` / `ctx.overrideRender(fn)` / `ctx.resetRender()` | Manual render control (see below).                                                                              |
| `ctx.on(event, fn)` / `ctx.off(...)` / `ctx.once(...)`                                  | Event bus (see below).                                                                                          |
| `ctx.canvasContainer` / `ctx.isMounted` / `ctx.isLoopRunning`                           | Mount and loop status flags.                                                                                    |

Full list with types is in the [`ThreeContext` API reference](/docs/api/three-context).

Time [#time]

Use `ctx.getDeltaTime()` inside any render loop event method (`onUpdate`, `onBeforeRender`, `onAfterRender`) for frame-rate-independent motion. The timer auto-pauses when the page tab loses focus and resumes on return — no extra setup needed.

```ts
class Move extends Object3DBehaviour {
  speed = 5;
  onUpdate() {
    this.object.position.x += this.speed * this.ctx.getDeltaTime();
  }
}
```

`ctx.getTime()` returns the total elapsed seconds since the loop started, scaled by `timescale`. Use it for any animation parameterised by absolute time — bobbing, oscillation, phase-locked motion across multiple objects:

```ts
class Bob extends Object3DBehaviour {
  amplitude = 0.5;
  frequency = 2;       // cycles per second
  private baseY = 0;

  onAwake() {
    this.baseY = this.object.position.y;
  }

  onUpdate() {
    const t = this.ctx.getTime();
    this.object.position.y = this.baseY + Math.sin(t * this.frequency * Math.PI * 2) * this.amplitude;
  }
}
```

`ctx.setTimescale(0)` freezes time across every component and module — `getDeltaTime()` returns 0 and `getTime()` stops advancing — without disabling anything. This is the cleanest "global pause" you can do without touching `setActive` or per-component flags.

```ts
ctx.setTimescale(0);     // freeze world time
ctx.setTimescale(1);     // resume
ctx.setTimescale(0.25);  // bullet-time
```

Mounting and the render loop [#mounting-and-the-render-loop]

`mount(container)` and `start()` are independent:

* `starter.mount(el)` attaches the canvas to `el`, starts a `ResizeObserver`, and begins the render loop (unless `manageLoopManually: true`). Fires `Mount`.
* `starter.start()` runs the bootstrap traversal — modules `onAwake` / `onStart`, then components `onAwake` / `onEnable` / `onStart` on every active-in-hierarchy object.

You can call them in any order. `mount` without `start` gives you a black canvas with the loop running but no module/component lifecycle. `start` without `mount` runs all the lifecycle but never draws.

`runLoop()` / `stopLoop()` start and stop the animation loop independently of mount.

Events [#events]

`ThreeContext` extends [`TypedEmitter`](/docs/api/typed-emitter) and is the central event bus of the runtime. Subscribe via `ctx.on(EventName, listener)`; the event names live on `ThreeContextEvents`:

```ts
import { ThreeContextEvents } from "three-start";

ctx.on(ThreeContextEvents.Resized, (w, h) => {
  hudCamera.aspect = w / h;
  hudCamera.updateProjectionMatrix();
});

ctx.on(ThreeContextEvents.LoopStop, () => savePauseState());
```

Common events:

| Event                                     | Payload                 | When                                                                                                                              |
| ----------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Update` / `RenderBefore` / `RenderAfter` | none                    | Each frame. Components and modules use these via their event methods, but you can subscribe directly when you need finer control. |
| `Resized`                                 | `width, height`         | First mount and every container resize.                                                                                           |
| `Mount` / `Unmount`                       | container / none        | Canvas attached / detached.                                                                                                       |
| `LoopRun` / `LoopStop`                    | none                    | Animation loop started / stopped.                                                                                                 |
| `CameraChanged`                           | `newCamera, prevCamera` | `ctx.camera = ...` was called.                                                                                                    |

Full list with payload tuples is in the [API reference](/docs/api/three-context#events).

Any subscription created inside a component's `onAwake` / `onEnable` should be undone in the matching `onDestroy` / `onDisable`. Modules don't have a destroy hook — listeners installed in `onAwake` live for the context's lifetime, which is what you usually want.

Manual render control [#manual-render-control]

The default render loop calls `ctx.render()` once per frame. You rarely need to override it, but two escape hatches exist:

```ts
// Coalesce many state changes into a single out-of-loop draw:
ctx.requestRender();   // schedules one render on the next frame; multiple calls fold into one

// Replace the rendering implementation entirely:
ctx.overrideRender(() => {
  myCustomPipeline.render();
});
ctx.resetRender();     // back to the built-in pipeline
```

Use `requestRender` for editor-style UIs that don't want a continuous loop (set `manageLoopManually: true` in [`ThreeStartOptions`](/docs/api/three-start) and drive `requestRender` from your own dirty-tracking code). Use `overrideRender` if you've built a custom render pipeline (deferred shading, multi-pass, integrated post FX) and want three-start to drive it instead.

Swapping the camera [#swapping-the-camera]

```ts
ctx.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight);
```

Re-assignment updates the scene pass, attaches the new camera to the scene if it has no parent, recomputes the aspect from the current container, and fires `CameraChanged`. Subscribe to that event from anywhere holding a stale camera reference.

<Cards>
  <Card title="ThreeContext API" href="/docs/api/three-context" />

  <Card title="ThreeStart API" href="/docs/api/three-start" />

  <Card title="Lifecycle" href="/docs/core-guides/lifecycle" />

  <Card title="TypedEmitter" href="/docs/api/typed-emitter" />
</Cards>


# Lifecycle (/docs/core-guides/lifecycle)



Two kinds of entities share the same set of named event methods:

| Kind                                                                | What it is                                                                                          |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Module** ([`ContextModule`](/docs/api/context-module))            | Global system. Always on once registered.                                                           |
| **Component** ([`Object3DBehaviour`](/docs/api/object3d-behaviour)) | Behaviour added to a single `Object3D` via `addComponent()`. Can be enabled / disabled / destroyed. |

Modules can't be destroyed or disabled/enabled. Everything else they share with components.

Bootstrap [#bootstrap]

`start()` of `ThreeStart` instance is called once and it starts lifecycle.

<Mermaid
  chart="flowchart TB
    S{{&#x22;start() called&#x22;}} --> MODULES

    subgraph MODULES[Modules]
      MA[&#x22;onAwake()&#x22;] --> MS[&#x22;onStart()&#x22;]
    end

    MODULES --> COMPONENTS

    subgraph COMPONENTS[&#x22;Components&#x22;]
      CA[&#x22;onAwake()&#x22;] -->|if active/enabled| CE[&#x22;onEnable()&#x22;]
      CE --> CS[&#x22;onStart()&#x22;]
    end

    COMPONENTS --> R{{&#x22;render loop events&#x22;}}

  classDef phase font-family:monospace
  class MA,MS,CA,CE,CS phase;

  classDef _subgraphs fill:black
  class MODULES,COMPONENTS _subgraphs"
/>

* **Modules first, in `onAwake` → `onStart` order, fully finished before any component lifecycle starts.** Once a component's `onAwake` runs, every module has already gone through both phases — so `this.modules.<key>` is safe to use from `onAwake` onwards.
* **Then components, in `onAwake` → `onEnable` → `onStart` order.**
* **`onEnable` and `onStart` fire only on active components.** A component is active when it is `enabled` AND no ancestor was made inactive via `setActive(ancestor, false)`. Inactive components stop at `onAwake` and wait — the moment they become active, `onEnable` → `onStart` fires for them.
* **Inside one phase, the order matches registration / scene-graph order.** Modules run in the order you passed to `addModules()`. Components run in scene-graph traversal order.

<Callout type="warn">
  Bootstrap touches **only**:

  * **Modules** registered on your `ThreeStart` instance via `addModules()`.
  * **Components** added via `addComponent()` to objects that were added to the scene tree of your `ThreeStart` instance's context (`ctx`).
</Callout>

<Callout type="info">
  If a module's `onAwake` / `onStart` spawns objects with components, those components don't become active mid-bootstrap — they wait their turn.
</Callout>

Render loop [#render-loop]

Also known as the animation loop. Each frame, in order:

<Mermaid
  chart="flowchart LR
    U(&#x22;onUpdate()&#x22;) e1@--> BR(&#x22;onBeforeRender()&#x22;)
    BR e2@--> R{{render call}}
    R e3@--> AR(&#x22;onAfterRender()&#x22;)
  classDef phase fill:#553366,stroke:#333,color:#fff,text-align:left
  class R phase;
  classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
  class e1,e2,e3,e4 animate"
/>

<Callout type="info">
  In every render loop event method, **all modules run before any component**. A module can mutate state in `onUpdate` and every component sees the result the same frame.
</Callout>

<Callout type="note">
  Render loop event methods are subscribed **only if you override them**. An empty component pays nothing for dispatch.
</Callout>

When a component starts its lifecycle [#when-a-component-starts-its-lifecycle]

A component starts its lifecycle the moment **all three** of these are true:

* [x] The component was added to an `Object3D` via `addComponent(obj, MyComp)`.
* [x] That object was added to the scene tree of your `ThreeStart` instance's context (`ctx.scene`).
* [x] `ThreeStart.start()` was called.

<Callout type="info">
  **The order in which you do these doesn't matter.** You can call `addComponent` before `start()` or after; you can add the component to an object that's already in the scene tree, or to one that gets added later. Whichever route you take, the moment the last of the three holds, the component becomes active and runs its first-activation event methods.
</Callout>

```ts title="1. addComponent → object added to scene → start"
const starter = new ThreeStart();
const obj = new THREE.Object3D();

addComponent(obj, MyComp);
starter.ctx.scene.add(obj);
starter.start();  // MyComp: onAwake -> onStart -> onEnable -> render loop events // [!code highlight]
```

```ts title="2. object added to scene tree → start → addComponent"
const starter = new ThreeStart();
const obj = new THREE.Object3D();

starter.ctx.scene.add(obj);
starter.start();
addComponent(obj, MyComp); // MyComp: onAwake -> onStart -> onEnable -> render loop events // [!code highlight]
```

```ts title="3. start → addComponent → object added to scene"
const starter = new ThreeStart();
const obj = new THREE.Object3D();

starter.start();
addComponent(obj, MyComp);
starter.ctx.scene.add(obj); // MyComp: onAwake -> onStart -> onEnable -> render loop events // [!code highlight]
```

Enable/Disable an alive component [#enabledisable-an-alive-component]

Component's `enable()` / `disable()` methods fire the matching event method and flip `enabled`.

<Mermaid
  chart="flowchart LR
    EN[/&#x22;enable()&#x22;/] -->|enabled = true| HE(&#x22;onEnable()&#x22;)
    DI[/&#x22;disable()&#x22;/] -->|enabled = false| HD(&#x22;onDisable()&#x22;)"
/>

<Callout type="info">
  While a component is disabled, its render loop event methods (`onUpdate` / `onBeforeRender` / `onAfterRender`) don't fire. They resume on the next `enable()`.
</Callout>

[`setActive(obj, ...)`](/docs/core-guides/set-active) toggles whole subtrees — fires `onEnable` / `onDisable` on every component inside. The object's active state takes priority over the component's own `enabled`: an enabled component on an inactive-in-hierarchy object sits idle. By default every object is active; `setActive` is the opt-in switch for parts of the hierarchy. See the [setActive guide](/docs/core-guides/set-active) for the full story.

Destroy [#destroy]

Pass a component to [`destroy(comp)`](/docs/api/operations) to destroy it:

* `onDisable()` (if it was enabled) → `onDestroy()` fires.
* The lifecycle stops; the component unsubscribes from the render loop.
* The component is removed from the object's component list.

Pass an `Object3D` to [`destroy(obj)`](/docs/api/operations) to destroy every component on `obj` and on its whole subtree. Order: scene-graph traversal (the object itself first, then each descendant), and on each object — components in the order they were added. After every component is destroyed, the object is removed from its parent.

Cheat sheet [#cheat-sheet]

| Hook             | When                                                       | Component | Module |
| ---------------- | ---------------------------------------------------------- | --------- | ------ |
| `onAwake`        | Once, on first activation                                  | ✓         | ✓      |
| `onStart`        | Once, after `onAwake` (and after `onEnable` on components) | ✓         | ✓      |
| `onEnable`       | Each entry into Active                                     | ✓         | —      |
| `onDisable`      | Each exit from Active                                      | ✓         | —      |
| `onUpdate`       | Each frame, before render                                  | ✓         | ✓      |
| `onBeforeRender` | Each frame, just before draw                               | ✓         | ✓      |
| `onAfterRender`  | Each frame, just after draw                                | ✓         | ✓      |
| `onDestroy`      | Once, on `destroy(...)`                                    | ✓         | —      |


# setActive (/docs/core-guides/set-active)



[`setActive(obj, ...)`](/docs/api/operations) switches entire branches of the scene tree on and off without removing them. Useful for pause overlays, hidden rooms, pooled enemies, level changes — any time you want a clean "freeze a slice of the world" toggle.

By default every object is active. `setActive` is **opt-in** — you only call it when you specifically want to disable a piece of the hierarchy.

activeSelf vs activeInHierarchy [#activeself-vs-activeinhierarchy]

Each `Object3D` carries two notions of "active":

| API                                            | Meaning                                                                                   | Default |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------- | ------- |
| [`getIsActiveSelf(obj)`](/docs/api/operations) | The object's own flag — whatever was last passed to `setActive(obj, ...)`.                | `true`  |
| [`getIsActive(obj)`](/docs/api/operations)     | The effective state: own flag is `true` **and** every ancestor's own flag is also `true`. | `true`  |

Components run their lifecycle ([`onAwake`](/docs/api/object3d-behaviour) → [`onEnable`](/docs/api/object3d-behaviour) → [`onStart`](/docs/api/object3d-behaviour), render loop event methods) only while their object's `getIsActive(obj)` is `true`. The component's own [`enabled`](/docs/api/object3d-behaviour) is a second gate **inside** that — if the object is inactive in hierarchy, even an enabled component sits idle.

Cascade [#cascade]

`setActive(obj, ...)` flips the object's own flag and walks down the subtree, stopping at any descendant whose own flag is already `false` (its subtree was already inactive and stays so).

Take this tree as the starting state — only `A2` was previously set inactive:

<Mermaid
  chart="flowchart TB
    Scene((Scene))
    A((A))
    A1((A1))
    A2((A2))
    B((B))
    B1((B1))
    Scene --- A
    Scene --- B
    A --- A1
    A --- A2
    B --- B1
    classDef off stroke-dasharray: 4 4,opacity:0.5
    class A2 off"
/>

| Object                        | own flag | effective | components  |
| ----------------------------- | -------- | --------- | ----------- |
| `Scene`, `A`, `B`, `A1`, `B1` | `true`   | `true`    | running     |
| `A2`                          | `false`  | `false`   | not running |

Call &#x2A;*`setActive(A, false)`**:

| Object             | own flag | effective | what fires                                    |
| ------------------ | -------- | --------- | --------------------------------------------- |
| `A`                | `false`  | `false`   | `onDisable` on every active component on `A`  |
| `A1`               | `true`   | `false`   | `onDisable` on every active component on `A1` |
| `A2`               | `false`  | `false`   | nothing — was already not running             |
| `Scene`, `B`, `B1` | `true`   | `true`    | untouched                                     |

Call &#x2A;*`setActive(A, true)`** (after the above):

| Object    | own flag | effective | what fires                                                                                               |
| --------- | -------- | --------- | -------------------------------------------------------------------------------------------------------- |
| `A`, `A1` | `true`   | `true`    | `onEnable` on enabled components (or full `onAwake → onEnable → onStart` if it's their first activation) |
| `A2`      | `false`  | `false`   | still not running — `A2`'s own flag is still `false`                                                     |

To bring `A2`'s subtree back, call `setActive(A2, true)` separately.

Priority over `enabled` [#priority-over-enabled]

A component's [`enable()`](/docs/api/object3d-behaviour) only fires `onEnable` if the object is currently `getIsActive(obj) === true`. Calling `comp.enable()` while an ancestor is inactive flips the component's flag silently — `onEnable` fires later, the moment the next `setActive(ancestor, true)` cascade reaches the component.

```ts title="enabled flips silently on an inactive subtree"
const room = new THREE.Group();
const lamp = new THREE.Object3D();
room.add(lamp);
starter.ctx.scene.add(room);
const lampBhv = addComponent(lamp, LampBehaviour);
starter.start();
// → lampBhv: onAwake → onEnable → onStart fired

setActive(room, false);  // room inactive → lampBhv.onDisable fires
lampBhv.disable();       // already-not-running, no event fires
lampBhv.enable();        // [!code highlight] no onEnable yet — room is still inactive
setActive(room, true);   // [!code highlight] cascade reaches lamp; lampBhv is enabled → onEnable fires now
```

Code recipes [#code-recipes]

Toggle a HUD subtree without unmounting [#toggle-a-hud-subtree-without-unmounting]

```ts title="pause / resume any subtree"
import { setActive } from "three-start";

const hud = starter.ctx.scene.getObjectByName("hud")!;

setActive(hud, false); // [!code highlight] every component under `hud` gets onDisable, render loop event methods stop
// ...later
setActive(hud, true);  // [!code highlight] every still-enabled component gets onEnable back
```

Pre-build an inactive subtree, switch it on later [#pre-build-an-inactive-subtree-switch-it-on-later]

You can stage objects, attach components, even add to the scene — all while the parent is inactive. Lifecycle is held off until the cascade reaches them.

```ts title="stage now, activate on demand"
import { setActive, addComponent } from "three-start";

const wave = new THREE.Group();
setActive(wave, false); // [!code highlight] turn off BEFORE adding components

for (const i of [0, 1, 2]) {
  const enemy = new THREE.Mesh(geo, mat);
  addComponent(enemy, EnemyAI); // dormant — `wave` is inactive
  wave.add(enemy);
}
starter.ctx.scene.add(wave);
starter.start();
// → none of the EnemyAI instances have run onAwake yet

// gameplay-ready: spawn them all at once
setActive(wave, true); // [!code highlight] cascade fires onAwake → onEnable → onStart on every EnemyAI
```

Disable a child while its parent is also disabled [#disable-a-child-while-its-parent-is-also-disabled]

`setActive` doesn't care about ordering. The own flag flips immediately; the cascade only fires when the parent chain is also active.

```ts title="setActive on a node deep in an inactive subtree"
import { setActive, getIsActiveSelf, getIsActive } from "three-start";

setActive(room, false);
setActive(lamp, false);   // [!code highlight] no events — `room` is still inactive

getIsActiveSelf(lamp);    // false (we just set it)
getIsActive(lamp);        // false (and `room` is also off)

setActive(room, true);    // cascade walks down → stops at `lamp` because lamp.activeSelf is now false
getIsActive(lamp);        // still false — own flag wins

setActive(lamp, true);    // [!code highlight] now lamp's chain is fully active → onEnable fires on its components
```

Untouched objects are active by default [#untouched-objects-are-active-by-default]

`setActive` is opt-in. An object you never pass to it has `activeSelf === true`, no extension allocated:

```ts
const random = new THREE.Object3D();
getIsActiveSelf(random); // true
getIsActive(random);     // true (assuming its ancestors are also untouched)
```

If you later add `random` under an inactive parent, `getIsActive(random)` flips to `false` automatically — its own flag is unchanged, but the chain above it is broken.


# TypeScript (/docs/core-guides/typescript)



three-start is written in TypeScript and ships its `.d.ts` files. Out of the box, components and modules are typed; the only piece you opt into is the **module registry**, because the library can't know what modules your project registers until you tell it.

Module registry [#module-registry]

Without the registry, `this.modules` falls back to `Record<string, ContextModule>` — every key is loose and every access loses the concrete subclass type. Augment the `ThreeStartRegister` interface once in your project, and every site that reads `modules` becomes fully typed:

```ts title="src/types.d.ts (or any file imported once at the top level)"
import "three-start";
import { InputModule } from "./modules/input";
import { AudioModule } from "./modules/audio";
import { AssetsModule } from "./modules/assets";

declare module "three-start" {
  interface ThreeStartRegister {
    modules: {
      input: InputModule;
      audio: AudioModule;
      assets: AssetsModule;
    };
  }
}
```

After this:

```ts
class Player extends Object3DBehaviour {
  onUpdate() {
    this.modules.input.isPressed("KeyA"); // ✓ typed as InputModule
    this.modules.unknown;                  // ✗ Property 'unknown' does not exist
  }
}

// also typed at the registration site:
const starter = new ThreeStart().addModules({
  input: new InputModule(),
  audio: new AudioModule(),
  // assets: ...   missing keys are allowed; addModules is incremental
});
```

The same registry powers `this.modules` in components, in other modules, and `ctx.modules` for top-level code.

Typed events on components and modules [#typed-events-on-components-and-modules]

[`Object3DBehaviour`](/docs/api/object3d-behaviour) and [`ContextModule`](/docs/api/context-module) both extend [`TypedEmitter`](/docs/api/typed-emitter). Pass an event map as a generic to type `emit` / `on` / `off` / `once`:

```ts
type HealthEvents = {
  damaged: [amount: number, source: THREE.Object3D];
  died: [];
};

class Health extends Object3DBehaviour<HealthEvents> {
  damage(n: number, source: THREE.Object3D) {
    this.emit("damaged", n, source);   // ✓ tuple matches
    // this.emit("damaged");            // ✗ missing args
    // this.emit("dyed");               // ✗ unknown event
  }
}

// listener side:
const h = getComponent(player, Health)!;
h.on("damaged", (amount, source) => { /* both typed */ });
```

The event map is `Record<string, unknown[]>` — each key is an event name, each value is the tuple of listener arguments.

Typed component access [#typed-component-access]

`getComponent` and `getComponents` are generic over the component class:

```ts
import { getComponent, getComponents } from "three-start";

const health = getComponent(obj, Health);          // Health | null
const allDamageables = getComponents(obj, Health); // Health[]
const everything = getComponents(obj);             // Object3DBehaviour[]
```

`addComponent` returns the instance with its concrete type, so chaining stays typed:

```ts
const ai = addComponent(enemy, EnemyAI);
ai.target = player; // ✓ typed
```

Typing the host object [#typing-the-host-object]

`this.object` on a component is `THREE.Object3D`. If your component only makes sense on a specific subclass, narrow it once in `onAwake` and store the narrower type:

```ts
class GlowOnHover extends Object3DBehaviour {
  private mesh!: THREE.Mesh<THREE.BufferGeometry, THREE.MeshStandardMaterial>;

  onAwake() {
    if (!(this.object instanceof THREE.Mesh)) {
      throw new Error("GlowOnHover requires a Mesh");
    }
    this.mesh = this.object;
  }
}
```

For internal components where you trust the call site, a one-line cast is fine:

```ts
private mesh = this.object as THREE.Mesh;
```

Type-only re-exports [#type-only-re-exports]

three-start re-exports the lifecycle / API contracts as types — use them when accepting components or modules from outside:

```ts
import type {
  ThreeStartOptions,
  ThreeStartModules,
  Object3DBehaviourConstructor,
  EventMap,
} from "three-start";

function spawn<T extends Object3DBehaviour>(
  obj: THREE.Object3D,
  klass: Object3DBehaviourConstructor<T>,
): T {
  return addComponent(obj, klass);
}
```

Importing Three.js [#importing-threejs]

three-start uses the WebGPU build internally (`three/webgpu`). For most user code, the standard `three` import is enough — `Object3D`, `Mesh`, materials and geometries all match. If you touch renderer-specific APIs (TSL nodes, the `RenderPipeline`, `WebGPURenderer`-only options), import from `three/webgpu`:

```ts
import * as THREE from "three";              // most code
import { WebGPURenderer } from "three/webgpu"; // renderer-specific
```

Type-checking the project [#type-checking-the-project]

```bash
npm run types:check
```

Runs `fumadocs-mdx` (regenerates derived module declarations) followed by `tsc --noEmit`. Run it before each commit if you've touched component or module shapes.


# Modules (/docs/core-guides/writing-modules)



Use a module when you have logic that lives for the entire 3D-app lifetime AND is shared between components (or other modules). Input, physics, audio, asset loading, scoring, networking — anything that exists once and gets read from everywhere — fits here.

A module is any class extending [`ContextModule`](/docs/api/context-module). It has the same render loop event methods as a component, plus `onAwake` and `onStart`.

```ts
import { ContextModule } from "three-start";

class InputModule extends ContextModule {
  private keys = new Set<string>();

  onAwake() {
    window.addEventListener("keydown", (e) => this.keys.add(e.code));
    window.addEventListener("keyup", (e) => this.keys.delete(e.code));
  }

  isPressed(code: string) {
    return this.keys.has(code);
  }
}
```

Registering [#registering]

Pass instances to [`starter.addModules({...})`](/docs/api/three-start) **before** `start()`:

```ts
const starter = new ThreeStart()
  .addModules({
    input: new InputModule(),
    audio: new AudioModule(),
  });

starter.start();
```

`addModules` can be called multiple times — registrations accumulate. Order matters: modules awake in the order they were registered, and a later module's `onAwake` can call methods on previously-registered ones via `this.modules.<key>`.

<Callout type="warn">
  After `start()`, `addModules` throws. Modules participate in the bootstrap traversal; they cannot be added once it has run.
</Callout>

What's available inside [#whats-available-inside]

| Access         | What                                                                                                                                                         |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `this.ctx`     | The shared [`ThreeContext`](/docs/api/three-context). Available from `onAwake`.                                                                              |
| `this.modules` | Sibling modules, keyed by registration name (`this.modules.input`). Available from `onAwake` (own and earlier-registered) and from `onStart` (every module). |

Lifecycle [#lifecycle]

Modules have a smaller surface than components — they're always on, can't be disabled, can't be destroyed:

| Method             | Fires                                                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `onAwake()`        | Once during `start()`, before any component lifecycle. Subscribe to DOM events, allocate buffers, kick off async loads.                |
| `onStart()`        | Once after every module has awakened. Use for cross-module wiring that requires other modules to be ready.                             |
| `onUpdate()`       | Each frame, &#x2A;*before any component's `onUpdate`**. Drive global state per frame — clear last-frame buffers, advance physics, etc. |
| `onBeforeRender()` | Each frame, before the renderer draws. Before any component's `onBeforeRender`.                                                        |
| `onAfterRender()`  | Each frame, after the draw. Before any component's `onAfterRender`.                                                                    |

The "modules first" guarantee means a component's `onUpdate` always sees state mutated in the same frame's module `onUpdate`. See [Lifecycle](/docs/core-guides/lifecycle) for the ordering rules.

<Callout type="info">
  Like components, render loop event methods are subscribed **only if you override them**. A module without `onUpdate` doesn't pay for dispatch.
</Callout>

Cross-module wiring [#cross-module-wiring]

`onAwake` is for self-init that doesn't need anyone else. `onStart` is for hooks that depend on siblings being ready:

```ts
class AudioModule extends ContextModule {
  private listener!: THREE.AudioListener;

  onAwake() {
    this.listener = new THREE.AudioListener();
    this.ctx.camera.add(this.listener);
  }

  onStart() {
    // every module finished onAwake by now — assets module can hand us its registry
    const buffers = this.modules.assets.getAll<AudioBuffer>("audio/*");
    // ...
  }
}
```

If two modules genuinely need each other in `onAwake`, register them in the right order — the earlier one is fully awake before the later one runs.

Emitting events from a module [#emitting-events-from-a-module]

`ContextModule` extends [`TypedEmitter`](/docs/api/typed-emitter). Same pattern as components — pass an event map as a generic:

```ts
type ScoreEvents = {
  changed: [score: number];
  highscore: [score: number, prev: number];
};

class ScoreModule extends ContextModule<ScoreEvents> {
  private score = 0;
  private high = 0;

  add(n: number) {
    this.score += n;
    this.emit("changed", this.score);
    if (this.score > this.high) {
      const prev = this.high;
      this.high = this.score;
      this.emit("highscore", this.score, prev);
    }
  }
}

// from any component:
this.modules.score.on("highscore", (s, prev) => this.celebrate(s - prev));
```

Type safety: register the module map [#type-safety-register-the-module-map]

Without augmentation, `this.modules` falls back to `Record<string, ContextModule>` — every key is loose, every access is generically-typed. Add the registry once in your project and `this.modules.input` becomes fully typed everywhere:

```ts
declare module "three-start" {
  interface ThreeStartRegister {
    modules: {
      input: InputModule;
      audio: AudioModule;
      score: ScoreModule;
    };
  }
}
```

See [TypeScript → module registry](/docs/core-guides/typescript#module-registry) for the full pattern, including how the same registry powers `addModules` autocomplete at the call site.

Things to remember [#things-to-remember]

* **No `onEnable` / `onDisable` / `onDestroy`.** Modules don't toggle and don't get destroyed by the lib — they're meant for app-lifetime logic. If you need a temporary system, that's a component.
* **`onUpdate` / `onBeforeRender` / `onAfterRender` run every frame.** Same rule as in components — keep them lean, don't allocate or initialise inside.
* **No destroy hook means cleanup is on you.** If your module owns resources that must be released (DOM listeners are usually fine to leave for the page lifetime, but websocket sockets, audio nodes, etc. may not be), subscribe a `Unmount` or `LoopStop` handler via `this.ctx.on(...)` and tear them down there. `starter.dispose()` fires both.


# Overview (/docs)



**three-start** is a minimal foundation layer for Three.js. It bootstraps the renderer, scene, camera, animation loop, and resize handling for you, and gives you a single component/lifecycle model to build the rest of your logic on — without replacing anything Three.js already does well.

No talk, just show me some code! [#no-talk-just-show-me-some-code]

```ts
import * as THREE from "three";
import { ThreeStart, Object3DBehaviour, addComponent } from "three-start";

const starter = new ThreeStart(); // [!code highlight]

const { scene, camera } = starter.ctx;
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshMatcapMaterial());
scene.add(cube);

addComponent(cube, Spin); // [!code ++]
addComponent(cube, Bob); // [!code ++]

starter.mount(document.getElementById("three")!); // [!code highlight]
starter.start(); // [!code highlight]
```

<ThreeDemo />

```ts title="Spin.ts"
class Spin extends Object3DBehaviour {
  speed = 2;
  private _initRotY;

  onAwake() {
    this._initRotY = this.object.rotation.y;
  }

  onUpdate() {
    this.object.rotation.y += this.speed * this.ctx.getDeltaTime();
  }

  onDestroy() {
    // restore the rotation Spin was started with
    this.object.rotation.y = this._initRotY;
  }
}
```

```ts title="Bob.ts"
class Bob extends Object3DBehaviour {
  amplitude = 0.5;
  frequency = 2; // cycles per second
  private baseY = 0;

  onAwake() {
    this.baseY = this.object.position.y;
  }

  onUpdate() {
    const t = this.ctx.getTime();
    this.object.position.y = this.baseY + Math.sin(t * this.frequency * Math.PI * 2) * this.amplitude;
  }
}

```

That snippet above is the entire project. The render loop, frame time (`getDeltaTime()` / `getTime()`), canvas resize, and lifecycle teardown are already wired up — the only code you wrote is the per-component logic itself.

The same handful of operations — `addComponent`, `getComponent`, `setActive`, `destroy` — and the same lifecycle event methods cover every component on every object, at any depth.

What it gives you [#what-it-gives-you]

* **Zero boilerplate.** Renderer, scene, camera, animation loop, resize — already set up. Mount the canvas and start.
* **Components on objects.** Write a class that extends `Object3DBehaviour`, override `onUpdate` / `onAwake` / `onDestroy` — attach it to an `Object3D`. The component hooks itself into the loop and the lifecycle.
* **A single way to turn logic on and off.** `component.enable()/disable()` or `setActive(obj, true/false)` — cascades activation or deactivation through a whole slice of the world, with all its components. No public "handle" methods, no passing references around.
* **Global systems as context modules.** Input, physics, asset manager, audio live in a `ContextModule` with the same lifecycle. Any component accesses them via `this.modules.<key>`.
* **Uniform code.** Every component looks the same. You can read someone else's scene at a glance. Teams collaborate more easily. LLMs don't destroy your architecture — they have a clear frame to work within.
* **Minimal overhead.** Per-frame event subscriptions are created **only if the method is overridden**. An empty component pays nothing for dispatch.

Who it's for [#who-its-for]

three-start is for you if you:

* write Three.js in vanilla + OOP, not through React Three Fiber
* are tired of writing the same bootstrap in every new project
* want your code to look uniform and stay readable without context
* are planning a project more complex than a single demo
* share code, work on a team, or teach others
* vibe-code and want the AI to stop destroying your architecture a week in

three-start is **not for you** if you prefer the React paradigm in 3D. In that case R3F is a great choice, and these aren't competing tools.


# Installation (/docs/installation)



Install [#install]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install three-start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add three-start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add three-start
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add three-start
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Peer dependencies [#peer-dependencies]

three-start keeps its own bundle small by declaring `three` and `eventemitter3` as peer dependencies — meaning they live in your project, not inside three-start. Install them alongside three-start:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install three eventemitter3
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add three eventemitter3
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add three eventemitter3
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add three eventemitter3
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Minimum versions:

| Package         | Minimum     | Tested against |
| --------------- | ----------- | -------------- |
| `three`         | `>=0.183.0` | `^0.183.2`     |
| `eventemitter3` | `>=5.0.4`   | `^5.0.4`       |

<Callout type="warn">
  **`three >= 0.183` is required**, not optional. three-start uses [`THREE.RenderPipeline`](https://threejs.org/docs/#api/en/renderers/webgpu/RenderPipeline) (a WebGPU-era addition introduced in r183) to drive rendering. Older releases simply don't ship the symbol — installation will succeed but `new ThreeStart()` will fail at runtime.
</Callout>

TypeScript [#typescript]

If you use TypeScript, also install the Three.js type definitions:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install -D @types/three@^0.183.1
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add -D @types/three@^0.183.1
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add --dev @types/three@^0.183.1
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add --dev @types/three@^0.183.1
    ```
  </CodeBlockTab>
</CodeBlockTabs>

three-start's own types ship with the package — no extra `@types/*` needed.

Sanity check [#sanity-check]

A minimal import to verify the package resolves:

```ts
import { ThreeStart } from "three-start";

const starter = new ThreeStart();
console.log(starter.ctx); // a ThreeContext with renderer, scene, camera, …
```

If this compiles and logs the context, you're good. From here, head to [Quick Start](/docs/quick-start) for a full working scene.

Renderer note (WebGPU vs WebGL) [#renderer-note-webgpu-vs-webgl]

By default `new ThreeStart()` instantiates a **WebGPU** renderer (`THREE.WebGPURenderer` from `three/webgpu`). If your target browser doesn't support WebGPU yet, or you just prefer WebGL, pass a renderer explicitly:

```ts
import * as THREE from "three";
import { ThreeStart } from "three-start";

const starter = new ThreeStart({
  ctx: {
    renderer: new THREE.WebGLRenderer({ antialias: true }),
  },
});
```

Everything else in three-start is renderer-agnostic.

Browser support [#browser-support]

three-start is a native ES module. It targets modern evergreen browsers and Node 20+ for tooling. WebGPU features follow what `three/webgpu` supports in the chosen browser.


# Quick Start (A) (/docs/quick-start-v2)



Three concepts cover almost everything you'll write with three-start:

* **Components** — pieces of behaviour you attach to a `THREE.Object3D` ([`Object3DBehaviour`](/docs/api/object3d-behaviour))
* **Modules** — global systems on the shared context ([`ContextModule`](/docs/api/context-module))
* **Lifecycle control** — turning behaviour on/off without ripping objects out of the scene

The snippet below shows them working together in a tiny shooter scene. Each line of the example exists because the design needs it — there's no API tour for its own sake.

```ts title="main.ts"
import * as THREE from "three/webgpu";
import {
  ThreeStart,
  Object3DBehaviour,
  ContextModule,
  addComponent,
  getComponent,
  setActive,
  getIsActive,
  destroy,
} from "three-start";

// ── Module: turn pointer clicks into "hit" events on whatever's under them ──
class PointerModule extends ContextModule {
  private raycaster = new THREE.Raycaster();
  private ndc = new THREE.Vector2();

  onAwake() {
    this.ctx.renderer.domElement.addEventListener("pointerdown", this.handle);
  }

  private handle = (e: PointerEvent) => {
    const r = this.ctx.renderer.domElement.getBoundingClientRect();
    this.ndc.x = ((e.clientX - r.left) / r.width) * 2 - 1;
    this.ndc.y = -((e.clientY - r.top) / r.height) * 2 + 1;
    this.raycaster.setFromCamera(this.ndc, this.ctx.camera);

    const [hit] = this.raycaster.intersectObjects(this.ctx.scene.children, true);
    if (!hit) return;

    // The module doesn't know which objects are damageable — it asks. // [!code highlight]
    getComponent(hit.object, Health)?.takeDamage(25);                  // [!code highlight]
  };
}

declare module "three-start" {
  interface ThreeStartRegister {
    modules: { pointer: PointerModule };
  }
}

// ── Component: idle rotation. Pure visual. ──
class Spin extends Object3DBehaviour {
  speed = 1;
  onUpdate() {
    this.object.rotation.y += this.speed * this.ctx.getDeltaTime();
  }
}

// ── Component: HP + reactions. Coexists with Spin on every enemy. ──
class Health extends Object3DBehaviour {
  hp = 100;
  private mat!: THREE.MeshStandardMaterial;

  onAwake() {
    this.mat = (this.object as THREE.Mesh).material as THREE.MeshStandardMaterial;
  }

  takeDamage(amount: number) {
    this.hp -= amount;
    this.mat.color.setRGB(1, this.hp / 100, this.hp / 100); // bleed red

    // Wounded enemies stop rotating — disable the sibling Spin component. // [!code highlight]
    const spin = getComponent(this.object, Spin);                          // [!code highlight]
    spin?.setEnabled(this.hp > 30);                                        // [!code highlight]

    // Dead enemies are removed from the scene entirely.                   // [!code highlight]
    if (this.hp <= 0) destroy(this.object);                                // [!code highlight]
  }

  onDestroy() {
    console.log("enemy down, hp was", this.hp);
  }
}

// ── Bootstrap ──
const starter = new ThreeStart()
  .addModules({ pointer: new PointerModule() });

const enemies = new THREE.Group();
starter.ctx.scene.add(enemies);

for (let i = 0; i < 5; i++) {
  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(),
    new THREE.MeshStandardMaterial({ color: 0xffffff }),
  );
  cube.position.x = i * 1.5 - 3;
  enemies.add(cube);

  addComponent(cube, Spin);
  addComponent(cube, Health);
}

starter.ctx.scene.add(new THREE.AmbientLight(0xffffff, 1));
starter.ctx.camera.position.set(0, 2, 8);
starter.ctx.camera.lookAt(0, 0, 0);

starter.mount(document.getElementById("app")!);
starter.start();

// Pause/resume the entire enemies branch with `P`.                        // [!code highlight]
window.addEventListener("keydown", (e) => {                                // [!code highlight]
  if (e.code === "KeyP") setActive(enemies, !getIsActive(enemies));        // [!code highlight]
});
```

Walk through what each concept does in this scene:

| Concept                             | Where it lives                              | Why the example needs it                                                                                                                                                                                                                      |
| ----------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Component** (`Object3DBehaviour`) | `Spin`, `Health`                            | Per-cube state and per-frame logic. Two components on the same cube prove they coexist independently — `Spin` doesn't care about HP, `Health` doesn't care about rotation.                                                                    |
| **Module** (`ContextModule`)        | `PointerModule`                             | One global piece of state (the raycaster + canvas listener). Every cube reuses it; the module doesn't need to know how many cubes exist or which ones are alive.                                                                              |
| `getComponent(obj, Class)`          | `PointerModule.handle`, `Health.takeDamage` | Both cases the caller only has an `Object3D` and needs to discover whether a particular behaviour is attached. The pointer module finds `Health` on the hit object; `Health` finds its sibling `Spin` to stun it.                             |
| `comp.setEnabled(false)`            | `Health.takeDamage` (stun)                  | Pause **one behaviour** on an object without affecting others. Cube keeps rendering; only its rotation halts.                                                                                                                                 |
| `setActive(group, false)`           | `KeyP` handler (pause)                      | Freeze an **entire subtree** — every component under `group` runs `onDisable`. Reverse with `setActive(group, true)`. (`setActive` is lifecycle-only; set `group.visible = false` separately if you also want the subtree to stop rendering.) |
| `destroy(object)`                   | `Health.takeDamage` (death)                 | Permanently remove. Fires `onDestroy` on every component on the object before unparenting.                                                                                                                                                    |

These three layers cover the whole library. The rest of the docs goes deep into each:


# Quick Start (B) (/docs/quick-start)



Three concepts cover almost everything you'll write with three-start:

1. **Components** — pieces of behaviour you attach to a `THREE.Object3D` ([`Object3DBehaviour`](/docs/api/object3d-behaviour)).
2. **Modules** — global systems that live on the context ([`ContextModule`](/docs/api/context-module)).
3. **Control** — turning behaviour on/off without ripping objects out of the scene.

This page walks through each, then ends with a tiny demo that uses all three together.

Bootstrap [#bootstrap]

```ts
import * as THREE from "three/webgpu";
import { ThreeStart } from "three-start";

const starter = new ThreeStart();

starter.mount(document.getElementById("app")!);
starter.start();
```

That's the minimum. `starter.ctx` exposes the renderer, scene, camera, and event bus — see [`ThreeContext`](/docs/api/three-context).

<Callout type="info">
  `mount()` and `start()` are independent — call them in any order. `mount()` controls the canvas + render loop; `start()` runs the bootstrap that wakes registered modules and components.
</Callout>

1\. Components — extend `Object3DBehaviour` [#1-components--extend-object3dbehaviour]

A component is a class that extends [`Object3DBehaviour`](/docs/api/object3d-behaviour) and overrides whichever lifecycle hooks it needs. Inside, `this.object` is the Three.js object it's attached to, `this.ctx` is the shared runtime.

```ts
import { Object3DBehaviour, addComponent } from "three-start";

class Spin extends Object3DBehaviour {
  speed = 1;

  onAwake() {                                 // [!code highlight]
    // one-time setup
  }

  onUpdate() {                                // [!code highlight]
    const dt = this.ctx.getDeltaTime();
    this.object.rotation.y += this.speed * dt;
  }

  onDestroy() {                               // [!code highlight]
    // cleanup if needed
  }
}

const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial());
starter.ctx.scene.add(cube);

addComponent(cube, Spin);                     // attach
```

Lifecycle in short: `onAwake` (once) → `onEnable` → `onStart` (once) → `onUpdate` / `onBeforeRender` / `onAfterRender` (per frame) → `onDisable` → `onDestroy`.

<Callout type="info">
  Per-frame hooks are subscribed **only if you override them** — a component without `onUpdate` pays nothing for dispatch. See [`Object3DBehaviour`](/docs/api/object3d-behaviour) for the full lifecycle.
</Callout>

Querying and removing [#querying-and-removing]

```ts
import { getComponent, getComponents, destroy } from "three-start";

const spin = getComponent(cube, Spin);          // first match, or null
const allSpins = getComponents(cube, Spin);     // all matches
const everything = getComponents(cube);         // every behaviour on cube

destroy(spin!);   // remove just this component (fires onDisable → onDestroy)
destroy(cube);    // remove the whole object + every component on it / its descendants
```

2\. Modules — extend `ContextModule` [#2-modules--extend-contextmodule]

A module is a singleton that lives on the context. Use it for anything global — input, physics, asset loading, audio, scoring. It has the same lifecycle hooks as components (minus the `onEnable`/`onDisable`/`onDestroy` set, since modules are always on).

```ts
import { ContextModule, ThreeStart } from "three-start";

class InputModule extends ContextModule {
  private keys = new Set<string>();

  onAwake() {                                 // [!code highlight]
    window.addEventListener("keydown", (e) => this.keys.add(e.code));
    window.addEventListener("keyup", (e) => this.keys.delete(e.code));
  }

  isPressed(code: string) {
    return this.keys.has(code);
  }
}
```

Register modules on the starter **before** calling `start()`:

```ts
const starter = new ThreeStart()
  .addModules({ input: new InputModule() });    // [!code highlight]
```

<Callout type="warn">
  Modules can only be registered before `start()`. After bootstrap, `addModules` throws — the lifecycle has already run.
</Callout>

Type-safe access from anywhere [#type-safe-access-from-anywhere]

Augment the registry once, and `this.modules.input` becomes fully typed in every component and other module:

```ts
declare module "three-start" {
  interface ThreeStartRegister {
    modules: {
      input: InputModule;
    };
  }
}
```

Now any component can read it via `this.modules.input`:

```ts
class PlayerControl extends Object3DBehaviour {
  speed = 5;
  onUpdate() {
    const dt = this.ctx.getDeltaTime();
    if (this.modules.input.isPressed("KeyD")) this.object.position.x += this.speed * dt;
    if (this.modules.input.isPressed("KeyA")) this.object.position.x -= this.speed * dt;
  }
}
```

3\. Control — `setActive`, `enable` / `disable`, `destroy` [#3-control--setactive-enable--disable-destroy]

Three levels of control, smallest to largest scope:

| API                              | What it does                                                                                                                                                                                                                                               |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comp.disable()` / `enable()`    | Toggle a single component on/off. Other components on the same object keep running.                                                                                                                                                                        |
| `setActive(obj, false)`          | Freeze the entire object subtree. Every component on it (and its descendants) goes through `onDisable`. Reverse with `setActive(obj, true)`. (`setActive` doesn't touch `obj.visible` — set that yourself if you also want the subtree to stop rendering.) |
| `destroy(comp)` / `destroy(obj)` | Permanent removal. Fires `onDestroy`; the component / object is gone.                                                                                                                                                                                      |

```ts
import { setActive, getIsActive } from "three-start";

const ctrl = getComponent(player, PlayerControl)!;

ctrl.disable();              // player stops moving but stays visible/animated by other components
ctrl.enable();               // resumes

setActive(player, false);    // freeze the whole player branch (pause menu)
setActive(player, true);     // unfreeze
getIsActive(player);         // → true
```

<Callout type="info">
  `setActive` is the right choice for **pausing a subtree** (a paused enemy, a hidden room). `disable`/`enable` is for **temporarily turning off one behaviour** (mute a behaviour while another is in charge). `destroy` is for **permanent removal**.
</Callout>

Putting it together [#putting-it-together]

A self-contained example combining all three concepts. WASD moves the cube; pressing `P` toggles pause via `setActive`.

```ts title="main.ts"
import * as THREE from "three/webgpu";
import {
  ThreeStart,
  Object3DBehaviour,
  ContextModule,
  addComponent,
  getComponent,
  setActive,
  getIsActive,
} from "three-start";

// ── 1. Module: keyboard input ───────────────────────────────────────────
class InputModule extends ContextModule {
  private keys = new Set<string>();
  private justPressed = new Set<string>();

  onAwake() {
    window.addEventListener("keydown", (e) => {
      if (!this.keys.has(e.code)) this.justPressed.add(e.code);
      this.keys.add(e.code);
    });
    window.addEventListener("keyup", (e) => this.keys.delete(e.code));
  }

  /** Called automatically before any component's onUpdate. */
  onUpdate() {
    this.justPressed.clear();
  }

  isPressed(code: string) { return this.keys.has(code); }
  wasJustPressed(code: string) { return this.justPressed.has(code); }
}

declare module "three-start" {
  interface ThreeStartRegister {
    modules: { input: InputModule };
  }
}

// ── 2. Component: read input, move object ───────────────────────────────
class PlayerControl extends Object3DBehaviour {
  speed = 5;

  onUpdate() {
    const dt = this.ctx.getDeltaTime();
    const k = this.modules.input;
    if (k.isPressed("KeyD")) this.object.position.x += this.speed * dt;
    if (k.isPressed("KeyA")) this.object.position.x -= this.speed * dt;
    if (k.isPressed("KeyW")) this.object.position.z -= this.speed * dt;
    if (k.isPressed("KeyS")) this.object.position.z += this.speed * dt;
  }
}

// ── 3. Bootstrap ────────────────────────────────────────────────────────
const starter = new ThreeStart()
  .addModules({ input: new InputModule() });

const player = new THREE.Mesh(
  new THREE.BoxGeometry(),
  new THREE.MeshNormalMaterial(),
);
starter.ctx.scene.add(player);
starter.ctx.camera.position.set(0, 5, 10);
starter.ctx.camera.lookAt(player.position);

addComponent(player, PlayerControl);

starter.mount(document.getElementById("app")!);
starter.start();

// ── 4. Pause / resume on `P` ────────────────────────────────────────────
window.addEventListener("keydown", (e) => {
  if (e.code !== "KeyP") return;
  setActive(player, !getIsActive(player));    // [!code highlight]
});

// ── 5. Demonstrate per-component control ────────────────────────────────
const ctrl = getComponent(player, PlayerControl)!;
ctrl.disable();   // input stops being read — but the cube still renders
ctrl.enable();    // back to controllable
```

That's the whole loop:

* `InputModule` provides global state any component can read.
* `PlayerControl` is per-object behaviour reading from the module.
* `setActive`, `disable`/`enable`, and `destroy` form a control toolkit at three different scopes.


# Why it exists (/docs/why)



Three.js is a rendering library. Everything around it, you build yourself — and you build it again in every new project. That "everything around it" is where the trouble starts.

The boilerplate [#the-boilerplate]

Spinning up a Three.js scene always starts with the same dozens of lines: renderer, scene, camera, DPR-aware sizing, a `ResizeObserver` or window listener, a `requestAnimationFrame` loop, a timer for delta and elapsed time. None of this is interesting, none of it is project-specific, and none of it is standardized across the ecosystem — everyone copies their own preferred snippet.

No model for logic [#no-model-for-logic]

Once you have a renderer and a scene, you need logic. Things that move, react, initialize, and eventually go away. Three.js has no opinion about how that works — and filling that gap yourself is where most projects start to crack.

1\. Where does `update` come from? [#1-where-does-update-come-from]

The moment a project has more than one thing happening per frame, this becomes the central question. What you see in the wild:

* a single `animate()` function that imports every moving part and calls them in order
* a growing list of callbacks registered to a hand-rolled event bus
* per-object closures captured over shared state, triggered from a god-loop
* framework-shaped abstractions reinvented from scratch in every project

All of these work for a single cube. None of them scale cleanly. The loop becomes the dumping ground where every module's per-frame concerns collide.

2\. Lifecycle is the same story. [#2-lifecycle-is-the-same-story]

Every piece of logic also has something to set up on start, and something to clean up when it's done. Without a shared pattern, cleanup is forgotten or re-invented per project. Init code scatters across constructors, factory functions, and ad-hoc `setup()` calls that may or may not run in the right order. Adding a new system means answering the same questions again from scratch: where does it live, how does it start, where does its update go, what happens on destroy?

3\. And then there's control. [#3-and-then-theres-control]

A complex object — a player, an enemy, a UI overlay — is often many systems working together: input, animation, physics, sound. All of them need to start together and stop together. Without a standard interface, pausing the game means reaching into each one manually and calling whatever you happened to name the method — `disable()`, `pause()`, `setSleeping()`, `mute()` — using references you threaded there yourself. Every piece has its own API. Every new system makes the wiring bigger.

> **Picture this:** your player has four systems and you want to pause the game. You call `inputManager.disable()`, `animator.pause()`, `physicsBody.setSleeping(true)`, `soundEmitter.mute()` — all different APIs, all in different places, held together by memory and discipline. Add two more systems next week. Forget to update the pause function. Now you have a bug that only appears after a specific sequence in production.

The pattern doesn't get better on its own — it just grows with the project.

Fragmented community code [#fragmented-community-code]

Any Three.js example you open — on CodeSandbox, in a Twitter thread, in a repo — is structured differently from the last one. There's no shared mental model, so to understand a demo you read it end to end. This slows the whole ecosystem: harder to learn from, harder to contribute to, harder to onboard into a team.

Why this isn't R3F [#why-this-isnt-r3f]

React Three Fiber addresses the same organizational problem, and does it well — but by importing the React paradigm, designed for UI, into interactive 3D logic. For people who prefer vanilla + OOP (and there are a lot of us), that trade-off isn't the right one.

**three-start is the answer for them.** A thin layer that standardizes the bootstrap, the lifecycle, and the way logic is split and wired — without pulling in a rendering paradigm from another domain.
