lecodes-cli 0.17.2 → 0.18.1
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.
- package/README.md +1 -1
- package/dist/index.js +2376 -755
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +52 -8
- package/runtime/sdk/compile/assetMacro.ts +116 -15
- package/runtime/sdk/compile/bundler.ts +39 -4
- package/runtime/sdk/compile/compileProject.ts +16 -1
- package/runtime/sdk/compile/header.ts +6 -1
- package/runtime/sdk/compile/index.ts +31 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/sceneEditor.ts +11 -1
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +81 -0
- package/runtime/sdk/core/Aspect.ts +363 -95
- package/runtime/sdk/core/compWrite.ts +42 -0
- package/runtime/sdk/core/fields.ts +1 -1
- package/runtime/sdk/core/time.ts +81 -0
- package/runtime/sdk/g2/Camera2D.ts +8 -1
- package/runtime/sdk/g2/CharacterController2D.ts +253 -53
- package/runtime/sdk/g2/Node2D.ts +80 -10
- package/runtime/sdk/g2/OneWay2D.ts +66 -0
- package/runtime/sdk/g2/Physics2D.ts +240 -30
- package/runtime/sdk/g2/Scene2D.ts +33 -1
- package/runtime/sdk/g2/Shape2D.ts +218 -22
- package/runtime/sdk/g2/Trigger2D.ts +42 -12
- package/runtime/sdk/g2/groups2d.ts +106 -0
- package/runtime/sdk/g2/loop.ts +15 -4
- package/runtime/sdk/gl/Camera.ts +41 -0
- package/runtime/sdk/gl/CameraPlace.ts +52 -0
- package/runtime/sdk/gl/CharacterController.ts +184 -55
- package/runtime/sdk/gl/Gearbox.ts +212 -0
- package/runtime/sdk/gl/Geometry.ts +70 -9
- package/runtime/sdk/gl/IK.ts +193 -174
- package/runtime/sdk/gl/Light.ts +64 -2
- package/runtime/sdk/gl/Lightmap.ts +179 -0
- package/runtime/sdk/gl/Material.ts +36 -0
- package/runtime/sdk/gl/Mesh.ts +6 -23
- package/runtime/sdk/gl/Model.ts +23 -8
- package/runtime/sdk/gl/Node.ts +350 -285
- package/runtime/sdk/gl/Physics.ts +222 -126
- package/runtime/sdk/gl/Scene.ts +175 -8
- package/runtime/sdk/gl/Shape.ts +255 -12
- package/runtime/sdk/gl/Trigger.ts +1 -6
- package/runtime/sdk/gl/Vehicle.ts +473 -0
- package/runtime/sdk/gl/Wheel.ts +240 -0
- package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
- package/runtime/sdk/gl/animation/Animator.ts +87 -0
- package/runtime/sdk/gl/animation/Layer.ts +29 -0
- package/runtime/sdk/gl/animation/Loop.ts +25 -0
- package/runtime/sdk/gl/animation/Playback.ts +43 -0
- package/runtime/sdk/gl/animation/core.ts +294 -0
- package/runtime/sdk/gl/scenarios.ts +291 -349
- package/runtime/sdk/inject.ts +186 -162
- package/runtime/sdk/runtime/app.ts +13 -0
- package/runtime/sdk/runtime/input.ts +169 -6
- package/runtime/sdk/scene/defineScene.ts +1227 -1016
- package/runtime/sdk/scene/gizmos.ts +148 -0
- package/runtime/sdk/scene/material.ts +188 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/gl/Animator.ts +0 -642
- package/runtime/sdk/gl/ModelAnimation.ts +0 -95
package/runtime/sdk/gl/Shape.ts
CHANGED
|
@@ -8,15 +8,42 @@
|
|
|
8
8
|
// wall.aspect(Shape, { box: [1, 2, 1] }).aspect(Physics, { motion: 'static' })
|
|
9
9
|
// coin.aspect(Shape, { sphere: 0.5 }).aspect(Trigger)
|
|
10
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
|
|
11
13
|
//
|
|
12
14
|
// Explicit dims are world-space half-extents (not scaled by the node). Use {} ("auto") to track the
|
|
13
|
-
// mesh × world scale.
|
|
14
|
-
//
|
|
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.
|
|
15
19
|
|
|
16
20
|
import { Aspect } from "../core/Aspect"
|
|
17
21
|
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
18
22
|
import type { Node } from "./Node"
|
|
19
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
|
+
|
|
20
47
|
export class Shape extends Aspect<"shape", Node> {
|
|
21
48
|
static readonly aspect = "shape"
|
|
22
49
|
|
|
@@ -33,11 +60,29 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
33
60
|
* only (a dynamic body can't be a triangle mesh; Physics throws). `'convex'` = convex hull of the
|
|
34
61
|
* vertices, works for dynamic bodies too. A Model uses its bind pose (skinned parts skipped). */
|
|
35
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
|
|
36
77
|
/** Whether pointer rays can hit this shape (click / touchstart). Default true. */
|
|
37
78
|
raycast = true
|
|
38
79
|
|
|
39
80
|
private _shapeId = 0
|
|
40
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 = ""
|
|
41
86
|
|
|
42
87
|
/** @internal native shape id (0 if the build has no physics). */
|
|
43
88
|
get shapeId(): number { return this._shapeId }
|
|
@@ -53,16 +98,130 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
53
98
|
onDetach(): void {
|
|
54
99
|
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
55
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 }
|
|
56
207
|
}
|
|
57
208
|
|
|
58
|
-
|
|
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). */
|
|
59
213
|
_claim(): number {
|
|
60
214
|
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
215
|
+
this.node._xf = 0
|
|
216
|
+
this.node._xfKind = 0
|
|
61
217
|
return this._shapeId
|
|
62
218
|
}
|
|
63
|
-
/** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability.
|
|
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. */
|
|
64
221
|
_ownBody(bodyId: number): void {
|
|
65
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
|
|
66
225
|
if (!this.raycast && bodyId) _creator.physicsSetPickable(bodyId, false)
|
|
67
226
|
}
|
|
68
227
|
/** @internal restore a pick-only body when Physics/Trigger detach but the Shape remains. */
|
|
@@ -72,19 +231,97 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
72
231
|
|
|
73
232
|
private _createPickBody(): void {
|
|
74
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
|
|
75
238
|
if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
|
|
76
239
|
}
|
|
77
240
|
|
|
78
241
|
/** @internal true for an exact triangle mesh — Physics refuses a dynamic body on it. */
|
|
79
242
|
get _isTriangleMesh(): boolean { return this.mesh === true }
|
|
80
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
|
+
|
|
81
289
|
private _build(): number {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (this.
|
|
86
|
-
if (this.
|
|
87
|
-
|
|
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)}`
|
|
88
325
|
}
|
|
89
326
|
|
|
90
327
|
// mesh: a Mesh node hands its CPU geometry over; anything else is tried as a GLB root (the host
|
|
@@ -111,7 +348,10 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
111
348
|
return id
|
|
112
349
|
}
|
|
113
350
|
|
|
114
|
-
// auto: a box from the mesh AABB × world scale (so the physical box matches the
|
|
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.
|
|
115
355
|
private _buildAutoBox(): number {
|
|
116
356
|
const s = this.node.worldScale
|
|
117
357
|
let hx = 0.5, hy = 0.5, hz = 0.5
|
|
@@ -127,7 +367,10 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
127
367
|
if (v[i + 2] < minZ) minZ = v[i + 2]
|
|
128
368
|
if (v[i + 2] > maxZ) maxZ = v[i + 2]
|
|
129
369
|
}
|
|
130
|
-
|
|
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
|
+
}
|
|
131
374
|
}
|
|
132
375
|
return _creator.physicsBuildBox(hx * s.x, hy * s.y, hz * s.z)
|
|
133
376
|
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// goal.addEventListener('exit', other => …)
|
|
10
10
|
|
|
11
11
|
import { Aspect } from "../core/Aspect"
|
|
12
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
13
12
|
import { Shape } from "./Shape"
|
|
14
13
|
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
15
14
|
import type { Node } from "./Node"
|
|
@@ -42,9 +41,5 @@ export class Trigger extends Aspect<"trigger", Node> {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
moveTo(p: Vec3Like): this {
|
|
47
|
-
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
48
|
-
return this
|
|
49
|
-
}
|
|
44
|
+
// No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
|
|
50
45
|
}
|