@bitmagic/cli 0.1.18 → 0.1.20

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.
@@ -0,0 +1,455 @@
1
+ /**
2
+ * The editor shell: the page that plays the Creator's role for a CLI-lane project.
3
+ *
4
+ * The whole feature rests on one fact — the visual editor is ALREADY in every scaffolded project.
5
+ * `VENDORED_DIRS` in `scaffold/project.ts` ships `engine/editor/` and `engine/debug/`, and
6
+ * `GameEngine` constructs `EditorManager` unconditionally, building the transform gizmo, the object
7
+ * inspector and the scene hierarchy into a hidden `#debug-container`. What the pro lane lacked was
8
+ * a parent frame speaking the Creator's `postMessage` protocol. This page is that frame, and
9
+ * nothing under `game/` changes to support it.
10
+ *
11
+ * Rendered from TypeScript rather than shipped as a `.html` asset because the package builds with
12
+ * plain `tsc`, which copies no static files. `smoke/harness.ts` and `scaffold/project-files.ts`
13
+ * both do the same.
14
+ *
15
+ * ── Four things that fail silently if changed ────────────────────────────────────────────────
16
+ *
17
+ * 1. `?source=creator` on the iframe URL. Without it `isCreatorMode` is false
18
+ * (`game/src/engine/CreatorMode.ts`), the engine never registers its message listener, and
19
+ * every message below is discarded with no error anywhere.
20
+ * 2. `GAME_TEMPLATE_READY` must arrive before `LOAD_GAME` is posted. The engine registers its
21
+ * listener only after `await initI18n()`, and a `LOAD_GAME` landing before that is DROPPED,
22
+ * not queued — the symptom is a game that never loads, pointing at the wrong culprit.
23
+ * 3. `REQUEST_ASSETS` / `ADD_OBJECT` / `MARK_OBJECT_MODIFIED` use a FLAT envelope
24
+ * (`{ type, assets }`), not the `{ type, data }` one the rest of the protocol uses. That is
25
+ * the Creator's existing shape (`useIframeMessages.ts:1149`, `:332`) and the engine reads the
26
+ * fields off the message directly.
27
+ * 4. The autosave poll, rather than an event. `TransformControlsManager`'s mouseUp reaches
28
+ * `EditorManager.commitTransformChange()`, which only mutates a `Set` — the engine posts
29
+ * nothing. Polling `CHECK_SCENE_CHANGES` is what the Creator does too, just on tab switch
30
+ * instead of on a timer, and it covers drags, deletes, adds and inspector edits with one path.
31
+ */
32
+ /** How often the shell asks the engine whether anything changed. A `Set` read; effectively free. */
33
+ const POLL_INTERVAL_MS = 500;
34
+ /** Attribute-safe. `gameId` comes from `bitmagic.json`, but it lands inside an HTML attribute. */
35
+ function escapeAttr(value) {
36
+ return value
37
+ .replace(/&/g, '&')
38
+ .replace(/"/g, '"')
39
+ .replace(/</g, '&lt;')
40
+ .replace(/>/g, '&gt;');
41
+ }
42
+ export function renderEditorShell(options) {
43
+ const gameUrl = `http://localhost:${options.gamePort}/?source=creator&gameId=${encodeURIComponent(options.gameId)}`;
44
+ return `<!doctype html>
45
+ <html lang="en">
46
+ <head>
47
+ <meta charset="utf-8">
48
+ <title>bitmagic edit — ${escapeAttr(options.gameId)}</title>
49
+ <style>
50
+ :root { color-scheme: dark; }
51
+ * { box-sizing: border-box; }
52
+ /* Flex column rather than calc() heights: the banners come and go, and two of them at once
53
+ must not push the viewport into a scrollbar. */
54
+ body { margin: 0; height: 100vh; display: flex; flex-direction: column;
55
+ background: #101014; color: #e6e6ea; font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; }
56
+ #bar { display: flex; align-items: center; gap: 12px; height: 36px; flex: none; padding: 0 12px;
57
+ background: #17171d; border-bottom: 1px solid #26262e; }
58
+ #bar .id { color: #8a8a99; font-family: ui-monospace, monospace; }
59
+ #bar .spacer { flex: 1; }
60
+ #status { display: flex; align-items: center; gap: 6px; }
61
+ #dot { width: 8px; height: 8px; border-radius: 50%; background: #4a4a57; }
62
+ #dot.saving { background: #d9a441; }
63
+ #dot.saved { background: #4caf72; }
64
+ #dot.error { background: #e0564f; }
65
+ button { background: #26262e; color: #e6e6ea; border: 1px solid #34343f; border-radius: 5px;
66
+ padding: 4px 10px; font: inherit; cursor: pointer; }
67
+ button:hover { background: #30303a; }
68
+ .banner { display: none; align-items: center; gap: 12px; flex: none; padding: 8px 12px;
69
+ background: #3a2c12; border-bottom: 1px solid #574018; color: #f0d9a8; }
70
+ .banner.show { display: flex; }
71
+ #terrain-banner { background: #3d1f1c; border-bottom-color: #6b2f28; color: #f3c3bd; }
72
+ #frame { display: block; flex: 1; width: 100%; border: 0; min-height: 0; }
73
+ #hq-overlay { display: none; position: fixed; inset: 0; background: rgba(8,8,12,.72);
74
+ align-items: center; justify-content: center; z-index: 10; }
75
+ #hq-overlay.show { display: flex; }
76
+ #hq-dialog { width: min(560px, 92vw); background: #17171d; border: 1px solid #34343f;
77
+ border-radius: 8px; padding: 18px; }
78
+ #hq-dialog h2 { margin: 0 0 6px; font-size: 15px; }
79
+ #hq-dialog p { margin: 0 0 12px; color: #9a9aab; }
80
+ #hq-prompt { width: 100%; min-height: 88px; resize: vertical; padding: 8px; border-radius: 6px;
81
+ background: #101014; color: #e6e6ea; border: 1px solid #34343f; font: inherit; }
82
+ #hq-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
83
+ #hq-confirm { background: #3352c4; border-color: #3f61d8; }
84
+ #hq-note { display: none; margin-top: 10px; padding: 10px; border-radius: 6px;
85
+ background: #101014; border: 1px solid #26262e; }
86
+ #hq-note.show { display: block; }
87
+ #hq-note code { display: block; margin-top: 6px; color: #9fd0a8; word-break: break-all; }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <div id="bar">
92
+ <strong>bitmagic edit</strong>
93
+ <span class="id">${escapeAttr(options.gameId)}</span>
94
+ <span class="spacer"></span>
95
+ <span id="status"><span id="dot"></span><span id="status-text">Loading…</span></span>
96
+ <button id="reload" type="button">Reload</button>
97
+ </div>
98
+ <div id="banner" class="banner">
99
+ <span>src/work/world.json changed on disk — this scene is out of date.</span>
100
+ <button id="banner-reload" type="button">Reload scene</button>
101
+ </div>
102
+ <div id="terrain-banner" class="banner">
103
+ <span><strong>Terrain edits are not saved yet.</strong>
104
+ Sculpting the ground needs an asset upload that <code>bitmagic edit</code> does not do — these
105
+ voxel changes will be lost. Discard them, or press Cancel in the terrain toolbar.</span>
106
+ <button id="terrain-discard" type="button">Discard terrain edits</button>
107
+ </div>
108
+ <iframe id="frame" allow="autoplay; fullscreen; xr-spatial-tracking; clipboard-write"></iframe>
109
+ <div id="hq-overlay">
110
+ <div id="hq-dialog">
111
+ <h2>Generate a high-quality <span id="hq-name"></span></h2>
112
+ <p>Describe what this object should be. The text is saved as the asset's description and
113
+ used as the generation prompt.</p>
114
+ <textarea id="hq-prompt" placeholder="e.g. a weathered stone archway covered in moss"></textarea>
115
+ <div id="hq-actions">
116
+ <button id="hq-cancel" type="button">Cancel</button>
117
+ <button id="hq-confirm" type="button">Hand to my agent</button>
118
+ </div>
119
+ <div id="hq-note">
120
+ <span id="hq-note-text"></span>
121
+ <code id="hq-command"></code>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ <script>
126
+ (function () {
127
+ 'use strict';
128
+ var GAME_URL = ${JSON.stringify(gameUrl)};
129
+ var POLL_MS = ${POLL_INTERVAL_MS};
130
+
131
+ var frame = document.getElementById('frame');
132
+ var dot = document.getElementById('dot');
133
+ var statusText = document.getElementById('status-text');
134
+ var banner = document.getElementById('banner');
135
+ var terrainBanner = document.getElementById('terrain-banner');
136
+
137
+ var loaded = false;
138
+ var saving = false;
139
+ var markersDirty = false;
140
+ var lastKnownMtime = 0;
141
+ var pollTimer = null;
142
+ var waiters = [];
143
+ // Bumped by reload(). Every async step checks it, so a reload triggered mid-boot cannot leave
144
+ // two polling loops running against one iframe.
145
+ var generation = 0;
146
+
147
+ function setStatus(kind, text) {
148
+ dot.className = kind || '';
149
+ statusText.textContent = text;
150
+ }
151
+
152
+ function post(type, data) {
153
+ if (frame.contentWindow) frame.contentWindow.postMessage({ type: type, data: data }, '*');
154
+ }
155
+
156
+ // The flat envelope — see note 3 in this file's header.
157
+ function postFlat(message) {
158
+ if (frame.contentWindow) frame.contentWindow.postMessage(message, '*');
159
+ }
160
+
161
+ /** Resolve on the next message of this type, or null after timeoutMs. Never rejects. */
162
+ function await_(type, timeoutMs) {
163
+ return new Promise(function (resolve) {
164
+ var waiter = { type: type, resolve: resolve, timer: null };
165
+ waiter.timer = setTimeout(function () {
166
+ var i = waiters.indexOf(waiter);
167
+ if (i !== -1) waiters.splice(i, 1);
168
+ resolve(null);
169
+ }, timeoutMs);
170
+ waiters.push(waiter);
171
+ });
172
+ }
173
+
174
+ window.addEventListener('message', function (event) {
175
+ var message = event.data;
176
+ if (!message || typeof message.type !== 'string') return;
177
+
178
+ for (var i = waiters.length - 1; i >= 0; i--) {
179
+ if (waiters[i].type === message.type) {
180
+ clearTimeout(waiters[i].timer);
181
+ waiters[i].resolve(message);
182
+ waiters.splice(i, 1);
183
+ }
184
+ }
185
+
186
+ switch (message.type) {
187
+ case 'REQUEST_ASSETS':
188
+ // Re-read rather than serving the copy captured at boot: an agent may have generated an
189
+ // asset since this page loaded, and a stale list is an empty asset palette with no
190
+ // explanation.
191
+ fetch('/api/game-data').then(function (r) { return r.json(); }).then(function (payload) {
192
+ var data = payload && payload.gameData;
193
+ postFlat({ type: 'ASSETS_RESPONSE', assets: (data && data.assets) || [] });
194
+ }).catch(function () {
195
+ postFlat({ type: 'ASSETS_RESPONSE', assets: [] });
196
+ });
197
+ break;
198
+
199
+ case 'ADD_OBJECT':
200
+ // The engine already placed the object; all the host owes it is a dirty mark so the next
201
+ // poll picks the new id up. (The Creator also sets environmentObjectsManuallyEdited here —
202
+ // that flag exists only to gate the web lane's world-edit CLI and has no meaning for a
203
+ // project whose agent edits world.json directly.)
204
+ if (message.objectId) postFlat({ type: 'MARK_OBJECT_MODIFIED', objectId: message.objectId });
205
+ break;
206
+
207
+ case 'ADD_MARKER':
208
+ case 'UPDATE_MARKER':
209
+ markersDirty = true;
210
+ break;
211
+
212
+ case 'GENERATE_HQ_ASSET':
213
+ openHqDialog(message.assetId, message.assetName);
214
+ break;
215
+
216
+ // No other tabs exist here, and asset re-baking is a "bitmagic generate" concern.
217
+ case 'SWITCH_TO_TAB':
218
+ case 'OPEN_ASSET_ACTION':
219
+ break;
220
+ }
221
+ });
222
+
223
+ /**
224
+ * The "Generate high-quality version" button in the engine's object inspector.
225
+ *
226
+ * The engine only announces the intent — the HOST owns the flow from here, in the web lane too.
227
+ * There, the Creator collects a description and POSTs a background job. Here the editor cannot
228
+ * generate anything (the Asset Forger credentials are server-side and never reach this machine),
229
+ * so it collects the same description, saves it onto the asset, and journals the request for the
230
+ * agent to run. A placeholder often carries no description at all, which is why the text is
231
+ * asked for rather than assumed.
232
+ */
233
+ var hqOverlay = document.getElementById('hq-overlay');
234
+ var hqNote = document.getElementById('hq-note');
235
+ var hqPrompt = document.getElementById('hq-prompt');
236
+ var hqAsset = null;
237
+
238
+ function closeHqDialog() {
239
+ hqOverlay.classList.remove('show');
240
+ hqNote.classList.remove('show');
241
+ hqAsset = null;
242
+ }
243
+
244
+ async function openHqDialog(assetId, assetName) {
245
+ if (!assetId) return;
246
+ hqAsset = { id: assetId, name: assetName || assetId };
247
+ document.getElementById('hq-name').textContent = hqAsset.name;
248
+ hqNote.classList.remove('show');
249
+ // Prefill from the description ON DISK rather than anything cached here: the record the save
250
+ // patches is the stored one, and the agent may have written a description since boot.
251
+ var existing = '';
252
+ try {
253
+ var project = await (await fetch('/api/game-data')).json();
254
+ var asset = ((project.gameData && project.gameData.assets) || [])
255
+ .filter(function (a) { return a && a.id === assetId; })[0];
256
+ if (asset && typeof asset.description === 'string') existing = asset.description;
257
+ } catch (error) {
258
+ // No description to prefill is a worse dialog, not a broken one.
259
+ }
260
+ hqPrompt.value = existing;
261
+ hqOverlay.classList.add('show');
262
+ hqPrompt.focus();
263
+ }
264
+
265
+ async function confirmHq() {
266
+ if (!hqAsset) return;
267
+ var prompt = hqPrompt.value.trim();
268
+ if (prompt === '') { hqPrompt.focus(); return; }
269
+ var response = await fetch('/api/editor/hq-request', {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({ assetId: hqAsset.id, assetName: hqAsset.name, prompt: prompt })
273
+ });
274
+ var result = await response.json();
275
+ if (!response.ok || !result.ok) {
276
+ setStatus('error', result.error || 'Could not record the request');
277
+ closeHqDialog();
278
+ return;
279
+ }
280
+ lastKnownMtime = result.worldMtimeMs || lastKnownMtime;
281
+ // The description is saved either way; only the handoff depends on whether this asset can
282
+ // actually be regenerated. Saying so beats printing a command that would refuse to run.
283
+ if (result.canGenerate === false) {
284
+ document.getElementById('hq-note-text').textContent =
285
+ 'Description saved. ' + (result.reason || 'This asset cannot be regenerated.');
286
+ document.getElementById('hq-command').textContent = '';
287
+ hqNote.classList.add('show');
288
+ setStatus('saved', 'Description saved');
289
+ return;
290
+ }
291
+ document.getElementById('hq-note-text').textContent =
292
+ 'Saved and journalled. Generating the mesh needs credentials this editor does not hold, '
293
+ + 'so your agent runs it:';
294
+ document.getElementById('hq-command').textContent = result.command;
295
+ hqNote.classList.add('show');
296
+ setStatus('saved', 'Handed to your agent');
297
+ }
298
+
299
+ document.getElementById('hq-cancel').addEventListener('click', closeHqDialog);
300
+ document.getElementById('hq-confirm').addEventListener('click', function () {
301
+ void confirmHq();
302
+ });
303
+
304
+ async function boot(gen) {
305
+ loaded = false;
306
+ markersDirty = false;
307
+ banner.classList.remove('show');
308
+ terrainBanner.classList.remove('show');
309
+ setStatus('', 'Loading…');
310
+
311
+ var response = await fetch('/api/game-data');
312
+ if (gen !== generation) return;
313
+ if (!response.ok) {
314
+ setStatus('error', 'Could not read the project');
315
+ return;
316
+ }
317
+ var project = await response.json();
318
+ lastKnownMtime = project.worldMtimeMs || 0;
319
+
320
+ // Both waits are armed BEFORE navigation: either signal can arrive while the page loads.
321
+ var templateReady = await_('GAME_TEMPLATE_READY', 60000);
322
+ var gameLoaded = await_('GAME_LOADED', 120000);
323
+ frame.src = GAME_URL;
324
+
325
+ var ready = await templateReady;
326
+ if (gen !== generation) return;
327
+ if (!ready) {
328
+ setStatus('error', 'The game never finished booting — try "bitmagic verify"');
329
+ return;
330
+ }
331
+ post('LOAD_GAME', { gameId: project.gameId, gameData: project.gameData, skipMenu: true });
332
+ var started = await gameLoaded;
333
+ if (gen !== generation) return;
334
+ if (!started) {
335
+ setStatus('error', 'The world never loaded — try "bitmagic verify"');
336
+ return;
337
+ }
338
+
339
+ // Mirrors the Creator's enterEditorMode(): pause, turn the editor on (this is what unpacks
340
+ // InstancedMeshes so clicks can hit an individual instance), then configure the free camera
341
+ // and hide the HUD.
342
+ post('SET_PAUSE', { paused: true });
343
+ post('SET_EDITOR_MODE', { enabled: true });
344
+ post('SET_EDITOR_TAB', { tab: 'scene' });
345
+
346
+ loaded = true;
347
+ setStatus('saved', 'Ready');
348
+ }
349
+
350
+ async function flush() {
351
+ if (!loaded || saving) return;
352
+
353
+ post('CHECK_SCENE_CHANGES');
354
+ var changes = await await_('SCENE_HAS_CHANGES', 2000);
355
+ if (!changes) return;
356
+ if (!changes.hasChanges && !markersDirty) return;
357
+
358
+ post('GET_SCENE_EDITING_STATUS');
359
+ var status = await await_('SCENE_EDITING_STATUS', 5000);
360
+ if (!status) return;
361
+
362
+ saving = true;
363
+ setStatus('saving', 'Saving…');
364
+ try {
365
+ var response = await fetch('/api/scene/save', {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({ changes: changes, status: status, markersDirty: markersDirty })
369
+ });
370
+ var result = await response.json();
371
+ if (!response.ok || !result.ok) {
372
+ setStatus('error', result.error || 'Save failed');
373
+ return;
374
+ }
375
+ lastKnownMtime = result.worldMtimeMs || lastKnownMtime;
376
+ markersDirty = false;
377
+ // Only once the write landed: clearing earlier would drop the edit on a failed save.
378
+ post('CLEAR_SCENE_CHANGES');
379
+ setStatus('saved', result.applied > 0 ? 'Saved to world.json' : 'Ready');
380
+ } catch (error) {
381
+ setStatus('error', 'Save failed: ' + error.message);
382
+ } finally {
383
+ saving = false;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Terrain sculpting is out of scope for this command, but it is not out of REACH: clicking the
389
+ * ground drops the engine straight into a whole-terrain voxel session (EditorManager.selectObject
390
+ * routes every terrain-chunk hit to startTerrainVoxelSession, with no lock to stop it). Its
391
+ * "Save & Exit" uploads the terrain VXL to a signed URL that only the web lane and the forge can
392
+ * mint — here saveToS3 fails, logs a console warning, and marks the session committed. The
393
+ * creator would lose the work with no visible sign.
394
+ *
395
+ * So say so, within one poll of the first voxel edit, and offer the revert the engine already
396
+ * implements. hasUnsavedTerrainChanges() is false for a session that was merely opened, so
397
+ * clicking the ground by accident stays silent.
398
+ */
399
+ async function checkTerrainEdits() {
400
+ if (!loaded) return;
401
+ post('CHECK_TERRAIN_CHANGES');
402
+ var terrain = await await_('TERRAIN_HAS_CHANGES', 2000);
403
+ if (!terrain) return;
404
+ terrainBanner.classList.toggle('show', terrain.hasChanges === true);
405
+ }
406
+
407
+ async function checkExternalEdits() {
408
+ if (!loaded || saving) return;
409
+ try {
410
+ var state = await (await fetch('/api/state')).json();
411
+ // Anything other than the mtime our own last write produced is someone else's edit — most
412
+ // likely the creator's agent. Offer a reload rather than taking one: an unprompted reload
413
+ // would discard a drag in progress.
414
+ if (state.worldMtimeMs && state.worldMtimeMs !== lastKnownMtime
415
+ && state.worldMtimeMs !== state.lastWrittenMtimeMs) {
416
+ banner.classList.add('show');
417
+ }
418
+ } catch (error) {
419
+ // The sidecar is this page's own server; if it is gone the reload button is the only
420
+ // meaningful action left, and the poll below will keep trying.
421
+ }
422
+ }
423
+
424
+ // One serial loop rather than two timers: a flush and an mtime check must never overlap, or the
425
+ // check reads the mtime of a write that has not finished being accounted for.
426
+ async function loop(gen) {
427
+ if (gen !== generation) return;
428
+ await flush();
429
+ await checkTerrainEdits();
430
+ await checkExternalEdits();
431
+ if (gen !== generation) return;
432
+ pollTimer = setTimeout(function () { loop(gen); }, POLL_MS);
433
+ }
434
+
435
+ function start() {
436
+ if (pollTimer) clearTimeout(pollTimer);
437
+ var gen = ++generation;
438
+ boot(gen).then(function () { loop(gen); });
439
+ }
440
+
441
+ document.getElementById('reload').addEventListener('click', start);
442
+ document.getElementById('banner-reload').addEventListener('click', start);
443
+ document.getElementById('terrain-discard').addEventListener('click', function () {
444
+ post('REVERT_TERRAIN_CHANGES');
445
+ terrainBanner.classList.remove('show');
446
+ });
447
+
448
+ start();
449
+ })();
450
+ </script>
451
+ </body>
452
+ </html>
453
+ `;
454
+ }
455
+ //# sourceMappingURL=shell-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,kGAAkG;AAClG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AASD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,oBAAoB,OAAO,CAAC,QAAQ,2BAA2B,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACpH,OAAO;;;;yBAIgB,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA6C5B,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAmC9B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;kBACxB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoUjC,CAAC;AACF,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { Environment } from '../config/environments.js';
2
+ import type { ProjectContext } from '../project/context.js';
3
+ interface FitBox {
4
+ x: number;
5
+ z: number;
6
+ height: number;
7
+ }
8
+ export interface PropVoxelizeOptions {
9
+ context: ProjectContext;
10
+ environment: Environment;
11
+ token: string;
12
+ assetId: string;
13
+ prompt: string;
14
+ glbUrl: string;
15
+ log: (message: string) => void;
16
+ }
17
+ export interface PropVoxelizeResult {
18
+ assetId: string;
19
+ assetName: string;
20
+ assetUrl?: string;
21
+ /** How many placed instances now point at the upgraded asset. */
22
+ instanceCount: number;
23
+ }
24
+ /**
25
+ * The asset this command upgrades, with the `fitBox` the bake has to be constrained to.
26
+ *
27
+ * A missing `fitBox` is refused rather than defaulted. It records the size the object occupied
28
+ * when the level was baked, and every instance was placed against it — voxelizing to a different
29
+ * envelope produces an asset that is the wrong size everywhere it already sits, which nothing
30
+ * downstream would flag.
31
+ */
32
+ export declare function findPropAsset(world: Record<string, unknown>, assetId: string): {
33
+ asset: Record<string, unknown>;
34
+ fitBox: FitBox;
35
+ };
36
+ /**
37
+ * Building-scale boxes voxelize hollow (a closed shell); prop-scale fills solid. Same thresholds
38
+ * as the HQ job — a solid-filled building is millions of wasted voxels, and a hollow prop reads as
39
+ * a shell the moment anything clips into it.
40
+ */
41
+ export declare function shouldFillInterior(fitBox: FitBox): boolean;
42
+ /**
43
+ * Steps 2 and 3. Split from the command so the browser work is callable directly on a retry that
44
+ * already has a GLB, and so the command file stays about flags and output.
45
+ */
46
+ export declare function voxelizeAndInstall(options: PropVoxelizeOptions): Promise<PropVoxelizeResult>;
47
+ export {};