incanto 0.28.0 → 0.30.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 (63) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +219 -29
  3. package/dist/2d.d.ts +54 -6
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +353 -16
  6. package/dist/3d.js +5 -5
  7. package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
  8. package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
  9. package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
  10. package/dist/debug-draw-BM3DsvtT.js +18 -0
  11. package/dist/debug.d.ts +51 -6
  12. package/dist/debug.js +242 -19
  13. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8554 -0
  16. package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +31 -3
  20. package/dist/index.js +2 -2
  21. package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
  22. package/dist/net.d.ts +1 -1
  23. package/dist/net.js +2 -2
  24. package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
  25. package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
  26. package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
  30. package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
  31. package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
  32. package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
  33. package/dist/teardown-ByzfDPyu.js +240 -0
  34. package/dist/test.d.ts +35 -3
  35. package/dist/test.js +104 -10
  36. package/dist/vite.d.ts +139 -1
  37. package/dist/vite.js +355 -4
  38. package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
  39. package/editor/assets/debug-BoEYfbqK.js +2 -0
  40. package/editor/assets/index-BO6WU8by.js +10696 -0
  41. package/editor/index.html +3 -157
  42. package/package.json +3 -2
  43. package/schemas/scene.schema.json +102 -4
  44. package/skills/incanto-3d-models.md +38 -0
  45. package/skills/incanto-assets.md +13 -0
  46. package/skills/incanto-building-3d-games.md +83 -6
  47. package/skills/incanto-editor.md +212 -8
  48. package/skills/incanto-environment.md +65 -1
  49. package/skills/incanto-node-reference.md +33 -1
  50. package/skills/incanto-physics-and-input.md +60 -6
  51. package/skills/incanto-verifying-your-game.md +57 -2
  52. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  55. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/tps-3d/package.json +1 -1
  57. package/templates-app/tps-3d/vite.config.ts +7 -0
  58. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  59. package/templates-app/village-quest-3d/package.json +1 -1
  60. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  61. package/dist/debug-draw-CZmOYjL2.js +0 -13
  62. package/editor/assets/index-D6RQgROR.js +0 -8330
  63. package/editor/assets/index-D8QvwvOm.css +0 -1
@@ -1,9 +1,206 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-DEG-TP7D.js";
4
- import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
5
- import { A as Joint3D, E as Terrain3D } from "./register-6R75AC7-.js";
6
- import { Euler, Quaternion } from "three";
3
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-Cfr6aFZ1.js";
4
+ import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
5
+ import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-DJ0SByQg.js";
6
+ import { Euler, Matrix4, Quaternion, Vector3 } from "three";
7
+ //#region src/3d/physics/collider-lines.ts
8
+ /**
9
+ * Wireframe segments for INDIVIDUAL colliders.
10
+ *
11
+ * Rapier's own `world.debugRender()` is all-or-nothing: it draws every collider
12
+ * in the world, which on a real map means a terrain heightfield's grid drowning
13
+ * the one shape you were actually looking at. Drawing a chosen few means
14
+ * generating the outlines ourselves, from the same shape data the solver holds.
15
+ *
16
+ * Shapes are drawn at their true size — no bounding boxes standing in for real
17
+ * geometry, since a box around a rotated shape is exactly the lie this view
18
+ * exists to expose.
19
+ */
20
+ const RING = 16;
21
+ const point = new Vector3();
22
+ const quat = new Quaternion();
23
+ function transformer(collider, out) {
24
+ const t = collider.translation();
25
+ const r = collider.rotation();
26
+ quat.set(r.x, r.y, r.z, r.w);
27
+ const put = (x, y, z) => {
28
+ point.set(x, y, z).applyQuaternion(quat);
29
+ out.push(point.x + t.x, point.y + t.y, point.z + t.z);
30
+ };
31
+ return (ax, ay, az, bx, by, bz) => {
32
+ put(ax, ay, az);
33
+ put(bx, by, bz);
34
+ };
35
+ }
36
+ /** A circle in the plane spanned by two axes, at `offset` along the third. */
37
+ function ring(push, radius, plane, offset, arc = Math.PI * 2, from = 0) {
38
+ const at = (a) => {
39
+ const c = Math.cos(a) * radius;
40
+ const s = Math.sin(a) * radius;
41
+ if (plane === "xy") return [
42
+ c,
43
+ s,
44
+ offset
45
+ ];
46
+ if (plane === "xz") return [
47
+ c,
48
+ offset,
49
+ s
50
+ ];
51
+ return [
52
+ offset,
53
+ c,
54
+ s
55
+ ];
56
+ };
57
+ const steps = Math.max(3, Math.round(RING * arc / (Math.PI * 2)));
58
+ for (let i = 0; i < steps; i++) {
59
+ const [ax, ay, az] = at(from + arc * i / steps);
60
+ const [bx, by, bz] = at(from + arc * (i + 1) / steps);
61
+ push(ax, ay, az, bx, by, bz);
62
+ }
63
+ }
64
+ function triangleEdges(push, vertices, indices) {
65
+ const v = (i) => [
66
+ vertices[i * 3],
67
+ vertices[i * 3 + 1],
68
+ vertices[i * 3 + 2]
69
+ ];
70
+ if (indices) {
71
+ for (let i = 0; i < indices.length; i += 3) {
72
+ const a = v(indices[i]);
73
+ const b = v(indices[i + 1]);
74
+ const c = v(indices[i + 2]);
75
+ push(...a, ...b);
76
+ push(...b, ...c);
77
+ push(...c, ...a);
78
+ }
79
+ return;
80
+ }
81
+ for (let i = 0; i + 5 < vertices.length; i += 9) {
82
+ const a = v(i / 3);
83
+ const b = v(i / 3 + 1);
84
+ const c = v(i / 3 + 2);
85
+ push(...a, ...b);
86
+ push(...b, ...c);
87
+ push(...c, ...a);
88
+ }
89
+ }
90
+ /** The heights grid, drawn as its own quad lines (column-major, like Rapier). */
91
+ function heightfield(push, shape) {
92
+ const { nrows, ncols, heights, scale } = shape;
93
+ const rows = nrows + 1;
94
+ const cols = ncols + 1;
95
+ const at = (i, j) => [
96
+ (i / nrows - .5) * scale.x,
97
+ heights[j * rows + i] * scale.y,
98
+ (j / ncols - .5) * scale.z
99
+ ];
100
+ for (let j = 0; j < cols; j++) for (let i = 0; i < rows; i++) {
101
+ if (i + 1 < rows) push(...at(i, j), ...at(i + 1, j));
102
+ if (j + 1 < cols) push(...at(i, j), ...at(i, j + 1));
103
+ }
104
+ }
105
+ /** Append one collider's wireframe to `out` (flat xyz, segment pairs). */
106
+ function colliderLines3D(collider, out) {
107
+ const push = transformer(collider, out);
108
+ const shape = collider.shape;
109
+ switch (collider.shape.type) {
110
+ case 0: {
111
+ const r = shape.radius;
112
+ ring(push, r, "xy", 0);
113
+ ring(push, r, "xz", 0);
114
+ ring(push, r, "yz", 0);
115
+ return;
116
+ }
117
+ case 1: {
118
+ const h = shape.halfExtents;
119
+ for (const [sy, sz] of [
120
+ [-1, -1],
121
+ [-1, 1],
122
+ [1, -1],
123
+ [1, 1]
124
+ ]) push(-h.x, sy * h.y, sz * h.z, h.x, sy * h.y, sz * h.z);
125
+ for (const [sx, sz] of [
126
+ [-1, -1],
127
+ [-1, 1],
128
+ [1, -1],
129
+ [1, 1]
130
+ ]) push(sx * h.x, -h.y, sz * h.z, sx * h.x, h.y, sz * h.z);
131
+ for (const [sx, sy] of [
132
+ [-1, -1],
133
+ [-1, 1],
134
+ [1, -1],
135
+ [1, 1]
136
+ ]) push(sx * h.x, sy * h.y, -h.z, sx * h.x, sy * h.y, h.z);
137
+ return;
138
+ }
139
+ case 2: {
140
+ const { radius, halfHeight } = shape;
141
+ for (const sign of [-1, 1]) {
142
+ ring(push, radius, "xz", sign * halfHeight);
143
+ for (const plane of ["xy", "yz"]) {
144
+ const from = sign > 0 ? 0 : Math.PI;
145
+ const cap = (ax, ay, az, bx, by, bz) => push(ax, ay + sign * halfHeight, az, bx, by + sign * halfHeight, bz);
146
+ ring(cap, radius, plane, 0, Math.PI, from);
147
+ }
148
+ }
149
+ for (const [x, z] of [
150
+ [1, 0],
151
+ [-1, 0],
152
+ [0, 1],
153
+ [0, -1]
154
+ ]) push(x * radius, -halfHeight, z * radius, x * radius, halfHeight, z * radius);
155
+ return;
156
+ }
157
+ case 3: {
158
+ const seg = shape;
159
+ push(seg.a.x, seg.a.y, seg.a.z, seg.b.x, seg.b.y, seg.b.z);
160
+ return;
161
+ }
162
+ case 6:
163
+ case 9:
164
+ case 16: {
165
+ const mesh = shape;
166
+ triangleEdges(push, mesh.vertices, mesh.indices);
167
+ return;
168
+ }
169
+ case 7:
170
+ heightfield(push, shape);
171
+ return;
172
+ case 10:
173
+ case 14: {
174
+ const { radius, halfHeight } = shape;
175
+ ring(push, radius, "xz", -halfHeight);
176
+ ring(push, radius, "xz", halfHeight);
177
+ for (const [x, z] of [
178
+ [1, 0],
179
+ [-1, 0],
180
+ [0, 1],
181
+ [0, -1]
182
+ ]) push(x * radius, -halfHeight, z * radius, x * radius, halfHeight, z * radius);
183
+ return;
184
+ }
185
+ case 11:
186
+ case 15: {
187
+ const { radius, halfHeight } = shape;
188
+ ring(push, radius, "xz", -halfHeight);
189
+ for (let i = 0; i < 4; i++) {
190
+ const a = i / 4 * Math.PI * 2;
191
+ push(Math.cos(a) * radius, -halfHeight, Math.sin(a) * radius, 0, halfHeight, 0);
192
+ }
193
+ return;
194
+ }
195
+ default:
196
+ if (collider.shape.type === 12 || collider.shape.type === 13) {
197
+ const h = shape.halfExtents;
198
+ if (h) push(-h.x, -h.y, -h.z, h.x, h.y, h.z);
199
+ }
200
+ return;
201
+ }
202
+ }
203
+ //#endregion
7
204
  //#region src/3d/physics/physics-3d.ts
8
205
  var physics_3d_exports = /* @__PURE__ */ __exportAll({
9
206
  Physics3D: () => Physics3D,
@@ -26,6 +223,8 @@ var Physics3D = class {
26
223
  engine;
27
224
  /** Render collider outlines in the GAME view (renderers pick this up). */
28
225
  debugDraw = false;
226
+ /** Narrow those outlines to one node's subtree (see DebugLineSource). */
227
+ debugScope = null;
29
228
  dimension = "3d";
30
229
  unregisterDebug;
31
230
  warnedNoCollider = /* @__PURE__ */ new WeakSet();
@@ -42,6 +241,7 @@ var Physics3D = class {
42
241
  this.R = R;
43
242
  this.engine = engine;
44
243
  this.optsGravity = opts?.gravity;
244
+ this.simulate = opts?.simulate ?? true;
45
245
  this.lastScene = engine.scene;
46
246
  const declared = engine.scene?.physics?.gravity;
47
247
  const g = opts?.gravity ?? declared ?? [
@@ -60,9 +260,16 @@ var Physics3D = class {
60
260
  this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
61
261
  this.syncBodies();
62
262
  }
263
+ /** Whether the solver runs at all. False = collider outlines only. */
264
+ simulate;
63
265
  /** @internal Driven by engine.fixedUpdated. */
64
266
  step(dt) {
65
267
  this.lastDt = dt;
268
+ if (!this.simulate) {
269
+ this.syncBodies();
270
+ this.poseFromTree();
271
+ return;
272
+ }
66
273
  if (this.engine.scene !== this.lastScene) {
67
274
  this.lastScene = this.engine.scene;
68
275
  if (!this.optsGravity) {
@@ -133,9 +340,37 @@ var Physics3D = class {
133
340
  }
134
341
  });
135
342
  }
343
+ /**
344
+ * Move every body to its node's current transform.
345
+ *
346
+ * Only used in outline mode: normally physics owns the pose and writes it
347
+ * INTO the node, so nothing re-reads the tree. Deliberately computed the same
348
+ * way `ensureEntry` computes a body's initial pose — world position, node-local
349
+ * euler — so an outline in the editor is the outline the game will draw, right
350
+ * down to the places where that pair disagrees with a rotated parent.
351
+ */
352
+ poseFromTree() {
353
+ for (const [node, e] of this.entries) {
354
+ const [wx, wy, wz] = worldPosition3D(node);
355
+ e.body.setTranslation({
356
+ x: wx,
357
+ y: wy,
358
+ z: wz
359
+ }, false);
360
+ quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
361
+ e.body.setRotation({
362
+ x: quatScratch.x,
363
+ y: quatScratch.y,
364
+ z: quatScratch.z,
365
+ w: quatScratch.w
366
+ }, false);
367
+ }
368
+ this.world.propagateModifiedBodyPositionsToColliders();
369
+ }
136
370
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
137
371
  moveAndSlide(node) {
138
372
  const e = this.ensureEntry(node);
373
+ if (!e) return;
139
374
  const vy = node.velocity[1] ?? 0;
140
375
  if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
141
376
  else this.kcc.disableSnapToGround();
@@ -158,7 +393,36 @@ var Physics3D = class {
158
393
  /** Rapier debug segments (meters). Null while debugDraw is off. */
159
394
  debugLines() {
160
395
  if (!this.debugDraw) return null;
161
- return this.world.debugRender().vertices;
396
+ const scope = this.debugScope;
397
+ if (!scope) return this.world.debugRender().vertices;
398
+ const bodies = this.scopedBodies(scope);
399
+ const out = [];
400
+ for (const body of bodies) for (let i = 0; i < body.numColliders(); i++) {
401
+ const collider = body.collider(i);
402
+ if (collider) colliderLines3D(collider, out);
403
+ }
404
+ return new Float32Array(out);
405
+ }
406
+ /**
407
+ * The Rapier bodies a selected node stands for: its own, its descendants',
408
+ * and — when it owns none — the nearest body ABOVE it, so clicking the visual
409
+ * mesh child of a StaticBody3D shows that body's collider rather than nothing.
410
+ */
411
+ scopedBodies(node) {
412
+ const out = [];
413
+ const collect = (n) => {
414
+ const entry = this.entries.get(n);
415
+ if (entry) out.push(entry.body);
416
+ const scatter = this.scatterBodies.get(n);
417
+ if (scatter) out.push(scatter);
418
+ for (const child of n.children) collect(child);
419
+ };
420
+ collect(node);
421
+ for (let p = node.parent; p && out.length === 0; p = p.parent) {
422
+ const entry = this.entries.get(p);
423
+ if (entry) out.push(entry.body);
424
+ }
425
+ return out;
162
426
  }
163
427
  dispose() {
164
428
  this.disconnect();
@@ -169,7 +433,63 @@ var Physics3D = class {
169
433
  lastRoot = null;
170
434
  jointSet = /* @__PURE__ */ new Set();
171
435
  joints = /* @__PURE__ */ new Map();
436
+ /** One fixed body per solid scatter field, carrying a collider per row. */
437
+ scatterBodies = /* @__PURE__ */ new Map();
172
438
  bodySet = /* @__PURE__ */ new Set();
439
+ /**
440
+ * Give every row of a scatter field its own static collider.
441
+ *
442
+ * A stone field is one node with fifty transforms in it, and a player walking
443
+ * through all fifty is what gives a world away as scenery. All of them hang
444
+ * off ONE fixed body — they never move, so there is nothing to step — and the
445
+ * whole set is rebuilt only when the tree's shape changes.
446
+ */
447
+ syncScatters(root) {
448
+ for (const [, entry] of this.scatterBodies) this.world.removeRigidBody(entry);
449
+ this.scatterBodies.clear();
450
+ const fields = /* @__PURE__ */ new Set();
451
+ collectScatters(root, fields);
452
+ for (const field of fields) {
453
+ const spec = field.collider;
454
+ if (!spec) continue;
455
+ const [ox, oy, oz] = worldPosition3D(field);
456
+ const body = this.world.createRigidBody(this.R.RigidBodyDesc.fixed().setTranslation(0, 0, 0));
457
+ const [sx, sy, sz] = field.size;
458
+ let basePoints = null;
459
+ for (const row of field.transforms) {
460
+ const scale = row[4] ?? 1;
461
+ const x = ox + (row[0] ?? 0);
462
+ const y = oy + (row[1] ?? 0);
463
+ const z = oz + (row[2] ?? 0);
464
+ let desc = null;
465
+ if (spec.shape === "sphere") desc = this.R.ColliderDesc.ball(Math.max((spec.radius ?? Math.max(sx, sz) / 2) * scale, .01));
466
+ else if (spec.shape === "box") {
467
+ const ext = spec.size ?? [
468
+ sx,
469
+ sy,
470
+ sz
471
+ ];
472
+ desc = this.R.ColliderDesc.cuboid(Math.max((ext[0] ?? 1) * scale / 2, .01), Math.max((ext[1] ?? 1) * scale / 2, .01), Math.max((ext[2] ?? 1) * scale / 2, .01));
473
+ } else {
474
+ basePoints ??= meshPoints(field.mesh, field.size);
475
+ const pts = new Float32Array(basePoints.length);
476
+ for (let i = 0; i < pts.length; i++) pts[i] = basePoints[i] * scale;
477
+ desc = this.R.ColliderDesc.convexHull(pts);
478
+ }
479
+ if (!desc) continue;
480
+ desc.setTranslation(x, y, z);
481
+ const yaw = (row[3] ?? 0) * Math.PI / 180;
482
+ if (yaw !== 0) desc.setRotation({
483
+ x: 0,
484
+ y: Math.sin(yaw / 2),
485
+ z: 0,
486
+ w: Math.cos(yaw / 2)
487
+ });
488
+ this.world.createCollider(desc, body);
489
+ }
490
+ this.scatterBodies.set(field, body);
491
+ }
492
+ }
173
493
  syncBodies() {
174
494
  const scene = this.engine.scene;
175
495
  if (!scene) return;
@@ -184,6 +504,7 @@ var Physics3D = class {
184
504
  if (structureChanged) {
185
505
  this.jointSet.clear();
186
506
  collectJoints(scene.root, this.jointSet);
507
+ this.syncScatters(scene.root);
187
508
  }
188
509
  const seen = this.bodySet;
189
510
  for (const node of seen) {
@@ -223,10 +544,18 @@ var Physics3D = class {
223
544
  }
224
545
  }
225
546
  }
547
+ /**
548
+ * Null while a `{"shape":"auto"}` body is waiting on its model — the shape
549
+ * lives in the model's triangles, and there is nothing honest to guess in the
550
+ * meantime. syncBodies runs every step, so the body appears the moment the
551
+ * download lands.
552
+ */
226
553
  ensureEntry(node) {
227
554
  let e = this.entries.get(node);
228
555
  const R = this.R;
229
556
  if (!e) {
557
+ const colDesc = parseCollider3D(R, node);
558
+ if (!colDesc) return null;
230
559
  let desc;
231
560
  if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
232
561
  else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
@@ -246,7 +575,6 @@ var Physics3D = class {
246
575
  if (node.fixedRotation) desc.lockRotations();
247
576
  }
248
577
  const body = this.world.createRigidBody(desc);
249
- const colDesc = parseCollider3D(R, node);
250
578
  colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
251
579
  colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
252
580
  if (node instanceof Area3D) colDesc.setSensor(true);
@@ -448,6 +776,120 @@ function collectBodies(node, out) {
448
776
  for (const c of node.children) collectBodies(c, out);
449
777
  }
450
778
  /**
779
+ * A collider shaped like the mesh actually being drawn.
780
+ *
781
+ * Hand-written extents drift from what the player sees — a box "around" a
782
+ * bridge is wider than the bridge, a sphere "for" a stone is smaller than the
783
+ * stone — and nobody notices until they walk into thin air. So the collider is
784
+ * built from the SAME geometry factory the renderer draws with: box → cuboid,
785
+ * sphere → ball, cylinder/capsule → their own shapes, and anything faceted
786
+ * (the `gem` stone) → a convex hull of its real vertices.
787
+ *
788
+ * Convex, always: a hull is a few planes to test against, where a triangle
789
+ * mesh is a tree of them, and nothing scattered across a map needs concavity.
790
+ */
791
+ function meshShapedDesc(R, kind, size, scale) {
792
+ const x = (size[0] ?? 1) * scale;
793
+ const y = (size[1] ?? 1) * scale;
794
+ const z = (size[2] ?? 1) * scale;
795
+ switch (kind) {
796
+ case "box": return R.ColliderDesc.cuboid(x / 2, y / 2, z / 2);
797
+ case "sphere": return R.ColliderDesc.ball(x);
798
+ case "capsule": return R.ColliderDesc.capsule(y / 2, x);
799
+ case "cylinder": return R.ColliderDesc.cylinder(y / 2, x);
800
+ case "plane": return R.ColliderDesc.cuboid(x / 2, .01, z / 2);
801
+ default: return R.ColliderDesc.convexHull(meshPoints(kind, [
802
+ x,
803
+ y,
804
+ z
805
+ ]));
806
+ }
807
+ }
808
+ /** The drawn geometry's vertices, flat xyz — the same mesh the player sees. */
809
+ function meshPoints(kind, size) {
810
+ const geo = buildMeshGeometry(kind, size);
811
+ const pos = geo.getAttribute("position");
812
+ const points = new Float32Array(pos.count * 3);
813
+ for (let i = 0; i < pos.count; i++) {
814
+ points[i * 3] = pos.getX(i);
815
+ points[i * 3 + 1] = pos.getY(i);
816
+ points[i * 3 + 2] = pos.getZ(i);
817
+ }
818
+ geo.dispose();
819
+ return points;
820
+ }
821
+ function isMountedModel(node) {
822
+ return typeof node._mountedObject3D === "function";
823
+ }
824
+ const localMat = new Matrix4();
825
+ const meshMat = new Matrix4();
826
+ const vertScratch = new Vector3();
827
+ /**
828
+ * The triangles of a LOADED model, in the body's own frame.
829
+ *
830
+ * A GLB is not a primitive — its shape lives in its vertices, and those are the
831
+ * only honest source for a collider around it. Every drawn mesh under the model
832
+ * is baked through its own world matrix into the body's local space, so a model
833
+ * that is offset, turned or fitted to `targetHeight` collides where it is drawn
834
+ * rather than where it was authored.
835
+ *
836
+ * Null while the asset is still downloading — the caller retries next step.
837
+ */
838
+ function modelTriangles(body, model) {
839
+ const mounted = model._mountedObject3D();
840
+ if (!mounted) return null;
841
+ const bodyObj = body._ensureObject3D();
842
+ bodyObj.updateWorldMatrix(true, true);
843
+ localMat.copy(bodyObj.matrixWorld).invert();
844
+ const verts = [];
845
+ const idx = [];
846
+ mounted.traverse((obj) => {
847
+ const mesh = obj;
848
+ if (!mesh.isMesh) return;
849
+ const pos = mesh.geometry?.getAttribute("position");
850
+ if (!pos) return;
851
+ meshMat.multiplyMatrices(localMat, mesh.matrixWorld);
852
+ const base = verts.length / 3;
853
+ for (let i = 0; i < pos.count; i++) {
854
+ vertScratch.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(meshMat);
855
+ verts.push(vertScratch.x, vertScratch.y, vertScratch.z);
856
+ }
857
+ const index = mesh.geometry.getIndex();
858
+ if (index) for (let i = 0; i < index.count; i++) idx.push(base + index.getX(i));
859
+ else for (let i = 0; i < pos.count; i++) idx.push(base + i);
860
+ });
861
+ if (idx.length < 3) return null;
862
+ return {
863
+ vertices: new Float32Array(verts),
864
+ indices: new Uint32Array(idx),
865
+ triangles: idx.length / 3
866
+ };
867
+ }
868
+ /** Past this, a trimesh is worth a word — level art is not collision art. */
869
+ const TRIMESH_BUDGET = 2e4;
870
+ /** A body Rapier will never move, so a hollow triangle mesh is safe on it. */
871
+ function isStaticShape(node) {
872
+ return !(node instanceof RigidBody3D || node instanceof CharacterBody3D);
873
+ }
874
+ /** Every mesh a body carries, itself first — `auto` shapes itself on one. */
875
+ function meshesUnder(node, out, models) {
876
+ if (isMountedModel(node)) models.push(node);
877
+ else {
878
+ const self = node;
879
+ if (typeof self.mesh === "string" && Array.isArray(self.size)) out.push({
880
+ name: node.name,
881
+ mesh: self.mesh,
882
+ size: self.size
883
+ });
884
+ }
885
+ for (const child of node.children) meshesUnder(child, out, models);
886
+ }
887
+ /** Every scatter field asking to be solid (see InstancedMesh3D.collider). */
888
+ function collectScatters(node, out) {
889
+ if (node instanceof InstancedMesh3D && node.collider) out.add(node);
890
+ for (const c of node.children) collectScatters(c, out);
891
+ }
892
+ /**
451
893
  * Composed world position (translation only — ancestor rotation/scale are NOT
452
894
  * supported for physics bodies; keep body ancestors untransformed or offset-only).
453
895
  */
@@ -495,7 +937,8 @@ function parseCollider3D(R, node) {
495
937
  const collider = node.collider;
496
938
  validateCollider3D(collider, node.name);
497
939
  if (collider.shape === "heightfield") return buildHeightfieldDesc3D(R, node);
498
- const desc = buildColliderDesc3D(R, collider, node.name);
940
+ const desc = buildColliderDesc3D(R, collider, node.name, node);
941
+ if (!desc) return null;
499
942
  const offset = collider.offset;
500
943
  if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
501
944
  return desc;
@@ -522,7 +965,8 @@ function buildHeightfieldDesc3D(R, node) {
522
965
  desc.setTranslation((terrain.position[0] ?? 0) + (offset[0] ?? 0), (terrain.position[1] ?? 0) + (offset[1] ?? 0), (terrain.position[2] ?? 0) + (offset[2] ?? 0));
523
966
  return desc;
524
967
  }
525
- function buildColliderDesc3D(R, collider, nodeName) {
968
+ /** Null means "not ready yet" (a model still downloading), never "no shape". */
969
+ function buildColliderDesc3D(R, collider, nodeName, node) {
526
970
  const shape = collider.shape;
527
971
  if (shape === "box") {
528
972
  const size = collider.size;
@@ -537,13 +981,35 @@ function buildColliderDesc3D(R, collider, nodeName) {
537
981
  if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
538
982
  return R.ColliderDesc.capsule(collider.height / 2, collider.radius);
539
983
  }
984
+ if (shape === "auto") {
985
+ const meshes = [];
986
+ const models = [];
987
+ if (node) meshesUnder(node, meshes, models);
988
+ const mesh = meshes[0];
989
+ const model = models[0];
990
+ if (!mesh && !model) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': shape 'auto' needs a MeshInstance3D or ModelInstance3D on the body (or as a child) to take its shape from.`);
991
+ const parts = meshes.length + models.length;
992
+ if (parts > 1) console.warn(`[incanto] Collider on '${nodeName}': shape 'auto' fits ONE mesh and took '${(mesh ?? model)?.name}'; ${parts - 1} other mesh(es) here are not solid. Give each part its own body.`);
993
+ if (mesh) {
994
+ const desc = meshShapedDesc(R, mesh.mesh, mesh.size, 1);
995
+ if (desc) return desc;
996
+ } else if (model && node) {
997
+ const tri = modelTriangles(node, model);
998
+ if (!tri) return null;
999
+ if (isStaticShape(node)) {
1000
+ if (tri.triangles > TRIMESH_BUDGET) console.warn(`[incanto] Collider on '${nodeName}': shape 'auto' built a ${tri.triangles}-triangle mesh from '${model.name}'. Consider a low-poly collision model or an explicit box.`);
1001
+ return R.ColliderDesc.trimesh(tri.vertices, tri.indices);
1002
+ }
1003
+ return R.ColliderDesc.convexHull(tri.vertices);
1004
+ }
1005
+ }
540
1006
  if (shape === "trimesh") {
541
1007
  const vertices = collider.vertices;
542
1008
  const indices = collider.indices;
543
1009
  if (!Array.isArray(vertices) || !Array.isArray(indices)) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz) and "indices" (triangles).`);
544
1010
  return R.ColliderDesc.trimesh(new Float32Array(vertices), new Uint32Array(indices));
545
1011
  }
546
- throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
1012
+ throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'auto', 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
547
1013
  }
548
1014
  //#endregion
549
1015
  export { enablePhysics3D as n, physics_3d_exports as r, Physics3D as t };
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as JsonValue } from "./schema-CcoWb32N.js";
2
- import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-CPibUfnH.js";
2
+ import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
5
5
  //#region src/react/index.d.ts
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BRgWpNsa.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-71TJjW1T.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DuBTv2zI.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-5z_QVtLx.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1432,6 +1432,16 @@ var Engine = class {
1432
1432
  * `engine.music.play(src)` / `crossfadeTo(src, secs)` / `stop(fadeOut)`;
1433
1433
  * routes through the music bus and is advanced each frame by the loop. */
1434
1434
  music = new MusicManager(this.audio);
1435
+ /**
1436
+ * Where nodes that build DOM (HudLayer and its widgets) mount their elements.
1437
+ *
1438
+ * `null` — the default — means `document.body` with `position: fixed`, i.e. a
1439
+ * game's HUD covers the window, which is what a game wants. A host that only
1440
+ * OWNS PART of the page sets this to its own (positioned) container so the
1441
+ * HUD stays inside it: the scene editor points it at the viewport pane, which
1442
+ * is why an edited scene's HUD no longer floats over the inspector.
1443
+ */
1444
+ uiHost = null;
1435
1445
  fixedStep;
1436
1446
  maxFixedSteps;
1437
1447
  scheduler;
@@ -1566,7 +1576,7 @@ var Engine = class {
1566
1576
  this.lastMs = nowMs;
1567
1577
  return;
1568
1578
  }
1569
- const rawDt = Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS);
1579
+ const rawDt = Math.max(0, Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS));
1570
1580
  const dt = rawDt * this.timeScale;
1571
1581
  this._time += dt;
1572
1582
  this._unscaledTime += rawDt;
@@ -1836,8 +1846,27 @@ var HudLayer = class extends Node {
1836
1846
  const el = document.createElement("div");
1837
1847
  el.dataset.incantoHud = this.name;
1838
1848
  el.style.cssText = `position:fixed;inset:0;pointer-events:none;z-index:${this.zIndex};font:${FONT};`;
1839
- document.body.appendChild(el);
1840
1849
  this._element = el;
1850
+ this._mount();
1851
+ }
1852
+ /**
1853
+ * Put the overlay where the page's owner says UI goes: `engine.uiHost`, or
1854
+ * the window when nobody claims it (a game — a HUD covers the screen).
1855
+ *
1856
+ * Re-checked every frame because a scene's tree is BUILT before it is handed
1857
+ * to an engine: at onEnterTree there is no engine to ask yet, so the first
1858
+ * update is where the answer actually arrives. appendChild moves the element
1859
+ * if it already landed somewhere else, so the HUD follows the host across a
1860
+ * scene swap into a differently-hosted engine (game → editor) instead of
1861
+ * being stranded on document.body over the inspector.
1862
+ */
1863
+ _mount() {
1864
+ const el = this._element;
1865
+ if (!el) return;
1866
+ const host = this.tree?.engine?.uiHost ?? document.body;
1867
+ if (el.parentElement === host) return;
1868
+ el.style.position = host === document.body ? "fixed" : "absolute";
1869
+ host.appendChild(el);
1841
1870
  }
1842
1871
  onExitTree() {
1843
1872
  this._element?.remove();
@@ -1845,7 +1874,10 @@ var HudLayer = class extends Node {
1845
1874
  this.slots.clear();
1846
1875
  }
1847
1876
  update() {
1848
- if (this._element) this._element.style.display = this.visible ? "" : "none";
1877
+ if (this._element) {
1878
+ this._mount();
1879
+ this._element.style.display = this.visible ? "" : "none";
1880
+ }
1849
1881
  }
1850
1882
  /** @internal Widgets mount into per-anchor flex columns. */
1851
1883
  _slot(anchor) {