incanto 0.46.0 → 0.48.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-check.mjs +5 -1
- package/bin/incanto-feel.mjs +5 -1
- package/bin/incanto-frame.mjs +115 -7
- package/bin/incanto-logs.mjs +127 -0
- package/bin/incanto-model.mjs +79 -10
- package/bin/incanto-playtest.mjs +5 -1
- package/bin/incanto-verify.mjs +245 -0
- package/dist/3d.d.ts +89 -4
- package/dist/3d.js +90 -3
- package/dist/{create-game-BtCNdkjI.js → create-game-akjPkFv5.js} +99 -3
- package/dist/{frame-report-Ct8XgsmV.d.ts → frame-report-DZ70IY26.d.ts} +25 -0
- package/dist/{frame-report-Lr3VO24R.js → frame-report-njybhZon.js} +130 -3
- package/dist/index.d.ts +101 -1
- package/dist/index.js +3 -2
- package/dist/log-report-lxrQY9cH.js +0 -0
- package/dist/react.js +1 -1
- package/dist/{src-B43b6bZo.js → src-Bl1Kire1.js} +1 -1
- package/dist/{test-BboGNQr-.js → test-E4-otKqK.js} +51 -4
- package/dist/test.d.ts +55 -1
- package/dist/test.js +2 -2
- package/dist/vite.d.ts +13 -2
- package/dist/vite.js +65 -10
- package/editor/assets/{agent8-Dl5ZZLu-.js → agent8-_007gPF8.js} +1 -1
- package/editor/assets/{debug-j9Pi6RvO.js → debug-0DI_MJaq.js} +1 -1
- package/editor/assets/{index-Y-93awAL.js → index-B-6eYZEi.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +4 -2
- package/skills/incanto-3d-models.md +30 -0
- package/skills/incanto-verifying-your-game.md +113 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* incanto-verify — the whole ladder, one command, one next action.
|
|
4
|
+
*
|
|
5
|
+
* bunx incanto-verify # finds your scene
|
|
6
|
+
* bunx incanto-verify src/game.scene.json --behaviors src/behaviors.ts
|
|
7
|
+
*
|
|
8
|
+
* The rungs already existed and were used one at a time, because using them
|
|
9
|
+
* together means knowing three things nobody writes down: the order, that the
|
|
10
|
+
* first red rung makes the ones above it meaningless, and that "no dev server"
|
|
11
|
+
* is a question unasked rather than a failing game.
|
|
12
|
+
*
|
|
13
|
+
* loads incanto-check the scene is legal and every asset resolves
|
|
14
|
+
* plays incanto-playtest a seeded run can actually finish it
|
|
15
|
+
* draws incanto-frame the GPU drew something, and the subject is in shot
|
|
16
|
+
* says incanto-logs what the running game is complaining about
|
|
17
|
+
*
|
|
18
|
+
* Exit 1 when a rung FAILED. An unmeasured rung is not a failure — it prints
|
|
19
|
+
* what to arrange, and says so in the summary.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const { ladderText, ladderVerdict } = await import(
|
|
28
|
+
pathToFileURL(join(PKG, 'dist', 'test.js')).href
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const argv = process.argv.slice(2);
|
|
32
|
+
const asJson = argv.includes('--json');
|
|
33
|
+
const flag = (name) => {
|
|
34
|
+
const i = argv.indexOf(name);
|
|
35
|
+
return i >= 0 ? argv[i + 1] : null;
|
|
36
|
+
};
|
|
37
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
38
|
+
console.log(`Usage: incanto-verify [scene.json] [--behaviors FILE] [--json]
|
|
39
|
+
|
|
40
|
+
Walks the whole ladder and tells you the ONE thing to do next:
|
|
41
|
+
|
|
42
|
+
loads the scene is legal and every asset resolves (incanto-check)
|
|
43
|
+
plays a seeded run can actually finish it (incanto-playtest)
|
|
44
|
+
draws the GPU drew something and the subject is in shot (incanto-frame)
|
|
45
|
+
says the running game logs no errors and lost no assets (incanto-logs)
|
|
46
|
+
|
|
47
|
+
Without a scene it looks for one *.scene.json under the current directory.
|
|
48
|
+
"draws" needs a dev server with the page open; without one it is reported as
|
|
49
|
+
NOT MEASURED rather than failed — a missing measurement is not a broken game.`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The scene to verify: the one named, or the only one that can be found. */
|
|
54
|
+
function findScene() {
|
|
55
|
+
const named = argv.find((a) => !a.startsWith('-') && a.endsWith('.json'));
|
|
56
|
+
if (named) return named;
|
|
57
|
+
const found = [];
|
|
58
|
+
const walk = (dir, depth) => {
|
|
59
|
+
if (depth > 4 || found.length > 8) return;
|
|
60
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
61
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
62
|
+
const full = join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
64
|
+
else if (entry.name.endsWith('.scene.json')) found.push(full);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
walk(process.cwd(), 0);
|
|
69
|
+
} catch {
|
|
70
|
+
/* unreadable tree — the caller can always name the file */
|
|
71
|
+
}
|
|
72
|
+
return { scene: found.length === 1 ? found[0] : null, candidates: found };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const bin = (name) => join(PKG, 'bin', `incanto-${name}.mjs`);
|
|
76
|
+
const run = (name, args) =>
|
|
77
|
+
spawnSync(process.execPath, [bin(name), ...args], {
|
|
78
|
+
encoding: 'utf-8',
|
|
79
|
+
timeout: 180_000,
|
|
80
|
+
// A playtest report carries a replay per losing run; the 1MB default
|
|
81
|
+
// truncates it and a truncated report is indistinguishable from a tool
|
|
82
|
+
// that would not start.
|
|
83
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { scene, candidates } = findScene();
|
|
87
|
+
if (!scene) {
|
|
88
|
+
const report = ladderVerdict([], { candidates });
|
|
89
|
+
console.log(asJson ? JSON.stringify(report, null, 2) : ladderText(report));
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
if (!existsSync(scene) || !statSync(scene).isFile()) {
|
|
93
|
+
console.error(`no such scene: ${scene}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const behaviors = flag('--behaviors');
|
|
98
|
+
const rungs = [];
|
|
99
|
+
|
|
100
|
+
// ---- loads ---------------------------------------------------------------
|
|
101
|
+
{
|
|
102
|
+
const r = run('check', [scene, '--json']);
|
|
103
|
+
const out = safeJson(r.stdout);
|
|
104
|
+
const problems = out?.files?.flatMap((f) => f.problems ?? []) ?? [];
|
|
105
|
+
rungs.push(
|
|
106
|
+
r.status === 0
|
|
107
|
+
? { name: 'loads', status: 'pass', summary: 'the scene is legal and its assets resolve' }
|
|
108
|
+
: {
|
|
109
|
+
name: 'loads',
|
|
110
|
+
status: 'fail',
|
|
111
|
+
summary: problems[0]?.why ?? firstLine(r.stdout || r.stderr) ?? 'incanto-check failed',
|
|
112
|
+
fix: `fix that, then run again — nothing above this rung means anything until it is green (\`incanto-check ${scene}\` for the detail)`,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- plays ---------------------------------------------------------------
|
|
118
|
+
if (rungs[0].status === 'pass') {
|
|
119
|
+
const args = [scene, '--json', '--runs', '8'];
|
|
120
|
+
if (behaviors) args.push('--behaviors', behaviors);
|
|
121
|
+
const r = run('playtest', args);
|
|
122
|
+
const out = safeJson(r.stdout);
|
|
123
|
+
const won = out?.runs?.filter((x) => x.outcome === 'won').length ?? 0;
|
|
124
|
+
const total = out?.runs?.length ?? 0;
|
|
125
|
+
// A scene that declares no win is not a scene that cannot be won: a
|
|
126
|
+
// walkabout template has no end, and calling that a failure sends its author
|
|
127
|
+
// hunting a bug that was never there.
|
|
128
|
+
const noGoal = out && out.declaresWin === false;
|
|
129
|
+
rungs.push(
|
|
130
|
+
r.status === 0
|
|
131
|
+
? { name: 'plays', status: 'pass', summary: `${won} of ${total} seeded runs finished it` }
|
|
132
|
+
: noGoal
|
|
133
|
+
? {
|
|
134
|
+
name: 'plays',
|
|
135
|
+
status: 'unmeasured',
|
|
136
|
+
summary: `nothing declares a win — ${total} runs played without error, and there was no end to reach`,
|
|
137
|
+
fix: 'if it is meant to be finishable, emit `won` (GameFlow, ScoreKeeper, or your own behaviour)',
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
name: 'plays',
|
|
141
|
+
status: 'fail',
|
|
142
|
+
summary:
|
|
143
|
+
total > 0 ? `no run finished it (${total} tried)` : 'the playtest could not run',
|
|
144
|
+
fix: behaviors
|
|
145
|
+
? `see which runs stalled and where: \`incanto-playtest ${scene} --behaviors ${behaviors}\``
|
|
146
|
+
: `run it with your behaviours — without them the structure plays and your game logic does not: \`incanto-playtest ${scene} --behaviors src/behaviors.ts\``,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
rungs.push({ name: 'plays', status: 'skipped', summary: 'not run — the scene does not load' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- draws ---------------------------------------------------------------
|
|
154
|
+
{
|
|
155
|
+
const r = run('frame', ['--json']);
|
|
156
|
+
const out = safeJson(r.stdout);
|
|
157
|
+
const report = out?.report;
|
|
158
|
+
if (!report) {
|
|
159
|
+
rungs.push({
|
|
160
|
+
name: 'draws',
|
|
161
|
+
status: 'unmeasured',
|
|
162
|
+
summary: firstLine(r.stderr) ?? 'no dev server with a page open',
|
|
163
|
+
fix: /not drawing/.test(r.stderr ?? '')
|
|
164
|
+
? 'bring the preview window to the front — a hidden tab does not render, and a frame is captured inside a render'
|
|
165
|
+
: 'start your dev server, open the page, and run this again — the pixels are in the browser',
|
|
166
|
+
});
|
|
167
|
+
} else if (report.black) {
|
|
168
|
+
rungs.push({
|
|
169
|
+
name: 'draws',
|
|
170
|
+
status: 'fail',
|
|
171
|
+
summary: 'BLACK SCREEN — nothing was drawn',
|
|
172
|
+
fix: 'no light, no current camera, or nothing in view: `incanto-frame --out shot.png` and look',
|
|
173
|
+
});
|
|
174
|
+
} else if (!report.subject) {
|
|
175
|
+
rungs.push({
|
|
176
|
+
name: 'draws',
|
|
177
|
+
status: 'fail',
|
|
178
|
+
summary: 'nothing but background — the camera is pointed away from the scene',
|
|
179
|
+
fix: 'check the camera: `incanto-frame --out shot.png` and look at what it is aimed at',
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
rungs.push({
|
|
183
|
+
name: 'draws',
|
|
184
|
+
status: 'pass',
|
|
185
|
+
summary: `subject fills ${(report.subject.coverage * 100).toFixed(1)}% of the frame`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The JSON in `text`, past whatever a dependency printed first. Rapier's
|
|
192
|
+
* deprecation notice lands on stdout ahead of the report, and treating that as
|
|
193
|
+
* "the tool could not run" is how this reported a healthy scene as broken.
|
|
194
|
+
*/
|
|
195
|
+
// ---- says ---------------------------------------------------------------
|
|
196
|
+
{
|
|
197
|
+
const r = run('logs', ['--json']);
|
|
198
|
+
const report = safeJson(r.stdout);
|
|
199
|
+
if (!report) {
|
|
200
|
+
rungs.push({
|
|
201
|
+
name: 'says',
|
|
202
|
+
status: 'unmeasured',
|
|
203
|
+
summary: firstLine(r.stderr) ?? 'no page to ask',
|
|
204
|
+
fix: 'open the preview page and run this again — the log buffer is in the browser',
|
|
205
|
+
});
|
|
206
|
+
} else if (report.ok) {
|
|
207
|
+
rungs.push({ name: 'says', status: 'pass', summary: 'no errors, no failed assets' });
|
|
208
|
+
} else {
|
|
209
|
+
const asset = report.assetErrors?.[0];
|
|
210
|
+
rungs.push({
|
|
211
|
+
name: 'says',
|
|
212
|
+
status: 'fail',
|
|
213
|
+
// A game whose grass and water are fine still fails here when its player
|
|
214
|
+
// never loaded — which is the case this rung exists for, and the one
|
|
215
|
+
// `draws` calls healthy.
|
|
216
|
+
summary: asset
|
|
217
|
+
? `asset ${asset.ref} never loaded: ${asset.error}`
|
|
218
|
+
: (report.worst?.message ?? `${report.stats?.errors ?? 0} errors were swallowed`),
|
|
219
|
+
fix: 'read it in full: `incanto-logs`',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function safeJson(text) {
|
|
225
|
+
const raw = text ?? '';
|
|
226
|
+
const start = raw.indexOf('{');
|
|
227
|
+
if (start < 0) return null;
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(raw.slice(start));
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function firstLine(text) {
|
|
235
|
+
const line = (text ?? '').split('\n').find((l) => l.trim());
|
|
236
|
+
return line ? line.trim() : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const verdict = ladderVerdict(rungs);
|
|
240
|
+
console.log(asJson ? JSON.stringify({ scene, ...verdict }, null, 2) : ladderText(verdict));
|
|
241
|
+
// `exitCode`, never `process.exit()`: stdout to a PIPE is written
|
|
242
|
+
// asynchronously, and exiting discards whatever has not flushed. A --json
|
|
243
|
+
// report read by another program came back truncated — silently, and only
|
|
244
|
+
// when piped, which is the only way a program reads it.
|
|
245
|
+
process.exitCode = verdict.ok ? 0 : 1;
|
package/dist/3d.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { D as QualityCaps, Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-62q0HWBO.js";
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
|
|
3
|
-
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
|
+
import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
4
|
import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
|
|
5
5
|
import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
|
|
6
|
-
import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-
|
|
6
|
+
import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DZ70IY26.js";
|
|
7
7
|
import { n as PathGrid, s as SpatialPose } from "./pathfinding-C49JSNNq.js";
|
|
8
8
|
import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
|
|
9
9
|
import { VRM } from "@pixiv/three-vrm";
|
|
@@ -633,7 +633,7 @@ interface Game3D {
|
|
|
633
633
|
* make "edit the scene" mean "edit wherever the simulation happens to be",
|
|
634
634
|
* so the source is kept beside the game and handed over unchanged.
|
|
635
635
|
*/
|
|
636
|
-
sourceJson: SceneJson;
|
|
636
|
+
sourceJson: SceneJson$1;
|
|
637
637
|
renderer: GameRenderer;
|
|
638
638
|
physics: Physics3D | null;
|
|
639
639
|
/**
|
|
@@ -931,6 +931,91 @@ declare class Environment3D {
|
|
|
931
931
|
*/
|
|
932
932
|
declare function setEnvironment3D(engine: Engine, patch: JsonObject): void;
|
|
933
933
|
//#endregion
|
|
934
|
+
//#region src/3d/model-verdict.d.ts
|
|
935
|
+
/**
|
|
936
|
+
* What a GLB will DO in an incanto scene, judged from what is in the file.
|
|
937
|
+
*
|
|
938
|
+
* Finding an asset is somebody else's job and they do it better: the agent8
|
|
939
|
+
* service has a vector search over the same library, and matching "a stocky
|
|
940
|
+
* dwarf blacksmith" to a URL is not something a filename filter should attempt.
|
|
941
|
+
* What no search can answer is whether the thing it found will work HERE —
|
|
942
|
+
*
|
|
943
|
+
* will the library's animation clips play on it? (bone NAMES decide, and a
|
|
944
|
+
* rig that does not match keeps its bind pose with only a console warning)
|
|
945
|
+
* how tall is it? (a model authored in centimetres dwarfs the scene)
|
|
946
|
+
* does it stand on its own origin? (`monster_trumble_mega` sits ten units
|
|
947
|
+
* above its root, so a node at [0,0,0] puts it in the sky — `targetHeight`
|
|
948
|
+
* normalises SIZE, not position, which cost a day of this repo's time)
|
|
949
|
+
*
|
|
950
|
+
* Every one of those is in the file, and every one of them was learned the
|
|
951
|
+
* expensive way. Pure arithmetic over an already-parsed report: no three, no
|
|
952
|
+
* network, no GPU.
|
|
953
|
+
*/
|
|
954
|
+
/** The parts of `incanto-model`'s report this judges. */
|
|
955
|
+
interface ModelFacts {
|
|
956
|
+
nodes: Array<{
|
|
957
|
+
name: string;
|
|
958
|
+
}>;
|
|
959
|
+
bbox: {
|
|
960
|
+
size: [number, number, number];
|
|
961
|
+
center: [number, number, number];
|
|
962
|
+
min: [number, number, number];
|
|
963
|
+
max: [number, number, number];
|
|
964
|
+
} | null;
|
|
965
|
+
animations: Array<{
|
|
966
|
+
name: string;
|
|
967
|
+
duration: number;
|
|
968
|
+
}>;
|
|
969
|
+
/** Meshes in the file. Zero means there is nothing to draw — see `kind`. */
|
|
970
|
+
meshes: number;
|
|
971
|
+
/** glTF skins. Zero means a prop, not a character. */
|
|
972
|
+
skins: number;
|
|
973
|
+
vrm: {
|
|
974
|
+
humanBones: number;
|
|
975
|
+
} | null;
|
|
976
|
+
}
|
|
977
|
+
type Rig = "mixamorig" | "vrm-humanoid" | "other" | "none";
|
|
978
|
+
interface SceneJson {
|
|
979
|
+
assets: Record<string, {
|
|
980
|
+
type: "model" | "animation";
|
|
981
|
+
url: string;
|
|
982
|
+
}>;
|
|
983
|
+
/** Null for a clip: it is played BY a model, it is not one. */
|
|
984
|
+
node: {
|
|
985
|
+
name: string;
|
|
986
|
+
type: "ModelInstance3D";
|
|
987
|
+
props: Record<string, unknown>;
|
|
988
|
+
} | null;
|
|
989
|
+
}
|
|
990
|
+
interface ModelVerdict {
|
|
991
|
+
/**
|
|
992
|
+
* What the file IS. A clip is a `.glb` with a rig and no mesh — declared as a
|
|
993
|
+
* model it renders nothing at all, with no error, and every other signal
|
|
994
|
+
* about it says "character".
|
|
995
|
+
*/
|
|
996
|
+
kind: "model" | "animation";
|
|
997
|
+
rig: Rig;
|
|
998
|
+
/** The `3d/animations` shelf plays on this as-is. */
|
|
999
|
+
takesLibraryClips: boolean;
|
|
1000
|
+
/** Height in the file's own units (0 when there is no geometry to measure). */
|
|
1001
|
+
height: number;
|
|
1002
|
+
/** Where the model sits relative to its own origin: bottom-centre, in units. */
|
|
1003
|
+
originOffset: [number, number, number];
|
|
1004
|
+
/** Standing on its own origin — a node at [0,0,0] puts its feet on the floor. */
|
|
1005
|
+
groundedAtOrigin: boolean;
|
|
1006
|
+
/** Clip names baked into the file itself (playable by name, no asset entry). */
|
|
1007
|
+
embeddedClips: string[];
|
|
1008
|
+
sceneJson: SceneJson | null;
|
|
1009
|
+
}
|
|
1010
|
+
interface VerdictTarget {
|
|
1011
|
+
url: string;
|
|
1012
|
+
/** Scene asset key. Also names the node. */
|
|
1013
|
+
key: string;
|
|
1014
|
+
}
|
|
1015
|
+
declare function modelVerdict(facts: ModelFacts, target?: VerdictTarget): ModelVerdict;
|
|
1016
|
+
/** The verdict as sentences: what to do, and what will bite if you do not. */
|
|
1017
|
+
declare function verdictText(v: ModelVerdict): string;
|
|
1018
|
+
//#endregion
|
|
934
1019
|
//#region src/3d/nodes/billboard-3d.d.ts
|
|
935
1020
|
/** Valid `Billboard3D.mode` values — drives the prop options AND validateJson. */
|
|
936
1021
|
declare const BILLBOARD_GROUP_MODES: readonly ["screen", "y", "none"];
|
|
@@ -3621,4 +3706,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
|
|
|
3621
3706
|
name?: string;
|
|
3622
3707
|
}): InstancedMesh3D;
|
|
3623
3708
|
//#endregion
|
|
3624
|
-
export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameDiff, type FrameSignature, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, SIGNATURE_GRID, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
|
|
3709
|
+
export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameDiff, type FrameSignature, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, type ModelFacts, ModelInstance3D, type ModelVerdict, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type Rig, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, SIGNATURE_GRID, type SceneJson, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
|
package/dist/3d.js
CHANGED
|
@@ -1,9 +1,96 @@
|
|
|
1
1
|
import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BQOeAid6.js";
|
|
2
2
|
import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-D1b0ydTS.js";
|
|
3
|
-
import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-
|
|
4
|
-
import { a as
|
|
3
|
+
import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-akjPkFv5.js";
|
|
4
|
+
import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.js";
|
|
5
5
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
6
6
|
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CSoGjM8P.js";
|
|
7
|
+
//#region src/3d/model-verdict.ts
|
|
8
|
+
/** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
|
|
9
|
+
const MIXAMO = /^mixamorig[:_]?/i;
|
|
10
|
+
/**
|
|
11
|
+
* Anything outside this is not a character at this scale, and the fix is one
|
|
12
|
+
* prop. Wide on purpose: a 2.4m ogre is a design choice, a 40m one is
|
|
13
|
+
* centimetres.
|
|
14
|
+
*/
|
|
15
|
+
const CHARACTER_HEIGHT = {
|
|
16
|
+
min: .4,
|
|
17
|
+
max: 4
|
|
18
|
+
};
|
|
19
|
+
/** What `targetHeight` gets set to when a model is plainly the wrong scale. */
|
|
20
|
+
const DEFAULT_TARGET_HEIGHT = 1.7;
|
|
21
|
+
/** Fraction of the model's own height that its origin may drift before it matters. */
|
|
22
|
+
const ORIGIN_TOLERANCE = .25;
|
|
23
|
+
function rigOf(facts) {
|
|
24
|
+
if (facts.vrm && facts.vrm.humanBones > 0) return "vrm-humanoid";
|
|
25
|
+
if (facts.nodes.some((n) => MIXAMO.test(n.name))) return "mixamorig";
|
|
26
|
+
return facts.skins > 0 ? "other" : "none";
|
|
27
|
+
}
|
|
28
|
+
function modelVerdict(facts, target) {
|
|
29
|
+
const rig = rigOf(facts);
|
|
30
|
+
const kind = facts.meshes === 0 && facts.animations.length > 0 ? "animation" : "model";
|
|
31
|
+
const height = facts.bbox ? facts.bbox.size[1] : 0;
|
|
32
|
+
const offset = facts.bbox ? [
|
|
33
|
+
facts.bbox.center[0],
|
|
34
|
+
facts.bbox.min[1],
|
|
35
|
+
facts.bbox.center[2]
|
|
36
|
+
] : [
|
|
37
|
+
0,
|
|
38
|
+
0,
|
|
39
|
+
0
|
|
40
|
+
];
|
|
41
|
+
const slack = Math.max(height * ORIGIN_TOLERANCE, .05);
|
|
42
|
+
const grounded = !facts.bbox || Math.abs(offset[0]) <= slack && Math.abs(offset[1]) <= slack && Math.abs(offset[2]) <= slack;
|
|
43
|
+
const oddSize = height > 0 && (height < CHARACTER_HEIGHT.min || height > CHARACTER_HEIGHT.max);
|
|
44
|
+
const props = { model: `$${target?.key ?? "model"}` };
|
|
45
|
+
if (oddSize) props.targetHeight = DEFAULT_TARGET_HEIGHT;
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
rig,
|
|
49
|
+
takesLibraryClips: kind === "model" && (rig === "mixamorig" || rig === "vrm-humanoid"),
|
|
50
|
+
height,
|
|
51
|
+
originOffset: offset,
|
|
52
|
+
groundedAtOrigin: grounded,
|
|
53
|
+
embeddedClips: facts.animations.map((a) => a.name),
|
|
54
|
+
sceneJson: target ? {
|
|
55
|
+
assets: { [target.key]: {
|
|
56
|
+
type: kind,
|
|
57
|
+
url: target.url
|
|
58
|
+
} },
|
|
59
|
+
node: kind === "animation" ? null : {
|
|
60
|
+
name: titleCase(target.key),
|
|
61
|
+
type: "ModelInstance3D",
|
|
62
|
+
props
|
|
63
|
+
}
|
|
64
|
+
} : null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const titleCase = (key) => key.split(/[-_\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || "Model";
|
|
68
|
+
const round = (n) => String(Math.round(n * 100) / 100);
|
|
69
|
+
/** The verdict as sentences: what to do, and what will bite if you do not. */
|
|
70
|
+
function verdictText(v) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
if (v.kind === "animation") {
|
|
73
|
+
lines.push("an animation CLIP — no mesh of its own. It plays ON a character: declare it as {\"type\":\"animation\"} and point a ModelInstance3D's `animation` at it. Used as a `model` it draws nothing at all, with no error");
|
|
74
|
+
if (v.embeddedClips.length > 0) lines.push(`clip: ${v.embeddedClips.join(", ")}`);
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
if (v.rig === "mixamorig") lines.push("mixamorig rig — the 3d/animations clips play on this as they are");
|
|
78
|
+
else if (v.rig === "vrm-humanoid") lines.push("VRM humanoid — clips are retargeted onto it automatically");
|
|
79
|
+
else if (v.rig === "other") lines.push("skinned, but NOT a mixamorig rig — the 3d/animations clips will not bind to it (bones are matched by name; a mismatch keeps the bind pose and only warns)");
|
|
80
|
+
else lines.push("no skeleton — a prop, not a character; `animation` does nothing here");
|
|
81
|
+
if (v.height > 0) {
|
|
82
|
+
const odd = v.height < CHARACTER_HEIGHT.min || v.height > CHARACTER_HEIGHT.max;
|
|
83
|
+
lines.push(`stands ${round(v.height)} units tall` + (odd ? ` — not a character's size; set targetHeight (${DEFAULT_TARGET_HEIGHT}) to fix it` : ""));
|
|
84
|
+
}
|
|
85
|
+
if (!v.groundedAtOrigin) {
|
|
86
|
+
const [x, y, z] = v.originOffset;
|
|
87
|
+
const where = Math.abs(y) > Math.abs(x) && Math.abs(y) > Math.abs(z) ? `${round(Math.abs(y))} units ${y > 0 ? "above" : "below"} the origin` : `off-centre by [${round(x)}, ${round(y)}, ${round(z)}]`;
|
|
88
|
+
lines.push(`does NOT stand on its own origin — it sits ${where}. A node at [0,0,0] puts it there too; offset the node, or expect an empty-looking frame`);
|
|
89
|
+
}
|
|
90
|
+
if (v.embeddedClips.length > 0) lines.push(`clips in the file: ${v.embeddedClips.join(", ")} (play by name, no asset needed)`);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
7
94
|
//#region src/3d/terrain-nav.ts
|
|
8
95
|
function buildTerrainNav(terrain, opts) {
|
|
9
96
|
const cellSize = opts?.cellSize ?? 2;
|
|
@@ -115,4 +202,4 @@ function createNavDebugNode(nav, opts) {
|
|
|
115
202
|
return node;
|
|
116
203
|
}
|
|
117
204
|
//#endregion
|
|
118
|
-
export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, SIGNATURE_GRID, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
|
|
205
|
+
export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, SIGNATURE_GRID, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, modelVerdict, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath, verdictText };
|
|
@@ -2,12 +2,13 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
|
2
2
|
import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
|
|
3
3
|
import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-BSXV8T9F.js";
|
|
4
4
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
5
|
+
import { r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
|
|
5
6
|
import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
|
|
6
7
|
import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
|
|
7
8
|
import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
|
|
8
9
|
import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
|
|
9
10
|
import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-D1b0ydTS.js";
|
|
10
|
-
import { a as
|
|
11
|
+
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
|
|
11
12
|
import { n as enablePhysics3D } from "./physics-3d-CSoGjM8P.js";
|
|
12
13
|
import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
13
14
|
import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
|
|
@@ -1545,6 +1546,12 @@ function prune(obj, visited) {
|
|
|
1545
1546
|
//#endregion
|
|
1546
1547
|
//#region src/3d/renderer.ts
|
|
1547
1548
|
/**
|
|
1549
|
+
* How long a capture waits for the game's own loop before drawing its own
|
|
1550
|
+
* frame. Long enough that a running game answers from its normal loop (16ms),
|
|
1551
|
+
* short enough that a paused or occluded tab still answers promptly.
|
|
1552
|
+
*/
|
|
1553
|
+
const CAPTURE_NUDGE_MS = 120;
|
|
1554
|
+
/**
|
|
1548
1555
|
* WebGL presentation layer: subscribes to `engine.updated`, mirrors the active
|
|
1549
1556
|
* scene's node tree onto a three.js scene, applies the scene `environment`
|
|
1550
1557
|
* header (ambient/background/sky/fog/shadows/exposure — see Environment3D),
|
|
@@ -1890,6 +1897,14 @@ var Renderer3D = class {
|
|
|
1890
1897
|
resolve,
|
|
1891
1898
|
reject
|
|
1892
1899
|
});
|
|
1900
|
+
setTimeout(() => {
|
|
1901
|
+
if (this.pendingCaptures.length > 0) try {
|
|
1902
|
+
this.render();
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
1905
|
+
for (const w of this.pendingCaptures.splice(0)) w.reject(e);
|
|
1906
|
+
}
|
|
1907
|
+
}, CAPTURE_NUDGE_MS);
|
|
1893
1908
|
});
|
|
1894
1909
|
}
|
|
1895
1910
|
pendingCaptures = [];
|
|
@@ -2441,16 +2456,73 @@ async function createGame3D(opts) {
|
|
|
2441
2456
|
if (!capture) throw new IncantoError("TREE_VIOLATION", "this renderer cannot capture frames");
|
|
2442
2457
|
return capture.call(renderer);
|
|
2443
2458
|
};
|
|
2459
|
+
/**
|
|
2460
|
+
* Run a drive script against the LIVE game, in real time.
|
|
2461
|
+
*
|
|
2462
|
+
* `step` waits on the wall clock rather than advancing a fixed timestep: this
|
|
2463
|
+
* is the running game, with its own loop, its own physics and its own
|
|
2464
|
+
* animation. Stepping it by hand would fight all three.
|
|
2465
|
+
*/
|
|
2466
|
+
const applyDrive = async (steps) => {
|
|
2467
|
+
let frames = 0;
|
|
2468
|
+
let watching = true;
|
|
2469
|
+
const tick = () => {
|
|
2470
|
+
if (!watching) return;
|
|
2471
|
+
frames += 1;
|
|
2472
|
+
requestAnimationFrame(tick);
|
|
2473
|
+
};
|
|
2474
|
+
requestAnimationFrame(tick);
|
|
2475
|
+
for (const s of steps) switch (s.kind) {
|
|
2476
|
+
case "press":
|
|
2477
|
+
engine.input.pressAction(s.action);
|
|
2478
|
+
break;
|
|
2479
|
+
case "release":
|
|
2480
|
+
engine.input.releaseAction(s.action);
|
|
2481
|
+
break;
|
|
2482
|
+
case "vector":
|
|
2483
|
+
engine.input.setActionVector(s.action, s.x, s.y);
|
|
2484
|
+
break;
|
|
2485
|
+
case "key":
|
|
2486
|
+
engine.input.handleKey(s.code, s.down);
|
|
2487
|
+
break;
|
|
2488
|
+
case "pointer":
|
|
2489
|
+
engine.input.handlePointerMove(s.dx, s.dy);
|
|
2490
|
+
break;
|
|
2491
|
+
case "step":
|
|
2492
|
+
await new Promise((r) => setTimeout(r, s.ms));
|
|
2493
|
+
break;
|
|
2494
|
+
}
|
|
2495
|
+
watching = false;
|
|
2496
|
+
return frames;
|
|
2497
|
+
};
|
|
2444
2498
|
const hot = import.meta.hot;
|
|
2445
2499
|
if (hot) {
|
|
2446
2500
|
const onFrameRequest = (data) => {
|
|
2447
2501
|
const request = data;
|
|
2448
2502
|
const id = request?.id;
|
|
2449
|
-
|
|
2503
|
+
hot.send("incanto:frame-ack", { id });
|
|
2504
|
+
const drive = request?.drive ? parseDrive(request.drive) : null;
|
|
2505
|
+
if (drive?.error) {
|
|
2506
|
+
hot.send("incanto:frame-report", {
|
|
2507
|
+
id,
|
|
2508
|
+
report: { error: drive.error }
|
|
2509
|
+
});
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
let droveFrames = null;
|
|
2513
|
+
(drive ? applyDrive(drive.steps) : Promise.resolve(null)).then((frames) => {
|
|
2514
|
+
droveFrames = frames;
|
|
2515
|
+
return captureFramePixels();
|
|
2516
|
+
}).then((shot) => hot.send("incanto:frame-report", {
|
|
2450
2517
|
id,
|
|
2451
2518
|
report: {
|
|
2452
2519
|
...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
|
|
2453
|
-
signature: frameSignature(shot.pixels, shot.width, shot.height)
|
|
2520
|
+
signature: frameSignature(shot.pixels, shot.width, shot.height),
|
|
2521
|
+
...droveFrames === null ? {} : { droveFrames },
|
|
2522
|
+
...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
|
|
2523
|
+
width: w,
|
|
2524
|
+
height: h
|
|
2525
|
+
}), request.image) } : {}
|
|
2454
2526
|
}
|
|
2455
2527
|
})).catch((error) => hot.send("incanto:frame-report", {
|
|
2456
2528
|
id,
|
|
@@ -2459,6 +2531,30 @@ async function createGame3D(opts) {
|
|
|
2459
2531
|
};
|
|
2460
2532
|
hot.on("incanto:frame-request", onFrameRequest);
|
|
2461
2533
|
cleanups.push(() => hot.off?.("incanto:frame-request", onFrameRequest));
|
|
2534
|
+
const onLogsRequest = (data) => {
|
|
2535
|
+
const id = data?.id;
|
|
2536
|
+
try {
|
|
2537
|
+
hot.send("incanto:logs-report", {
|
|
2538
|
+
id,
|
|
2539
|
+
report: logReport({
|
|
2540
|
+
entries: engine.log.entries(),
|
|
2541
|
+
stats: {
|
|
2542
|
+
...engine.stats(),
|
|
2543
|
+
...renderer.stats?.() ?? {}
|
|
2544
|
+
},
|
|
2545
|
+
assetErrors: renderer.assets?.errors() ?? [],
|
|
2546
|
+
hidden: typeof document !== "undefined" && document.hidden
|
|
2547
|
+
})
|
|
2548
|
+
});
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
hot.send("incanto:logs-report", {
|
|
2551
|
+
id,
|
|
2552
|
+
report: { error: error instanceof Error ? error.message : String(error) }
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
hot.on("incanto:logs-request", onLogsRequest);
|
|
2557
|
+
cleanups.push(() => hot.off?.("incanto:logs-request", onLogsRequest));
|
|
2462
2558
|
}
|
|
2463
2559
|
cleanups.push(pauseWhenHidden(engine));
|
|
2464
2560
|
await report(1, "ready");
|
|
@@ -28,6 +28,31 @@ interface FrameStats {
|
|
|
28
28
|
meanLuma: number;
|
|
29
29
|
/** Coarse mean colour per cell, rows top to bottom. */
|
|
30
30
|
grid: GridCell[][];
|
|
31
|
+
/** What the frame is OF, or null when it is all background. */
|
|
32
|
+
subject: Subject | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The thing in the shot: everything that is not the background.
|
|
36
|
+
*
|
|
37
|
+
* `black` and `uniform` answer "did anything render at all". They do not answer
|
|
38
|
+
* the question that actually goes wrong — "is the thing IN the shot, and can
|
|
39
|
+
* you see it" — and every visual defect reported against the editor's model
|
|
40
|
+
* previews walked straight past them. A white square with a faint sky gradient
|
|
41
|
+
* is neither black nor uniform; a character drawn as a speck in the corner has
|
|
42
|
+
* a perfectly ordinary mean luminance.
|
|
43
|
+
*/
|
|
44
|
+
interface Subject {
|
|
45
|
+
/** Bounding box in pixels, origin top-left. */
|
|
46
|
+
box: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
w: number;
|
|
50
|
+
h: number;
|
|
51
|
+
};
|
|
52
|
+
/** Share of the frame's pixels that are subject, 0..1. */
|
|
53
|
+
coverage: number;
|
|
54
|
+
/** Touching an edge — the shot is a crop, and something is outside it. */
|
|
55
|
+
clipped: boolean;
|
|
31
56
|
}
|
|
32
57
|
interface FrameStatsOptions {
|
|
33
58
|
/** Cells across and down. Default 16x9 — 144 cells, printable. */
|