incanto 0.42.0 → 0.44.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.
- package/bin/incanto-feel.mjs +20 -1
- package/bin/incanto-frame.mjs +55 -11
- package/dist/2d.js +1 -1
- package/dist/3d.d.ts +2 -40
- package/dist/3d.js +6 -5
- package/dist/{create-game-DItIRyXH.js → create-game-D90pyPMx.js} +17 -81
- package/dist/{create-game-9KPppR0L.js → create-game-DFRbG0Bj.js} +3 -2
- package/dist/debug.d.ts +44 -3
- package/dist/debug.js +562 -4
- package/dist/{environment-presets-Vl5xBsXp.js → environment-presets-Ds5kXLoF.js} +1 -1
- package/dist/frame-report-Ct8XgsmV.d.ts +80 -0
- package/dist/frame-report-Lr3VO24R.js +197 -0
- package/dist/{gameplay-DRi9524r.js → gameplay-BQOeAid6.js} +37 -0
- package/dist/gameplay.d.ts +17 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{physics-3d-C2G604O1.js → physics-3d-CLPFv99o.js} +2 -2
- package/dist/react.js +1 -1
- package/dist/{src-cU57Uwdw.js → src-B3n06SsL.js} +1 -1
- package/dist/{teardown-CCtAMDLB.js → teardown-yePMOE1K.js} +32 -1
- package/dist/{test-DuOD1DO8.js → test-BMgiiD5i.js} +117 -5
- package/dist/test.d.ts +35 -1
- package/dist/test.js +2 -2
- package/dist/vite.d.ts +15 -1
- package/dist/vite.js +49 -6
- package/editor/assets/{agent8-Csw0T7jh.js → agent8-MgX6kYWH.js} +1 -1
- package/editor/assets/debug-Czri9cgX.js +3 -0
- package/editor/assets/{index-B1rUkWxB.js → index-D9MX3UHF.js} +60 -60
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-3d-character.md +19 -0
- package/skills/incanto-gameplay-behaviors.md +27 -0
- package/skills/incanto-node-reference.md +2 -0
- package/skills/incanto-verifying-your-game.md +60 -4
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +47 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +53 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/grove.scene.json +43 -0
- package/editor/assets/debug-C3qeBD4X.js +0 -3
package/dist/debug.js
CHANGED
|
@@ -1,6 +1,186 @@
|
|
|
1
1
|
import { t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
2
2
|
import { s as mergeStaticProps } from "./registry-IyWCGe4q.js";
|
|
3
3
|
import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
|
|
4
|
+
//#region src/debug/capture.ts
|
|
5
|
+
/**
|
|
6
|
+
* A drag, in CSS pixels, turned into a rectangle inside the pixel buffer.
|
|
7
|
+
*
|
|
8
|
+
* `scale` is `devicePixelRatio`: the canvas backing store is that many times
|
|
9
|
+
* the CSS box, and a crop taken in CSS coordinates cuts out the wrong part of
|
|
10
|
+
* the image on every retina display.
|
|
11
|
+
*/
|
|
12
|
+
function clampRegion(drag, width, height, scale = 1) {
|
|
13
|
+
const x0 = Math.min(drag.x0, drag.x1) * scale;
|
|
14
|
+
const x1 = Math.max(drag.x0, drag.x1) * scale;
|
|
15
|
+
const y0 = Math.min(drag.y0, drag.y1) * scale;
|
|
16
|
+
const y1 = Math.max(drag.y0, drag.y1) * scale;
|
|
17
|
+
const x = Math.max(0, Math.min(Math.round(x0), width - 1));
|
|
18
|
+
const y = Math.max(0, Math.min(Math.round(y0), height - 1));
|
|
19
|
+
return {
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
w: Math.max(1, Math.min(Math.round(x1 - x0), width - x)),
|
|
23
|
+
h: Math.max(1, Math.min(Math.round(y1 - y0), height - y))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Cut a rectangle out of an RGBA buffer, keeping row order. */
|
|
27
|
+
function cropPixels(pixels, width, height, region) {
|
|
28
|
+
const { x, y, w, h } = region;
|
|
29
|
+
const out = new Uint8ClampedArray(w * h * 4);
|
|
30
|
+
for (let row = 0; row < h; row++) {
|
|
31
|
+
const src = ((y + row) * width + x) * 4;
|
|
32
|
+
const dst = row * w * 4;
|
|
33
|
+
out.set(pixels.subarray(src, src + w * 4), dst);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
pixels: out,
|
|
37
|
+
width: w,
|
|
38
|
+
height: h
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The scene, pretty-printed for pasting into a conversation. */
|
|
42
|
+
function sceneStateText(scene) {
|
|
43
|
+
if (!scene) return "// no scene loaded";
|
|
44
|
+
const source = scene.source ?? scene;
|
|
45
|
+
return JSON.stringify(source, null, 2);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Put text on the clipboard, telling the truth about whether it worked.
|
|
49
|
+
*
|
|
50
|
+
* `navigator.clipboard` needs a secure context (https, or localhost — which a
|
|
51
|
+
* dev server is) and a user gesture, which a menu click is. Anything else
|
|
52
|
+
* returns false rather than throwing into the overlay.
|
|
53
|
+
*/
|
|
54
|
+
async function writeClipboardText(text) {
|
|
55
|
+
const nav = globalThis.navigator;
|
|
56
|
+
if (!nav?.clipboard?.writeText) return false;
|
|
57
|
+
try {
|
|
58
|
+
await nav.clipboard.writeText(text);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Put an RGBA crop on the clipboard as a PNG.
|
|
66
|
+
*
|
|
67
|
+
* Encoding needs a canvas, so this goes through one — but a THROWAWAY 2D
|
|
68
|
+
* canvas holding the crop, never the game's WebGL canvas, whose drawing buffer
|
|
69
|
+
* is already gone by the time anything outside a render can read it.
|
|
70
|
+
*/
|
|
71
|
+
async function writeClipboardImage(doc, image) {
|
|
72
|
+
const g = globalThis;
|
|
73
|
+
const write = g.navigator?.clipboard?.write;
|
|
74
|
+
if (!write || !g.ImageData || !g.ClipboardItem) return false;
|
|
75
|
+
try {
|
|
76
|
+
const canvas = doc.createElement("canvas");
|
|
77
|
+
canvas.width = image.width;
|
|
78
|
+
canvas.height = image.height;
|
|
79
|
+
const ctx = canvas.getContext("2d");
|
|
80
|
+
if (!ctx) return false;
|
|
81
|
+
ctx.putImageData(new g.ImageData(image.pixels, image.width, image.height), 0, 0);
|
|
82
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
|
|
83
|
+
if (!blob) return false;
|
|
84
|
+
await write.call(g.navigator?.clipboard, [new g.ClipboardItem({ "image/png": blob })]);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const HANDLE_IDS = [
|
|
91
|
+
"nw",
|
|
92
|
+
"n",
|
|
93
|
+
"ne",
|
|
94
|
+
"e",
|
|
95
|
+
"se",
|
|
96
|
+
"s",
|
|
97
|
+
"sw",
|
|
98
|
+
"w"
|
|
99
|
+
];
|
|
100
|
+
/** Where each handle sits on the rectangle, as fractions of its size. */
|
|
101
|
+
const HANDLE_ANCHOR = {
|
|
102
|
+
nw: [0, 0],
|
|
103
|
+
n: [.5, 0],
|
|
104
|
+
ne: [1, 0],
|
|
105
|
+
e: [1, .5],
|
|
106
|
+
se: [1, 1],
|
|
107
|
+
s: [.5, 1],
|
|
108
|
+
sw: [0, 1],
|
|
109
|
+
w: [0, .5]
|
|
110
|
+
};
|
|
111
|
+
/** The cursor is the affordance: it says which way an edge will move. */
|
|
112
|
+
const HANDLE_CURSOR = {
|
|
113
|
+
nw: "nwse-resize",
|
|
114
|
+
se: "nwse-resize",
|
|
115
|
+
ne: "nesw-resize",
|
|
116
|
+
sw: "nesw-resize",
|
|
117
|
+
n: "ns-resize",
|
|
118
|
+
s: "ns-resize",
|
|
119
|
+
e: "ew-resize",
|
|
120
|
+
w: "ew-resize"
|
|
121
|
+
};
|
|
122
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
123
|
+
/** A drag in any direction, as a top-left rectangle. */
|
|
124
|
+
function normalizeDrag(drag) {
|
|
125
|
+
return {
|
|
126
|
+
x: Math.min(drag.x0, drag.x1),
|
|
127
|
+
y: Math.min(drag.y0, drag.y1),
|
|
128
|
+
w: Math.abs(drag.x1 - drag.x0),
|
|
129
|
+
h: Math.abs(drag.y1 - drag.y0)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Drag one handle by `(dx, dy)`. Only the edges that handle owns move — `n`
|
|
134
|
+
* leaves x and width exactly alone, which is the whole reason edge handles
|
|
135
|
+
* exist next to corner ones.
|
|
136
|
+
*
|
|
137
|
+
* Dragging an edge past its opposite FLIPS the rectangle rather than refusing.
|
|
138
|
+
* Refusing leaves the pointer detached from the thing it is holding, and the
|
|
139
|
+
* user has to work out which invisible rule stopped them.
|
|
140
|
+
*/
|
|
141
|
+
function resizeRect(rect, id, dx, dy, viewW, viewH) {
|
|
142
|
+
let left = rect.x;
|
|
143
|
+
let top = rect.y;
|
|
144
|
+
let right = rect.x + rect.w;
|
|
145
|
+
let bottom = rect.y + rect.h;
|
|
146
|
+
if (id.includes("w")) left = clamp(left + dx, 0, viewW);
|
|
147
|
+
if (id.includes("e")) right = clamp(right + dx, 0, viewW);
|
|
148
|
+
if (id.includes("n")) top = clamp(top + dy, 0, viewH);
|
|
149
|
+
if (id.includes("s")) bottom = clamp(bottom + dy, 0, viewH);
|
|
150
|
+
return {
|
|
151
|
+
x: Math.min(left, right),
|
|
152
|
+
y: Math.min(top, bottom),
|
|
153
|
+
w: Math.max(1, Math.abs(right - left)),
|
|
154
|
+
h: Math.max(1, Math.abs(bottom - top))
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Slide the whole rectangle. It stops at the edge of the view rather than
|
|
159
|
+
* shrinking against it: a move that silently resizes is a move you have to undo.
|
|
160
|
+
*/
|
|
161
|
+
function moveRect(rect, dx, dy, viewW, viewH) {
|
|
162
|
+
return {
|
|
163
|
+
...rect,
|
|
164
|
+
x: clamp(rect.x + dx, 0, Math.max(0, viewW - rect.w)),
|
|
165
|
+
y: clamp(rect.y + dy, 0, Math.max(0, viewH - rect.h))
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Where to float a small panel next to the selection: under it by preference,
|
|
170
|
+
* over it when there is no room below, always fully on screen. It must never
|
|
171
|
+
* cover the selection — the whole point is that you can still see what you
|
|
172
|
+
* chose while you decide.
|
|
173
|
+
*/
|
|
174
|
+
function popupPosition(rect, popupW, popupH, viewW, viewH, gap = 8) {
|
|
175
|
+
const x = clamp(rect.x + rect.w / 2 - popupW / 2, gap, Math.max(gap, viewW - popupW - gap));
|
|
176
|
+
const below = rect.y + rect.h + gap;
|
|
177
|
+
const above = rect.y - popupH - gap;
|
|
178
|
+
return {
|
|
179
|
+
x,
|
|
180
|
+
y: below + popupH <= viewH - gap ? below : above >= gap ? above : clamp(below, gap, Math.max(gap, viewH - popupH - gap))
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
4
184
|
//#region src/debug/panel.ts
|
|
5
185
|
/** Keep a panel rect on screen and above the minimum usable size. */
|
|
6
186
|
function clampPanelRect(x, y, w, h, viewW, viewH, minW = 180, minH = 120) {
|
|
@@ -16,7 +196,7 @@ function clampPanelRect(x, y, w, h, viewW, viewH, minW = 180, minH = 120) {
|
|
|
16
196
|
function applyStyle(el, rules) {
|
|
17
197
|
for (const [key, value] of Object.entries(rules)) el.style[key] = value;
|
|
18
198
|
}
|
|
19
|
-
const PANEL_BG = "rgba(18, 20, 26, 0.92)";
|
|
199
|
+
const PANEL_BG$1 = "rgba(18, 20, 26, 0.92)";
|
|
20
200
|
const PANEL_BORDER = "1px solid rgba(255,255,255,0.14)";
|
|
21
201
|
const FONT = "12px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
22
202
|
/**
|
|
@@ -42,7 +222,7 @@ var FloatingPanel = class {
|
|
|
42
222
|
this.el = doc.createElement("div");
|
|
43
223
|
applyStyle(this.el, {
|
|
44
224
|
position: "absolute",
|
|
45
|
-
background: PANEL_BG,
|
|
225
|
+
background: PANEL_BG$1,
|
|
46
226
|
border: PANEL_BORDER,
|
|
47
227
|
borderRadius: "8px",
|
|
48
228
|
color: "rgba(255,255,255,0.88)",
|
|
@@ -152,6 +332,289 @@ var FloatingPanel = class {
|
|
|
152
332
|
}
|
|
153
333
|
};
|
|
154
334
|
//#endregion
|
|
335
|
+
//#region src/debug/region-select.ts
|
|
336
|
+
/**
|
|
337
|
+
* Choosing a rectangle on screen, and being allowed to change your mind.
|
|
338
|
+
*
|
|
339
|
+
* A drag is a first guess. The rectangle you actually want is a few pixels off,
|
|
340
|
+
* every time, and a tool that makes you re-drag from scratch to fix one edge is
|
|
341
|
+
* a tool people stop reaching for. So the selection stays live after the
|
|
342
|
+
* pointer comes up: eight handles to pull, a middle to grab, a running size, and
|
|
343
|
+
* an explicit Copy — nothing is sent anywhere until you say so.
|
|
344
|
+
*
|
|
345
|
+
* The DOM half of `capture.ts`, whose geometry it drives. Everything decidable
|
|
346
|
+
* without a browser lives there and is tested; what is left here is event
|
|
347
|
+
* plumbing and CSS.
|
|
348
|
+
*/
|
|
349
|
+
/** Below this, a drag was a click — not a selection anybody meant to make. */
|
|
350
|
+
const MIN_DRAG = 4;
|
|
351
|
+
const ACCENT = "#6ee7dc";
|
|
352
|
+
const PANEL_BG = "rgba(18,20,26,0.94)";
|
|
353
|
+
const MONO = "12px ui-monospace, Menlo, monospace";
|
|
354
|
+
/** Open the selection layer. Returns the handle that closes it. */
|
|
355
|
+
function openRegionSelect(opts) {
|
|
356
|
+
const { doc, container } = opts;
|
|
357
|
+
const layer = doc.createElement("div");
|
|
358
|
+
applyStyle(layer, {
|
|
359
|
+
position: "absolute",
|
|
360
|
+
inset: "0",
|
|
361
|
+
cursor: "crosshair",
|
|
362
|
+
zIndex: "80",
|
|
363
|
+
pointerEvents: "auto",
|
|
364
|
+
userSelect: "none",
|
|
365
|
+
touchAction: "none"
|
|
366
|
+
});
|
|
367
|
+
const veil = doc.createElement("div");
|
|
368
|
+
applyStyle(veil, {
|
|
369
|
+
position: "absolute",
|
|
370
|
+
inset: "0",
|
|
371
|
+
background: "rgba(10,14,20,0.35)",
|
|
372
|
+
pointerEvents: "none"
|
|
373
|
+
});
|
|
374
|
+
layer.appendChild(veil);
|
|
375
|
+
const box = doc.createElement("div");
|
|
376
|
+
applyStyle(box, {
|
|
377
|
+
position: "absolute",
|
|
378
|
+
outline: `1px solid ${ACCENT}`,
|
|
379
|
+
boxShadow: "0 0 0 9999px rgba(10,14,20,0.35)",
|
|
380
|
+
cursor: "grab",
|
|
381
|
+
display: "none",
|
|
382
|
+
pointerEvents: "auto"
|
|
383
|
+
});
|
|
384
|
+
layer.appendChild(box);
|
|
385
|
+
const size = doc.createElement("div");
|
|
386
|
+
applyStyle(size, {
|
|
387
|
+
position: "absolute",
|
|
388
|
+
padding: "2px 6px",
|
|
389
|
+
borderRadius: "4px",
|
|
390
|
+
background: PANEL_BG,
|
|
391
|
+
color: ACCENT,
|
|
392
|
+
font: MONO,
|
|
393
|
+
pointerEvents: "none",
|
|
394
|
+
display: "none",
|
|
395
|
+
whiteSpace: "nowrap"
|
|
396
|
+
});
|
|
397
|
+
layer.appendChild(size);
|
|
398
|
+
const hint = doc.createElement("div");
|
|
399
|
+
hint.textContent = "drag to select · Esc to cancel";
|
|
400
|
+
applyStyle(hint, {
|
|
401
|
+
position: "absolute",
|
|
402
|
+
top: "10px",
|
|
403
|
+
left: "50%",
|
|
404
|
+
transform: "translateX(-50%)",
|
|
405
|
+
padding: "4px 10px",
|
|
406
|
+
borderRadius: "6px",
|
|
407
|
+
background: PANEL_BG,
|
|
408
|
+
color: "rgba(255,255,255,0.85)",
|
|
409
|
+
font: MONO,
|
|
410
|
+
pointerEvents: "none",
|
|
411
|
+
whiteSpace: "nowrap"
|
|
412
|
+
});
|
|
413
|
+
layer.appendChild(hint);
|
|
414
|
+
let sel = null;
|
|
415
|
+
let mode = "idle";
|
|
416
|
+
let origin = {
|
|
417
|
+
x: 0,
|
|
418
|
+
y: 0
|
|
419
|
+
};
|
|
420
|
+
let startRect = {
|
|
421
|
+
x: 0,
|
|
422
|
+
y: 0,
|
|
423
|
+
w: 0,
|
|
424
|
+
h: 0
|
|
425
|
+
};
|
|
426
|
+
const view = () => ({
|
|
427
|
+
w: container.clientWidth || 1,
|
|
428
|
+
h: container.clientHeight || 1
|
|
429
|
+
});
|
|
430
|
+
for (const id of HANDLE_IDS) {
|
|
431
|
+
const dot = doc.createElement("div");
|
|
432
|
+
const [fx, fy] = HANDLE_ANCHOR[id];
|
|
433
|
+
applyStyle(dot, {
|
|
434
|
+
position: "absolute",
|
|
435
|
+
left: `${fx * 100}%`,
|
|
436
|
+
top: `${fy * 100}%`,
|
|
437
|
+
width: "10px",
|
|
438
|
+
height: "10px",
|
|
439
|
+
marginLeft: "-5px",
|
|
440
|
+
marginTop: "-5px",
|
|
441
|
+
borderRadius: "2px",
|
|
442
|
+
background: ACCENT,
|
|
443
|
+
border: "1px solid rgba(10,14,20,0.75)",
|
|
444
|
+
cursor: HANDLE_CURSOR[id],
|
|
445
|
+
pointerEvents: "auto"
|
|
446
|
+
});
|
|
447
|
+
dot.addEventListener("pointerdown", (event) => {
|
|
448
|
+
begin(event, id);
|
|
449
|
+
});
|
|
450
|
+
box.appendChild(dot);
|
|
451
|
+
}
|
|
452
|
+
const popup = doc.createElement("div");
|
|
453
|
+
applyStyle(popup, {
|
|
454
|
+
position: "absolute",
|
|
455
|
+
display: "none",
|
|
456
|
+
gap: "6px",
|
|
457
|
+
padding: "6px",
|
|
458
|
+
borderRadius: "8px",
|
|
459
|
+
background: PANEL_BG,
|
|
460
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
461
|
+
font: MONO,
|
|
462
|
+
pointerEvents: "auto",
|
|
463
|
+
whiteSpace: "nowrap",
|
|
464
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.45)"
|
|
465
|
+
});
|
|
466
|
+
const button = (label, primary, onClick) => {
|
|
467
|
+
const el = doc.createElement("div");
|
|
468
|
+
el.textContent = label;
|
|
469
|
+
applyStyle(el, {
|
|
470
|
+
padding: "5px 10px",
|
|
471
|
+
borderRadius: "5px",
|
|
472
|
+
cursor: "pointer",
|
|
473
|
+
color: primary ? "#08121a" : "rgba(255,255,255,0.85)",
|
|
474
|
+
background: primary ? ACCENT : "rgba(255,255,255,0.08)",
|
|
475
|
+
userSelect: "none"
|
|
476
|
+
});
|
|
477
|
+
el.addEventListener("pointerdown", (event) => {
|
|
478
|
+
stop(event);
|
|
479
|
+
onClick();
|
|
480
|
+
});
|
|
481
|
+
return el;
|
|
482
|
+
};
|
|
483
|
+
popup.appendChild(button("Copy to clipboard", true, () => confirm()));
|
|
484
|
+
popup.appendChild(button("Cancel", false, () => close()));
|
|
485
|
+
layer.appendChild(popup);
|
|
486
|
+
function stop(event) {
|
|
487
|
+
event.preventDefault?.();
|
|
488
|
+
event.stopPropagation?.();
|
|
489
|
+
}
|
|
490
|
+
function pointAt(event) {
|
|
491
|
+
const e = event;
|
|
492
|
+
const rect = container.getBoundingClientRect?.() ?? {
|
|
493
|
+
left: 0,
|
|
494
|
+
top: 0
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
x: (e.clientX ?? 0) - rect.left,
|
|
498
|
+
y: (e.clientY ?? 0) - rect.top
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function begin(event, next) {
|
|
502
|
+
stop(event);
|
|
503
|
+
origin = pointAt(event);
|
|
504
|
+
mode = next;
|
|
505
|
+
if (next === "create") sel = {
|
|
506
|
+
x: origin.x,
|
|
507
|
+
y: origin.y,
|
|
508
|
+
w: 0,
|
|
509
|
+
h: 0
|
|
510
|
+
};
|
|
511
|
+
else if (sel) startRect = sel;
|
|
512
|
+
applyStyle(popup, { display: "none" });
|
|
513
|
+
applyStyle(box, { cursor: next === "move" ? "grabbing" : "crosshair" });
|
|
514
|
+
const id = event.pointerId;
|
|
515
|
+
if (id !== void 0) layer.setPointerCapture?.(id);
|
|
516
|
+
render();
|
|
517
|
+
}
|
|
518
|
+
function render() {
|
|
519
|
+
if (!sel) {
|
|
520
|
+
applyStyle(box, { display: "none" });
|
|
521
|
+
applyStyle(size, { display: "none" });
|
|
522
|
+
applyStyle(veil, { display: "block" });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
applyStyle(veil, { display: "none" });
|
|
526
|
+
applyStyle(box, {
|
|
527
|
+
display: "block",
|
|
528
|
+
left: `${sel.x}px`,
|
|
529
|
+
top: `${sel.y}px`,
|
|
530
|
+
width: `${sel.w}px`,
|
|
531
|
+
height: `${sel.h}px`
|
|
532
|
+
});
|
|
533
|
+
const k = opts.scale();
|
|
534
|
+
size.textContent = `${Math.round(sel.w * k)}×${Math.round(sel.h * k)}`;
|
|
535
|
+
const above = sel.y - 24;
|
|
536
|
+
applyStyle(size, {
|
|
537
|
+
display: "block",
|
|
538
|
+
left: `${Math.max(2, sel.x)}px`,
|
|
539
|
+
top: `${above >= 2 ? above : sel.y + 4}px`
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function showPopup() {
|
|
543
|
+
if (!sel || sel.w < MIN_DRAG || sel.h < MIN_DRAG) return;
|
|
544
|
+
applyStyle(popup, { display: "flex" });
|
|
545
|
+
const v = view();
|
|
546
|
+
const w = popup.offsetWidth || 200;
|
|
547
|
+
const h = popup.offsetHeight || 36;
|
|
548
|
+
const p = popupPosition(sel, w, h, v.w, v.h);
|
|
549
|
+
applyStyle(popup, {
|
|
550
|
+
left: `${p.x}px`,
|
|
551
|
+
top: `${p.y}px`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
layer.addEventListener("pointerdown", (event) => {
|
|
555
|
+
begin(event, "create");
|
|
556
|
+
});
|
|
557
|
+
box.addEventListener("pointerdown", (event) => {
|
|
558
|
+
begin(event, "move");
|
|
559
|
+
});
|
|
560
|
+
layer.addEventListener("pointermove", (event) => {
|
|
561
|
+
if (mode === "idle" || !sel) return;
|
|
562
|
+
const p = pointAt(event);
|
|
563
|
+
const v = view();
|
|
564
|
+
if (mode === "create") {
|
|
565
|
+
const r = normalizeDrag({
|
|
566
|
+
x0: origin.x,
|
|
567
|
+
y0: origin.y,
|
|
568
|
+
x1: p.x,
|
|
569
|
+
y1: p.y
|
|
570
|
+
});
|
|
571
|
+
sel = {
|
|
572
|
+
x: Math.max(0, Math.min(r.x, v.w)),
|
|
573
|
+
y: Math.max(0, Math.min(r.y, v.h)),
|
|
574
|
+
w: Math.min(r.w, v.w - Math.max(0, Math.min(r.x, v.w))),
|
|
575
|
+
h: Math.min(r.h, v.h - Math.max(0, Math.min(r.y, v.h)))
|
|
576
|
+
};
|
|
577
|
+
} else if (mode === "move") sel = moveRect(startRect, p.x - origin.x, p.y - origin.y, v.w, v.h);
|
|
578
|
+
else sel = resizeRect(startRect, mode, p.x - origin.x, p.y - origin.y, v.w, v.h);
|
|
579
|
+
render();
|
|
580
|
+
});
|
|
581
|
+
const end = () => {
|
|
582
|
+
if (mode === "idle") return;
|
|
583
|
+
if (mode === "create" && sel && (sel.w < MIN_DRAG || sel.h < MIN_DRAG)) sel = null;
|
|
584
|
+
mode = "idle";
|
|
585
|
+
applyStyle(box, { cursor: "grab" });
|
|
586
|
+
hint.textContent = sel ? "drag the handles or the middle · Enter to copy · Esc to cancel" : "drag to select · Esc to cancel";
|
|
587
|
+
render();
|
|
588
|
+
showPopup();
|
|
589
|
+
};
|
|
590
|
+
layer.addEventListener("pointerup", end);
|
|
591
|
+
layer.addEventListener("pointercancel", end);
|
|
592
|
+
function confirm() {
|
|
593
|
+
const rect = sel;
|
|
594
|
+
close();
|
|
595
|
+
if (rect) opts.onCopy(rect);
|
|
596
|
+
}
|
|
597
|
+
const onKey = (event) => {
|
|
598
|
+
const key = event.key;
|
|
599
|
+
if (key === "Escape") close();
|
|
600
|
+
else if (key === "Enter" && sel) confirm();
|
|
601
|
+
};
|
|
602
|
+
doc.addEventListener?.("keydown", onKey);
|
|
603
|
+
let closed = false;
|
|
604
|
+
function close() {
|
|
605
|
+
if (closed) return;
|
|
606
|
+
closed = true;
|
|
607
|
+
doc.removeEventListener?.("keydown", onKey);
|
|
608
|
+
layer.remove();
|
|
609
|
+
opts.onClose();
|
|
610
|
+
}
|
|
611
|
+
container.appendChild(layer);
|
|
612
|
+
return {
|
|
613
|
+
element: layer,
|
|
614
|
+
close
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
155
618
|
//#region src/debug/index.ts
|
|
156
619
|
/**
|
|
157
620
|
* incanto/debug — the runtime debug overlay: play the game WITH X-ray vision.
|
|
@@ -201,7 +664,7 @@ function attachDebugOverlay(engine, opts = {}) {
|
|
|
201
664
|
if (!doc) return null;
|
|
202
665
|
const container = opts.container ?? (typeof document !== "undefined" ? document.body : null);
|
|
203
666
|
if (!container || typeof container.appendChild !== "function") return null;
|
|
204
|
-
return new DebugOverlay(engine, container, doc, opts.statsSource, opts.actions);
|
|
667
|
+
return new DebugOverlay(engine, container, doc, opts.statsSource, opts.actions, opts.frameSource);
|
|
205
668
|
}
|
|
206
669
|
const MAX_LOG_ROWS = 300;
|
|
207
670
|
const CONSOLE_LEVELS = [
|
|
@@ -217,6 +680,7 @@ var DebugOverlay = class {
|
|
|
217
680
|
doc;
|
|
218
681
|
statsSource;
|
|
219
682
|
actions;
|
|
683
|
+
frameSource;
|
|
220
684
|
panels = /* @__PURE__ */ new Map();
|
|
221
685
|
cleanups = [];
|
|
222
686
|
menuButton;
|
|
@@ -249,12 +713,13 @@ var DebugOverlay = class {
|
|
|
249
713
|
timeEls = null;
|
|
250
714
|
/** Pointer inside the inspector — it stops refreshing under your hand. */
|
|
251
715
|
hovering = false;
|
|
252
|
-
constructor(engine, container, doc, statsSource, actions = []) {
|
|
716
|
+
constructor(engine, container, doc, statsSource, actions = [], frameSource) {
|
|
253
717
|
this.engine = engine;
|
|
254
718
|
this.container = container;
|
|
255
719
|
this.doc = doc;
|
|
256
720
|
this.statsSource = statsSource;
|
|
257
721
|
this.actions = actions;
|
|
722
|
+
this.frameSource = frameSource;
|
|
258
723
|
this.menuButton = doc.createElement("div");
|
|
259
724
|
this.menuButton.textContent = "☰ debug";
|
|
260
725
|
applyStyle(this.menuButton, {
|
|
@@ -298,6 +763,7 @@ var DebugOverlay = class {
|
|
|
298
763
|
}));
|
|
299
764
|
}
|
|
300
765
|
isOpen(id) {
|
|
766
|
+
if (id === "copyScene" || id === "captureRegion") return false;
|
|
301
767
|
if (id === "colliders") return this.colliderMode !== "off";
|
|
302
768
|
return id === "stats" ? this.statsChip !== null : this.panels.has(id);
|
|
303
769
|
}
|
|
@@ -393,6 +859,14 @@ var DebugOverlay = class {
|
|
|
393
859
|
this.panels.delete(id);
|
|
394
860
|
}
|
|
395
861
|
toggle(id) {
|
|
862
|
+
if (id === "copyScene") {
|
|
863
|
+
this.copyScene();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (id === "captureRegion") {
|
|
867
|
+
this.captureRegion();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
396
870
|
if (id === "colliders") {
|
|
397
871
|
this.setColliders({
|
|
398
872
|
off: "all",
|
|
@@ -481,6 +955,8 @@ var DebugOverlay = class {
|
|
|
481
955
|
["inspector", "Inspector"],
|
|
482
956
|
["logs", "Logs"],
|
|
483
957
|
["time", "Time"],
|
|
958
|
+
["copyScene", "Copy scene JSON"],
|
|
959
|
+
["captureRegion", "Capture region"],
|
|
484
960
|
["stats", "Stats"],
|
|
485
961
|
["colliders", "Colliders"]
|
|
486
962
|
]) {
|
|
@@ -878,6 +1354,88 @@ var DebugOverlay = class {
|
|
|
878
1354
|
this.renderLogs();
|
|
879
1355
|
}
|
|
880
1356
|
/**
|
|
1357
|
+
* The scene, on the clipboard, ready to paste into a conversation.
|
|
1358
|
+
*
|
|
1359
|
+
* This is half of "here is what I am looking at" — the half a screenshot
|
|
1360
|
+
* cannot carry. The other half is the region capture below.
|
|
1361
|
+
*/
|
|
1362
|
+
async copyScene() {
|
|
1363
|
+
const ok = await writeClipboardText(sceneStateText(this.engine.scene ?? null));
|
|
1364
|
+
this.toast(ok ? "scene JSON copied" : "could not reach the clipboard");
|
|
1365
|
+
return ok;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Drag a rectangle over the game; the selection copies as an image.
|
|
1369
|
+
*
|
|
1370
|
+
* The pixels come from `frameSource` — the renderer's own end-of-frame read —
|
|
1371
|
+
* and never from `canvas.toDataURL()`, which returns black once the frame has
|
|
1372
|
+
* composited unless `preserveDrawingBuffer` is on (it is not, because it costs
|
|
1373
|
+
* bandwidth on every frame).
|
|
1374
|
+
*/
|
|
1375
|
+
captureRegion() {
|
|
1376
|
+
if (this.capture) return;
|
|
1377
|
+
this.captureScale = globalThis.devicePixelRatio ?? 1;
|
|
1378
|
+
this.frameSource?.().then((shot) => {
|
|
1379
|
+
this.captureScale = shot.width / Math.max(1, this.container.clientWidth || shot.width);
|
|
1380
|
+
}).catch(() => {});
|
|
1381
|
+
this.capture = openRegionSelect({
|
|
1382
|
+
doc: this.doc,
|
|
1383
|
+
container: this.container,
|
|
1384
|
+
scale: () => this.captureScale,
|
|
1385
|
+
onCopy: (rect) => void this.copyRegion({
|
|
1386
|
+
x0: rect.x,
|
|
1387
|
+
y0: rect.y,
|
|
1388
|
+
x1: rect.x + rect.w,
|
|
1389
|
+
y1: rect.y + rect.h
|
|
1390
|
+
}),
|
|
1391
|
+
onClose: () => {
|
|
1392
|
+
this.capture = null;
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
capture = null;
|
|
1397
|
+
captureScale = 1;
|
|
1398
|
+
/** Crop the last frame to `drag` and put the image on the clipboard. */
|
|
1399
|
+
async copyRegion(drag) {
|
|
1400
|
+
const source = this.frameSource;
|
|
1401
|
+
if (!source) {
|
|
1402
|
+
this.toast("no renderer to capture from");
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
const shot = await source();
|
|
1407
|
+
const scale = shot.width / Math.max(1, this.container.clientWidth || shot.width);
|
|
1408
|
+
const region = clampRegion(drag, shot.width, shot.height, scale);
|
|
1409
|
+
const cut = cropPixels(shot.pixels, shot.width, shot.height, region);
|
|
1410
|
+
const ok = await writeClipboardImage(this.doc, cut);
|
|
1411
|
+
this.toast(ok ? `copied ${cut.width}×${cut.height}` : "could not copy the image");
|
|
1412
|
+
return ok;
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
this.toast(`capture failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/** A line that says what happened and gets out of the way. */
|
|
1419
|
+
toast(text) {
|
|
1420
|
+
const el = this.doc.createElement("div");
|
|
1421
|
+
el.textContent = text;
|
|
1422
|
+
applyStyle(el, {
|
|
1423
|
+
position: "absolute",
|
|
1424
|
+
bottom: "16px",
|
|
1425
|
+
left: "50%",
|
|
1426
|
+
transform: "translateX(-50%)",
|
|
1427
|
+
padding: "6px 12px",
|
|
1428
|
+
borderRadius: "6px",
|
|
1429
|
+
background: "rgba(18,20,26,0.92)",
|
|
1430
|
+
color: "rgba(255,255,255,0.9)",
|
|
1431
|
+
font: "12px ui-monospace, Menlo, monospace",
|
|
1432
|
+
zIndex: "90",
|
|
1433
|
+
pointerEvents: "none"
|
|
1434
|
+
});
|
|
1435
|
+
this.container.appendChild(el);
|
|
1436
|
+
setTimeout(() => el.remove(), 1800);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
881
1439
|
* Set game time, defensively. `timeScale` multiplies every dt in the engine,
|
|
882
1440
|
* so a NaN from a text field would poison physics, timers and animation in
|
|
883
1441
|
* one frame, and a negative would run the simulation backwards through code
|
|
@@ -4,7 +4,7 @@ import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
|
4
4
|
import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
|
|
5
5
|
import { t as createNoise2D } from "./noise-CGUMx44x.js";
|
|
6
6
|
import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
|
|
7
|
-
import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-
|
|
7
|
+
import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BQOeAid6.js";
|
|
8
8
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
9
9
|
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
|
|
10
10
|
import { clone } from "three/addons/utils/SkeletonUtils.js";
|