lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,377 +0,0 @@
1
- // A collision shape, as an aspect on a 3D Node. PURE GEOMETRY: attaching a Shape does not by itself
2
- // make the node rigid or a sensor — pair it with a Physics (rigid body) or a Trigger (sensor), both
3
- // of which require a Shape. The Shape also makes the node pointer-pickable: on its own it creates a
4
- // lightweight "pick-only" JoltPhysics body (collides with nothing, exists only for the raycast), so
5
- // any node with a Shape can receive click/touchstart. Physics/Trigger REPLACE that body with the real
6
- // one. Set `raycast = false` to opt a shape out of pointer picking.
7
- //
8
- // wall.aspect(Shape, { box: [1, 2, 1] }).aspect(Physics, { motion: 'static' })
9
- // coin.aspect(Shape, { sphere: 0.5 }).aspect(Trigger)
10
- // crate.aspect(Shape, {}).aspect(Physics) // auto box from the mesh AABB × world scale
11
- // barrel.aspect(Shape, { capsule: {…}, origin: [0, 0.9, 0] }) // collider lifted off the pivot
12
- // tree.aspect(Shape, {}).shape.fit('capsule') // dimensions measured from the model
13
- //
14
- // Explicit dims are world-space half-extents (not scaled by the node). Use {} ("auto") to track the
15
- // mesh × world scale. `origin` moves the shape off the node's pivot (world units, in the node's
16
- // rotated frame) — the usual case is a model whose art sits above its origin. Note: a Shape-only
17
- // pick body is a STATIC snapshot at attach time — for a moving pickable object add a Physics body
18
- // (which follows the sim), rather than relying on the pick body.
19
-
20
- import { Aspect } from "../core/Aspect"
21
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
22
- import type { Node } from "./Node"
23
-
24
- /** Which geometry a Shape uses — the one-of the fields below encode (see `_build`'s order). */
25
- export type ShapeKind = "auto" | "box" | "sphere" | "cylinder" | "capsule" | "mesh" | "convex"
26
-
27
- /** A node-local AABB, in the node's OWN frame (its own transform excluded), before world scale. */
28
- export type ShapeBounds = { min: [number, number, number], max: [number, number, number] }
29
-
30
- /** Dimensions measured from a node's rendered geometry — what `Shape.fit` writes, and the numbers
31
- * the scene editor's "Fit to mesh" button puts in the file. Exactly one dimension key is set (none
32
- * for a mesh/convex kind, whose geometry is the mesh itself); `origin` centres it on the art. */
33
- export type ShapeFit = {
34
- box?: [number, number, number]
35
- sphere?: number
36
- cylinder?: { halfHeight: number, radius: number }
37
- capsule?: { halfHeight: number, radius: number }
38
- origin: [number, number, number]
39
- }
40
-
41
- /** Half-extents below this collapse a shape (Jolt refuses a box thinner than its convex radius,
42
- * and a zero-radius sphere) — a fitted plane or a decal quad lands here. */
43
- const MIN_HALF = 0.001
44
- const r4 = (v: number): number => Math.round(v * 1e4) / 1e4
45
- const dim = (v: number): number => Math.max(MIN_HALF, r4(v))
46
-
47
- export class Shape extends Aspect<"shape", Node> {
48
- static readonly aspect = "shape"
49
-
50
- /** Box half-extents [hx, hy, hz] (world units). */
51
- box?: Vec3Like
52
- /** Sphere radius. */
53
- sphere?: number
54
- /** Cylinder aligned on Y: half-height + radius. */
55
- cylinder?: { halfHeight: number, radius: number }
56
- /** Capsule aligned on Y: half-height of the cylinder section + radius. */
57
- capsule?: { halfHeight: number, radius: number }
58
- /** The node's own triangles (a Mesh's geometry or a Model's GLB, world scale applied).
59
- * `true` = exact triangle mesh — static/kinematic bodies, triggers, characters' ground and picking
60
- * only (a dynamic body can't be a triangle mesh; Physics throws). `'convex'` = convex hull of the
61
- * vertices, works for dynamic bodies too. A Model uses its bind pose (skinned parts skipped). */
62
- mesh?: boolean | "convex"
63
- /**
64
- * The shape's centre, relative to the node's origin — world units in the node's ROTATED frame
65
- * (the node's scale is ignored, exactly like the explicit dimensions above). Default [0, 0, 0].
66
- *
67
- * A model's pivot is rarely its centre of volume (a character stands ON its origin, a wheel hangs
68
- * off its axle), so a collider centred on the pivot is half sunk into the floor. `origin` lifts it
69
- * without moving the node: `{ capsule: { halfHeight: 0.6, radius: 0.3 }, origin: [0, 0.9, 0] }`.
70
- * `fit()` and the scene editor's "Fit to mesh" fill it in from the rendered bounds.
71
- *
72
- * On a DYNAMIC body the offset also moves the centre of mass, so a lopsided collider tips the way
73
- * you would expect. With no `origin` an auto shape (`{}`) still centres itself on the node's own
74
- * mesh bounds — only explicit dimensions sit on the pivot by default.
75
- */
76
- origin?: Vec3Like
77
- /** Whether pointer rays can hit this shape (click / touchstart). Default true. */
78
- raycast = true
79
-
80
- private _shapeId = 0
81
- private _bodyId = 0
82
- /** Centre the "auto" box measured for itself — used when `origin` isn't set explicitly. */
83
- private _autoOrigin: [number, number, number] | null = null
84
- /** Geometry props as of the last build (see onReconfigure). */
85
- private _sig = ""
86
-
87
- /** @internal native shape id (0 if the build has no physics). */
88
- get shapeId(): number { return this._shapeId }
89
- /** @internal current native body id (a pick-only body until Physics/Trigger claim it). */
90
- get bodyId(): number { return this._bodyId }
91
-
92
- onAttach(): void {
93
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
94
- this._shapeId = this._build()
95
- if (this._shapeId) this._createPickBody()
96
- }
97
-
98
- onDetach(): void {
99
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
100
- if (this._shapeId) { _creator.physicsDestroyShape(this._shapeId); this._shapeId = 0 }
101
- this.node._xf = 0
102
- this.node._xfKind = 0
103
- }
104
-
105
- /** Re-configuring (`node.aspect(Shape, { origin })`) rebuilds the geometry when a dimension, the
106
- * kind or the origin changed, and re-applies pickability. The body keeps its id and velocity. */
107
- onReconfigure(): void {
108
- if (this._signature() !== this._sig) this._rebuild()
109
- if (this._bodyId) _creator.physicsSetPickable(this._bodyId, this.raycast)
110
- }
111
-
112
- /**
113
- * Measure the node's rendered geometry and size the collider to it — the code half of the scene
114
- * editor's "Fit to mesh". Keeps the current kind unless one is passed; `auto` becomes an explicit
115
- * box, and a mesh/convex shape only has its `origin` cleared (its geometry IS the mesh).
116
- *
117
- * crate.aspect(Shape, {}).shape.fit() // box around what the model actually draws
118
- * hero.aspect(Shape, {}).shape.fit('capsule') // capsule of the same height + girth
119
- *
120
- * A model loads asynchronously, so fit AFTER its `load()` resolves — an empty subtree measures as
121
- * nothing and the call is a no-op (it warns). Rebuilds the live shape, keeping the body.
122
- */
123
- fit(kind?: ShapeKind): this {
124
- const f = Shape.fitTo(this.node, kind ?? this.kind)
125
- if (!f) {
126
- console.warn(`Shape.fit: "${this.node.name}" has no geometry to measure yet (a model still loading, or a node with no mesh)`)
127
- return this
128
- }
129
- this.box = f.box
130
- this.sphere = f.sphere
131
- this.cylinder = f.cylinder
132
- this.capsule = f.capsule
133
- if (kind === "mesh") this.mesh = true
134
- else if (kind === "convex") this.mesh = "convex"
135
- else if (kind !== undefined) this.mesh = undefined
136
- this.origin = f.origin
137
- this._rebuild()
138
- return this
139
- }
140
-
141
- /** Which geometry this shape currently uses (the `_build` dispatch, as a name). */
142
- get kind(): ShapeKind {
143
- if (this.mesh === "convex") return "convex"
144
- if (this.mesh) return "mesh"
145
- if (this.sphere !== undefined) return "sphere"
146
- if (this.cylinder) return "cylinder"
147
- if (this.capsule) return "capsule"
148
- if (this.box) return "box"
149
- return "auto"
150
- }
151
-
152
- // ---- fitting (static, so the scene-editor harness can measure without attaching) -------------
153
-
154
- /**
155
- * The node's subtree AABB in its OWN local frame (its own transform excluded), or null when there
156
- * is nothing to measure. Reads the engine's bounds — which cover a loaded GLB and every child —
157
- * and falls back to a Mesh node's own CPU geometry on hosts without them.
158
- */
159
- static boundsOf(node: Node): ShapeBounds | null {
160
- const box = _creator.computeBoundingBox?.(node.id)
161
- if (box && (box[3] - box[0] > 1e-6 || box[4] - box[1] > 1e-6 || box[5] - box[2] > 1e-6)) {
162
- return { min: [box[0], box[1], box[2]], max: [box[3], box[4], box[5]] }
163
- }
164
- const v = node.geometry?.vertices
165
- if (!v || v.length < 3) return null
166
- const min: [number, number, number] = [Infinity, Infinity, Infinity]
167
- const max: [number, number, number] = [-Infinity, -Infinity, -Infinity]
168
- for (let i = 0; i + 2 < v.length; i += 3) {
169
- for (let k = 0; k < 3; k++) {
170
- if (v[i + k] < min[k]) min[k] = v[i + k]
171
- if (v[i + k] > max[k]) max[k] = v[i + k]
172
- }
173
- }
174
- return isFinite(min[0]) ? { min, max } : null
175
- }
176
-
177
- /**
178
- * Dimensions + origin that make `kind` hug what `node` renders — world units, so the node's world
179
- * scale is baked in exactly as the Shape fields expect. Null when the node has no geometry (a
180
- * model that hasn't loaded, an empty).
181
- */
182
- static fitTo(node: Node, kind: ShapeKind = "box"): ShapeFit | null {
183
- const b = Shape.boundsOf(node)
184
- return b ? Shape.fitBounds(b, node.worldScale, kind) : null
185
- }
186
-
187
- /**
188
- * The measuring itself, over plain numbers: a node-local AABB + the node's world scale in, shape
189
- * dimensions out. Separate from `fitTo` so the scene-editor harness can measure a node from
190
- * ANOTHER bundle without touching accessors the bundler may have shaken out of it.
191
- *
192
- * A sphere takes the LARGEST half-extent and a cylinder/capsule the larger of X/Z, so the fit
193
- * reads as the object's silhouette rather than as an enclosing ball; adjust from there.
194
- */
195
- static fitBounds(bounds: ShapeBounds, scale: Vec3Like, kind: ShapeKind = "box"): ShapeFit {
196
- const sc = [Math.abs(cx(scale)), Math.abs(cy(scale)), Math.abs(cz(scale))]
197
- const half = [0, 1, 2].map((k) => (bounds.max[k] - bounds.min[k]) * 0.5 * sc[k])
198
- const origin = [0, 1, 2].map((k) => r4((bounds.max[k] + bounds.min[k]) * 0.5 * sc[k])) as [number, number, number]
199
- if (kind === "mesh" || kind === "convex") return { origin: [0, 0, 0] } // the mesh IS the geometry
200
- if (kind === "sphere") return { sphere: dim(Math.max(half[0], half[1], half[2])), origin }
201
- if (kind === "cylinder" || kind === "capsule") {
202
- const radius = dim(Math.max(half[0], half[2]))
203
- const halfHeight = kind === "capsule" ? Math.max(0, r4(half[1] - radius)) : dim(half[1])
204
- return { [kind]: { halfHeight, radius }, origin }
205
- }
206
- return { box: [dim(half[0]), dim(half[1]), dim(half[2])], origin }
207
- }
208
-
209
- // ---- body/shape lifetime --------------------------------------------------------------------
210
-
211
- /** @internal Physics/Trigger/CharacterController/Vehicle take over: drop the pick body and hand back
212
- * the shape id. The node's transform owner goes with the body (the new owner sets its own). */
213
- _claim(): number {
214
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
215
- this.node._xf = 0
216
- this.node._xfKind = 0
217
- return this._shapeId
218
- }
219
- /** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability.
220
- * 0 = they are detaching and no longer own it. */
221
- _ownBody(bodyId: number): void {
222
- this._bodyId = bodyId
223
- this.node._xf = bodyId // route node.position writes to the body (see Node._setOwnedPosition)
224
- this.node._xfKind = bodyId ? 2 : 0
225
- if (!this.raycast && bodyId) _creator.physicsSetPickable(bodyId, false)
226
- }
227
- /** @internal restore a pick-only body when Physics/Trigger detach but the Shape remains. */
228
- _recreatePickBody(): void {
229
- if (!this._bodyId && this._shapeId) this._createPickBody()
230
- }
231
-
232
- private _createPickBody(): void {
233
- this._bodyId = _creator.physicsCreateBody(this.node.id, this._shapeId, 0, 0, false, /*pickOnly*/ true)
234
- // Route position writes here too: the pick body is a static snapshot, so without this a moved node
235
- // would keep taking its clicks at the old place.
236
- this.node._xf = this._bodyId
237
- this.node._xfKind = this._bodyId ? 2 : 0
238
- if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
239
- }
240
-
241
- /** @internal true for an exact triangle mesh — Physics refuses a dynamic body on it. */
242
- get _isTriangleMesh(): boolean { return this.mesh === true }
243
-
244
- // Rebuild the geometry in place: the body keeps its id, velocity and world transform (Physics
245
- // holds that id, so re-creating the body instead would strand it). A shape a CharacterController
246
- // or a Vehicle claimed is theirs — they were handed the old one and keep it.
247
- private _rebuild(): void {
248
- if (!this._shapeId) return // never attached, or a build without physics
249
- const previous = this._shapeId
250
- const previousSig = this._sig
251
- const id = this._build()
252
- if (!id) return // build failed — keep what works
253
-
254
- // A CharacterController owns its shape and can REFUSE the swap: standing up under a pipe doesn't
255
- // fit. Nothing is half-applied on refusal, so the fix is to stay UNCOMMITTED — leave `_sig` on the
256
- // old geometry, and the next `node.aspect(Shape, …)` with the same dimensions sees a difference
257
- // and tries again. That retry is the headroom test, and it is exact (the real capsule, not a ray).
258
- const charId = this.node._xfKind === 1 ? this.node._xf : 0
259
- if (charId) {
260
- if (_creator.characterSetShape?.(charId, id)) {
261
- this._shapeId = id
262
- this._resizeRefused = false
263
- _creator.physicsDestroyShape(previous)
264
- } else {
265
- _creator.physicsDestroyShape(id) // drop what we just built; the old collider stays live
266
- this._sig = previousSig
267
- this._resizeRefused = !!_creator.characterSetShape
268
- if (!_creator.characterSetShape) {
269
- console.warn(`Shape: this host can't resize a live CharacterController — "${this.node.name}" keeps its current collider`)
270
- }
271
- }
272
- return
273
- }
274
-
275
- this._shapeId = id
276
- if (this._bodyId) {
277
- if (_creator.physicsSetBodyShape) _creator.physicsSetBodyShape(this._bodyId, id, true)
278
- else console.warn("Shape: this host can't re-shape a live body — the new dimensions apply from the next attach")
279
- } else {
280
- console.warn(`Shape: "${this.node.name}"'s geometry is owned by a Vehicle — re-attach it to pick up the new dimensions`)
281
- }
282
- _creator.physicsDestroyShape(previous)
283
- }
284
-
285
- /** @internal True while a requested resize has been refused — the fields say one thing, the engine
286
- * is still using another. Surfaced as `controller.resizing`. */
287
- _resizeRefused = false
288
-
289
- private _build(): number {
290
- this._autoOrigin = null
291
- this._sig = this._signature()
292
- let id = 0
293
- if (this.mesh) id = this._buildMesh(this.mesh === "convex")
294
- else if (this.sphere !== undefined) id = _creator.physicsBuildSphere(this.sphere)
295
- else if (this.cylinder) id = _creator.physicsBuildCylinder(this.cylinder.halfHeight, this.cylinder.radius)
296
- else if (this.capsule) id = _creator.physicsBuildCapsule(this.capsule.halfHeight, this.capsule.radius)
297
- else if (this.box) id = _creator.physicsBuildBox(cx(this.box), cy(this.box), cz(this.box))
298
- else id = this._buildAutoBox()
299
- if (id) this._applyOrigin(id)
300
- return id
301
- }
302
-
303
- // Push `origin` (or, for an auto box, the mesh centre it measured) onto the built shape. A host
304
- // without the call keeps the old "centred on the node" behaviour; only an EXPLICIT origin warns,
305
- // since the auto centre is a silent improvement nobody asked for.
306
- private _applyOrigin(shapeId: number): void {
307
- const o = this.origin
308
- const x = o ? cx(o) : this._autoOrigin?.[0] ?? 0
309
- const y = o ? cy(o) : this._autoOrigin?.[1] ?? 0
310
- const z = o ? cz(o) : this._autoOrigin?.[2] ?? 0
311
- if (x === 0 && y === 0 && z === 0) return
312
- if (!_creator.physicsSetShapeOrigin) {
313
- if (o) console.warn("Shape { origin } needs a newer host — the collider stays centred on the node")
314
- return
315
- }
316
- _creator.physicsSetShapeOrigin(shapeId, x, y, z)
317
- }
318
-
319
- // What a rebuild has to notice: the kind, its dimensions and the origin (`raycast` is applied to
320
- // the body directly, and the node's own transform is the body's, not the shape's).
321
- private _signature(): string {
322
- const v = (a: Vec3Like | undefined) => (a ? `${cx(a)},${cy(a)},${cz(a)}` : "")
323
- const t = (o: { halfHeight: number, radius: number } | undefined) => (o ? `${o.halfHeight},${o.radius}` : "")
324
- return `${v(this.box)}|${this.sphere ?? ""}|${t(this.cylinder)}|${t(this.capsule)}|${this.mesh ?? ""}|${v(this.origin)}`
325
- }
326
-
327
- // mesh: a Mesh node hands its CPU geometry over; anything else is tried as a GLB root (the host
328
- // reads the asset's triangles itself — 0 if the node is neither). Always × world scale: the
329
- // triangles are node-local, so the collider must follow the rendered size.
330
- private _buildMesh(convex: boolean): number {
331
- if (!_creator.physicsBuildMesh) {
332
- console.warn("Shape { mesh } needs a newer host — falling back to a box")
333
- return this._buildAutoBox()
334
- }
335
- const s = this.node.worldScale
336
- const geom = this.node.geometry
337
- let id = 0
338
- if (geom) {
339
- if (geom._meshKind !== 0) throw new Error("Shape { mesh } needs a triangle geometry (not edges/vertices)")
340
- id = _creator.physicsBuildMesh(geom.vertices, Uint32Array.from(geom.indices), convex, s.x, s.y, s.z)
341
- } else {
342
- id = _creator.physicsBuildMeshFromEntity(this.node.id, convex, s.x, s.y, s.z)
343
- }
344
- if (!id) {
345
- console.warn(`Shape { mesh } could not build a ${convex ? "convex hull" : "triangle mesh"} for "${this.node.name}" (no triangles / not a Mesh or Model) — falling back to a box`)
346
- return this._buildAutoBox()
347
- }
348
- return id
349
- }
350
-
351
- // auto: a box from the node's own mesh AABB × world scale (so the physical box matches the
352
- // rendered size), CENTRED on those bounds — geometry modelled off its pivot gets a box that still
353
- // wraps it. A node with no geometry of its own (a Model root, an empty) keeps the 0.5 default;
354
- // `fit()` measures the whole subtree instead.
355
- private _buildAutoBox(): number {
356
- const s = this.node.worldScale
357
- let hx = 0.5, hy = 0.5, hz = 0.5
358
- const geom = this.node.geometry
359
- if (geom) {
360
- const v = geom.vertices
361
- let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
362
- for (let i = 0; i + 2 < v.length; i += 3) {
363
- if (v[i] < minX) minX = v[i]
364
- if (v[i] > maxX) maxX = v[i]
365
- if (v[i + 1] < minY) minY = v[i + 1]
366
- if (v[i + 1] > maxY) maxY = v[i + 1]
367
- if (v[i + 2] < minZ) minZ = v[i + 2]
368
- if (v[i + 2] > maxZ) maxZ = v[i + 2]
369
- }
370
- if (isFinite(minX)) {
371
- hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
372
- this._autoOrigin = [(maxX + minX) * 0.5 * s.x, (maxY + minY) * 0.5 * s.y, (maxZ + minZ) * 0.5 * s.z]
373
- }
374
- }
375
- return _creator.physicsBuildBox(hx * s.x, hy * s.y, hz * s.z)
376
- }
377
- }
@@ -1,46 +0,0 @@
1
- // A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
2
-
3
- import { fetch, type FetchResponse, type File } from "../runtime/fetch"
4
-
5
- export class Texture {
6
- /** @internal native texture handle. */
7
- readonly _id: number
8
- readonly width: number
9
- readonly height: number
10
-
11
- /** Horizontal wrap mode (U). */
12
- wrapS = 1
13
- /** Vertical wrap mode (V). */
14
- wrapT = 1
15
-
16
- constructor(width: number, height: number, id: number) {
17
- this.width = width
18
- this.height = height
19
- this._id = id
20
- }
21
-
22
- // Wrap a Canvas's baked surface as a 3D texture (RGBA8, already rasterized → synchronous, no decode).
23
- // The id is cached on the canvas (opaque _tex3dId), so backing several materials shares one texture and
24
- // canvas.update() re-uploads it. Structural type avoids a Canvas import (keeps 3D free of 2D-canvas code).
25
- static fromCanvas(canvas: { _bakeSurface(): number, width: number, height: number, pixelRatio: number, _tex3dId: number }): Texture {
26
- if (!canvas._tex3dId) {
27
- const surfaceId = canvas._bakeSurface()
28
- canvas._tex3dId = _creator.createTextureFromCanvas(surfaceId)
29
- }
30
- return new Texture(Math.round(canvas.width * canvas.pixelRatio), Math.round(canvas.height * canvas.pixelRatio), canvas._tex3dId)
31
- }
32
-
33
- static load(source: string | FetchResponse | File): Promise<Texture> {
34
- if (typeof source === "string") {
35
- return fetch(source, { useOnce: true }).then((resp) => {
36
- if (resp.status >= 400) {
37
- return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
38
- }
39
- return Texture.load(resp)
40
- })
41
- }
42
- return new Promise<Texture>((resolve, reject) => {
43
- _creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject)
44
- })
45
- }
46
- }
@@ -1,45 +0,0 @@
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
- }