@bitmagic/cli 0.1.39 → 0.1.41
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/README.md +27 -5
- package/dist/commands/forge.js +1 -1
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/verify.js +16 -3
- package/dist/commands/verify.js.map +1 -1
- package/dist/editor/asset-detail-panel.d.ts +22 -0
- package/dist/editor/asset-detail-panel.js +72 -0
- package/dist/editor/asset-detail-panel.js.map +1 -0
- package/dist/editor/asset-preview-page.d.ts +6 -0
- package/dist/editor/asset-preview-page.js +103 -0
- package/dist/editor/asset-preview-page.js.map +1 -0
- package/dist/editor/asset-viewer.d.ts +19 -0
- package/dist/editor/asset-viewer.js +218 -0
- package/dist/editor/asset-viewer.js.map +1 -0
- package/dist/editor/server.js +159 -3
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +412 -50
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/forge/progress.d.ts +9 -1
- package/dist/forge/progress.js +8 -0
- package/dist/forge/progress.js.map +1 -1
- package/dist/forge/stream.js +6 -0
- package/dist/forge/stream.js.map +1 -1
- package/dist/project/jobs.d.ts +24 -1
- package/dist/project/jobs.js +77 -2
- package/dist/project/jobs.js.map +1 -1
- package/dist/scaffold/project-files.d.ts +6 -0
- package/dist/scaffold/project-files.js +7 -1
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/verify/browser.js +30 -0
- package/dist/verify/browser.js.map +1 -1
- package/dist/verify/classify.d.ts +4 -0
- package/dist/verify/classify.js +26 -0
- package/dist/verify/classify.js.map +1 -1
- package/dist/verify/events.d.ts +41 -0
- package/dist/verify/events.js +72 -0
- package/dist/verify/events.js.map +1 -0
- package/dist/verify/iterate.js +3 -1
- package/dist/verify/iterate.js.map +1 -1
- package/dist/verify/result.d.ts +5 -1
- package/dist/verify/result.js.map +1 -1
- package/dist/verify/url.d.ts +12 -0
- package/dist/verify/url.js +14 -0
- package/dist/verify/url.js.map +1 -0
- package/package.json +3 -2
|
@@ -48,8 +48,39 @@
|
|
|
48
48
|
* nothing. Polling `CHECK_SCENE_CHANGES` is what the Creator does too, just on tab switch
|
|
49
49
|
* instead of on a timer, and it covers drags, deletes, adds and inspector edits with one path.
|
|
50
50
|
*/
|
|
51
|
+
import { decodeForgeMapHeights, renderForgeMapRgba, } from '@bitmagic/world-forger/pipeline/forge-map-render.js';
|
|
52
|
+
import { FORGE_MAP_PALETTE } from '@bitmagic/world-forger/pipeline/layout-map.js';
|
|
53
|
+
import { ASSET_DETAIL_CSS, ASSET_DETAIL_HTML } from './asset-detail-panel.js';
|
|
51
54
|
/** How often the shell asks the engine whether anything changed. A `Set` read; effectively free. */
|
|
52
55
|
const POLL_INTERVAL_MS = 500;
|
|
56
|
+
/**
|
|
57
|
+
* What the map is drawn at, in device pixels — about twice the assets panel's inner width.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately larger than it displays. A 128-sample raster drawn 1:1 and then stretched to the
|
|
60
|
+
* panel gives single-pixel object dots blown up to blocks; drawn at 2-3x and scaled DOWN by the
|
|
61
|
+
* browser, the same dots land as clean marks and the terrain keeps its shading.
|
|
62
|
+
*/
|
|
63
|
+
const MAP_CANVAS_WIDTH = 464;
|
|
64
|
+
/**
|
|
65
|
+
* The forge map's renderer, as SOURCE, so this page and the Creator draw the same picture.
|
|
66
|
+
*
|
|
67
|
+
* This page has no module loader — it is a string this file assembles — so it cannot import the
|
|
68
|
+
* way the Creator does. The alternative to shipping the function's own text is a hand-written
|
|
69
|
+
* second implementation, and the first time the hillshade is tuned the two maps stop agreeing
|
|
70
|
+
* about what a level looks like.
|
|
71
|
+
*
|
|
72
|
+
* Two things make this safe rather than clever: `cli` builds with plain `tsc` and never minifies,
|
|
73
|
+
* and `forge-map-render.ts` is written to have no free identifiers (its header says so and says
|
|
74
|
+
* why), so its text is complete on its own. A test round-trips the interpolated copy against the
|
|
75
|
+
* imported one, so a change that breaks the trick fails there rather than in a creator's browser.
|
|
76
|
+
*/
|
|
77
|
+
function forgeMapRendererSource() {
|
|
78
|
+
return [
|
|
79
|
+
`const FORGE_MAP_PALETTE = ${JSON.stringify(FORGE_MAP_PALETTE)};`,
|
|
80
|
+
`const decodeForgeMapHeights = ${decodeForgeMapHeights.toString()};`,
|
|
81
|
+
`const renderForgeMapRgba = ${renderForgeMapRgba.toString()};`,
|
|
82
|
+
].join('\n ');
|
|
83
|
+
}
|
|
53
84
|
/**
|
|
54
85
|
* How recently a scene save must have landed for the creator to count as "still working".
|
|
55
86
|
*
|
|
@@ -121,6 +152,8 @@ export function renderEditorShell(options) {
|
|
|
121
152
|
#assets-head .count { margin-left: auto; color: #6a6a7a; letter-spacing: 0; }
|
|
122
153
|
#assets-empty { padding: 14px 10px; color: #6a6a7a; }
|
|
123
154
|
.row { display: flex; gap: 9px; padding: 8px 10px; border-bottom: 1px solid #1e1e26; }
|
|
155
|
+
.row.clickable { cursor: pointer; }
|
|
156
|
+
.row.clickable:hover, .row.clickable:focus-visible { background: #1b1b23; outline: none; }
|
|
124
157
|
.row .thumb { width: 46px; height: 46px; flex: none; border-radius: 4px; background: #1e1e26;
|
|
125
158
|
display: flex; align-items: center; justify-content: center; font-size: 18px;
|
|
126
159
|
color: #55556a; overflow: hidden; }
|
|
@@ -139,14 +172,11 @@ export function renderEditorShell(options) {
|
|
|
139
172
|
.row.job .thumb { color: #d9a441; }
|
|
140
173
|
.row.job .message { color: #9a9aab; font-size: 11px; overflow: hidden; text-overflow: ellipsis;
|
|
141
174
|
white-space: nowrap; }
|
|
142
|
-
.row.job.failed .thumb { color: #e0564f; }
|
|
143
|
-
.row.job.failed .message { color: #e0938e; white-space: normal; }
|
|
144
175
|
/* The forge's five steps. A whole run is half an hour, so this is what the panel is for. */
|
|
145
176
|
.steps { margin-top: 5px; font-size: 11px; }
|
|
146
177
|
.step { display: flex; align-items: baseline; gap: 5px; color: #55556a; line-height: 1.6; }
|
|
147
178
|
.step.running { color: #d9d9e2; }
|
|
148
179
|
.step.done { color: #7c8f7f; }
|
|
149
|
-
.step.stopped { color: #e0938e; }
|
|
150
180
|
.step .glyph { width: 9px; flex: none; text-align: center; }
|
|
151
181
|
.step .label { flex: none; }
|
|
152
182
|
.step .tail { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis;
|
|
@@ -155,6 +185,12 @@ export function renderEditorShell(options) {
|
|
|
155
185
|
.bar { height: 3px; margin: 3px 0 4px 14px; border-radius: 2px; background: #26262e;
|
|
156
186
|
overflow: hidden; }
|
|
157
187
|
.bar .fill { height: 100%; background: #d9a441; }
|
|
188
|
+
/* The level's shape, drawn from the forge artifact — twenty-plus minutes before it is playable.
|
|
189
|
+
image-rendering: pixelated because the canvas is drawn at an integer multiple of the raster
|
|
190
|
+
and any further scaling should keep the object dots as dots. */
|
|
191
|
+
.job-map { margin-top: 7px; }
|
|
192
|
+
.job-map canvas { display: block; width: 100%; height: auto; border-radius: 4px;
|
|
193
|
+
border: 1px solid #26262e; image-rendering: pixelated; }
|
|
158
194
|
/* What the level being built contains — known minutes before the level itself exists. */
|
|
159
195
|
.job-summary { margin-top: 6px; padding-top: 6px; border-top: 1px solid #1e1e26;
|
|
160
196
|
color: #8a8a99; font-size: 11px; line-height: 1.6; }
|
|
@@ -177,6 +213,7 @@ export function renderEditorShell(options) {
|
|
|
177
213
|
background: #101014; border: 1px solid #26262e; }
|
|
178
214
|
#hq-note.show { display: block; }
|
|
179
215
|
#hq-note code { display: block; margin-top: 6px; color: #9fd0a8; word-break: break-all; }
|
|
216
|
+
${ASSET_DETAIL_CSS}
|
|
180
217
|
</style>
|
|
181
218
|
</head>
|
|
182
219
|
<body>
|
|
@@ -235,12 +272,16 @@ export function renderEditorShell(options) {
|
|
|
235
272
|
</div>
|
|
236
273
|
</div>
|
|
237
274
|
</div>
|
|
275
|
+
${ASSET_DETAIL_HTML}
|
|
238
276
|
<script>
|
|
239
277
|
(function () {
|
|
240
278
|
'use strict';
|
|
241
279
|
var GAME_URL = ${JSON.stringify(gameUrl)};
|
|
242
280
|
var POLL_MS = ${POLL_INTERVAL_MS};
|
|
243
281
|
var ACTIVE_EDIT_MS = ${ACTIVE_EDIT_WINDOW_MS};
|
|
282
|
+
var MAP_CANVAS_WIDTH = ${MAP_CANVAS_WIDTH};
|
|
283
|
+
// The forge map's renderer, shared verbatim with the Creator — see forgeMapRendererSource().
|
|
284
|
+
${forgeMapRendererSource()}
|
|
244
285
|
var TAB_KEY = 'bitmagic.dev.tab';
|
|
245
286
|
var AUTO_KEY = 'bitmagic.dev.autoReload';
|
|
246
287
|
var ASSETS_KEY = 'bitmagic.dev.assets';
|
|
@@ -330,6 +371,13 @@ export function renderEditorShell(options) {
|
|
|
330
371
|
var message = event.data;
|
|
331
372
|
if (!message || typeof message.type !== 'string') return;
|
|
332
373
|
|
|
374
|
+
// The asset overlay's viewer is a second sender on this window. Its replies are routed by
|
|
375
|
+
// source, before the waiter list below gets to match anything on type alone.
|
|
376
|
+
if (assetFrame && event.source === assetFrame.contentWindow) {
|
|
377
|
+
onAssetPreviewMessage(message);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
333
381
|
waiters = waiters.filter(function (waiter) {
|
|
334
382
|
if (waiter.type !== message.type) return true;
|
|
335
383
|
clearTimeout(waiter.timer);
|
|
@@ -836,7 +884,7 @@ export function renderEditorShell(options) {
|
|
|
836
884
|
return typeof value === 'number' && isFinite(value) ? value.toLocaleString() : '';
|
|
837
885
|
}
|
|
838
886
|
|
|
839
|
-
var STEP_GLYPHS = { pending: '○', running: '⟳', done: '✓'
|
|
887
|
+
var STEP_GLYPHS = { pending: '○', running: '⟳', done: '✓' };
|
|
840
888
|
|
|
841
889
|
/**
|
|
842
890
|
* The trailing text for one step: its duration once finished, its counts while it runs.
|
|
@@ -861,14 +909,11 @@ export function renderEditorShell(options) {
|
|
|
861
909
|
* snapshot (cli/src/forge/progress.ts), so this draws one list and never has to know which
|
|
862
910
|
* machine a row came from.
|
|
863
911
|
*/
|
|
864
|
-
function stepsBlock(progress
|
|
912
|
+
function stepsBlock(progress) {
|
|
865
913
|
var block = el('div', 'steps');
|
|
866
914
|
for (var i = 0; i < progress.steps.length; i++) {
|
|
867
915
|
var step = progress.steps[i];
|
|
868
|
-
|
|
869
|
-
// say otherwise. The job's own status is the authority, so the row that was in flight is the
|
|
870
|
-
// one that stopped.
|
|
871
|
-
var status = jobFailed && step.status === 'running' ? 'stopped' : step.status;
|
|
916
|
+
var status = step.status;
|
|
872
917
|
var line = el('div', 'step ' + status);
|
|
873
918
|
line.appendChild(el('span', 'glyph' + (status === 'running' ? ' spin' : ''), STEP_GLYPHS[status]));
|
|
874
919
|
line.appendChild(el('span', 'label', step.label + (step.resumed ? ' (resumed)' : '')));
|
|
@@ -888,6 +933,53 @@ export function renderEditorShell(options) {
|
|
|
888
933
|
return block;
|
|
889
934
|
}
|
|
890
935
|
|
|
936
|
+
/**
|
|
937
|
+
* A forge's map, fetched once per job and drawn into a canvas.
|
|
938
|
+
*
|
|
939
|
+
* Fetched rather than carried on the job record, because the record is re-read at 1 Hz for the
|
|
940
|
+
* whole run and the map never changes after it arrives. Cached by job id so the panel's rebuild
|
|
941
|
+
* (which throws every row away and makes new ones) does not re-request it every second.
|
|
942
|
+
*/
|
|
943
|
+
var mapCache = {};
|
|
944
|
+
var mapFetching = {};
|
|
945
|
+
|
|
946
|
+
function drawMap(canvas, map) {
|
|
947
|
+
var scale = Math.max(1, Math.floor(MAP_CANVAS_WIDTH / Math.max(map.width, map.height)));
|
|
948
|
+
var render = renderForgeMapRgba(map, decodeForgeMapHeights(map), FORGE_MAP_PALETTE, scale);
|
|
949
|
+
canvas.width = render.width;
|
|
950
|
+
canvas.height = render.height;
|
|
951
|
+
var ctx = canvas.getContext('2d');
|
|
952
|
+
if (!ctx) return;
|
|
953
|
+
var image = ctx.createImageData(render.width, render.height);
|
|
954
|
+
image.data.set(render.rgba);
|
|
955
|
+
ctx.putImageData(image, 0, 0);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function mapBlock(jobId) {
|
|
959
|
+
var block = el('div', 'job-map');
|
|
960
|
+
var canvas = document.createElement('canvas');
|
|
961
|
+
block.appendChild(canvas);
|
|
962
|
+
var cached = mapCache[jobId];
|
|
963
|
+
if (cached) {
|
|
964
|
+
drawMap(canvas, cached);
|
|
965
|
+
return block;
|
|
966
|
+
}
|
|
967
|
+
if (!mapFetching[jobId]) {
|
|
968
|
+
mapFetching[jobId] = true;
|
|
969
|
+
fetch('/api/jobs/' + encodeURIComponent(jobId) + '/layout')
|
|
970
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
971
|
+
.then(function (payload) {
|
|
972
|
+
if (!payload || !payload.layout) return;
|
|
973
|
+
mapCache[jobId] = payload.layout;
|
|
974
|
+
// Re-render rather than drawing into this canvas: by the time the fetch lands the 1 Hz
|
|
975
|
+
// ticker has almost certainly replaced the row this element belongs to.
|
|
976
|
+
void refreshAssets();
|
|
977
|
+
})
|
|
978
|
+
.catch(function () { /* no map is a panel without a picture, never an error to show */ });
|
|
979
|
+
}
|
|
980
|
+
return block;
|
|
981
|
+
}
|
|
982
|
+
|
|
891
983
|
/** What the level contains — the answer that exists long before the level does. */
|
|
892
984
|
function summaryBlock(summary) {
|
|
893
985
|
var headline = [];
|
|
@@ -911,50 +1003,81 @@ export function renderEditorShell(options) {
|
|
|
911
1003
|
return block;
|
|
912
1004
|
}
|
|
913
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* A generation in flight. Only ever in flight: the sidecar serves running jobs and nothing else,
|
|
1008
|
+
* because a failure is a tool diagnostic for whoever ran the command — see project/jobs.ts. A job
|
|
1009
|
+
* that fails leaves this list the same way one that succeeds does, by no longer being in it.
|
|
1010
|
+
*/
|
|
914
1011
|
function jobRow(job) {
|
|
915
|
-
var
|
|
916
|
-
var row = el('div', 'row job' + (failed ? ' failed' : ''));
|
|
1012
|
+
var row = el('div', 'row job');
|
|
917
1013
|
var thumb = el('div', 'thumb');
|
|
918
|
-
thumb.appendChild(el('span',
|
|
1014
|
+
thumb.appendChild(el('span', 'spin', '⟳'));
|
|
919
1015
|
row.appendChild(thumb);
|
|
920
1016
|
var body = el('div', 'body');
|
|
921
1017
|
body.appendChild(el('div', 'name', job.label || job.kind));
|
|
922
|
-
|
|
923
|
-
body.appendChild(meta);
|
|
1018
|
+
body.appendChild(el('div', 'meta', job.kind + ' · ' + formatElapsed(job.startedAt)));
|
|
924
1019
|
var progress = job.progress;
|
|
925
1020
|
var hasSteps = progress && Array.isArray(progress.steps) && progress.steps.length > 0;
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
if (failed || !hasSteps) {
|
|
929
|
-
body.appendChild(el('div', 'message', failed ? (job.error || 'Failed') : (job.message || '')));
|
|
930
|
-
}
|
|
1021
|
+
// A forge is the one kind with more than a sentence to say. Everything else keeps the single
|
|
1022
|
+
// line, which for those is the whole story.
|
|
931
1023
|
if (hasSteps) {
|
|
932
|
-
body.appendChild(stepsBlock(progress
|
|
1024
|
+
body.appendChild(stepsBlock(progress));
|
|
933
1025
|
var summary = progress.summary ? summaryBlock(progress.summary) : null;
|
|
934
1026
|
if (summary) body.appendChild(summary);
|
|
1027
|
+
if (job.hasLayout) body.appendChild(mapBlock(job.jobId));
|
|
1028
|
+
} else {
|
|
1029
|
+
body.appendChild(el('div', 'message', job.message || ''));
|
|
935
1030
|
}
|
|
936
1031
|
row.appendChild(body);
|
|
937
1032
|
return row;
|
|
938
1033
|
}
|
|
939
1034
|
|
|
940
1035
|
/**
|
|
941
|
-
*
|
|
1036
|
+
* How the engine should draw this asset's thumbnail, or null when there is nothing to draw.
|
|
1037
|
+
*
|
|
1038
|
+
* 'sourceGlbUrl' before 'url' because many CLI-lane meshes are voxelized: 'url' is then a .vxl,
|
|
1039
|
+
* and the GLB the forger started from renders in one message with nothing to fetch here.
|
|
942
1040
|
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
1041
|
+
* A .vxl with NO GLB source used to fall through to a glyph, and that was most of the panel —
|
|
1042
|
+
* across the sampled project worlds it is 153 such assets against 144 with a GLB source, and
|
|
1043
|
+
* every fresh project starts in the bad half: templates/standard-3d ships nine assets, all vxl,
|
|
1044
|
+
* none with a sourceGlbUrl or a screenshotUrl. So the whole assets panel of a new project was
|
|
1045
|
+
* grey boxes. Those now go the voxel route, which the engine has been able to draw the entire
|
|
1046
|
+
* time (VoxelMessageHandlers.handleGenerateVoxelPreview) and which the web Creator has used since
|
|
1047
|
+
* before this lane existed.
|
|
1048
|
+
*
|
|
1049
|
+
* An asset with neither — a sound, an animation, a level — keeps its glyph, which is the honest
|
|
1050
|
+
* answer for something with no appearance.
|
|
947
1051
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1052
|
+
var MAX_VXL_PREVIEW_BYTES = 8 * 1024 * 1024;
|
|
1053
|
+
|
|
1054
|
+
function previewJobFor(asset) {
|
|
1055
|
+
var glb = asset.sourceGlbUrl || asset.url;
|
|
951
1056
|
// Double-escaped because this whole page is a template literal: a single backslash is consumed
|
|
952
1057
|
// by the literal, and the browser would receive the invalid regex /.(glb|gltf)(?|$)/.
|
|
953
|
-
|
|
1058
|
+
if (typeof glb === 'string' && /\\.(glb|gltf)(\\?|$)/i.test(glb)) return { kind: 'glb', url: glb };
|
|
1059
|
+
var url = asset.url;
|
|
1060
|
+
if (typeof url !== 'string' || !/\\.vxl(\\?|$)/i.test(url)) return null;
|
|
1061
|
+
// The voxel message carries the FILE, so this one costs a fetch and a structured clone of the
|
|
1062
|
+
// whole thing. The templates' files are 0.5-2.5 KB; a multi-megabyte one is a hitch on the
|
|
1063
|
+
// render thread in exchange for a 46px tile, and the glyph is the better trade.
|
|
1064
|
+
if (typeof asset.size === 'number' && asset.size > MAX_VXL_PREVIEW_BYTES) return null;
|
|
1065
|
+
return { kind: 'vxl', url: url };
|
|
954
1066
|
}
|
|
955
1067
|
|
|
956
1068
|
function assetRow(asset, isFresh, hasPreview) {
|
|
957
|
-
var row = el('div', 'row' + (isFresh ? ' fresh' : ''));
|
|
1069
|
+
var row = el('div', 'row clickable' + (isFresh ? ' fresh' : ''));
|
|
1070
|
+
// A row is a button: the panel is a list of things you can look at. Job rows deliberately are
|
|
1071
|
+
// not — there is nothing to show for an asset that does not exist yet.
|
|
1072
|
+
row.setAttribute('role', 'button');
|
|
1073
|
+
row.setAttribute('tabindex', '0');
|
|
1074
|
+
row.title = 'Open a preview of this asset';
|
|
1075
|
+
row.addEventListener('click', function () { openAssetDetail(asset, hasPreview); });
|
|
1076
|
+
row.addEventListener('keydown', function (event) {
|
|
1077
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
1078
|
+
event.preventDefault();
|
|
1079
|
+
openAssetDetail(asset, hasPreview);
|
|
1080
|
+
});
|
|
958
1081
|
var thumb = el('div', 'thumb');
|
|
959
1082
|
var type = String(asset.type || '').toLowerCase();
|
|
960
1083
|
if (type === 'image' && typeof asset.url === 'string' && asset.url !== '') {
|
|
@@ -972,7 +1095,7 @@ export function renderEditorShell(options) {
|
|
|
972
1095
|
thumb.appendChild(cached);
|
|
973
1096
|
} else {
|
|
974
1097
|
thumb.appendChild(el('span', '', glyphFor(asset.type)));
|
|
975
|
-
if (
|
|
1098
|
+
if (previewJobFor(asset) !== null) queuePreview(asset);
|
|
976
1099
|
}
|
|
977
1100
|
row.appendChild(thumb);
|
|
978
1101
|
|
|
@@ -1035,26 +1158,71 @@ export function renderEditorShell(options) {
|
|
|
1035
1158
|
}
|
|
1036
1159
|
|
|
1037
1160
|
/**
|
|
1038
|
-
* Ask the game to draw the thumbnails the cache is missing — one at a time, and
|
|
1161
|
+
* Ask the game to draw the thumbnails the cache is missing — one at a time, and at most twice for
|
|
1039
1162
|
* the same asset in one session.
|
|
1040
1163
|
*
|
|
1041
|
-
* Serial on purpose. Each render is a
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1164
|
+
* Serial on purpose. Each render is a fetch and a draw in the SAME renderer the creator is playing
|
|
1165
|
+
* in, and a forged city has hundreds of assets: firing them all at once would stutter the game to
|
|
1166
|
+
* decorate a side panel.
|
|
1167
|
+
*
|
|
1168
|
+
* Two rather than one attempt, and the second is not defensive padding. GENERATE_VOXEL_PREVIEW is
|
|
1169
|
+
* in the engine's REQUIRES_GAME_LOADED set (the GLB one is not), so a request arriving while a
|
|
1170
|
+
* game is loading is QUEUED and silently never answered. Marked attempted-once, that asset would
|
|
1171
|
+
* keep its glyph for the rest of the session; the retry costs one extra message and the cap keeps
|
|
1172
|
+
* a genuinely broken asset from asking forever.
|
|
1045
1173
|
*/
|
|
1174
|
+
var MAX_PREVIEW_ATTEMPTS = 2;
|
|
1046
1175
|
var previewQueue = [];
|
|
1047
|
-
var
|
|
1176
|
+
var previewAttempts = {};
|
|
1048
1177
|
var previewRunning = false;
|
|
1049
1178
|
var previewRequestId = 0;
|
|
1050
1179
|
|
|
1051
1180
|
function queuePreview(asset) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1181
|
+
var attempts = previewAttempts[asset.id] || 0;
|
|
1182
|
+
if (attempts >= MAX_PREVIEW_ATTEMPTS) return;
|
|
1183
|
+
previewAttempts[asset.id] = attempts + 1;
|
|
1054
1184
|
previewQueue.push(asset);
|
|
1055
1185
|
void drainPreviewQueue();
|
|
1056
1186
|
}
|
|
1057
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* Both preview messages use the FLAT envelope and both are answered by requestId, so neither can
|
|
1190
|
+
* go through the request() helper — it matches on type alone.
|
|
1191
|
+
*/
|
|
1192
|
+
function requestGlbPreview(url, requestId) {
|
|
1193
|
+
postFlat({ type: 'GENERATE_GLB_PREVIEW', requestId: requestId, glbUrl: url });
|
|
1194
|
+
return await_('GLB_PREVIEW_RESPONSE', 15000);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* The voxel counterpart, ported from the web Creator's useAssetsEditor.requestVoxelPreview.
|
|
1199
|
+
*
|
|
1200
|
+
* The bytes come through the sidecar rather than straight from the asset host: this message
|
|
1201
|
+
* carries the file itself, and a browser fetch would make voxel thumbnails depend on that host's
|
|
1202
|
+
* CORS headers — see /api/assets/source.
|
|
1203
|
+
*/
|
|
1204
|
+
async function requestVoxelPreview(url, requestId) {
|
|
1205
|
+
var bytes;
|
|
1206
|
+
try {
|
|
1207
|
+
var source = await fetch('/api/assets/source?url=' + encodeURIComponent(url));
|
|
1208
|
+
if (!source.ok) throw new Error('sidecar answered ' + source.status);
|
|
1209
|
+
bytes = new Uint8Array(await source.arrayBuffer());
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
// Worth a line: without one, an asset the sidecar cannot reach is a grey tile with no reason.
|
|
1212
|
+
console.warn('[bitmagic dev] no voxel preview for ' + url, error);
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
postFlat({
|
|
1216
|
+
type: 'GENERATE_VOXEL_PREVIEW',
|
|
1217
|
+
requestId: requestId,
|
|
1218
|
+
// A plain number array, which is what the engine handler reads (new Uint8Array(data.vxlData)).
|
|
1219
|
+
// Not an ArrayBuffer: this page ships with the CLI and talks to whatever engine the project
|
|
1220
|
+
// has vendored, so the wire shape has to keep working against older ones.
|
|
1221
|
+
vxlData: Array.prototype.slice.call(bytes)
|
|
1222
|
+
});
|
|
1223
|
+
return await_('VOXEL_PREVIEW_RESPONSE', 15000);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1058
1226
|
async function drainPreviewQueue() {
|
|
1059
1227
|
if (previewRunning) return;
|
|
1060
1228
|
previewRunning = true;
|
|
@@ -1065,20 +1233,24 @@ export function renderEditorShell(options) {
|
|
|
1065
1233
|
// pass rather than being dropped — refreshAssets re-queues on every render.
|
|
1066
1234
|
if (!loaded) return;
|
|
1067
1235
|
var asset = previewQueue.shift();
|
|
1068
|
-
var
|
|
1069
|
-
if (
|
|
1236
|
+
var job = previewJobFor(asset);
|
|
1237
|
+
if (job === null) continue;
|
|
1070
1238
|
|
|
1071
1239
|
previewRequestId += 1;
|
|
1072
|
-
// Deliberately not the request() helper: this message uses the FLAT envelope, and the reply
|
|
1073
|
-
// has to be matched on requestId (below) rather than on type alone.
|
|
1074
1240
|
var requestId = 'preview-' + previewRequestId;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
|
|
1241
|
+
var response = job.kind === 'vxl'
|
|
1242
|
+
? await requestVoxelPreview(job.url, requestId)
|
|
1243
|
+
: await requestGlbPreview(job.url, requestId);
|
|
1244
|
+
|
|
1245
|
+
// The requestId is verified rather than assumed: a late answer from a request that already
|
|
1246
|
+
// timed out would otherwise be cached under the NEXT asset's id, which is a wrong picture
|
|
1247
|
+
// rather than a missing one — much the worse failure.
|
|
1248
|
+
if (!response || response.requestId !== requestId || !response.success || !response.previewBase64) {
|
|
1249
|
+
// Back of the queue, behind everything else, and only while attempts remain. This is the
|
|
1250
|
+
// path a request swallowed by REQUIRES_GAME_LOADED takes.
|
|
1251
|
+
queuePreview(asset);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1082
1254
|
|
|
1083
1255
|
var saved = await (await fetch('/api/assets/preview', {
|
|
1084
1256
|
method: 'POST',
|
|
@@ -1126,6 +1298,196 @@ export function renderEditorShell(options) {
|
|
|
1126
1298
|
}, 1000);
|
|
1127
1299
|
}
|
|
1128
1300
|
|
|
1301
|
+
/**
|
|
1302
|
+
* The asset overlay — everything a 46px tile cannot show.
|
|
1303
|
+
*
|
|
1304
|
+
* The viewer itself is an iframe onto '/asset-preview', which the dev sidecar serves from this
|
|
1305
|
+
* same origin. It is not part of the game: the game keeps its own WebGL context in the frame
|
|
1306
|
+
* behind this dialog, and the two never share a renderer.
|
|
1307
|
+
*/
|
|
1308
|
+
var assetOverlay = document.getElementById('asset-overlay');
|
|
1309
|
+
var assetFrame = document.getElementById('asset-frame');
|
|
1310
|
+
var assetImage = document.getElementById('asset-image');
|
|
1311
|
+
var assetAudio = document.getElementById('asset-audio');
|
|
1312
|
+
var assetNote = document.getElementById('asset-note');
|
|
1313
|
+
var assetFacts = document.getElementById('asset-facts');
|
|
1314
|
+
var assetResetButton = document.getElementById('asset-reset');
|
|
1315
|
+
var assetPausedByOverlay = false;
|
|
1316
|
+
var assetReturnFocus = null;
|
|
1317
|
+
var assetLoadTimer = null;
|
|
1318
|
+
|
|
1319
|
+
var IMAGE_TYPES = { image: 1, skybox: 1 };
|
|
1320
|
+
var AUDIO_TYPES = { audio: 1, sound: 1, 'sound-effect': 1 };
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* What the VIEWER should load — which is not always what the thumbnail drew.
|
|
1324
|
+
*
|
|
1325
|
+
* previewJobFor prefers a voxelized asset's GLB source, because one message with no fetch is the
|
|
1326
|
+
* cheaper way to fill a 46px tile. Here the creator has asked to look at a specific asset, so it
|
|
1327
|
+
* shows the asset: a .vxl is drawn as voxels even when the mesh it was baked from is on record.
|
|
1328
|
+
*/
|
|
1329
|
+
function detailJobFor(asset) {
|
|
1330
|
+
var url = asset.url;
|
|
1331
|
+
if (typeof url === 'string' && /\\.vxl(\\?|$)/i.test(url)) {
|
|
1332
|
+
if (typeof asset.size === 'number' && asset.size > MAX_VXL_PREVIEW_BYTES) return null;
|
|
1333
|
+
return { kind: 'vxl', url: url };
|
|
1334
|
+
}
|
|
1335
|
+
var glb = asset.sourceGlbUrl || url;
|
|
1336
|
+
if (typeof glb === 'string' && /\\.(glb|gltf)(\\?|$)/i.test(glb)) return { kind: 'glb', url: glb };
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function fact(term, value) {
|
|
1341
|
+
if (!value) return;
|
|
1342
|
+
assetFacts.appendChild(el('dt', '', term));
|
|
1343
|
+
assetFacts.appendChild(el('dd', term === 'Id' ? 'mono' : '', value));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** Extent in world units, which is the number that says whether a prop is door-sized. */
|
|
1347
|
+
function dimensionsOf(asset) {
|
|
1348
|
+
var box = asset.boundingBox;
|
|
1349
|
+
if (!box || typeof box.minX !== 'number') return '';
|
|
1350
|
+
var round = function (n) { return Math.round(n * 10) / 10; };
|
|
1351
|
+
return round(box.maxX - box.minX) + ' × ' + round(box.maxY - box.minY) + ' × ' + round(box.maxZ - box.minZ);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function showAssetNote(text) {
|
|
1355
|
+
assetNote.textContent = text;
|
|
1356
|
+
assetNote.classList.remove('hidden');
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function openAssetDetail(asset, hasPreview) {
|
|
1360
|
+
assetReturnFocus = document.activeElement;
|
|
1361
|
+
document.getElementById('asset-name').textContent = asset.name || asset.id || 'unnamed';
|
|
1362
|
+
document.getElementById('asset-sub').textContent =
|
|
1363
|
+
(asset.hqGeneratedAt ? 'High quality · ' : asset.placeholder === true ? 'Placeholder · ' : '')
|
|
1364
|
+
+ (asset.type || 'unknown');
|
|
1365
|
+
|
|
1366
|
+
assetFacts.textContent = '';
|
|
1367
|
+
fact('Size', formatSize(asset.size));
|
|
1368
|
+
fact('Extent', dimensionsOf(asset));
|
|
1369
|
+
fact('Voxels', typeof asset.voxelCount === 'number' ? String(asset.voxelCount) : '');
|
|
1370
|
+
fact('Id', asset.id || '');
|
|
1371
|
+
|
|
1372
|
+
assetNote.classList.add('hidden');
|
|
1373
|
+
assetFrame.classList.add('hidden');
|
|
1374
|
+
assetImage.classList.add('hidden');
|
|
1375
|
+
assetAudio.classList.add('hidden');
|
|
1376
|
+
assetResetButton.classList.add('hidden');
|
|
1377
|
+
if (assetLoadTimer) clearTimeout(assetLoadTimer);
|
|
1378
|
+
|
|
1379
|
+
var type = String(asset.type || '').toLowerCase();
|
|
1380
|
+
var job = detailJobFor(asset);
|
|
1381
|
+
|
|
1382
|
+
if (IMAGE_TYPES[type] === 1 && typeof asset.url === 'string' && asset.url !== '') {
|
|
1383
|
+
// An image is already the thing. Loading a 3D viewer to show a picture would be theatre.
|
|
1384
|
+
assetImage.src = asset.url;
|
|
1385
|
+
assetImage.classList.remove('hidden');
|
|
1386
|
+
} else if (AUDIO_TYPES[type] === 1 && typeof asset.url === 'string' && asset.url !== '') {
|
|
1387
|
+
assetAudio.src = asset.url;
|
|
1388
|
+
assetAudio.classList.remove('hidden');
|
|
1389
|
+
} else if (job !== null) {
|
|
1390
|
+
assetResetButton.classList.remove('hidden');
|
|
1391
|
+
assetFrame.classList.remove('hidden');
|
|
1392
|
+
assetFrame.src = '/asset-preview?kind=' + job.kind + '&url=' + encodeURIComponent(job.url);
|
|
1393
|
+
// Not a deadline for the load — a big mesh over a slow CDN legitimately takes a while, and
|
|
1394
|
+
// replacing a working viewer with an apology would be worse than waiting. This only speaks
|
|
1395
|
+
// when the page has said nothing at all, which is what a module graph that failed to load
|
|
1396
|
+
// looks like: the page's own catch reports every error it can see, and cannot report that.
|
|
1397
|
+
assetLoadTimer = setTimeout(function () {
|
|
1398
|
+
showAssetNote('Still loading. If this does not clear, check the dev server output.');
|
|
1399
|
+
}, 12000);
|
|
1400
|
+
} else {
|
|
1401
|
+
showAssetThumbnailFallback(asset, hasPreview,
|
|
1402
|
+
'There is nothing to draw for this asset type — it has no mesh and no picture.');
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
assetOverlay.classList.add('show');
|
|
1406
|
+
document.getElementById('asset-close').focus();
|
|
1407
|
+
|
|
1408
|
+
// The game is a WebGL context and a physics step running behind a dialog nobody is looking at.
|
|
1409
|
+
// The Editor tab is already paused by applyTab, so this only fires on the Game tab.
|
|
1410
|
+
if (activeTab === 'game' && gameState === 'playing') {
|
|
1411
|
+
post('SET_PAUSE', { paused: true });
|
|
1412
|
+
assetPausedByOverlay = true;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** When the viewer cannot show it, show the tile that was already rendered, and say why. */
|
|
1417
|
+
function showAssetThumbnailFallback(asset, hasPreview, reason) {
|
|
1418
|
+
assetFrame.classList.add('hidden');
|
|
1419
|
+
assetResetButton.classList.add('hidden');
|
|
1420
|
+
if (hasPreview) {
|
|
1421
|
+
assetImage.src = '/api/assets/preview/' + encodeURIComponent(asset.id) + '.png';
|
|
1422
|
+
assetImage.classList.remove('hidden');
|
|
1423
|
+
}
|
|
1424
|
+
showAssetNote(reason);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function closeAssetDetail() {
|
|
1428
|
+
if (!assetOverlay.classList.contains('show')) return false;
|
|
1429
|
+
assetOverlay.classList.remove('show');
|
|
1430
|
+
if (assetLoadTimer) clearTimeout(assetLoadTimer);
|
|
1431
|
+
// about:blank rather than hiding it: an idle iframe would hold a WebGL context and a render
|
|
1432
|
+
// loop for the rest of the session, next to the game that needs both.
|
|
1433
|
+
assetFrame.src = 'about:blank';
|
|
1434
|
+
assetImage.removeAttribute('src');
|
|
1435
|
+
assetAudio.pause();
|
|
1436
|
+
assetAudio.removeAttribute('src');
|
|
1437
|
+
if (assetPausedByOverlay) {
|
|
1438
|
+
post('SET_PAUSE', { paused: false });
|
|
1439
|
+
assetPausedByOverlay = false;
|
|
1440
|
+
}
|
|
1441
|
+
if (assetReturnFocus && assetReturnFocus.focus) assetReturnFocus.focus();
|
|
1442
|
+
assetReturnFocus = null;
|
|
1443
|
+
return true;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Replies from the preview iframe, matched by SOURCE rather than by type.
|
|
1448
|
+
*
|
|
1449
|
+
* The message listener below resolves its waiters on type alone, so a second sender on this
|
|
1450
|
+
* window could answer a request meant for the game. Today their vocabularies do not overlap;
|
|
1451
|
+
* this is what keeps that from being a silent bug the day one of them gains a name the other
|
|
1452
|
+
* already uses.
|
|
1453
|
+
*/
|
|
1454
|
+
function onAssetPreviewMessage(message) {
|
|
1455
|
+
if (message.type === 'ASSET_PREVIEW_READY') {
|
|
1456
|
+
if (assetLoadTimer) clearTimeout(assetLoadTimer);
|
|
1457
|
+
assetNote.classList.add('hidden');
|
|
1458
|
+
if (typeof message.triangles === 'number' && message.triangles > 0) {
|
|
1459
|
+
fact('Triangles', message.triangles.toLocaleString());
|
|
1460
|
+
}
|
|
1461
|
+
} else if (message.type === 'ASSET_PREVIEW_FAILED') {
|
|
1462
|
+
if (assetLoadTimer) clearTimeout(assetLoadTimer);
|
|
1463
|
+
assetFrame.src = 'about:blank';
|
|
1464
|
+
showAssetNote(message.error || 'The preview could not be loaded.');
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
assetResetButton.addEventListener('click', function () {
|
|
1469
|
+
if (assetFrame.contentWindow) {
|
|
1470
|
+
assetFrame.contentWindow.postMessage({ type: 'RESET_VIEW' }, window.location.origin);
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
document.getElementById('asset-copy').addEventListener('click', function () {
|
|
1474
|
+
var id = assetFacts.querySelector('dd.mono');
|
|
1475
|
+
if (id && navigator.clipboard) void navigator.clipboard.writeText(id.textContent);
|
|
1476
|
+
});
|
|
1477
|
+
document.getElementById('asset-close').addEventListener('click', closeAssetDetail);
|
|
1478
|
+
assetOverlay.addEventListener('mousedown', function (event) {
|
|
1479
|
+
// mousedown, not click: a drag that starts inside the viewer and ends on the backdrop is a
|
|
1480
|
+
// creator turning the model, not a creator dismissing the dialog.
|
|
1481
|
+
if (event.target === assetOverlay) closeAssetDetail();
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// The page had no keyboard handling at all before this, so #hq-overlay gains Escape with it.
|
|
1485
|
+
document.addEventListener('keydown', function (event) {
|
|
1486
|
+
if (event.key !== 'Escape') return;
|
|
1487
|
+
if (closeAssetDetail()) return;
|
|
1488
|
+
hqOverlay.classList.remove('show');
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1129
1491
|
/**
|
|
1130
1492
|
* Lend the cached thumbnails to the engine's own asset palette.
|
|
1131
1493
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,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;;;;wBAIe,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"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"}
|
package/dist/forge/progress.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* (`ForgeTransportOptions.onProgress`), so the counts arrive as ordinary step events and the
|
|
26
26
|
* second, parallel path is gone.
|
|
27
27
|
*/
|
|
28
|
-
import { type ForgeProgressEvent, type ForgeProgressSummary, type ForgeStepKey } from '@bitmagic/world-forger/pipeline/index.js';
|
|
28
|
+
import { type ForgeLayoutMap, type ForgeProgressEvent, type ForgeProgressSummary, type ForgeStepKey } from '@bitmagic/world-forger/pipeline/index.js';
|
|
29
29
|
export type ForgeStepStatus = 'pending' | 'running' | 'done';
|
|
30
30
|
export interface ForgeStepState {
|
|
31
31
|
key: ForgeStepKey;
|
|
@@ -59,6 +59,14 @@ export interface ForgeProgressTrackerOptions {
|
|
|
59
59
|
log: (message: string) => void;
|
|
60
60
|
/** Job-record writer. Receives a fresh snapshot; may be omitted when nothing is watching. */
|
|
61
61
|
report?: (snapshot: ForgeProgressSnapshot) => void;
|
|
62
|
+
/**
|
|
63
|
+
* The level's map, handed over the once it arrives.
|
|
64
|
+
*
|
|
65
|
+
* A separate sink rather than a field on the snapshot, and that is the point: the snapshot is
|
|
66
|
+
* rewritten on every tick, and ~25KB of raster in it would be rewritten with it for the rest of
|
|
67
|
+
* a thirty-minute run. Routing it out here makes "written once" structural.
|
|
68
|
+
*/
|
|
69
|
+
onLayout?: (layout: ForgeLayoutMap) => void;
|
|
62
70
|
now?: () => number;
|
|
63
71
|
/** Overridden in tests. */
|
|
64
72
|
logIntervalMs?: number;
|