@bitmagic/cli 0.1.25 → 0.1.26
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 +22 -0
- package/dist/commands/dev.js +4 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/forge.js +32 -21
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.js +117 -56
- package/dist/commands/generate.js.map +1 -1
- package/dist/editor/previews.d.ts +23 -0
- package/dist/editor/previews.js +108 -0
- package/dist/editor/previews.js.map +1 -0
- package/dist/editor/server.js +111 -3
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +406 -5
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/project/jobs.d.ts +67 -0
- package/dist/project/jobs.js +295 -0
- package/dist/project/jobs.js.map +1 -0
- package/dist/scaffold/project-files.js +6 -0
- package/dist/scaffold/project-files.js.map +1 -1
- package/package.json +1 -1
|
@@ -108,7 +108,41 @@ export function renderEditorShell(options) {
|
|
|
108
108
|
.banner.show { display: flex; }
|
|
109
109
|
#terrain-banner, #build-banner { background: #3d1f1c; border-bottom-color: #6b2f28; color: #f3c3bd; }
|
|
110
110
|
#build-banner code { font-family: ui-monospace, monospace; }
|
|
111
|
-
|
|
111
|
+
/* The game and the panel share one row, so the panel takes width from the game rather than
|
|
112
|
+
overlaying it — an overlay would cover the thing you opened the panel to watch. */
|
|
113
|
+
#stage { flex: 1; display: flex; min-height: 0; }
|
|
114
|
+
#frame { display: block; flex: 1; border: 0; min-width: 0; height: 100%; }
|
|
115
|
+
#assets { width: 264px; flex: none; overflow-y: auto; background: #131318;
|
|
116
|
+
border-left: 1px solid #26262e; }
|
|
117
|
+
body.assets-closed #assets { display: none; }
|
|
118
|
+
#assets-head { position: sticky; top: 0; display: flex; align-items: center; gap: 8px;
|
|
119
|
+
padding: 8px 10px; background: #17171d; border-bottom: 1px solid #26262e;
|
|
120
|
+
font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: #8a8a99; }
|
|
121
|
+
#assets-head .count { margin-left: auto; color: #6a6a7a; letter-spacing: 0; }
|
|
122
|
+
#assets-empty { padding: 14px 10px; color: #6a6a7a; }
|
|
123
|
+
.row { display: flex; gap: 9px; padding: 8px 10px; border-bottom: 1px solid #1e1e26; }
|
|
124
|
+
.row .thumb { width: 46px; height: 46px; flex: none; border-radius: 4px; background: #1e1e26;
|
|
125
|
+
display: flex; align-items: center; justify-content: center; font-size: 18px;
|
|
126
|
+
color: #55556a; overflow: hidden; }
|
|
127
|
+
.row .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
128
|
+
.row .body { min-width: 0; flex: 1; }
|
|
129
|
+
.row .name { color: #e6e6ea; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
130
|
+
.row .meta { color: #6a6a7a; font-size: 11px; overflow: hidden; text-overflow: ellipsis;
|
|
131
|
+
white-space: nowrap; }
|
|
132
|
+
.row .chip { display: inline-block; margin-left: 5px; padding: 0 5px; border-radius: 3px;
|
|
133
|
+
font-size: 10px; background: #2a2a34; color: #9a9aab; vertical-align: 1px; }
|
|
134
|
+
.row .chip.hq { background: #1f3a2a; color: #8fd0a4; }
|
|
135
|
+
.row .chip.placeholder { background: #3a3320; color: #d9c08a; }
|
|
136
|
+
/* A new asset during a forge is the whole show, so it announces itself and then settles. */
|
|
137
|
+
.row.fresh { animation: landed 2.4s ease-out; }
|
|
138
|
+
@keyframes landed { from { background: #22314f; } to { background: transparent; } }
|
|
139
|
+
.row.job .thumb { color: #d9a441; }
|
|
140
|
+
.row.job .message { color: #9a9aab; font-size: 11px; overflow: hidden; text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap; }
|
|
142
|
+
.row.job.failed .thumb { color: #e0564f; }
|
|
143
|
+
.row.job.failed .message { color: #e0938e; white-space: normal; }
|
|
144
|
+
.spin { display: inline-block; animation: spin 1.4s linear infinite; }
|
|
145
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
112
146
|
#hq-overlay { display: none; position: fixed; inset: 0; background: rgba(8,8,12,.72);
|
|
113
147
|
align-items: center; justify-content: center; z-index: 10; }
|
|
114
148
|
#hq-overlay.show { display: flex; }
|
|
@@ -139,6 +173,7 @@ export function renderEditorShell(options) {
|
|
|
139
173
|
<input id="auto-toggle" type="checkbox" checked> Auto-reload
|
|
140
174
|
</label>
|
|
141
175
|
<span id="status"><span id="dot"></span><span id="status-text">Loading…</span></span>
|
|
176
|
+
<button id="assets-toggle" type="button" title="Show the assets in this project and what is being generated">Assets</button>
|
|
142
177
|
<button id="shot" type="button" title="Copy the current frame to the clipboard">Screenshot</button>
|
|
143
178
|
<button id="reload" type="button">Reload</button>
|
|
144
179
|
</div>
|
|
@@ -156,7 +191,15 @@ export function renderEditorShell(options) {
|
|
|
156
191
|
voxel changes will be lost. Discard them, or press Cancel in the terrain toolbar.</span>
|
|
157
192
|
<button id="terrain-discard" type="button">Discard terrain edits</button>
|
|
158
193
|
</div>
|
|
159
|
-
<
|
|
194
|
+
<div id="stage">
|
|
195
|
+
<iframe id="frame" allow="autoplay; fullscreen; xr-spatial-tracking; clipboard-write"></iframe>
|
|
196
|
+
<aside id="assets">
|
|
197
|
+
<div id="assets-head">Assets<span class="count" id="assets-count"></span></div>
|
|
198
|
+
<div id="assets-jobs"></div>
|
|
199
|
+
<div id="assets-list"></div>
|
|
200
|
+
<div id="assets-empty">Nothing yet. Assets appear here as they are generated.</div>
|
|
201
|
+
</aside>
|
|
202
|
+
</div>
|
|
160
203
|
<div id="hq-overlay">
|
|
161
204
|
<div id="hq-dialog">
|
|
162
205
|
<h2>Generate a high-quality <span id="hq-name"></span></h2>
|
|
@@ -181,6 +224,7 @@ export function renderEditorShell(options) {
|
|
|
181
224
|
var ACTIVE_EDIT_MS = ${ACTIVE_EDIT_WINDOW_MS};
|
|
182
225
|
var TAB_KEY = 'bitmagic.dev.tab';
|
|
183
226
|
var AUTO_KEY = 'bitmagic.dev.autoReload';
|
|
227
|
+
var ASSETS_KEY = 'bitmagic.dev.assets';
|
|
184
228
|
|
|
185
229
|
var frame = document.getElementById('frame');
|
|
186
230
|
var dot = document.getElementById('dot');
|
|
@@ -282,9 +326,8 @@ export function renderEditorShell(options) {
|
|
|
282
326
|
// Re-read rather than serving the copy captured at boot: an agent may have generated an
|
|
283
327
|
// asset since this page loaded, and a stale list is an empty asset palette with no
|
|
284
328
|
// explanation.
|
|
285
|
-
fetch('/api/
|
|
286
|
-
|
|
287
|
-
postFlat({ type: 'ASSETS_RESPONSE', assets: (data && data.assets) || [] });
|
|
329
|
+
fetch('/api/assets').then(function (r) { return r.json(); }).then(function (payload) {
|
|
330
|
+
postFlat({ type: 'ASSETS_RESPONSE', assets: withThumbnails(payload.assets || [], payload.previews || []) });
|
|
288
331
|
}).catch(function () {
|
|
289
332
|
postFlat({ type: 'ASSETS_RESPONSE', assets: [] });
|
|
290
333
|
});
|
|
@@ -517,6 +560,9 @@ export function renderEditorShell(options) {
|
|
|
517
560
|
loaded = false;
|
|
518
561
|
markersDirty = false;
|
|
519
562
|
gameState = '';
|
|
563
|
+
// A fresh iframe is a fresh engine with no memory of what was glowing, so the next push must
|
|
564
|
+
// not be suppressed as a repeat of what the PREVIOUS one was told.
|
|
565
|
+
lastGlowing = '';
|
|
520
566
|
banner.classList.remove('show');
|
|
521
567
|
terrainBanner.classList.remove('show');
|
|
522
568
|
setStatus('', 'Loading…');
|
|
@@ -559,6 +605,10 @@ export function renderEditorShell(options) {
|
|
|
559
605
|
|
|
560
606
|
loaded = true;
|
|
561
607
|
applyTab(activeTab);
|
|
608
|
+
// A boot almost always follows a world.json change, so the list has moved with it. Deliberately
|
|
609
|
+
// not resetting the seen-ids set: the asset that caused the reload is exactly the one worth
|
|
610
|
+
// flashing.
|
|
611
|
+
void refreshAssets();
|
|
562
612
|
}
|
|
563
613
|
|
|
564
614
|
async function flush() {
|
|
@@ -660,6 +710,347 @@ export function renderEditorShell(options) {
|
|
|
660
710
|
start();
|
|
661
711
|
}
|
|
662
712
|
|
|
713
|
+
/**
|
|
714
|
+
* The assets panel: what this project HAS, and what is being made for it right now.
|
|
715
|
+
*
|
|
716
|
+
* The second half is the reason it exists. Generating a game is mostly waiting — a forge bakes a
|
|
717
|
+
* level over twenty minutes, a prop takes a few — and during all of it the world on screen has
|
|
718
|
+
* not changed yet, so the dev view used to look identical to an idle one. The jobs come from
|
|
719
|
+
* '.bitmagic/jobs/', which every generate command writes, so a 'bitmagic generate' running in the
|
|
720
|
+
* creator's OTHER window shows up here with its live progress line.
|
|
721
|
+
*
|
|
722
|
+
* Fed by the sidecar's 'assets' SSE event (a bare "the list moved" ping — the panel re-fetches
|
|
723
|
+
* rather than the server diffing for it) with a slow poll behind it, because elapsed times have
|
|
724
|
+
* to tick even when nothing changed.
|
|
725
|
+
*/
|
|
726
|
+
var assetsOpen = true;
|
|
727
|
+
var assetsList = document.getElementById('assets-list');
|
|
728
|
+
var assetsJobs = document.getElementById('assets-jobs');
|
|
729
|
+
/** This page's own origin, for the absolute URL the game's asset palette needs. */
|
|
730
|
+
var SHELL_ORIGIN = window.location.origin;
|
|
731
|
+
var assetsEmpty = document.getElementById('assets-empty');
|
|
732
|
+
var assetsCount = document.getElementById('assets-count');
|
|
733
|
+
var assetsToggle = document.getElementById('assets-toggle');
|
|
734
|
+
// Ids rendered on the previous pass, so an asset that has just landed can announce itself. Null
|
|
735
|
+
// until the first render: on a cold open EVERY asset is new, and flashing all of them says
|
|
736
|
+
// nothing.
|
|
737
|
+
var seenAssetIds = null;
|
|
738
|
+
var lastJobs = [];
|
|
739
|
+
var assetsTimer = null;
|
|
740
|
+
|
|
741
|
+
var TYPE_GLYPHS = {
|
|
742
|
+
image: '🖼', skybox: '🖼',
|
|
743
|
+
glb: '🧊', gltf: '🧊', 'polygon-mesh': '🧊', vxl: '🧊', voxels: '🧊',
|
|
744
|
+
animation: '🏃', audio: '🔊', sound: '🔊', 'sound-effect': '🔊',
|
|
745
|
+
vwld: '🗺', spz: '✨'
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
function glyphFor(type) {
|
|
749
|
+
return TYPE_GLYPHS[String(type || '').toLowerCase()] || '📦';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function formatSize(bytes) {
|
|
753
|
+
if (typeof bytes !== 'number' || !isFinite(bytes) || bytes <= 0) return '';
|
|
754
|
+
if (bytes < 1024) return bytes + ' B';
|
|
755
|
+
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
|
756
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function formatElapsed(since) {
|
|
760
|
+
var ms = Date.now() - Date.parse(since);
|
|
761
|
+
if (!isFinite(ms) || ms < 0) return '';
|
|
762
|
+
var seconds = Math.floor(ms / 1000);
|
|
763
|
+
if (seconds < 60) return seconds + 's';
|
|
764
|
+
return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** When an asset was made, or 0 for the ones that came with the template. */
|
|
768
|
+
function madeAt(asset) {
|
|
769
|
+
var stamp = asset.hqGeneratedAt || (asset.production && asset.production.at);
|
|
770
|
+
var parsed = typeof stamp === 'string' ? Date.parse(stamp) : NaN;
|
|
771
|
+
return isFinite(parsed) ? parsed : 0;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Newest first, but only among assets that know when they were made.
|
|
776
|
+
*
|
|
777
|
+
* A forged world arrives with hundreds of undated assets and no meaningful order beyond the one
|
|
778
|
+
* world.json already gives them, so they keep it. The handful generated during this session are
|
|
779
|
+
* the ones the creator is looking for, and they go on top.
|
|
780
|
+
*/
|
|
781
|
+
function sortAssets(assets) {
|
|
782
|
+
var dated = [];
|
|
783
|
+
var undated = [];
|
|
784
|
+
for (var i = 0; i < assets.length; i++) {
|
|
785
|
+
(madeAt(assets[i]) > 0 ? dated : undated).push(assets[i]);
|
|
786
|
+
}
|
|
787
|
+
dated.sort(function (a, b) { return madeAt(b) - madeAt(a); });
|
|
788
|
+
return dated.concat(undated);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function el(tag, className, text) {
|
|
792
|
+
var node = document.createElement(tag);
|
|
793
|
+
if (className) node.className = className;
|
|
794
|
+
// textContent throughout: names, prompts and error strings are creator and model input, and
|
|
795
|
+
// this panel is not a place to start interpreting markup found in them.
|
|
796
|
+
if (text !== undefined) node.textContent = text;
|
|
797
|
+
return node;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function jobRow(job) {
|
|
801
|
+
var row = el('div', 'row job' + (job.status === 'failed' ? ' failed' : ''));
|
|
802
|
+
var thumb = el('div', 'thumb');
|
|
803
|
+
thumb.appendChild(el('span', job.status === 'failed' ? '' : 'spin', job.status === 'failed' ? '!' : '⟳'));
|
|
804
|
+
row.appendChild(thumb);
|
|
805
|
+
var body = el('div', 'body');
|
|
806
|
+
body.appendChild(el('div', 'name', job.label || job.kind));
|
|
807
|
+
var meta = el('div', 'meta', job.kind + (job.status === 'failed' ? '' : ' · ' + formatElapsed(job.startedAt)));
|
|
808
|
+
body.appendChild(meta);
|
|
809
|
+
body.appendChild(el('div', 'message', job.status === 'failed' ? (job.error || 'Failed') : (job.message || '')));
|
|
810
|
+
row.appendChild(body);
|
|
811
|
+
return row;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The URL the engine should draw a thumbnail from, or null when there is nothing to draw.
|
|
816
|
+
*
|
|
817
|
+
* 'sourceGlbUrl' before 'url' because most CLI-lane meshes are voxelized: 'url' is then a .vxl,
|
|
818
|
+
* which the GLB preview renderer cannot read, while 'sourceGlbUrl' is the original the forger or
|
|
819
|
+
* the generator started from. An asset with neither — a sound, an animation, a level — keeps its
|
|
820
|
+
* glyph, which is the honest answer for something with no appearance.
|
|
821
|
+
*/
|
|
822
|
+
function previewSourceFor(asset) {
|
|
823
|
+
var url = asset.sourceGlbUrl || asset.url;
|
|
824
|
+
if (typeof url !== 'string' || url === '') return null;
|
|
825
|
+
// Double-escaped because this whole page is a template literal: a single backslash is consumed
|
|
826
|
+
// by the literal, and the browser would receive the invalid regex /.(glb|gltf)(?|$)/.
|
|
827
|
+
return /\\.(glb|gltf)(\\?|$)/i.test(url) ? url : null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function assetRow(asset, isFresh, hasPreview) {
|
|
831
|
+
var row = el('div', 'row' + (isFresh ? ' fresh' : ''));
|
|
832
|
+
var thumb = el('div', 'thumb');
|
|
833
|
+
var type = String(asset.type || '').toLowerCase();
|
|
834
|
+
if (type === 'image' && typeof asset.url === 'string' && asset.url !== '') {
|
|
835
|
+
// An image asset IS its own thumbnail — skyboxes and cover art included. Nothing to render.
|
|
836
|
+
var direct = document.createElement('img');
|
|
837
|
+
direct.src = asset.url;
|
|
838
|
+
direct.alt = '';
|
|
839
|
+
thumb.appendChild(direct);
|
|
840
|
+
} else if (hasPreview) {
|
|
841
|
+
var cached = document.createElement('img');
|
|
842
|
+
cached.src = '/api/assets/preview/' + encodeURIComponent(asset.id) + '.png';
|
|
843
|
+
cached.alt = '';
|
|
844
|
+
// A cached file that has since been deleted falls back rather than showing a broken image.
|
|
845
|
+
cached.onerror = function () { thumb.textContent = glyphFor(asset.type); };
|
|
846
|
+
thumb.appendChild(cached);
|
|
847
|
+
} else {
|
|
848
|
+
thumb.appendChild(el('span', '', glyphFor(asset.type)));
|
|
849
|
+
if (previewSourceFor(asset) !== null) queuePreview(asset);
|
|
850
|
+
}
|
|
851
|
+
row.appendChild(thumb);
|
|
852
|
+
|
|
853
|
+
var body = el('div', 'body');
|
|
854
|
+
var name = el('div', 'name', asset.name || asset.id || 'unnamed');
|
|
855
|
+
if (asset.hqGeneratedAt) name.appendChild(el('span', 'chip hq', 'HQ'));
|
|
856
|
+
else if (asset.placeholder === true) name.appendChild(el('span', 'chip placeholder', 'placeholder'));
|
|
857
|
+
body.appendChild(name);
|
|
858
|
+
var bits = [asset.type || 'unknown'];
|
|
859
|
+
var size = formatSize(asset.size);
|
|
860
|
+
if (size) bits.push(size);
|
|
861
|
+
body.appendChild(el('div', 'meta', bits.join(' · ')));
|
|
862
|
+
row.appendChild(body);
|
|
863
|
+
return row;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Jobs and assets render separately, and that split is not cosmetic.
|
|
868
|
+
*
|
|
869
|
+
* The job rows carry an elapsed time, so they have to be redrawn every second whether or not
|
|
870
|
+
* anything changed. The asset rows must NOT be: they hold <img> elements, a forged world has
|
|
871
|
+
* hundreds of them, and rebuilding that list once a second would refetch and visibly flicker
|
|
872
|
+
* every thumbnail for as long as a generation runs — which is precisely when the creator is
|
|
873
|
+
* looking at it. So the asset list is rebuilt only when the set it is showing actually moved.
|
|
874
|
+
*/
|
|
875
|
+
var lastAssetSignature = null;
|
|
876
|
+
|
|
877
|
+
function renderAssets(assets, jobs, previews) {
|
|
878
|
+
var sorted = sortAssets(assets);
|
|
879
|
+
var cached = {};
|
|
880
|
+
for (var p = 0; p < previews.length; p++) cached[previews[p]] = true;
|
|
881
|
+
|
|
882
|
+
assetsJobs.textContent = '';
|
|
883
|
+
for (var j = 0; j < jobs.length; j++) assetsJobs.appendChild(jobRow(jobs[j]));
|
|
884
|
+
|
|
885
|
+
var ids = {};
|
|
886
|
+
var signature = [];
|
|
887
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
888
|
+
ids[sorted[i].id] = true;
|
|
889
|
+
// The preview state is part of the signature: a thumbnail landing is a change worth redrawing
|
|
890
|
+
// for, and it is the only one that alters a row without altering the asset set.
|
|
891
|
+
signature.push(sorted[i].id + (cached[sorted[i].id] ? '*' : ''));
|
|
892
|
+
}
|
|
893
|
+
var joined = signature.join(',');
|
|
894
|
+
|
|
895
|
+
if (joined !== lastAssetSignature) {
|
|
896
|
+
assetsList.textContent = '';
|
|
897
|
+
for (var k = 0; k < sorted.length; k++) {
|
|
898
|
+
var fresh = seenAssetIds !== null && !seenAssetIds[sorted[k].id];
|
|
899
|
+
assetsList.appendChild(assetRow(sorted[k], fresh, cached[sorted[k].id] === true));
|
|
900
|
+
}
|
|
901
|
+
seenAssetIds = ids;
|
|
902
|
+
lastAssetSignature = joined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
lastJobs = jobs;
|
|
906
|
+
if (loaded) pushGeneratingAssets(jobs);
|
|
907
|
+
assetsCount.textContent = String(sorted.length);
|
|
908
|
+
assetsEmpty.style.display = (sorted.length === 0 && jobs.length === 0) ? 'block' : 'none';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Ask the game to draw the thumbnails the cache is missing — one at a time, and never twice for
|
|
913
|
+
* the same asset in one session.
|
|
914
|
+
*
|
|
915
|
+
* Serial on purpose. Each render is a GLB fetch and a draw in the SAME renderer the creator is
|
|
916
|
+
* playing in, and a forged city has hundreds of assets: firing them all at once would stutter the
|
|
917
|
+
* game to decorate a side panel. 'attempted' covers the failures too, so an asset whose GLB 404s
|
|
918
|
+
* is asked for once rather than on every render pass forever.
|
|
919
|
+
*/
|
|
920
|
+
var previewQueue = [];
|
|
921
|
+
var previewAttempted = {};
|
|
922
|
+
var previewRunning = false;
|
|
923
|
+
var previewRequestId = 0;
|
|
924
|
+
|
|
925
|
+
function queuePreview(asset) {
|
|
926
|
+
if (previewAttempted[asset.id]) return;
|
|
927
|
+
previewAttempted[asset.id] = true;
|
|
928
|
+
previewQueue.push(asset);
|
|
929
|
+
void drainPreviewQueue();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function drainPreviewQueue() {
|
|
933
|
+
if (previewRunning) return;
|
|
934
|
+
previewRunning = true;
|
|
935
|
+
var stored = 0;
|
|
936
|
+
try {
|
|
937
|
+
while (previewQueue.length > 0) {
|
|
938
|
+
// The engine can only draw once it exists. Anything queued before then waits for the next
|
|
939
|
+
// pass rather than being dropped — refreshAssets re-queues on every render.
|
|
940
|
+
if (!loaded) return;
|
|
941
|
+
var asset = previewQueue.shift();
|
|
942
|
+
var source = previewSourceFor(asset);
|
|
943
|
+
if (source === null) continue;
|
|
944
|
+
|
|
945
|
+
previewRequestId += 1;
|
|
946
|
+
var requestId = 'preview-' + previewRequestId;
|
|
947
|
+
postFlat({ type: 'GENERATE_GLB_PREVIEW', requestId: requestId, glbUrl: source });
|
|
948
|
+
var response = await await_('GLB_PREVIEW_RESPONSE', 15000);
|
|
949
|
+
// Verified rather than assumed: a late answer from a request that already timed out would
|
|
950
|
+
// otherwise be cached under the NEXT asset's id, which is a wrong picture rather than a
|
|
951
|
+
// missing one — much the worse failure.
|
|
952
|
+
if (!response || response.requestId !== requestId) continue;
|
|
953
|
+
if (!response.success || !response.previewBase64) continue;
|
|
954
|
+
|
|
955
|
+
var saved = await (await fetch('/api/assets/preview', {
|
|
956
|
+
method: 'POST',
|
|
957
|
+
headers: { 'Content-Type': 'application/json' },
|
|
958
|
+
body: JSON.stringify({ assetId: asset.id, image: response.previewBase64 })
|
|
959
|
+
})).json();
|
|
960
|
+
if (saved.stored === true) stored += 1;
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
// A thumbnail is a decoration; losing the queue must not take the panel with it.
|
|
964
|
+
} finally {
|
|
965
|
+
previewRunning = false;
|
|
966
|
+
}
|
|
967
|
+
// Only when something actually landed. refreshAssets kicks this queue in turn, so an
|
|
968
|
+
// unconditional re-render here would be an endless render/drain ping-pong over an empty queue.
|
|
969
|
+
if (stored > 0) void refreshAssets();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function refreshAssets() {
|
|
973
|
+
if (!assetsOpen) return;
|
|
974
|
+
try {
|
|
975
|
+
var payload = await (await fetch('/api/assets')).json();
|
|
976
|
+
renderAssets(payload.assets || [], payload.jobs || [], payload.previews || []);
|
|
977
|
+
// Rendering is what discovers the missing thumbnails, and anything queued while the game was
|
|
978
|
+
// still booting has been sitting there since. Guarded on both sides, so this is a no-op
|
|
979
|
+
// whenever there is nothing to draw.
|
|
980
|
+
void drainPreviewQueue();
|
|
981
|
+
} catch (error) {
|
|
982
|
+
// The sidecar is this page's own server. If it is gone the reload button is the only useful
|
|
983
|
+
// action left, and the status bar already says so.
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* A slow tick behind the SSE push, for the two things an event cannot cover: elapsed times, which
|
|
989
|
+
* change with no write anywhere, and a missed event on a stream that dropped.
|
|
990
|
+
*/
|
|
991
|
+
function startAssetsTicker() {
|
|
992
|
+
if (assetsTimer) clearInterval(assetsTimer);
|
|
993
|
+
assetsTimer = setInterval(function () {
|
|
994
|
+
if (!assetsOpen) return;
|
|
995
|
+
// Cheap while idle: with nothing generating there is nothing whose label changes on its own.
|
|
996
|
+
if (lastJobs.length === 0) return;
|
|
997
|
+
void refreshAssets();
|
|
998
|
+
}, 1000);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Lend the cached thumbnails to the engine's own asset palette.
|
|
1003
|
+
*
|
|
1004
|
+
* Right-click in the editor offers "Add object", and that grid renders 'asset.screenshotUrl'
|
|
1005
|
+
* (SceneEditor.buildAssetGrid) — a field only the web Creator ever writes, so in a CLI project
|
|
1006
|
+
* every tile there reads "No Preview". The panel has just rendered those images anyway, so
|
|
1007
|
+
* handing them over on the way past costs nothing and needs no change under game/.
|
|
1008
|
+
*
|
|
1009
|
+
* Copies rather than mutating: this list is served straight out of world.json, and putting a
|
|
1010
|
+
* local URL onto the records the save path also reads would be a field the CLI invented leaking
|
|
1011
|
+
* into the creator's file.
|
|
1012
|
+
*/
|
|
1013
|
+
function withThumbnails(assets, previews) {
|
|
1014
|
+
var cached = {};
|
|
1015
|
+
for (var i = 0; i < previews.length; i++) cached[previews[i]] = true;
|
|
1016
|
+
return assets.map(function (asset) {
|
|
1017
|
+
if (asset.screenshotUrl || !cached[asset.id]) return asset;
|
|
1018
|
+
var copy = {};
|
|
1019
|
+
for (var key in asset) if (Object.prototype.hasOwnProperty.call(asset, key)) copy[key] = asset[key];
|
|
1020
|
+
copy.screenshotUrl = SHELL_ORIGIN + '/api/assets/preview/' + encodeURIComponent(asset.id) + '.png';
|
|
1021
|
+
return copy;
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Make the meshes being regenerated glow in the world itself.
|
|
1027
|
+
*
|
|
1028
|
+
* The engine has handled this message since before the CLI lane existed — HqGeneratingHandler
|
|
1029
|
+
* maps asset ids to their placed instances and drives HqGenerationGlow over them — and the web
|
|
1030
|
+
* Creator has always sent it. Nothing in the CLI ever did, so a 'generate prop' upgrading a
|
|
1031
|
+
* building gave no sign in the scene. The job records now carry the asset id, so it is free.
|
|
1032
|
+
*/
|
|
1033
|
+
var lastGlowing = '';
|
|
1034
|
+
function pushGeneratingAssets(jobs) {
|
|
1035
|
+
var ids = [];
|
|
1036
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
1037
|
+
if (jobs[i].status === 'running' && jobs[i].assetId) ids.push(jobs[i].assetId);
|
|
1038
|
+
}
|
|
1039
|
+
// The engine walks every mesh on a change, so it is worth not repeating ourselves — its own
|
|
1040
|
+
// setGeneratingHqAssetIds returns early on an unchanged set, and this saves even the message.
|
|
1041
|
+
var key = ids.join(',');
|
|
1042
|
+
if (key === lastGlowing) return;
|
|
1043
|
+
lastGlowing = key;
|
|
1044
|
+
postFlat({ type: 'HQ_ASSET_GENERATING', assetIds: ids });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function setAssetsOpen(open) {
|
|
1048
|
+
assetsOpen = open;
|
|
1049
|
+
document.body.classList.toggle('assets-closed', !open);
|
|
1050
|
+
writePref(ASSETS_KEY, open ? '1' : '0');
|
|
1051
|
+
if (open) void refreshAssets();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
663
1054
|
/**
|
|
664
1055
|
* Screenshot → clipboard.
|
|
665
1056
|
*
|
|
@@ -802,6 +1193,12 @@ export function renderEditorShell(options) {
|
|
|
802
1193
|
try { payload = JSON.parse(event.data); } catch (error) { /* reason is a label */ }
|
|
803
1194
|
onChangeSignal(payload.reason || 'change');
|
|
804
1195
|
});
|
|
1196
|
+
// A bare "the list moved" ping: a generation started, made progress, failed, or world.json was
|
|
1197
|
+
// rewritten. The panel re-reads rather than the server sending a diff, which keeps the whole
|
|
1198
|
+
// question of what changed on the side that is about to render it.
|
|
1199
|
+
events.addEventListener('assets', function () {
|
|
1200
|
+
void refreshAssets();
|
|
1201
|
+
});
|
|
805
1202
|
events.addEventListener('build', function (event) {
|
|
806
1203
|
var payload = {};
|
|
807
1204
|
try { payload = JSON.parse(event.data); } catch (error) { /* see above */ }
|
|
@@ -835,8 +1232,12 @@ export function renderEditorShell(options) {
|
|
|
835
1232
|
if (autoReload) banner.classList.remove('show');
|
|
836
1233
|
});
|
|
837
1234
|
|
|
1235
|
+
assetsToggle.addEventListener('click', function () { setAssetsOpen(!assetsOpen); });
|
|
1236
|
+
|
|
838
1237
|
autoReload = readPref(AUTO_KEY, '1') !== '0';
|
|
839
1238
|
autoToggle.checked = autoReload;
|
|
1239
|
+
setAssetsOpen(readPref(ASSETS_KEY, '1') !== '0');
|
|
1240
|
+
startAssetsTicker();
|
|
840
1241
|
// Applied through selectTab so the button states and the stored preference cannot disagree with
|
|
841
1242
|
// the variable. 'loaded' is false here, so it only sets up the UI — boot() applies the mode.
|
|
842
1243
|
activeTab = readPref(TAB_KEY, 'game') === 'editor' ? 'editor' : 'game';
|
|
@@ -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":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA2F3B,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAwD9B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;kBACxB,gBAAgB;yBACT,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmgC7C,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Which command is running. Shown as-is in the dev view, so these are creator-facing words. */
|
|
2
|
+
export type JobKind = 'skybox' | 'sound' | 'image' | 'character' | 'animation' | 'prop' | 'vehicle' | 'forge' | 'cover' | 'hq';
|
|
3
|
+
export interface JobRecord {
|
|
4
|
+
jobId: string;
|
|
5
|
+
kind: JobKind;
|
|
6
|
+
/** What the creator asked for — a prompt or a description. Trimmed for display, not parsed. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Known up front only for the kinds that upgrade an asset in place (prop, vehicle, hq). */
|
|
9
|
+
assetId?: string;
|
|
10
|
+
status: 'running' | 'failed';
|
|
11
|
+
/** The generation's most recent progress line. See `withJobRecord`. */
|
|
12
|
+
message: string;
|
|
13
|
+
pid: number;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const JOBS_DIR: string;
|
|
19
|
+
export declare function jobsDirPath(root: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Every job this project knows about, newest first, with dead `running` records reported as
|
|
22
|
+
* failed rather than left spinning in the dev view forever.
|
|
23
|
+
*
|
|
24
|
+
* Unreadable and unparseable files are skipped, not thrown: a record being written right now is a
|
|
25
|
+
* normal thing for a directory that several processes append to, and losing one tick of one job is
|
|
26
|
+
* not worth failing a read that also carries five healthy ones.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readJobs(root: string): JobRecord[];
|
|
29
|
+
/**
|
|
30
|
+
* Drop failed records older than a day. Called by `dev` at startup rather than on a timer: this is
|
|
31
|
+
* housekeeping, and the moment someone opens the dev view is the moment a day-old failure stops
|
|
32
|
+
* being worth showing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pruneJobs(root: string, now?: number): number;
|
|
35
|
+
export interface JobHandle {
|
|
36
|
+
readonly jobId: string;
|
|
37
|
+
/** Record the latest progress line. */
|
|
38
|
+
update(message: string): void;
|
|
39
|
+
}
|
|
40
|
+
export interface StartJobOptions {
|
|
41
|
+
root: string;
|
|
42
|
+
kind: JobKind;
|
|
43
|
+
label: string;
|
|
44
|
+
assetId?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function startJob(options: StartJobOptions): JobHandle;
|
|
47
|
+
/**
|
|
48
|
+
* Run `body` with its progress recorded for anyone watching.
|
|
49
|
+
*
|
|
50
|
+
* The `log` handed to the body is the caller's own logger with a tap on it, so every line a command
|
|
51
|
+
* already prints also becomes the job's current message. That is deliberate reuse rather than a
|
|
52
|
+
* second progress vocabulary to keep in step — the dev sidecar's in-process HQ job has worked this
|
|
53
|
+
* way since it shipped, and it means a command gains live progress in the dev view without a single
|
|
54
|
+
* new call site inside it.
|
|
55
|
+
*
|
|
56
|
+
* **Never fails the generation it is watching.** Bookkeeping that breaks a paid, minutes-long
|
|
57
|
+
* generation would be a far worse bug than the missing progress line it is trying to provide, so
|
|
58
|
+
* every filesystem error here is swallowed. The body's own errors propagate untouched, after being
|
|
59
|
+
* recorded.
|
|
60
|
+
*/
|
|
61
|
+
export declare function withJobRecord<T>(options: StartJobOptions & {
|
|
62
|
+
log: (message: string) => void;
|
|
63
|
+
}, body: (log: (message: string) => void) => Promise<T>): Promise<T>;
|
|
64
|
+
/** Success: the asset is in world.json, and that is the record. */
|
|
65
|
+
export declare function removeJob(root: string, jobId: string): void;
|
|
66
|
+
/** Failure: nothing else records it, so the record stays and carries the reason. */
|
|
67
|
+
export declare function failJob(root: string, jobId: string, error: string): void;
|