lecodes-sdk 0.20.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/global.d.ts +17 -5
  2. package/dist/types/canvas/Canvas.d.ts +2 -0
  3. package/dist/types/gl/DecalSet.d.ts +48 -3
  4. package/dist/types/gl/Geometry.d.ts +12 -0
  5. package/dist/types/gl/Lightmap.d.ts +9 -0
  6. package/dist/types/gl/Material.d.ts +4 -0
  7. package/dist/types/gl/Mesh.d.ts +7 -1
  8. package/dist/types/gl/Model.d.ts +34 -0
  9. package/dist/types/gl/Particles.d.ts +13 -0
  10. package/dist/types/gl/Scene.d.ts +1 -1
  11. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  12. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  13. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  14. package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
  15. package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
  16. package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
  17. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  18. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  19. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  20. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  21. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  22. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  23. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  24. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  25. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  26. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  27. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  28. package/dist/types/inject.d.ts +28 -26
  29. package/dist/types/runtime/files.d.ts +24 -1
  30. package/dist/types/scene/defineScene.d.ts +10 -3
  31. package/dist/types.json +1 -1
  32. package/package.json +1 -1
  33. package/prompts/README.md +142 -142
  34. package/src/bridges.d.ts +1599 -1481
  35. package/src/canvas/Canvas.ts +21 -0
  36. package/src/compile/__tests__/compile.test.ts +11 -0
  37. package/src/compile/header.ts +6 -3
  38. package/src/compile/index.ts +1 -0
  39. package/src/compile/sceneEditor.ts +42 -1
  40. package/src/gl/DecalSet.ts +132 -5
  41. package/src/gl/Geometry.ts +348 -284
  42. package/src/gl/Lightmap.ts +32 -5
  43. package/src/gl/Material.ts +21 -0
  44. package/src/gl/Mesh.ts +120 -102
  45. package/src/gl/Model.ts +33 -1
  46. package/src/gl/Node.ts +1 -1
  47. package/src/gl/Particles.ts +21 -3
  48. package/src/gl/Scene.ts +1 -1
  49. package/src/gl/animation/DynamicBone.ts +459 -0
  50. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  51. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  52. package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
  53. package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
  54. package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
  55. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  56. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  57. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  58. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  59. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
  60. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  61. package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
  62. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  63. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  64. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  65. package/src/gl/vehicle/Vehicle.ts +666 -0
  66. package/src/gl/vehicle/Wheel.ts +290 -0
  67. package/src/inject.ts +226 -224
  68. package/src/runtime/files.ts +32 -2
  69. package/src/scene/defineScene.ts +26 -10
  70. package/src/scene/level.ts +2 -2
  71. package/dist/types/gl/Gearbox.d.ts +0 -86
  72. package/dist/types/gl/Vehicle.d.ts +0 -191
  73. package/dist/types/gl/Wheel.d.ts +0 -95
  74. package/src/gl/Gearbox.ts +0 -212
  75. package/src/gl/Vehicle.ts +0 -473
  76. package/src/gl/Wheel.ts +0 -240
  77. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
@@ -1,75 +1,75 @@
1
- // A reverb zone, as an aspect on a Node (docs/audio-plan.md §2): a box or sphere volume that
2
- // follows the node; while the listener is inside, the zone's reverb blends onto a bus (the bus's
3
- // own reverb outside it). Zones nest: the smallest one containing the listener wins. `blend` is
4
- // how many metres inside the border the crossfade takes — walking through a doorway is smooth.
5
- //
6
- // new Node().aspect(AudioZone, { box: [6, 2.5, 6], reverb: 'hall', blend: 2 })
7
- // corridor.aspect(Shape, { box: [1, 1.5, 8] }).aspect(AudioZone, { reverb: 'bathroom' }) // the Shape's box
8
- //
9
- // Sizes follow Shape's convention: `box` = HALF-extents, `sphere` = radius, in the node's local
10
- // units (the node's scale applies). Without host audio the aspect is inert.
11
-
12
- import { Aspect } from "../core/Aspect"
13
- import { reverbBlock, type ReverbParams, type ReverbPreset } from "../audio/Bus"
14
- import { audio } from "../audio/audio"
15
- import { audioSupported } from "../audio/support"
16
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
17
- import type { Node } from "./Node"
18
- import { Shape } from "./Shape"
19
-
20
- export class AudioZone extends Aspect<"audioZone", Node> {
21
- static readonly aspect = "audioZone"
22
-
23
- private _box: Vec3Like | null = null
24
- private _sphere: number | null = null
25
- private _reverb: ReverbPreset | ReverbParams = "room"
26
- private _blend = 1
27
- private _bus = "sfx"
28
- /** @internal engine zone id. */
29
- _id = 0
30
- /** The engine's zone id — what `audio.stats.listenerZone` reports while the listener is inside. */
31
- get id(): number { return this._id }
32
-
33
- /** Half-extents [hx, hy, hz]; falls back to the node's Shape box, then a 1 m cube. */
34
- get box(): Vec3Like | null { return this._box }
35
- set box(v: Vec3Like | null) { this._box = v; if (v) this._sphere = null; this._push() }
36
- /** Radius; falls back to the node's Shape sphere. */
37
- get sphere(): number | null { return this._sphere }
38
- set sphere(v: number | null) { this._sphere = v; if (v !== null) this._box = null; this._push() }
39
- /** A preset name or explicit params (see Bus.reverb). */
40
- get reverb(): ReverbPreset | ReverbParams { return this._reverb }
41
- set reverb(v: ReverbPreset | ReverbParams) { this._reverb = v; this._push() }
42
- /** Crossfade depth in metres inside the border (0 = a hard edge). Default 1. */
43
- get blend(): number { return this._blend }
44
- set blend(v: number) { this._blend = v; this._push() }
45
- /** The bus the zone's reverb rides on. Default `sfx`. */
46
- get bus(): string { return this._bus }
47
- set bus(v: string) { this._bus = v; this._push() }
48
-
49
- onAttach(): void {
50
- if (!audioSupported) return
51
- this._id = _creatorAudio.createZone()
52
- if (!this._id) return
53
- _creatorAudio.attachZone(this._id, this.node.id)
54
- this._push()
55
- }
56
-
57
- onDetach(): void {
58
- if (this._id && audioSupported) _creatorAudio.destroyZone(this._id)
59
- this._id = 0
60
- }
61
-
62
- private _push(): void {
63
- if (!this._id || !audioSupported) return
64
- let box = this._box, sphere = this._sphere
65
- if (!box && sphere === null) {
66
- const shape = this.node.get(Shape)
67
- if (shape?.sphere !== undefined) sphere = shape.sphere
68
- else if (shape?.box) box = shape.box
69
- }
70
- const dims = sphere !== null
71
- ? new Float32Array([ sphere * 2, sphere * 2, sphere * 2 ])
72
- : new Float32Array(box ? [ cx(box) * 2, cy(box) * 2, cz(box) * 2 ] : [ 2, 2, 2 ])
73
- _creatorAudio.setZone(this._id, sphere !== null ? 1 : 0, dims, this._blend, audio._busId(this._bus), reverbBlock(this._reverb))
74
- }
75
- }
1
+ // A reverb zone, as an aspect on a Node (docs/audio-plan.md §2): a box or sphere volume that
2
+ // follows the node; while the listener is inside, the zone's reverb blends onto a bus (the bus's
3
+ // own reverb outside it). Zones nest: the smallest one containing the listener wins. `blend` is
4
+ // how many metres inside the border the crossfade takes — walking through a doorway is smooth.
5
+ //
6
+ // new Node().aspect(AudioZone, { box: [6, 2.5, 6], reverb: 'hall', blend: 2 })
7
+ // corridor.aspect(Shape, { box: [1, 1.5, 8] }).aspect(AudioZone, { reverb: 'bathroom' }) // the Shape's box
8
+ //
9
+ // Sizes follow Shape's convention: `box` = HALF-extents, `sphere` = radius, in the node's local
10
+ // units (the node's scale applies). Without host audio the aspect is inert.
11
+
12
+ import { Aspect } from "../../core/Aspect"
13
+ import { reverbBlock, type ReverbParams, type ReverbPreset } from "../../audio/Bus"
14
+ import { audio } from "../../audio/audio"
15
+ import { audioSupported } from "../../audio/support"
16
+ import { cx, cy, cz, type Vec3Like } from "../../math/vec"
17
+ import type { Node } from "../Node"
18
+ import { Shape } from "../physics/Shape"
19
+
20
+ export class AudioZone extends Aspect<"audioZone", Node> {
21
+ static readonly aspect = "audioZone"
22
+
23
+ private _box: Vec3Like | null = null
24
+ private _sphere: number | null = null
25
+ private _reverb: ReverbPreset | ReverbParams = "room"
26
+ private _blend = 1
27
+ private _bus = "sfx"
28
+ /** @internal engine zone id. */
29
+ _id = 0
30
+ /** The engine's zone id — what `audio.stats.listenerZone` reports while the listener is inside. */
31
+ get id(): number { return this._id }
32
+
33
+ /** Half-extents [hx, hy, hz]; falls back to the node's Shape box, then a 1 m cube. */
34
+ get box(): Vec3Like | null { return this._box }
35
+ set box(v: Vec3Like | null) { this._box = v; if (v) this._sphere = null; this._push() }
36
+ /** Radius; falls back to the node's Shape sphere. */
37
+ get sphere(): number | null { return this._sphere }
38
+ set sphere(v: number | null) { this._sphere = v; if (v !== null) this._box = null; this._push() }
39
+ /** A preset name or explicit params (see Bus.reverb). */
40
+ get reverb(): ReverbPreset | ReverbParams { return this._reverb }
41
+ set reverb(v: ReverbPreset | ReverbParams) { this._reverb = v; this._push() }
42
+ /** Crossfade depth in metres inside the border (0 = a hard edge). Default 1. */
43
+ get blend(): number { return this._blend }
44
+ set blend(v: number) { this._blend = v; this._push() }
45
+ /** The bus the zone's reverb rides on. Default `sfx`. */
46
+ get bus(): string { return this._bus }
47
+ set bus(v: string) { this._bus = v; this._push() }
48
+
49
+ onAttach(): void {
50
+ if (!audioSupported) return
51
+ this._id = _creatorAudio.createZone()
52
+ if (!this._id) return
53
+ _creatorAudio.attachZone(this._id, this.node.id)
54
+ this._push()
55
+ }
56
+
57
+ onDetach(): void {
58
+ if (this._id && audioSupported) _creatorAudio.destroyZone(this._id)
59
+ this._id = 0
60
+ }
61
+
62
+ private _push(): void {
63
+ if (!this._id || !audioSupported) return
64
+ let box = this._box, sphere = this._sphere
65
+ if (!box && sphere === null) {
66
+ const shape = this.node.get(Shape)
67
+ if (shape?.sphere !== undefined) sphere = shape.sphere
68
+ else if (shape?.box) box = shape.box
69
+ }
70
+ const dims = sphere !== null
71
+ ? new Float32Array([ sphere * 2, sphere * 2, sphere * 2 ])
72
+ : new Float32Array(box ? [ cx(box) * 2, cy(box) * 2, cz(box) * 2 ] : [ 2, 2, 2 ])
73
+ _creatorAudio.setZone(this._id, sphere !== null ? 1 : 0, dims, this._blend, audio._busId(this._bus), reverbBlock(this._reverb))
74
+ }
75
+ }
@@ -3,8 +3,8 @@
3
3
  // engine reads whatever camera it renders with. Set a node to listen from somewhere else (the
4
4
  // player's head in a third-person game, so sounds pan around the character, not the camera).
5
5
 
6
- import { audioSupported } from "../audio/support"
7
- import type { Node } from "./Node"
6
+ import { audioSupported } from "../../audio/support"
7
+ import type { Node } from "../Node"
8
8
 
9
9
  export class SceneAudio {
10
10
  private _listener: Node | null = null
@@ -20,12 +20,12 @@
20
20
  // Without a loaded NavMesh the aspect is inert — `goTo` returns false, nothing throws — so a
21
21
  // project stays runnable on a host without navigation.
22
22
 
23
- import { Aspect } from "../core/Aspect"
24
- import type { FieldMeta } from "../core/fields"
25
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
26
- import { CharacterController } from "./CharacterController"
23
+ import { Aspect } from "../../core/Aspect"
24
+ import type { FieldMeta } from "../../core/fields"
25
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
26
+ import { CharacterController } from "../physics/CharacterController"
27
27
  import { NavCrowd, NavMesh, NAV_AGENT, NAV_READ } from "./NavMesh"
28
- import type { Node } from "./Node"
28
+ import type { Node } from "../Node"
29
29
 
30
30
  export type NavAgentState = "idle" | "moving" | "arrived" | "blocked" | "offmesh"
31
31
  export type NavAgentAvoidance = "off" | "low" | "medium" | "high"
@@ -18,14 +18,14 @@
18
18
  // (creator-nav) is the algorithms and the per-step hot path: ONE crowd update + ONE flat read per
19
19
  // fixed step for every agent in the scene (`NavCrowd`, a system the mesh attaches to its scene).
20
20
 
21
- import { fetch } from "../runtime/fetch"
22
- import { System } from "../core/Aspect"
23
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
24
- import { Geometry } from "./Geometry"
25
- import { Material } from "./Material"
26
- import { Mesh } from "./Mesh"
27
- import type { Node } from "./Node"
28
- import type { Scene } from "./Scene"
21
+ import { fetch } from "../../runtime/fetch"
22
+ import { System } from "../../core/Aspect"
23
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
24
+ import { Geometry } from "../Geometry"
25
+ import { Material } from "../Material"
26
+ import { Mesh } from "../Mesh"
27
+ import type { Node } from "../Node"
28
+ import type { Scene } from "../Scene"
29
29
  import type { NavAgent } from "./NavAgent"
30
30
 
31
31
  // Fixed-order layouts — keep in step with creator-nav/creator-nav.h (CNAV_AGENT_* / CNAV_READ_*).
@@ -38,12 +38,12 @@
38
38
  // Note: it collides with solid bodies but PASSES THROUGH triggers, and (v1) is not itself detected by
39
39
  // triggers and isn't pointer-pickable while active.
40
40
 
41
- import { Aspect } from "../core/Aspect"
42
- import type { FieldMeta } from "../core/fields"
43
- import { Vec3, cx, cy, cz, type Vec2Like, type Vec3Like } from "../math/vec"
44
- import type { CompAxis, CompWriter } from "../core/compWrite"
41
+ import { Aspect } from "../../core/Aspect"
42
+ import type { FieldMeta } from "../../core/fields"
43
+ import { Vec3, cx, cy, cz, type Vec2Like, type Vec3Like } from "../../math/vec"
44
+ import type { CompAxis, CompWriter } from "../../core/compWrite"
45
45
  import { Shape } from "./Shape"
46
- import type { Node } from "./Node"
46
+ import type { Node } from "../Node"
47
47
 
48
48
  /** Where the character's feet are, as reported by the solver after the last step. */
49
49
  export type GroundState = "ground" | "slope" | "unsupported" | "air"
@@ -11,11 +11,11 @@
11
11
  // velocity / applyImpulse. Reading node.position is free (native already wrote it). A `position` write
12
12
  // is a TELEPORT that reaches the body (Shape routes it), which is how a kinematic body is placed too.
13
13
 
14
- import { Aspect } from "../core/Aspect"
15
- import type { FieldMeta } from "../core/fields"
16
- import type { CompAxis, CompWriter } from "../core/compWrite"
17
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
18
- import { nodeRegistry, type Node } from "./Node"
14
+ import { Aspect } from "../../core/Aspect"
15
+ import type { FieldMeta } from "../../core/fields"
16
+ import type { CompAxis, CompWriter } from "../../core/compWrite"
17
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
18
+ import { nodeRegistry, type Node } from "../Node"
19
19
  import { Shape } from "./Shape"
20
20
 
21
21
  export type MotionType = "static" | "dynamic" | "kinematic"
@@ -95,6 +95,7 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
95
95
  }
96
96
 
97
97
  private _bodyId = 0
98
+ private static _refusedWarned = false
98
99
 
99
100
  onAttach(): void {
100
101
  if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
@@ -107,6 +108,12 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
107
108
  }
108
109
  const shapeId = shape._claim() // drop the pick-only body; reuse its shape
109
110
  this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
111
+ // The engine refuses a body past its limit (4 096 unless configured) — silently, and a level that
112
+ // crosses it loses whatever loads LAST (the terrain, streamed in after the props): say so once.
113
+ if (!this._bodyId && shapeId && !Physics._refusedWarned) {
114
+ Physics._refusedWarned = true
115
+ console.warn(`Physics: the engine refused a body for "${this.node.name}" — the body limit is reached? Raise it before the level loads: Physics.configure({ maxBodies })`)
116
+ }
110
117
  // A fresh body already carries DEFAULT_FRICTION host-side, so only a NAMED surface has to
111
118
  // travel — the common case costs no bridge call at all.
112
119
  if (this._friction !== DEFAULT_FRICTION) _creator.physicsSetFriction?.(this._bodyId, this._friction)
@@ -26,10 +26,10 @@
26
26
  // lie: the pose holds, nothing can wake them, the solver skips them. Raycasts hit it either way, and
27
27
  // a frozen `'all'` body is an obstacle you can stand on.
28
28
 
29
- import { Aspect } from "../core/Aspect"
30
- import type { FieldMeta } from "../core/fields"
31
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
32
- import { Node } from "./Node"
29
+ import { Aspect } from "../../core/Aspect"
30
+ import type { FieldMeta } from "../../core/fields"
31
+ import { cx, cy, cz, type Vec3Like } from "../../math/vec"
32
+ import { Node } from "../Node"
33
33
  import { DEFAULT_FRICTION } from "./Physics"
34
34
 
35
35
  export interface RagdollHinge {
@@ -71,7 +71,8 @@ const STRIDE = 12
71
71
 
72
72
  // Bone names per humanoid role, tried in order: Mixamo / MotusMan / Unity-style first, then UE-style.
73
73
  // `node.bone()` already matches the part after a `:` / `|` prefix (`mixamorig:Hips`).
74
- const HUMANOID: Record<string, string[]> = {
74
+ /** @internal shared with DynamicBone's `colliders: 'humanoid'` */
75
+ export const HUMANOID: Record<string, string[]> = {
75
76
  hips: [ "Hips", "pelvis", "Pelvis", "hip" ],
76
77
  spine: [ "Spine", "spine_01", "spine", "Spine1" ],
77
78
  neck: [ "Neck", "neck_01", "neck" ],
@@ -96,7 +97,8 @@ const HUMANOID: Record<string, string[]> = {
96
97
  * legs, hands and feet where the rig has them): sizes for a ~1.8 m human, mass shares from body
97
98
  * segment tables, knees and elbows as hinges that fold the natural way for a model standing in a
98
99
  * T- or A-pose facing +Z. */
99
- const humanoidParts = (find: (role: string) => string | null): RagdollPart[] => {
100
+ /** @internal shared with DynamicBone's `colliders: 'humanoid'` */
101
+ export const humanoidParts = (find: (role: string) => string | null): RagdollPart[] => {
100
102
  const hips = find("hips"), spine = find("spine"), neck = find("neck"), head = find("head")
101
103
  if (!hips) throw new Error("Ragdoll 'humanoid': no hips bone (Hips / pelvis) on this model — list the parts by name")
102
104
  const parts: RagdollPart[] = []
@@ -17,9 +17,9 @@
17
17
  // pick body is a STATIC snapshot at attach time — for a moving pickable object add a Physics body
18
18
  // (which follows the sim), rather than relying on the pick body.
19
19
 
20
- import { Aspect } from "../core/Aspect"
21
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
22
- import type { Node } from "./Node"
20
+ import { Aspect } from "../../core/Aspect"
21
+ import { cx, cy, cz, type Vec3Like } from "../../math/vec"
22
+ import type { Node } from "../Node"
23
23
 
24
24
  /** Which geometry a Shape uses — the one-of the fields below encode (see `_build`'s order). */
25
25
  export type ShapeKind = "auto" | "box" | "sphere" | "cylinder" | "capsule" | "mesh" | "convex" | "heightfield"
@@ -1,45 +1,45 @@
1
- // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
- // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
- // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
- //
5
- // const goal = new Mesh(box(), material)
6
- // .aspect(Shape, { box: [1, 2, 1] })
7
- // .aspect(Trigger)
8
- // goal.addEventListener('enter', other => win(other))
9
- // goal.addEventListener('exit', other => …)
10
-
11
- import { Aspect } from "../core/Aspect"
12
- import { Shape } from "./Shape"
13
- import { ensurePhysicsEvents } from "./physicsEvents"
14
- import type { Node } from "./Node"
15
-
16
- export class Trigger extends Aspect<"trigger", Node> {
17
- static readonly aspect = "trigger"
18
-
19
- private _bodyId = 0
20
- /** Native body id (0 if no physics support). */
21
- get id(): number { return this._bodyId }
22
-
23
- onAttach(): void {
24
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
25
- const shape = this.node.get(Shape)
26
- if (!shape) {
27
- throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
28
- }
29
- const shapeId = shape._claim()
30
- this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
31
- shape._ownBody(this._bodyId)
32
- ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
33
- }
34
-
35
- onDetach(): void {
36
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
37
- const shape = this.node.get(Shape)
38
- if (shape) {
39
- shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
40
- shape._recreatePickBody()
41
- }
42
- }
43
-
44
- // No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
45
- }
1
+ // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
+ // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
+ // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
+ //
5
+ // const goal = new Mesh(box(), material)
6
+ // .aspect(Shape, { box: [1, 2, 1] })
7
+ // .aspect(Trigger)
8
+ // goal.addEventListener('enter', other => win(other))
9
+ // goal.addEventListener('exit', other => …)
10
+
11
+ import { Aspect } from "../../core/Aspect"
12
+ import { Shape } from "./Shape"
13
+ import { ensurePhysicsEvents } from "./physicsEvents"
14
+ import type { Node } from "../Node"
15
+
16
+ export class Trigger extends Aspect<"trigger", Node> {
17
+ static readonly aspect = "trigger"
18
+
19
+ private _bodyId = 0
20
+ /** Native body id (0 if no physics support). */
21
+ get id(): number { return this._bodyId }
22
+
23
+ onAttach(): void {
24
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
25
+ const shape = this.node.get(Shape)
26
+ if (!shape) {
27
+ throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
28
+ }
29
+ const shapeId = shape._claim()
30
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
31
+ shape._ownBody(this._bodyId)
32
+ ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
33
+ }
34
+
35
+ onDetach(): void {
36
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
37
+ const shape = this.node.get(Shape)
38
+ if (shape) {
39
+ shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
40
+ shape._recreatePickBody()
41
+ }
42
+ }
43
+
44
+ // No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
45
+ }
@@ -2,7 +2,7 @@
2
2
  // is delivered to BOTH nodes with the other as the argument. type: 0 = enter (contact/overlap began),
3
3
  // 1 = exit (ended). Registered lazily on the first trigger/physics body or 'enter'/'exit' listener.
4
4
 
5
- import { nodeRegistry } from "./Node"
5
+ import { nodeRegistry } from "../Node"
6
6
 
7
7
  let registered = false
8
8
  export const ensurePhysicsEvents = (): void => {
@@ -31,16 +31,16 @@
31
31
  // headless, an older build) gets the same chunks as child Mesh nodes from gl/terrainMesh.ts — the
32
32
  // project runs everywhere, normal maps just read flat there.
33
33
 
34
- import { Node } from "./Node"
35
- import { Mesh } from "./Mesh"
36
- import { Model } from "./Model"
37
- import { Material } from "./Material"
38
- import { Texture } from "./Texture"
39
- import { Geometry } from "./Geometry"
40
- import { Shape } from "./Shape"
41
- import { Color, type ColorInput } from "../core/color"
42
- import { fetch } from "../runtime/fetch"
43
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
34
+ import { Node } from "../Node"
35
+ import { Mesh } from "../Mesh"
36
+ import { Model } from "../Model"
37
+ import { Material } from "../Material"
38
+ import { Texture } from "../Texture"
39
+ import { Geometry } from "../Geometry"
40
+ import { Shape } from "../physics/Shape"
41
+ import { Color, type ColorInput } from "../../core/color"
42
+ import { fetch } from "../../runtime/fetch"
43
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
44
44
  import {
45
45
  buildTerrainChunk, chunkCounts, faceNormalAt, gridRaycast, heightAt as gridHeightAt, normalAt as gridNormalAt, type TerrainGrid,
46
46
  } from "./terrainMesh"
@@ -9,7 +9,7 @@
9
9
  // read wrong on those hosts — the accepted fallback cost.
10
10
  // Keep the hole / split rule in step with terrain.cpp buildChunk and Jolt's HeightFieldShape.
11
11
 
12
- import { Geometry } from "./Geometry"
12
+ import { Geometry } from "../Geometry"
13
13
 
14
14
  export type TerrainGrid = {
15
15
  sizeX: number