incanto 0.27.0 → 0.29.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 (58) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +118 -1
  3. package/dist/2d.d.ts +38 -5
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +496 -150
  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-DUAtTqID.js → create-game-CNKXGfpr.js} +270 -47
  9. package/dist/{create-game-C0bPoURs.js → create-game-CbuLWorm.js} +53 -14
  10. package/dist/debug.d.ts +51 -6
  11. package/dist/debug.js +242 -19
  12. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  13. package/dist/editor-switch-CAKlJMIY.js +161 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8439 -0
  16. package/dist/{gameplay-Ddk13pQ6.js → gameplay-L05WgWd1.js} +220 -37
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +22 -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-CCVTrKOd.js} +62 -2
  26. package/dist/{physics-3d-4mGzOZ2J.js → physics-3d-BZZLtwJu.js} +439 -8
  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-DcHXS1MA.js → register-C44aSduO.js} +5694 -5108
  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/test.d.ts +35 -3
  34. package/dist/test.js +104 -10
  35. package/dist/vite.d.ts +53 -1
  36. package/dist/vite.js +94 -2
  37. package/editor/assets/{agent8-DElIPozC.js → agent8-DEVkEa3d.js} +1 -1
  38. package/editor/assets/debug-zGAtpDF0.js +2 -0
  39. package/editor/assets/index-Bx4UtWYY.js +10586 -0
  40. package/editor/index.html +3 -157
  41. package/package.json +3 -2
  42. package/schemas/scene.schema.json +110 -4
  43. package/skills/incanto-3d-models.md +38 -0
  44. package/skills/incanto-assets.md +13 -0
  45. package/skills/incanto-building-3d-games.md +83 -6
  46. package/skills/incanto-editor.md +151 -0
  47. package/skills/incanto-environment.md +88 -1
  48. package/skills/incanto-node-reference.md +35 -1
  49. package/skills/incanto-physics-and-input.md +45 -6
  50. package/skills/incanto-verifying-your-game.md +51 -2
  51. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
  57. package/editor/assets/index-BrneVqN0.js +0 -8330
  58. 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-Ddk13pQ6.js";
3
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-L05WgWd1.js";
4
4
  import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
5
- import { T as Terrain3D, k as Joint3D } from "./register-DcHXS1MA.js";
6
- import { Euler, Quaternion } from "three";
5
+ import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-C44aSduO.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();
@@ -136,6 +335,7 @@ var Physics3D = class {
136
335
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
137
336
  moveAndSlide(node) {
138
337
  const e = this.ensureEntry(node);
338
+ if (!e) return;
139
339
  const vy = node.velocity[1] ?? 0;
140
340
  if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
141
341
  else this.kcc.disableSnapToGround();
@@ -158,7 +358,36 @@ var Physics3D = class {
158
358
  /** Rapier debug segments (meters). Null while debugDraw is off. */
159
359
  debugLines() {
160
360
  if (!this.debugDraw) return null;
161
- return this.world.debugRender().vertices;
361
+ const scope = this.debugScope;
362
+ if (!scope) return this.world.debugRender().vertices;
363
+ const bodies = this.scopedBodies(scope);
364
+ const out = [];
365
+ for (const body of bodies) for (let i = 0; i < body.numColliders(); i++) {
366
+ const collider = body.collider(i);
367
+ if (collider) colliderLines3D(collider, out);
368
+ }
369
+ return new Float32Array(out);
370
+ }
371
+ /**
372
+ * The Rapier bodies a selected node stands for: its own, its descendants',
373
+ * and — when it owns none — the nearest body ABOVE it, so clicking the visual
374
+ * mesh child of a StaticBody3D shows that body's collider rather than nothing.
375
+ */
376
+ scopedBodies(node) {
377
+ const out = [];
378
+ const collect = (n) => {
379
+ const entry = this.entries.get(n);
380
+ if (entry) out.push(entry.body);
381
+ const scatter = this.scatterBodies.get(n);
382
+ if (scatter) out.push(scatter);
383
+ for (const child of n.children) collect(child);
384
+ };
385
+ collect(node);
386
+ for (let p = node.parent; p && out.length === 0; p = p.parent) {
387
+ const entry = this.entries.get(p);
388
+ if (entry) out.push(entry.body);
389
+ }
390
+ return out;
162
391
  }
163
392
  dispose() {
164
393
  this.disconnect();
@@ -169,7 +398,63 @@ var Physics3D = class {
169
398
  lastRoot = null;
170
399
  jointSet = /* @__PURE__ */ new Set();
171
400
  joints = /* @__PURE__ */ new Map();
401
+ /** One fixed body per solid scatter field, carrying a collider per row. */
402
+ scatterBodies = /* @__PURE__ */ new Map();
172
403
  bodySet = /* @__PURE__ */ new Set();
404
+ /**
405
+ * Give every row of a scatter field its own static collider.
406
+ *
407
+ * A stone field is one node with fifty transforms in it, and a player walking
408
+ * through all fifty is what gives a world away as scenery. All of them hang
409
+ * off ONE fixed body — they never move, so there is nothing to step — and the
410
+ * whole set is rebuilt only when the tree's shape changes.
411
+ */
412
+ syncScatters(root) {
413
+ for (const [, entry] of this.scatterBodies) this.world.removeRigidBody(entry);
414
+ this.scatterBodies.clear();
415
+ const fields = /* @__PURE__ */ new Set();
416
+ collectScatters(root, fields);
417
+ for (const field of fields) {
418
+ const spec = field.collider;
419
+ if (!spec) continue;
420
+ const [ox, oy, oz] = worldPosition3D(field);
421
+ const body = this.world.createRigidBody(this.R.RigidBodyDesc.fixed().setTranslation(0, 0, 0));
422
+ const [sx, sy, sz] = field.size;
423
+ let basePoints = null;
424
+ for (const row of field.transforms) {
425
+ const scale = row[4] ?? 1;
426
+ const x = ox + (row[0] ?? 0);
427
+ const y = oy + (row[1] ?? 0);
428
+ const z = oz + (row[2] ?? 0);
429
+ let desc = null;
430
+ if (spec.shape === "sphere") desc = this.R.ColliderDesc.ball(Math.max((spec.radius ?? Math.max(sx, sz) / 2) * scale, .01));
431
+ else if (spec.shape === "box") {
432
+ const ext = spec.size ?? [
433
+ sx,
434
+ sy,
435
+ sz
436
+ ];
437
+ 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));
438
+ } else {
439
+ basePoints ??= meshPoints(field.mesh, field.size);
440
+ const pts = new Float32Array(basePoints.length);
441
+ for (let i = 0; i < pts.length; i++) pts[i] = basePoints[i] * scale;
442
+ desc = this.R.ColliderDesc.convexHull(pts);
443
+ }
444
+ if (!desc) continue;
445
+ desc.setTranslation(x, y, z);
446
+ const yaw = (row[3] ?? 0) * Math.PI / 180;
447
+ if (yaw !== 0) desc.setRotation({
448
+ x: 0,
449
+ y: Math.sin(yaw / 2),
450
+ z: 0,
451
+ w: Math.cos(yaw / 2)
452
+ });
453
+ this.world.createCollider(desc, body);
454
+ }
455
+ this.scatterBodies.set(field, body);
456
+ }
457
+ }
173
458
  syncBodies() {
174
459
  const scene = this.engine.scene;
175
460
  if (!scene) return;
@@ -184,6 +469,7 @@ var Physics3D = class {
184
469
  if (structureChanged) {
185
470
  this.jointSet.clear();
186
471
  collectJoints(scene.root, this.jointSet);
472
+ this.syncScatters(scene.root);
187
473
  }
188
474
  const seen = this.bodySet;
189
475
  for (const node of seen) {
@@ -223,10 +509,18 @@ var Physics3D = class {
223
509
  }
224
510
  }
225
511
  }
512
+ /**
513
+ * Null while a `{"shape":"auto"}` body is waiting on its model — the shape
514
+ * lives in the model's triangles, and there is nothing honest to guess in the
515
+ * meantime. syncBodies runs every step, so the body appears the moment the
516
+ * download lands.
517
+ */
226
518
  ensureEntry(node) {
227
519
  let e = this.entries.get(node);
228
520
  const R = this.R;
229
521
  if (!e) {
522
+ const colDesc = parseCollider3D(R, node);
523
+ if (!colDesc) return null;
230
524
  let desc;
231
525
  if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
232
526
  else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
@@ -246,7 +540,6 @@ var Physics3D = class {
246
540
  if (node.fixedRotation) desc.lockRotations();
247
541
  }
248
542
  const body = this.world.createRigidBody(desc);
249
- const colDesc = parseCollider3D(R, node);
250
543
  colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
251
544
  colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
252
545
  if (node instanceof Area3D) colDesc.setSensor(true);
@@ -448,6 +741,120 @@ function collectBodies(node, out) {
448
741
  for (const c of node.children) collectBodies(c, out);
449
742
  }
450
743
  /**
744
+ * A collider shaped like the mesh actually being drawn.
745
+ *
746
+ * Hand-written extents drift from what the player sees — a box "around" a
747
+ * bridge is wider than the bridge, a sphere "for" a stone is smaller than the
748
+ * stone — and nobody notices until they walk into thin air. So the collider is
749
+ * built from the SAME geometry factory the renderer draws with: box → cuboid,
750
+ * sphere → ball, cylinder/capsule → their own shapes, and anything faceted
751
+ * (the `gem` stone) → a convex hull of its real vertices.
752
+ *
753
+ * Convex, always: a hull is a few planes to test against, where a triangle
754
+ * mesh is a tree of them, and nothing scattered across a map needs concavity.
755
+ */
756
+ function meshShapedDesc(R, kind, size, scale) {
757
+ const x = (size[0] ?? 1) * scale;
758
+ const y = (size[1] ?? 1) * scale;
759
+ const z = (size[2] ?? 1) * scale;
760
+ switch (kind) {
761
+ case "box": return R.ColliderDesc.cuboid(x / 2, y / 2, z / 2);
762
+ case "sphere": return R.ColliderDesc.ball(x);
763
+ case "capsule": return R.ColliderDesc.capsule(y / 2, x);
764
+ case "cylinder": return R.ColliderDesc.cylinder(y / 2, x);
765
+ case "plane": return R.ColliderDesc.cuboid(x / 2, .01, z / 2);
766
+ default: return R.ColliderDesc.convexHull(meshPoints(kind, [
767
+ x,
768
+ y,
769
+ z
770
+ ]));
771
+ }
772
+ }
773
+ /** The drawn geometry's vertices, flat xyz — the same mesh the player sees. */
774
+ function meshPoints(kind, size) {
775
+ const geo = buildMeshGeometry(kind, size);
776
+ const pos = geo.getAttribute("position");
777
+ const points = new Float32Array(pos.count * 3);
778
+ for (let i = 0; i < pos.count; i++) {
779
+ points[i * 3] = pos.getX(i);
780
+ points[i * 3 + 1] = pos.getY(i);
781
+ points[i * 3 + 2] = pos.getZ(i);
782
+ }
783
+ geo.dispose();
784
+ return points;
785
+ }
786
+ function isMountedModel(node) {
787
+ return typeof node._mountedObject3D === "function";
788
+ }
789
+ const localMat = new Matrix4();
790
+ const meshMat = new Matrix4();
791
+ const vertScratch = new Vector3();
792
+ /**
793
+ * The triangles of a LOADED model, in the body's own frame.
794
+ *
795
+ * A GLB is not a primitive — its shape lives in its vertices, and those are the
796
+ * only honest source for a collider around it. Every drawn mesh under the model
797
+ * is baked through its own world matrix into the body's local space, so a model
798
+ * that is offset, turned or fitted to `targetHeight` collides where it is drawn
799
+ * rather than where it was authored.
800
+ *
801
+ * Null while the asset is still downloading — the caller retries next step.
802
+ */
803
+ function modelTriangles(body, model) {
804
+ const mounted = model._mountedObject3D();
805
+ if (!mounted) return null;
806
+ const bodyObj = body._ensureObject3D();
807
+ bodyObj.updateWorldMatrix(true, true);
808
+ localMat.copy(bodyObj.matrixWorld).invert();
809
+ const verts = [];
810
+ const idx = [];
811
+ mounted.traverse((obj) => {
812
+ const mesh = obj;
813
+ if (!mesh.isMesh) return;
814
+ const pos = mesh.geometry?.getAttribute("position");
815
+ if (!pos) return;
816
+ meshMat.multiplyMatrices(localMat, mesh.matrixWorld);
817
+ const base = verts.length / 3;
818
+ for (let i = 0; i < pos.count; i++) {
819
+ vertScratch.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(meshMat);
820
+ verts.push(vertScratch.x, vertScratch.y, vertScratch.z);
821
+ }
822
+ const index = mesh.geometry.getIndex();
823
+ if (index) for (let i = 0; i < index.count; i++) idx.push(base + index.getX(i));
824
+ else for (let i = 0; i < pos.count; i++) idx.push(base + i);
825
+ });
826
+ if (idx.length < 3) return null;
827
+ return {
828
+ vertices: new Float32Array(verts),
829
+ indices: new Uint32Array(idx),
830
+ triangles: idx.length / 3
831
+ };
832
+ }
833
+ /** Past this, a trimesh is worth a word — level art is not collision art. */
834
+ const TRIMESH_BUDGET = 2e4;
835
+ /** A body Rapier will never move, so a hollow triangle mesh is safe on it. */
836
+ function isStaticShape(node) {
837
+ return !(node instanceof RigidBody3D || node instanceof CharacterBody3D);
838
+ }
839
+ /** Every mesh a body carries, itself first — `auto` shapes itself on one. */
840
+ function meshesUnder(node, out, models) {
841
+ if (isMountedModel(node)) models.push(node);
842
+ else {
843
+ const self = node;
844
+ if (typeof self.mesh === "string" && Array.isArray(self.size)) out.push({
845
+ name: node.name,
846
+ mesh: self.mesh,
847
+ size: self.size
848
+ });
849
+ }
850
+ for (const child of node.children) meshesUnder(child, out, models);
851
+ }
852
+ /** Every scatter field asking to be solid (see InstancedMesh3D.collider). */
853
+ function collectScatters(node, out) {
854
+ if (node instanceof InstancedMesh3D && node.collider) out.add(node);
855
+ for (const c of node.children) collectScatters(c, out);
856
+ }
857
+ /**
451
858
  * Composed world position (translation only — ancestor rotation/scale are NOT
452
859
  * supported for physics bodies; keep body ancestors untransformed or offset-only).
453
860
  */
@@ -495,7 +902,8 @@ function parseCollider3D(R, node) {
495
902
  const collider = node.collider;
496
903
  validateCollider3D(collider, node.name);
497
904
  if (collider.shape === "heightfield") return buildHeightfieldDesc3D(R, node);
498
- const desc = buildColliderDesc3D(R, collider, node.name);
905
+ const desc = buildColliderDesc3D(R, collider, node.name, node);
906
+ if (!desc) return null;
499
907
  const offset = collider.offset;
500
908
  if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
501
909
  return desc;
@@ -522,7 +930,8 @@ function buildHeightfieldDesc3D(R, node) {
522
930
  desc.setTranslation((terrain.position[0] ?? 0) + (offset[0] ?? 0), (terrain.position[1] ?? 0) + (offset[1] ?? 0), (terrain.position[2] ?? 0) + (offset[2] ?? 0));
523
931
  return desc;
524
932
  }
525
- function buildColliderDesc3D(R, collider, nodeName) {
933
+ /** Null means "not ready yet" (a model still downloading), never "no shape". */
934
+ function buildColliderDesc3D(R, collider, nodeName, node) {
526
935
  const shape = collider.shape;
527
936
  if (shape === "box") {
528
937
  const size = collider.size;
@@ -537,13 +946,35 @@ function buildColliderDesc3D(R, collider, nodeName) {
537
946
  if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
538
947
  return R.ColliderDesc.capsule(collider.height / 2, collider.radius);
539
948
  }
949
+ if (shape === "auto") {
950
+ const meshes = [];
951
+ const models = [];
952
+ if (node) meshesUnder(node, meshes, models);
953
+ const mesh = meshes[0];
954
+ const model = models[0];
955
+ 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.`);
956
+ const parts = meshes.length + models.length;
957
+ 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.`);
958
+ if (mesh) {
959
+ const desc = meshShapedDesc(R, mesh.mesh, mesh.size, 1);
960
+ if (desc) return desc;
961
+ } else if (model && node) {
962
+ const tri = modelTriangles(node, model);
963
+ if (!tri) return null;
964
+ if (isStaticShape(node)) {
965
+ 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.`);
966
+ return R.ColliderDesc.trimesh(tri.vertices, tri.indices);
967
+ }
968
+ return R.ColliderDesc.convexHull(tri.vertices);
969
+ }
970
+ }
540
971
  if (shape === "trimesh") {
541
972
  const vertices = collider.vertices;
542
973
  const indices = collider.indices;
543
974
  if (!Array.isArray(vertices) || !Array.isArray(indices)) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz) and "indices" (triangles).`);
544
975
  return R.ColliderDesc.trimesh(new Float32Array(vertices), new Uint32Array(indices));
545
976
  }
546
- throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
977
+ throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'auto', 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
547
978
  }
548
979
  //#endregion
549
980
  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-DUAtTqID.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-C0bPoURs.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-CNKXGfpr.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CbuLWorm.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) {