@vgai/editor 0.4.0 → 0.4.1
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/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- package/template/tests/logic/example.test.ts +0 -31
|
@@ -21,8 +21,11 @@ import {
|
|
|
21
21
|
type ModelCameraPreset,
|
|
22
22
|
type ModelDiagnosticMode,
|
|
23
23
|
type ModelInspection,
|
|
24
|
+
modelSelectionObjects,
|
|
25
|
+
rootMotionTrailPoints,
|
|
24
26
|
supportedModelDiagnosticModes,
|
|
25
27
|
} from '../../asset-workflow/model-inspection';
|
|
28
|
+
import { registerPreviewResource } from '../../asset-workflow/preview-resource-lifetime';
|
|
26
29
|
import {
|
|
27
30
|
loadProjectThumbnailManifest,
|
|
28
31
|
resetProjectThumbnailManifestCache,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
text,
|
|
41
44
|
} from '../../design-system';
|
|
42
45
|
import { useEditorStore } from '../../EditorContext';
|
|
46
|
+
import { revealInFinder } from '../../editor-api';
|
|
43
47
|
import {
|
|
44
48
|
createModelPreviewSource,
|
|
45
49
|
loadModelThumbnailObject,
|
|
@@ -97,8 +101,13 @@ export function ModelViewer({
|
|
|
97
101
|
const [lighting, setLighting] = useState<'studio' | 'outdoor' | 'flat'>('studio');
|
|
98
102
|
const [inspection, setInspection] = useState<ModelInspection | null>(null);
|
|
99
103
|
const [diagnosticMode, setDiagnosticMode] = useState<ModelDiagnosticMode>('lit');
|
|
104
|
+
const [selectedStructure, setSelectedStructure] = useState<
|
|
105
|
+
{ kind: 'node' | 'material'; id: string } | undefined
|
|
106
|
+
>();
|
|
100
107
|
const [playing, setPlaying] = useState(false);
|
|
101
108
|
const [loop, setLoop] = useState(true);
|
|
109
|
+
const [showSkeleton, setShowSkeleton] = useState(false);
|
|
110
|
+
const [showRootMotionTrail, setShowRootMotionTrail] = useState(false);
|
|
102
111
|
const [speed, setSpeed] = useState(1);
|
|
103
112
|
const [time, setTime] = useState(0);
|
|
104
113
|
const [selectedClip, setSelectedClip] = useState(0);
|
|
@@ -126,6 +135,9 @@ export function ModelViewer({
|
|
|
126
135
|
const width = container.clientWidth;
|
|
127
136
|
const height = width;
|
|
128
137
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
138
|
+
const releaseContext = registerPreviewResource('webglContext');
|
|
139
|
+
const releaseAnimationFrame = registerPreviewResource('animationFrame');
|
|
140
|
+
const releaseResizeObserver = registerPreviewResource('resizeObserver');
|
|
129
141
|
renderer.setSize(width, height);
|
|
130
142
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
131
143
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
@@ -283,6 +295,9 @@ export function ModelViewer({
|
|
|
283
295
|
runtimeRef.current = null;
|
|
284
296
|
renderer.dispose();
|
|
285
297
|
renderer.forceContextLoss();
|
|
298
|
+
releaseResizeObserver();
|
|
299
|
+
releaseAnimationFrame();
|
|
300
|
+
releaseContext();
|
|
286
301
|
if (ownedSnapshot) disposeAssetPreviewSnapshot(ownedSnapshot);
|
|
287
302
|
if (ownedModel) disposeModel(ownedModel);
|
|
288
303
|
if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
|
|
@@ -367,6 +382,21 @@ export function ModelViewer({
|
|
|
367
382
|
if (runtimeRef.current) applyModelDiagnosticMode(runtimeRef.current, diagnosticMode);
|
|
368
383
|
}, [diagnosticMode]);
|
|
369
384
|
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
const runtime = runtimeRef.current;
|
|
387
|
+
if (runtime) applyModelSelectionHighlight(runtime, selectedStructure);
|
|
388
|
+
}, [inspection, selectedStructure]);
|
|
389
|
+
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
const runtime = runtimeRef.current;
|
|
392
|
+
if (runtime)
|
|
393
|
+
applyAnimationDiagnosticHelpers(runtime, {
|
|
394
|
+
selectedClip,
|
|
395
|
+
showSkeleton,
|
|
396
|
+
showRootMotionTrail,
|
|
397
|
+
});
|
|
398
|
+
}, [inspection, selectedClip, showRootMotionTrail, showSkeleton]);
|
|
399
|
+
|
|
370
400
|
useEffect(() => {
|
|
371
401
|
const runtime = runtimeRef.current;
|
|
372
402
|
if (!runtime?.mixer || runtime.model.animations.length === 0) return;
|
|
@@ -478,7 +508,32 @@ export function ModelViewer({
|
|
|
478
508
|
Retry
|
|
479
509
|
</Button>
|
|
480
510
|
)}
|
|
481
|
-
{issue?.recovery.includes('
|
|
511
|
+
{issue?.recovery.includes('reveal-source') && assetPath && (
|
|
512
|
+
<Button
|
|
513
|
+
type="button"
|
|
514
|
+
onClick={() => {
|
|
515
|
+
void revealInFinder(assetPath).catch((cause) =>
|
|
516
|
+
setError(cause instanceof Error ? cause.message : String(cause)),
|
|
517
|
+
);
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
Reveal source
|
|
521
|
+
</Button>
|
|
522
|
+
)}
|
|
523
|
+
{issue?.recovery.includes('reimport') && assetPath && (
|
|
524
|
+
<Button
|
|
525
|
+
type="button"
|
|
526
|
+
onClick={() =>
|
|
527
|
+
window.dispatchEvent(
|
|
528
|
+
new CustomEvent('editor:reimport-project-asset', {
|
|
529
|
+
detail: { path: assetPath },
|
|
530
|
+
}),
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
>
|
|
534
|
+
Reimport
|
|
535
|
+
</Button>
|
|
536
|
+
)}
|
|
482
537
|
</div>
|
|
483
538
|
)}
|
|
484
539
|
</div>
|
|
@@ -490,7 +545,11 @@ export function ModelViewer({
|
|
|
490
545
|
const deterministicCaptureSupported = !assetPath || pathFormat === 'glb' || pathFormat === 'gltf';
|
|
491
546
|
|
|
492
547
|
const modes = runtimeRef.current ? supportedModelDiagnosticModes(runtimeRef.current.model) : [];
|
|
493
|
-
const
|
|
548
|
+
const selectedAnimation = runtimeRef.current?.model.animations[selectedClip];
|
|
549
|
+
const selectedDuration = selectedAnimation?.duration ?? 0;
|
|
550
|
+
const rootMotionAvailable = selectedAnimation
|
|
551
|
+
? rootMotionTrailPoints(selectedAnimation).length > 1
|
|
552
|
+
: false;
|
|
494
553
|
const hierarchy = inspection?.hierarchy ?? [];
|
|
495
554
|
|
|
496
555
|
return (
|
|
@@ -605,9 +664,13 @@ export function ModelViewer({
|
|
|
605
664
|
type="button"
|
|
606
665
|
role="treeitem"
|
|
607
666
|
key={node.id}
|
|
667
|
+
aria-selected={
|
|
668
|
+
selectedStructure?.kind === 'node' && selectedStructure.id === node.id
|
|
669
|
+
}
|
|
608
670
|
variant="ghost"
|
|
609
671
|
size="compact"
|
|
610
672
|
className="vgai-model-tree-item"
|
|
673
|
+
onClick={() => setSelectedStructure({ kind: 'node', id: node.id })}
|
|
611
674
|
style={{
|
|
612
675
|
display: 'block',
|
|
613
676
|
width: '100%',
|
|
@@ -620,8 +683,19 @@ export function ModelViewer({
|
|
|
620
683
|
</div>
|
|
621
684
|
</SectionHeader>
|
|
622
685
|
<SectionHeader label="Materials">
|
|
623
|
-
{inspection?.
|
|
624
|
-
<
|
|
686
|
+
{inspection?.materialSlots.map((material) => (
|
|
687
|
+
<Button
|
|
688
|
+
type="button"
|
|
689
|
+
variant="ghost"
|
|
690
|
+
size="compact"
|
|
691
|
+
key={material.id}
|
|
692
|
+
aria-pressed={
|
|
693
|
+
selectedStructure?.kind === 'material' && selectedStructure.id === material.id
|
|
694
|
+
}
|
|
695
|
+
onClick={() => setSelectedStructure({ kind: 'material', id: material.id })}
|
|
696
|
+
>
|
|
697
|
+
{material.name} · {material.textureSlots.join(', ') || 'no textures'}
|
|
698
|
+
</Button>
|
|
625
699
|
))}
|
|
626
700
|
</SectionHeader>
|
|
627
701
|
</div>
|
|
@@ -638,6 +712,28 @@ export function ModelViewer({
|
|
|
638
712
|
</div>
|
|
639
713
|
))}
|
|
640
714
|
</SectionHeader>
|
|
715
|
+
<SectionHeader label="Textures">
|
|
716
|
+
{inspection?.textureBindings.length ? (
|
|
717
|
+
<dl>
|
|
718
|
+
{inspection.textureBindings.map((binding) => (
|
|
719
|
+
<div key={`${binding.materialId}:${binding.slot}`}>
|
|
720
|
+
<dt>
|
|
721
|
+
{binding.materialName} · {binding.slot}
|
|
722
|
+
</dt>
|
|
723
|
+
<dd>
|
|
724
|
+
{binding.textureName} ·{' '}
|
|
725
|
+
{binding.width && binding.height
|
|
726
|
+
? `${binding.width}×${binding.height}`
|
|
727
|
+
: 'unknown size'}{' '}
|
|
728
|
+
· format {binding.format} · {binding.colorSpace}
|
|
729
|
+
</dd>
|
|
730
|
+
</div>
|
|
731
|
+
))}
|
|
732
|
+
</dl>
|
|
733
|
+
) : (
|
|
734
|
+
<div>No texture dependencies.</div>
|
|
735
|
+
)}
|
|
736
|
+
</SectionHeader>
|
|
641
737
|
<SectionHeader label="Source">
|
|
642
738
|
<div style={{ wordBreak: 'break-all' }}>{assetPath ?? `Entity ${entityId}`}</div>
|
|
643
739
|
</SectionHeader>
|
|
@@ -702,7 +798,7 @@ export function ModelViewer({
|
|
|
702
798
|
/>
|
|
703
799
|
</label>
|
|
704
800
|
<span>
|
|
705
|
-
{time.toFixed(2)} / {selectedDuration.toFixed(2)}s
|
|
801
|
+
{time.toFixed(2)} / {selectedDuration.toFixed(2)}s · frame {Math.round(time * 30)}
|
|
706
802
|
</span>
|
|
707
803
|
<label>
|
|
708
804
|
<Checkbox checked={loop} onChange={(event) => setLoop(event.target.checked)} /> Loop
|
|
@@ -716,6 +812,21 @@ export function ModelViewer({
|
|
|
716
812
|
<option value={2}>2×</option>
|
|
717
813
|
</Select>
|
|
718
814
|
</label>
|
|
815
|
+
<label>
|
|
816
|
+
<Checkbox
|
|
817
|
+
checked={showSkeleton}
|
|
818
|
+
onChange={(event) => setShowSkeleton(event.target.checked)}
|
|
819
|
+
/>{' '}
|
|
820
|
+
Skeleton
|
|
821
|
+
</label>
|
|
822
|
+
<label title={rootMotionAvailable ? undefined : 'This clip has no root position track'}>
|
|
823
|
+
<Checkbox
|
|
824
|
+
checked={showRootMotionTrail}
|
|
825
|
+
disabled={!rootMotionAvailable}
|
|
826
|
+
onChange={(event) => setShowRootMotionTrail(event.target.checked)}
|
|
827
|
+
/>{' '}
|
|
828
|
+
Root motion trail
|
|
829
|
+
</label>
|
|
719
830
|
</div>
|
|
720
831
|
) : undefined
|
|
721
832
|
}
|
|
@@ -848,6 +959,58 @@ function applyModelDiagnosticMode(runtime: ModelViewerRuntime, mode: ModelDiagno
|
|
|
848
959
|
}
|
|
849
960
|
}
|
|
850
961
|
|
|
962
|
+
function removeNamedHelpers(runtime: ModelViewerRuntime, prefix: string): void {
|
|
963
|
+
for (const helper of [...runtime.helpers]) {
|
|
964
|
+
if (!helper.name.startsWith(prefix)) continue;
|
|
965
|
+
runtime.scene.remove(helper);
|
|
966
|
+
runtime.helpers.splice(runtime.helpers.indexOf(helper), 1);
|
|
967
|
+
disposeAssetPreviewSnapshot(helper);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function addSelectionBox(runtime: ModelViewerRuntime, object: THREE.Object3D): void {
|
|
972
|
+
const bounds = new THREE.Box3().setFromObject(object);
|
|
973
|
+
if (bounds.isEmpty()) return;
|
|
974
|
+
const helper = new THREE.BoxHelper(object, 0x67d8ff);
|
|
975
|
+
helper.name = '__asset_editor_selection_highlight';
|
|
976
|
+
runtime.scene.add(helper);
|
|
977
|
+
runtime.helpers.push(helper);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function applyModelSelectionHighlight(
|
|
981
|
+
runtime: ModelViewerRuntime,
|
|
982
|
+
selection: { kind: 'node' | 'material'; id: string } | undefined,
|
|
983
|
+
): void {
|
|
984
|
+
removeNamedHelpers(runtime, '__asset_editor_selection_highlight');
|
|
985
|
+
if (!selection) return;
|
|
986
|
+
for (const object of modelSelectionObjects(runtime.model, selection))
|
|
987
|
+
addSelectionBox(runtime, object);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function applyAnimationDiagnosticHelpers(
|
|
991
|
+
runtime: ModelViewerRuntime,
|
|
992
|
+
options: { selectedClip: number; showSkeleton: boolean; showRootMotionTrail: boolean },
|
|
993
|
+
): void {
|
|
994
|
+
removeNamedHelpers(runtime, '__asset_editor_animation_helper');
|
|
995
|
+
if (options.showSkeleton) {
|
|
996
|
+
const helper = new THREE.SkeletonHelper(runtime.model);
|
|
997
|
+
helper.name = '__asset_editor_animation_helper_skeleton';
|
|
998
|
+
runtime.scene.add(helper);
|
|
999
|
+
runtime.helpers.push(helper);
|
|
1000
|
+
}
|
|
1001
|
+
if (!options.showRootMotionTrail) return;
|
|
1002
|
+
const clip = runtime.model.animations[options.selectedClip];
|
|
1003
|
+
const points = clip ? rootMotionTrailPoints(clip) : [];
|
|
1004
|
+
if (points.length < 2) return;
|
|
1005
|
+
const line = new THREE.Line(
|
|
1006
|
+
new THREE.BufferGeometry().setFromPoints(points),
|
|
1007
|
+
new THREE.LineBasicMaterial({ color: 0xffb454 }),
|
|
1008
|
+
);
|
|
1009
|
+
line.name = '__asset_editor_animation_helper_root_motion';
|
|
1010
|
+
runtime.scene.add(line);
|
|
1011
|
+
runtime.helpers.push(line);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
851
1014
|
function disposeModel(root: THREE.Object3D): void {
|
|
852
1015
|
disposeAssetPreviewSnapshot(root);
|
|
853
1016
|
root.traverse((object) => {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { faCheck, faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
2
2
|
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { beginAssetImportJob, updateAssetImportJob } from '../../asset-workflow/asset-import-jobs';
|
|
3
4
|
import { Button, EditorIcon, SectionHeader, Select, themeVars } from '../../design-system';
|
|
4
5
|
import {
|
|
6
|
+
AssetDeliveryError,
|
|
5
7
|
type AssetFileOption,
|
|
6
|
-
|
|
8
|
+
assetDeliveryIssueMessage,
|
|
7
9
|
getOnlineAssetFiles,
|
|
8
10
|
getOnlineAssetPreview,
|
|
9
11
|
type OnlineAssetPreview,
|
|
10
12
|
} from '../../editor-api';
|
|
11
13
|
import type { OnlineAssetInfo } from '../../editor-store';
|
|
14
|
+
import { downloadOnlineAssetWithHistory } from '../asset-editor-persistence';
|
|
12
15
|
|
|
13
16
|
const ModelViewer = lazy(() =>
|
|
14
17
|
import('./ModelViewer').then((module) => ({ default: module.ModelViewer })),
|
|
@@ -37,6 +40,9 @@ interface Props {
|
|
|
37
40
|
/** The Library Inspector and center Asset Editor reuse one acquisition
|
|
38
41
|
* implementation while placing preview and properties in the outer shell. */
|
|
39
42
|
mode?: 'full' | 'inspector' | 'preview';
|
|
43
|
+
/** Selection-only Inspector details show one bounded live preview. Asset
|
|
44
|
+
* document details leave preview ownership to the center document. */
|
|
45
|
+
showPreview?: boolean;
|
|
40
46
|
/** W2: called with the local serving path once the download lands — the
|
|
41
47
|
* owning asset DOCUMENT (`asset-documents.tsx`) swaps to the local viewer
|
|
42
48
|
* in place (was `store.resolveOnlineAssetTab` in the right-rail era). */
|
|
@@ -44,7 +50,12 @@ interface Props {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one catalog document coordinates preview resolution, metadata, format selection, and explicit project import.
|
|
47
|
-
export function OnlineAssetDetail({
|
|
53
|
+
export function OnlineAssetDetail({
|
|
54
|
+
online,
|
|
55
|
+
mode = 'full',
|
|
56
|
+
showPreview = false,
|
|
57
|
+
onResolved,
|
|
58
|
+
}: Props) {
|
|
48
59
|
const [fileOptions, setFileOptions] = useState<AssetFileOption[]>([]);
|
|
49
60
|
const [selectedFile, setSelectedFile] = useState<AssetFileOption | null>(null);
|
|
50
61
|
const [loadingFiles, setLoadingFiles] = useState(true);
|
|
@@ -75,9 +86,11 @@ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props)
|
|
|
75
86
|
if (!cancelled) {
|
|
76
87
|
setFileOptions([]);
|
|
77
88
|
setFilesError(
|
|
78
|
-
error instanceof
|
|
79
|
-
? error.
|
|
80
|
-
:
|
|
89
|
+
error instanceof AssetDeliveryError
|
|
90
|
+
? assetDeliveryIssueMessage(error.code)
|
|
91
|
+
: error instanceof Error
|
|
92
|
+
? error.message
|
|
93
|
+
: 'Asset delivery metadata is unavailable while offline.',
|
|
81
94
|
);
|
|
82
95
|
}
|
|
83
96
|
})
|
|
@@ -94,7 +107,7 @@ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props)
|
|
|
94
107
|
(online.type === 'model' || online.type === 'animation' || online.type === 'source');
|
|
95
108
|
|
|
96
109
|
useEffect(() => {
|
|
97
|
-
if (mode === 'inspector') {
|
|
110
|
+
if (mode === 'inspector' && !showPreview) {
|
|
98
111
|
setLoadingPreview(false);
|
|
99
112
|
return;
|
|
100
113
|
}
|
|
@@ -122,29 +135,55 @@ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props)
|
|
|
122
135
|
return () => {
|
|
123
136
|
cancelled = true;
|
|
124
137
|
};
|
|
125
|
-
}, [mode, online.id, online.source, supportsInteractivePreview]);
|
|
138
|
+
}, [mode, online.id, online.source, showPreview, supportsInteractivePreview]);
|
|
126
139
|
|
|
140
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one explicit acquisition transition owns job state, atomic history, document resolution, cancellation, and recovery.
|
|
127
141
|
const onDownload = useCallback(async () => {
|
|
128
142
|
if (!selectedFile) return;
|
|
143
|
+
const jobId = `single:${online.source}:${online.id}`;
|
|
144
|
+
const job = beginAssetImportJob(jobId, online.name);
|
|
145
|
+
updateAssetImportJob(jobId, {
|
|
146
|
+
stage: 'fetch',
|
|
147
|
+
loaded: 0,
|
|
148
|
+
total: selectedFile.sizeBytes ?? 0,
|
|
149
|
+
status: 'running',
|
|
150
|
+
});
|
|
129
151
|
setDownloadStatus('downloading');
|
|
130
152
|
try {
|
|
131
|
-
const params: Parameters<typeof
|
|
153
|
+
const params: Parameters<typeof downloadOnlineAssetWithHistory>[0] = {
|
|
132
154
|
source: online.source,
|
|
133
155
|
id: online.id,
|
|
134
156
|
name: online.name,
|
|
135
157
|
url: selectedFile.url,
|
|
136
158
|
format: selectedFile.format,
|
|
159
|
+
jobId,
|
|
160
|
+
signal: job.controller.signal,
|
|
137
161
|
};
|
|
138
162
|
if (selectedFile.includes) params.includes = selectedFile.includes;
|
|
139
|
-
const result = await
|
|
163
|
+
const result = await downloadOnlineAssetWithHistory(params);
|
|
140
164
|
if (result.ok && result.path) {
|
|
165
|
+
updateAssetImportJob(jobId, {
|
|
166
|
+
stage: 'commit',
|
|
167
|
+
loaded: selectedFile.sizeBytes ?? 1,
|
|
168
|
+
total: selectedFile.sizeBytes ?? 1,
|
|
169
|
+
status: result.alreadyExists ? 'skipped' : 'imported',
|
|
170
|
+
message: result.path,
|
|
171
|
+
});
|
|
141
172
|
setDownloadStatus('done');
|
|
142
173
|
// Switch this document to the local asset viewer
|
|
143
174
|
onResolved?.(`/${result.path}`);
|
|
144
175
|
} else {
|
|
176
|
+
updateAssetImportJob(jobId, {
|
|
177
|
+
status: 'failed',
|
|
178
|
+
message: result.error ?? 'The import was rejected.',
|
|
179
|
+
});
|
|
145
180
|
setDownloadStatus('error');
|
|
146
181
|
}
|
|
147
|
-
} catch {
|
|
182
|
+
} catch (error) {
|
|
183
|
+
updateAssetImportJob(jobId, {
|
|
184
|
+
status: job.controller.signal.aborted ? 'cancelled' : 'failed',
|
|
185
|
+
message: error instanceof Error ? error.message : String(error),
|
|
186
|
+
});
|
|
148
187
|
setDownloadStatus('error');
|
|
149
188
|
}
|
|
150
189
|
}, [online, selectedFile, onResolved]);
|
|
@@ -164,13 +203,13 @@ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props)
|
|
|
164
203
|
</div>
|
|
165
204
|
)}
|
|
166
205
|
|
|
167
|
-
{mode !== 'inspector' && (
|
|
206
|
+
{(mode !== 'inspector' || showPreview) && (
|
|
168
207
|
<>
|
|
169
208
|
<div
|
|
170
209
|
style={{
|
|
171
210
|
width: '100%',
|
|
172
211
|
height: '100%',
|
|
173
|
-
minHeight: 240,
|
|
212
|
+
minHeight: mode === 'inspector' ? 160 : 240,
|
|
174
213
|
background: themeVars.surface.shell,
|
|
175
214
|
borderRadius: themeVars.shape.small,
|
|
176
215
|
overflow: 'hidden',
|
|
@@ -319,6 +358,7 @@ export function OnlineAssetDetail({ online, mode = 'full', onResolved }: Props)
|
|
|
319
358
|
) : fileOptions.length > 0 ? (
|
|
320
359
|
<>
|
|
321
360
|
<Select
|
|
361
|
+
aria-label="Asset delivery"
|
|
322
362
|
value={selectedFile ? fileOptions.indexOf(selectedFile) : 0}
|
|
323
363
|
onChange={(e) => setSelectedFile(fileOptions[Number(e.target.value)] ?? null)}
|
|
324
364
|
style={selectStyle}
|
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
import { resolveUrl } from '@engine/loader';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
+
import { assetCapabilities } from '../../asset-workflow/asset-capabilities';
|
|
3
4
|
import { themeVars } from '../../design-system';
|
|
4
5
|
import { AudioViewer } from './AudioViewer';
|
|
5
6
|
import { GenericJsonViewer } from './GenericJsonViewer';
|
|
6
7
|
import { ImageViewer } from './ImageViewer';
|
|
7
8
|
import { ModelViewer } from './ModelViewer';
|
|
8
9
|
|
|
9
|
-
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'webp', 'svg', 'gif']);
|
|
10
|
-
const AUDIO_EXTENSIONS = new Set(['mp3', 'ogg', 'wav', 'flac']);
|
|
11
|
-
const MODEL_EXTENSIONS = new Set(['glb', 'gltf', 'fbx', 'obj', 'dae', 'stl', 'ply', 'bvh', '3ds']);
|
|
12
|
-
const TEXT_EXTENSIONS = new Set([
|
|
13
|
-
'txt',
|
|
14
|
-
'md',
|
|
15
|
-
'ts',
|
|
16
|
-
'tsx',
|
|
17
|
-
'js',
|
|
18
|
-
'jsx',
|
|
19
|
-
'css',
|
|
20
|
-
'html',
|
|
21
|
-
'mtl',
|
|
22
|
-
'csv',
|
|
23
|
-
'xml',
|
|
24
|
-
'yaml',
|
|
25
|
-
'yml',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
10
|
export type SourceAssetRoute = 'model' | 'image' | 'audio' | 'json' | 'text' | 'binary';
|
|
29
11
|
|
|
30
12
|
export function sourceAssetRoute(assetPath: string): SourceAssetRoute {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
37
|
-
if (TEXT_EXTENSIONS.has(extension)) return 'text';
|
|
13
|
+
const capability = assetCapabilities(assetPath);
|
|
14
|
+
if (capability.editor === 'model') return 'model';
|
|
15
|
+
if (capability.editor === 'image') return 'image';
|
|
16
|
+
if (capability.editor === 'audio') return 'audio';
|
|
17
|
+
if (capability.editor === 'data') return 'json';
|
|
18
|
+
if (capability.editor === 'source' && capability.previewable) return 'text';
|
|
38
19
|
return 'binary';
|
|
39
20
|
}
|
|
40
21
|
|
|
@@ -53,6 +53,32 @@
|
|
|
53
53
|
font-size: 10px;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
.vgai-asset-import-jobs {
|
|
57
|
+
display: flex;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 4px;
|
|
62
|
+
color: var(--vgai-text-2);
|
|
63
|
+
font-size: 10px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.vgai-asset-import-jobs > span {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 4px;
|
|
71
|
+
padding: 2px 4px;
|
|
72
|
+
border: 1px solid var(--vgai-border-1);
|
|
73
|
+
border-radius: var(--vgai-radius-sm, 3px);
|
|
74
|
+
background: var(--vgai-bg-1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.vgai-asset-import-jobs [data-status="failed"],
|
|
78
|
+
.vgai-asset-import-error {
|
|
79
|
+
color: var(--vgai-danger, var(--vgai-text-1));
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
.vgai-asset-library-navigator__results {
|
|
57
83
|
flex: 1;
|
|
58
84
|
min-height: 0;
|
|
@@ -312,8 +338,9 @@
|
|
|
312
338
|
.vgai-asset-badge {
|
|
313
339
|
padding: 1px 5px;
|
|
314
340
|
border-radius: 999px;
|
|
315
|
-
background: var(--
|
|
341
|
+
background: var(--asset-badge-background);
|
|
316
342
|
color: var(--vgai-text-2);
|
|
343
|
+
font-size: var(--asset-font-floor);
|
|
317
344
|
white-space: nowrap;
|
|
318
345
|
}
|
|
319
346
|
|
|
@@ -328,7 +355,7 @@
|
|
|
328
355
|
|
|
329
356
|
.vgai-asset-facet-button {
|
|
330
357
|
width: 100%;
|
|
331
|
-
min-height:
|
|
358
|
+
min-height: var(--asset-row-height);
|
|
332
359
|
padding: 4px 7px;
|
|
333
360
|
border: 0;
|
|
334
361
|
border-radius: 4px;
|
|
@@ -340,7 +367,7 @@
|
|
|
340
367
|
|
|
341
368
|
.vgai-asset-facet-button:hover,
|
|
342
369
|
.vgai-asset-facet-button[aria-pressed="true"] {
|
|
343
|
-
background: var(--
|
|
370
|
+
background: var(--asset-selection);
|
|
344
371
|
color: var(--vgai-text-1);
|
|
345
372
|
}
|
|
346
373
|
|