@vgai/editor 0.4.0-canary.20260715.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/assets/{axe-CyiTVRi6.js → axe-BJjvBeOZ.js} +1 -1
  2. package/dist/assets/{browserAll-fGZ_P1P8.js → browserAll-Dn0pgD87.js} +1 -1
  3. package/dist/assets/{create-runtime-BzSHBTYw.js → create-runtime-BF9OD-U8.js} +1 -1
  4. package/dist/assets/{game-NUAnyPkS.js → game-B346oj_f.js} +1 -1
  5. package/dist/assets/{game-uR_lv3Mp.js → game-BCNN63uV.js} +1 -1
  6. package/dist/assets/{game-HqybI_X7.js → game-BR0GBNgF.js} +1 -1
  7. package/dist/assets/{game-DQ7YHNS8.js → game-CLNm2g0y.js} +1 -1
  8. package/dist/assets/{game-DX6Obomf.js → game-DQxhCYsZ.js} +1 -1
  9. package/dist/assets/{game-DoUJj8vY.js → game-DeTGdgYt.js} +1 -1
  10. package/dist/assets/{game-loop-o2CcBMFY.js → game-loop-t5W_oqxz.js} +1 -1
  11. package/dist/assets/{game-source-C8HM7gbP.js → game-source-DzE9taML.js} +1 -1
  12. package/dist/assets/{game-ck_dNIE6.js → game-u6c2Patu.js} +1 -1
  13. package/dist/assets/{game-hIf5LD4E.js → game-umrHIqFB.js} +1 -1
  14. package/dist/assets/{index-CoSRCKPS.js → index-CgcH1RpF.js} +1 -1
  15. package/dist/assets/{index-D93bBuUl.js → index-D3tcNB2T.js} +1 -1
  16. package/dist/assets/{index-B-HA1uu3.js → index-DbQzGLxv.js} +769 -769
  17. package/dist/assets/{index-CSBAZwMx.js → index-Fc8Ezr20.js} +1 -1
  18. package/dist/assets/index-bAGhEnnO.css +1 -0
  19. package/dist/assets/{ingest-siblings-CCkkE2h7.js → ingest-siblings-CW_xbvrF.js} +1 -1
  20. package/dist/assets/{mount-manifest-B2ECssU2.js → mount-manifest-DgUscyaD.js} +1 -1
  21. package/dist/assets/{react-18-Bmj05S8J.js → react-18-C4tVCii_.js} +1 -1
  22. package/dist/assets/webworkerAll-BA4aq7xq.js +1 -0
  23. package/dist/assets/{world-documents-DXPcAEA0.js → world-documents-BSaA5YIF.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/package.json +3 -3
  26. package/src/asset-editor-context.tsx +130 -0
  27. package/src/asset-preview.ts +4 -0
  28. package/src/asset-selection.ts +9 -4
  29. package/src/asset-workflow/thumbnail-system.ts +16 -7
  30. package/src/components/AssetBrowser.tsx +15 -7
  31. package/src/components/AssetEditorShell.tsx +42 -112
  32. package/src/components/AssetImportDetails.tsx +24 -6
  33. package/src/components/AssetLibraryPanel.tsx +1 -1
  34. package/src/components/GameHierarchy.tsx +26 -1
  35. package/src/components/Inspector.tsx +38 -2
  36. package/src/components/OnlineAssetBrowser.tsx +40 -409
  37. package/src/components/WorkspaceDock.tsx +6 -12
  38. package/src/components/asset-documents.tsx +41 -12
  39. package/src/components/asset-selection-section.tsx +34 -1
  40. package/src/components/asset-viewers/ModelViewer.tsx +7 -4
  41. package/src/components/asset-viewers/OnlineAssetDetail.tsx +233 -205
  42. package/src/components/asset-workflow.css +39 -220
  43. package/src/editor-api.ts +1 -1
  44. package/template/vgai.game.json +1 -1
  45. package/dist/assets/index-bu01fqch.css +0 -1
  46. package/dist/assets/webworkerAll-BC6gMOLZ.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { faCheck, faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
3
3
  import { Button, EditorIcon, SectionHeader, Select, themeVars } from '../../design-system';
4
4
  import {
5
5
  type AssetFileOption,
@@ -9,7 +9,10 @@ import {
9
9
  type OnlineAssetPreview,
10
10
  } from '../../editor-api';
11
11
  import type { OnlineAssetInfo } from '../../editor-store';
12
- import { ModelViewer } from './ModelViewer';
12
+
13
+ const ModelViewer = lazy(() =>
14
+ import('./ModelViewer').then((module) => ({ default: module.ModelViewer })),
15
+ );
13
16
 
14
17
  const selectStyle: React.CSSProperties = {
15
18
  background: themeVars.surface.inset,
@@ -31,14 +34,17 @@ function formatBytes(bytes: number): string {
31
34
 
32
35
  interface Props {
33
36
  online: OnlineAssetInfo;
37
+ /** The Library Inspector and center Asset Editor reuse one acquisition
38
+ * implementation while placing preview and properties in the outer shell. */
39
+ mode?: 'full' | 'inspector' | 'preview';
34
40
  /** W2: called with the local serving path once the download lands — the
35
41
  * owning asset DOCUMENT (`asset-documents.tsx`) swaps to the local viewer
36
42
  * in place (was `store.resolveOnlineAssetTab` in the right-rail era). */
37
- onResolved: (localPath: string) => void;
43
+ onResolved?: (localPath: string) => void;
38
44
  }
39
45
 
40
46
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one catalog document coordinates preview resolution, metadata, format selection, and explicit project import.
41
- export function OnlineAssetDetail({ online, onResolved }: Props) {
47
+ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props) {
42
48
  const [fileOptions, setFileOptions] = useState<AssetFileOption[]>([]);
43
49
  const [selectedFile, setSelectedFile] = useState<AssetFileOption | null>(null);
44
50
  const [loadingFiles, setLoadingFiles] = useState(true);
@@ -52,6 +58,10 @@ export function OnlineAssetDetail({ online, onResolved }: Props) {
52
58
  );
53
59
 
54
60
  useEffect(() => {
61
+ if (mode === 'preview') {
62
+ setLoadingFiles(false);
63
+ return;
64
+ }
55
65
  let cancelled = false;
56
66
  setLoadingFiles(true);
57
67
  setFilesError(null);
@@ -77,18 +87,22 @@ export function OnlineAssetDetail({ online, onResolved }: Props) {
77
87
  return () => {
78
88
  cancelled = true;
79
89
  };
80
- }, [online.source, online.id]);
90
+ }, [mode, online.source, online.id]);
81
91
 
82
- const supportsAssetLabPreview =
92
+ const supportsInteractivePreview =
83
93
  online.source === 'local' &&
84
94
  (online.type === 'model' || online.type === 'animation' || online.type === 'source');
85
95
 
86
96
  useEffect(() => {
97
+ if (mode === 'inspector') {
98
+ setLoadingPreview(false);
99
+ return;
100
+ }
87
101
  let cancelled = false;
88
102
  setPreview(null);
89
103
  setPreviewUnavailable(false);
90
104
  setThumbnailUnavailable(false);
91
- if (!supportsAssetLabPreview) {
105
+ if (!supportsInteractivePreview) {
92
106
  setLoadingPreview(false);
93
107
  return;
94
108
  }
@@ -108,7 +122,7 @@ export function OnlineAssetDetail({ online, onResolved }: Props) {
108
122
  return () => {
109
123
  cancelled = true;
110
124
  };
111
- }, [online.id, online.source, supportsAssetLabPreview]);
125
+ }, [mode, online.id, online.source, supportsInteractivePreview]);
112
126
 
113
127
  const onDownload = useCallback(async () => {
114
128
  if (!selectedFile) return;
@@ -126,7 +140,7 @@ export function OnlineAssetDetail({ online, onResolved }: Props) {
126
140
  if (result.ok && result.path) {
127
141
  setDownloadStatus('done');
128
142
  // Switch this document to the local asset viewer
129
- onResolved(`/${result.path}`);
143
+ onResolved?.(`/${result.path}`);
130
144
  } else {
131
145
  setDownloadStatus('error');
132
146
  }
@@ -137,217 +151,231 @@ export function OnlineAssetDetail({ online, onResolved }: Props) {
137
151
 
138
152
  return (
139
153
  <div style={{ padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
140
- {/* Name */}
141
- <div
142
- style={{
143
- fontWeight: 600,
144
- fontSize: 12,
145
- color: themeVars.content.primary,
146
- wordBreak: 'break-all',
147
- }}
148
- >
149
- {online.name}
150
- </div>
151
-
152
- {/* Preview image */}
153
- <div
154
- style={{
155
- width: '100%',
156
- aspectRatio: '1',
157
- background: themeVars.surface.shell,
158
- borderRadius: themeVars.shape.small,
159
- overflow: 'hidden',
160
- display: 'flex',
161
- alignItems: 'center',
162
- justifyContent: 'center',
163
- }}
164
- >
165
- {loadingPreview ? (
166
- <div style={{ color: themeVars.content.muted, fontSize: 11 }}>
167
- <EditorIcon icon={faSpinner} spin style={{ marginRight: 5 }} />
168
- Loading Asset Editor preview…
169
- </div>
170
- ) : preview ? (
171
- <ModelViewer
172
- assetPath={preview.path}
173
- entityId={undefined}
174
- materialPath={preview.materialPath}
175
- displayName={online.name}
176
- embedded
177
- />
178
- ) : (
179
- <img
180
- src={
181
- thumbnailUnavailable
182
- ? '/__editor/asset-library/local-placeholder.svg'
183
- : online.thumbnailUrl
184
- }
185
- alt={thumbnailUnavailable ? `${online.name} preview unavailable` : online.name}
186
- onError={() => setThumbnailUnavailable(true)}
187
- style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
188
- />
189
- )}
190
- </div>
191
- {thumbnailUnavailable && (
192
- <div role="status" style={{ color: themeVars.content.dim, fontSize: 10 }}>
193
- The catalog preview could not be loaded; showing the typed fallback.
194
- </div>
195
- )}
196
- {previewUnavailable && (
197
- <div style={{ color: themeVars.content.dim, fontSize: 10 }}>
198
- Interactive preview is unavailable for this variant; showing its catalog thumbnail.
154
+ {mode !== 'preview' && (
155
+ <div
156
+ style={{
157
+ fontWeight: 600,
158
+ fontSize: 12,
159
+ color: themeVars.content.primary,
160
+ wordBreak: 'break-all',
161
+ }}
162
+ >
163
+ {online.name}
199
164
  </div>
200
165
  )}
201
166
 
202
- {/* Info section */}
203
- <SectionHeader label="Info">
204
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
205
- <div>
206
- <div
207
- style={{
208
- fontSize: 11,
209
- color: themeVars.content.dim,
210
- marginBottom: 2,
211
- }}
212
- >
213
- Source
214
- </div>
215
- <div style={{ fontSize: 11, color: themeVars.content.primary }}>
216
- {online.source === 'polyhaven'
217
- ? 'Poly Haven'
218
- : online.source === 'ambientcg'
219
- ? 'ambientCG'
220
- : 'Full Catalog'}
221
- </div>
167
+ {mode !== 'inspector' && (
168
+ <>
169
+ <div
170
+ style={{
171
+ width: '100%',
172
+ height: '100%',
173
+ minHeight: 240,
174
+ background: themeVars.surface.shell,
175
+ borderRadius: themeVars.shape.small,
176
+ overflow: 'hidden',
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ justifyContent: 'center',
180
+ }}
181
+ >
182
+ {loadingPreview ? (
183
+ <div style={{ color: themeVars.content.muted, fontSize: 11 }}>
184
+ <EditorIcon icon={faSpinner} spin style={{ marginRight: 5 }} />
185
+ Loading Asset Editor preview…
186
+ </div>
187
+ ) : preview ? (
188
+ <Suspense
189
+ fallback={<span style={{ color: themeVars.content.muted }}>Loading preview…</span>}
190
+ >
191
+ <ModelViewer
192
+ assetPath={preview.path}
193
+ entityId={undefined}
194
+ materialPath={preview.materialPath}
195
+ displayName={online.name}
196
+ embedded
197
+ />
198
+ </Suspense>
199
+ ) : (
200
+ <img
201
+ src={
202
+ thumbnailUnavailable
203
+ ? '/__editor/asset-library/local-placeholder.svg'
204
+ : online.thumbnailUrl
205
+ }
206
+ alt={thumbnailUnavailable ? `${online.name} preview unavailable` : online.name}
207
+ onError={() => setThumbnailUnavailable(true)}
208
+ style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
209
+ />
210
+ )}
222
211
  </div>
223
- <div>
224
- <div
225
- style={{
226
- fontSize: 11,
227
- color: themeVars.content.dim,
228
- marginBottom: 2,
229
- }}
230
- >
231
- Type
212
+ {thumbnailUnavailable && (
213
+ <div role="status" style={{ color: themeVars.content.dim, fontSize: 10 }}>
214
+ The catalog preview could not be loaded; showing the typed fallback.
232
215
  </div>
233
- <div style={{ fontSize: 11, color: themeVars.content.primary }}>{online.type}</div>
234
- </div>
235
- {online.tags.length > 0 && (
236
- <div>
237
- <div
238
- style={{
239
- fontSize: 11,
240
- color: themeVars.content.dim,
241
- marginBottom: 2,
242
- }}
243
- >
244
- Tags
216
+ )}
217
+ {previewUnavailable && (
218
+ <div style={{ color: themeVars.content.dim, fontSize: 10 }}>
219
+ Interactive preview is unavailable for this variant; showing its catalog thumbnail.
220
+ </div>
221
+ )}
222
+ </>
223
+ )}
224
+
225
+ {mode !== 'preview' && (
226
+ <>
227
+ <SectionHeader label="Info">
228
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
229
+ <div>
230
+ <div
231
+ style={{
232
+ fontSize: 11,
233
+ color: themeVars.content.dim,
234
+ marginBottom: 2,
235
+ }}
236
+ >
237
+ Source
238
+ </div>
239
+ <div style={{ fontSize: 11, color: themeVars.content.primary }}>
240
+ {online.source === 'polyhaven'
241
+ ? 'Poly Haven'
242
+ : online.source === 'ambientcg'
243
+ ? 'ambientCG'
244
+ : 'Full Catalog'}
245
+ </div>
246
+ </div>
247
+ <div>
248
+ <div
249
+ style={{
250
+ fontSize: 11,
251
+ color: themeVars.content.dim,
252
+ marginBottom: 2,
253
+ }}
254
+ >
255
+ Type
256
+ </div>
257
+ <div style={{ fontSize: 11, color: themeVars.content.primary }}>{online.type}</div>
245
258
  </div>
246
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
247
- {online.tags.slice(0, 12).map((tag) => (
248
- <span
249
- key={tag}
259
+ {online.tags.length > 0 && (
260
+ <div>
261
+ <div
250
262
  style={{
251
- fontSize: 10,
252
- padding: '1px 5px',
253
- background: themeVars.surface.raised,
254
- borderRadius: themeVars.shape.small,
255
- color: themeVars.content.muted,
263
+ fontSize: 11,
264
+ color: themeVars.content.dim,
265
+ marginBottom: 2,
256
266
  }}
257
267
  >
258
- {tag}
259
- </span>
260
- ))}
261
- </div>
262
- </div>
263
- )}
264
- {online.author && (
265
- <div>
266
- <div
267
- style={{
268
- fontSize: 11,
269
- color: themeVars.content.dim,
270
- marginBottom: 2,
271
- }}
272
- >
273
- Author
274
- </div>
275
- <div style={{ fontSize: 11, color: themeVars.content.primary }}>{online.author}</div>
268
+ Tags
269
+ </div>
270
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
271
+ {online.tags.slice(0, 12).map((tag) => (
272
+ <span
273
+ key={tag}
274
+ style={{
275
+ fontSize: 10,
276
+ padding: '1px 5px',
277
+ background: themeVars.surface.raised,
278
+ borderRadius: themeVars.shape.small,
279
+ color: themeVars.content.muted,
280
+ }}
281
+ >
282
+ {tag}
283
+ </span>
284
+ ))}
285
+ </div>
286
+ </div>
287
+ )}
288
+ {online.author && (
289
+ <div>
290
+ <div
291
+ style={{
292
+ fontSize: 11,
293
+ color: themeVars.content.dim,
294
+ marginBottom: 2,
295
+ }}
296
+ >
297
+ Author
298
+ </div>
299
+ <div style={{ fontSize: 11, color: themeVars.content.primary }}>
300
+ {online.author}
301
+ </div>
302
+ </div>
303
+ )}
276
304
  </div>
277
- )}
278
- </div>
279
- </SectionHeader>
305
+ </SectionHeader>
280
306
 
281
- {/* Explicit project acquisition section */}
282
- <SectionHeader label="Add to Project">
283
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
284
- {loadingFiles ? (
285
- <div style={{ fontSize: 11, color: themeVars.content.dim }}>
286
- <EditorIcon icon={faSpinner} spin style={{ marginRight: 4 }} />
287
- Loading options...
288
- </div>
289
- ) : filesError ? (
290
- <div role="alert" style={{ fontSize: 11, color: themeVars.content.muted }}>
291
- Delivery unavailable: {filesError}
292
- </div>
293
- ) : fileOptions.length > 0 ? (
294
- <>
295
- <Select
296
- value={selectedFile ? fileOptions.indexOf(selectedFile) : 0}
297
- onChange={(e) => setSelectedFile(fileOptions[Number(e.target.value)] ?? null)}
298
- style={selectStyle}
299
- >
300
- {fileOptions.map((f, i) => (
301
- <option key={i} value={i}>
302
- {f.label}
303
- {f.sizeBytes ? ` (${formatBytes(f.sizeBytes)})` : ''}
304
- </option>
305
- ))}
306
- </Select>
307
+ {/* Explicit project acquisition section */}
308
+ <SectionHeader label="Add to Project">
309
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
310
+ {loadingFiles ? (
311
+ <div style={{ fontSize: 11, color: themeVars.content.dim }}>
312
+ <EditorIcon icon={faSpinner} spin style={{ marginRight: 4 }} />
313
+ Loading options...
314
+ </div>
315
+ ) : filesError ? (
316
+ <div role="alert" style={{ fontSize: 11, color: themeVars.content.muted }}>
317
+ Delivery unavailable: {filesError}
318
+ </div>
319
+ ) : fileOptions.length > 0 ? (
320
+ <>
321
+ <Select
322
+ value={selectedFile ? fileOptions.indexOf(selectedFile) : 0}
323
+ onChange={(e) => setSelectedFile(fileOptions[Number(e.target.value)] ?? null)}
324
+ style={selectStyle}
325
+ >
326
+ {fileOptions.map((f, i) => (
327
+ <option key={i} value={i}>
328
+ {f.label}
329
+ {f.sizeBytes ? ` (${formatBytes(f.sizeBytes)})` : ''}
330
+ </option>
331
+ ))}
332
+ </Select>
307
333
 
308
- <Button
309
- type="button"
310
- variant={downloadStatus === 'done' ? 'success' : 'primary'}
311
- size="comfortable"
312
- onClick={onDownload}
313
- disabled={downloadStatus === 'downloading'}
314
- style={{
315
- width: '100%',
316
- }}
317
- >
318
- <EditorIcon
319
- icon={
320
- downloadStatus === 'done'
321
- ? faCheck
334
+ <Button
335
+ type="button"
336
+ variant={downloadStatus === 'done' ? 'success' : 'primary'}
337
+ size="comfortable"
338
+ onClick={onDownload}
339
+ disabled={downloadStatus === 'downloading'}
340
+ style={{
341
+ width: '100%',
342
+ }}
343
+ >
344
+ <EditorIcon
345
+ icon={
346
+ downloadStatus === 'done'
347
+ ? faCheck
348
+ : downloadStatus === 'downloading'
349
+ ? faSpinner
350
+ : faDownload
351
+ }
352
+ spin={downloadStatus === 'downloading'}
353
+ style={{ fontSize: 10 }}
354
+ />
355
+ {downloadStatus === 'done'
356
+ ? 'Downloaded'
322
357
  : downloadStatus === 'downloading'
323
- ? faSpinner
324
- : faDownload
325
- }
326
- spin={downloadStatus === 'downloading'}
327
- style={{ fontSize: 10 }}
328
- />
329
- {downloadStatus === 'done'
330
- ? 'Downloaded'
331
- : downloadStatus === 'downloading'
332
- ? 'Downloading...'
333
- : downloadStatus === 'error'
334
- ? 'Retry'
335
- : 'Add to Project'}
336
- </Button>
337
- </>
338
- ) : (
339
- <div role="status" style={{ fontSize: 11, color: themeVars.content.dim }}>
340
- {online.source === 'local'
341
- ? 'No healthy SSD or cloud delivery is available for this asset.'
342
- : 'This provider has no compatible remote delivery for the selected asset.'}
358
+ ? 'Downloading...'
359
+ : downloadStatus === 'error'
360
+ ? 'Retry'
361
+ : 'Add to Project'}
362
+ </Button>
363
+ </>
364
+ ) : (
365
+ <div role="status" style={{ fontSize: 11, color: themeVars.content.dim }}>
366
+ {online.source === 'local'
367
+ ? 'No healthy SSD or cloud delivery is available for this asset.'
368
+ : 'This provider has no compatible remote delivery for the selected asset.'}
369
+ </div>
370
+ )}
343
371
  </div>
344
- )}
345
- </div>
346
- </SectionHeader>
372
+ </SectionHeader>
347
373
 
348
- <div style={{ fontSize: 10, color: themeVars.content.dim, padding: '4px 0' }}>
349
- License: {online.license ?? 'CC0 (Public Domain)'}
350
- </div>
374
+ <div style={{ fontSize: 10, color: themeVars.content.dim, padding: '4px 0' }}>
375
+ License: {online.license ?? 'CC0 (Public Domain)'}
376
+ </div>
377
+ </>
378
+ )}
351
379
  </div>
352
380
  );
353
381
  }