@volter/editor-game 0.5.65
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/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
// The section's own styles travel with it: the kit's `editor-styles.css` may
|
|
2
|
+
// not @import a package's stylesheet (it would name a package), so the module
|
|
3
|
+
// that draws them imports them.
|
|
4
|
+
import './model-asset-inspector-section.css';
|
|
5
|
+
import {
|
|
6
|
+
inspectModel,
|
|
7
|
+
inspectModelRig,
|
|
8
|
+
inspectObject3DNode,
|
|
9
|
+
type ModelInspection,
|
|
10
|
+
type ModelRigInspection,
|
|
11
|
+
modelSelectionObjects,
|
|
12
|
+
type Object3DNodeInspection,
|
|
13
|
+
} from '@volter/editor-core/asset-workflow/model-inspection';
|
|
14
|
+
import { setAuthoringSelection } from '@volter/editor-core/authoring/consumer-actions';
|
|
15
|
+
import { SourceObject3DAuthoringAdapter } from '@volter/editor-core/authoring/source-object3d-authoring-adapter';
|
|
16
|
+
import { openToolDocument } from '@volter/editor-core/components/tool-documents';
|
|
17
|
+
import { useEditorStore } from '@volter/editor-core/editor-runtime';
|
|
18
|
+
import { createHmrRegistrationGroup } from '@volter/editor-core/hmr-registration-group';
|
|
19
|
+
import {
|
|
20
|
+
CONTRIBUTED_SECTION_ORDER,
|
|
21
|
+
type InspectionSection,
|
|
22
|
+
PROPERTIES_SECTION_ID,
|
|
23
|
+
} from '@volter/editor-core/inspection/model';
|
|
24
|
+
import { registerInspectorSections } from '@volter/editor-core/inspector-section-registry';
|
|
25
|
+
import {
|
|
26
|
+
type ProjectOutputProvenance,
|
|
27
|
+
provenanceForProjectAsset,
|
|
28
|
+
} from '@volter/editor-core/project-provenance';
|
|
29
|
+
import {
|
|
30
|
+
getDocumentToolContributions,
|
|
31
|
+
getGlobalToolContributions,
|
|
32
|
+
subscribeToolContributions,
|
|
33
|
+
} from '@volter/editor-core/tool-loader';
|
|
34
|
+
import type { AuthoringAdapter, EditorNode } from '@volter/editor-project/adapter';
|
|
35
|
+
import { object3DAuthoringSubjectOf } from '@volter/threejs-runtime/adapter/object3d-authoring-subject';
|
|
36
|
+
import { getUserData } from '@volter/threejs-runtime/ecs/user-data';
|
|
37
|
+
import {
|
|
38
|
+
faBone,
|
|
39
|
+
faCircleNodes,
|
|
40
|
+
faCube,
|
|
41
|
+
faDiagramProject,
|
|
42
|
+
faFileLines,
|
|
43
|
+
faPalette,
|
|
44
|
+
faPersonRunning,
|
|
45
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
46
|
+
import type { SplatMesh } from '@sparkjsdev/spark';
|
|
47
|
+
import {
|
|
48
|
+
Button,
|
|
49
|
+
EditorBanner,
|
|
50
|
+
EditorIcon,
|
|
51
|
+
EditorSurface,
|
|
52
|
+
editorIcons,
|
|
53
|
+
FieldGroup,
|
|
54
|
+
FieldRow,
|
|
55
|
+
StateSurface,
|
|
56
|
+
Text,
|
|
57
|
+
TextInput,
|
|
58
|
+
} from '@volter/editor-sdk/widgets';
|
|
59
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
60
|
+
import * as THREE from 'three';
|
|
61
|
+
|
|
62
|
+
function isObject3DModelDocument(node: EditorNode | null, adapter: AuthoringAdapter): boolean {
|
|
63
|
+
return adapter instanceof SourceObject3DAuthoringAdapter && node?.id === adapter.documentId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isObject3DModelNode(node: EditorNode | null, adapter: AuthoringAdapter): boolean {
|
|
67
|
+
return (
|
|
68
|
+
adapter instanceof SourceObject3DAuthoringAdapter &&
|
|
69
|
+
node !== null &&
|
|
70
|
+
node.id !== adapter.documentId
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Readout({
|
|
75
|
+
children,
|
|
76
|
+
code = false,
|
|
77
|
+
}: {
|
|
78
|
+
readonly children: React.ReactNode;
|
|
79
|
+
readonly code?: boolean;
|
|
80
|
+
}) {
|
|
81
|
+
return (
|
|
82
|
+
<Text
|
|
83
|
+
variant={code ? 'code' : 'body'}
|
|
84
|
+
selectable
|
|
85
|
+
truncate
|
|
86
|
+
title={typeof children === 'string' ? children : undefined}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</Text>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findSplat(root: THREE.Object3D | null): SplatMesh | null {
|
|
94
|
+
let found: SplatMesh | null = null;
|
|
95
|
+
root?.traverse((object) => {
|
|
96
|
+
if (!found && getUserData(object, 'gaussianSplat')) found = object as SplatMesh;
|
|
97
|
+
});
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function SplatBody({ splat }: { readonly splat: SplatMesh }) {
|
|
102
|
+
const metadata = getUserData(splat, 'gaussianSplat')!;
|
|
103
|
+
const size = splat.getBoundingBox().getSize(new THREE.Vector3());
|
|
104
|
+
return (
|
|
105
|
+
<FieldGroup>
|
|
106
|
+
<FieldRow label="Gaussians">
|
|
107
|
+
<Readout>{metadata.numSplats.toLocaleString()}</Readout>
|
|
108
|
+
</FieldRow>
|
|
109
|
+
<FieldRow label="Format">
|
|
110
|
+
<Readout>SPZ</Readout>
|
|
111
|
+
</FieldRow>
|
|
112
|
+
<FieldRow label="Bounds">
|
|
113
|
+
<Readout>{`${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}`}</Readout>
|
|
114
|
+
</FieldRow>
|
|
115
|
+
<FieldRow label="Level of detail">
|
|
116
|
+
<Readout>Full source</Readout>
|
|
117
|
+
</FieldRow>
|
|
118
|
+
<FieldRow label="Picking">
|
|
119
|
+
<Readout>Gaussian raycast</Readout>
|
|
120
|
+
</FieldRow>
|
|
121
|
+
<FieldRow
|
|
122
|
+
label="Collision"
|
|
123
|
+
hint="Gaussian splats are visual environment data; use a separate mesh collider."
|
|
124
|
+
>
|
|
125
|
+
<Readout>Separate collider required</Readout>
|
|
126
|
+
</FieldRow>
|
|
127
|
+
</FieldGroup>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function GeometryBody({
|
|
132
|
+
inspection,
|
|
133
|
+
rig,
|
|
134
|
+
}: {
|
|
135
|
+
readonly inspection: ModelInspection | null;
|
|
136
|
+
readonly rig: ModelRigInspection | null;
|
|
137
|
+
}) {
|
|
138
|
+
if (!inspection) {
|
|
139
|
+
return (
|
|
140
|
+
<StateSurface
|
|
141
|
+
compact
|
|
142
|
+
tone="error"
|
|
143
|
+
title="Model unavailable"
|
|
144
|
+
description="The native Object3D graph could not be inspected."
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const issues = [
|
|
149
|
+
...(inspection.meshes === 0 ? ['The asset has no renderable geometry.'] : []),
|
|
150
|
+
...(inspection.geometry.nonFiniteAttributeValues > 0
|
|
151
|
+
? [
|
|
152
|
+
`${inspection.geometry.nonFiniteAttributeValues.toLocaleString()} non-finite geometry values can break bounds and selection.`,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
...(rig && !rig.valid ? ['Rig or animation bindings require attention.'] : []),
|
|
156
|
+
];
|
|
157
|
+
// No header of its own — every block in this file is an identified
|
|
158
|
+
// SECTION now, and the PROJECTION heads each one with its title and icon
|
|
159
|
+
// (`components/InspectionProjection.tsx`).
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{issues.length > 0 && (
|
|
163
|
+
<EditorBanner tone="warning" icon={<EditorIcon icon={editorIcons.status.warning} />}>
|
|
164
|
+
<div>
|
|
165
|
+
{issues.map((issue) => (
|
|
166
|
+
<div key={issue}>{issue}</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
</EditorBanner>
|
|
170
|
+
)}
|
|
171
|
+
<FieldGroup>
|
|
172
|
+
<FieldRow label="Meshes">
|
|
173
|
+
<Readout>{inspection.meshes.toLocaleString()}</Readout>
|
|
174
|
+
</FieldRow>
|
|
175
|
+
<FieldRow label="Primitives">
|
|
176
|
+
<Readout>{inspection.primitives.toLocaleString()}</Readout>
|
|
177
|
+
</FieldRow>
|
|
178
|
+
<FieldRow label="Vertices">
|
|
179
|
+
<Readout>{inspection.vertices.toLocaleString()}</Readout>
|
|
180
|
+
</FieldRow>
|
|
181
|
+
<FieldRow label="Triangles">
|
|
182
|
+
<Readout>{inspection.triangles.toLocaleString()}</Readout>
|
|
183
|
+
</FieldRow>
|
|
184
|
+
<FieldRow label="Indexing">
|
|
185
|
+
<Readout>
|
|
186
|
+
{inspection.geometry.indexedMeshes} indexed · {inspection.geometry.nonIndexedMeshes}{' '}
|
|
187
|
+
non-indexed
|
|
188
|
+
</Readout>
|
|
189
|
+
</FieldRow>
|
|
190
|
+
<FieldRow label="Attributes">
|
|
191
|
+
<Readout>
|
|
192
|
+
{inspection.geometry.attributes.map((attribute) => attribute.name).join(', ') || 'None'}
|
|
193
|
+
</Readout>
|
|
194
|
+
</FieldRow>
|
|
195
|
+
<FieldRow label="UV channels">
|
|
196
|
+
<Readout>{inspection.geometry.uvChannels.join(', ') || 'None'}</Readout>
|
|
197
|
+
</FieldRow>
|
|
198
|
+
<FieldRow label="Morph targets">
|
|
199
|
+
<Readout>{inspection.morphTargets}</Readout>
|
|
200
|
+
</FieldRow>
|
|
201
|
+
<FieldRow label="Bounds">
|
|
202
|
+
<Readout>
|
|
203
|
+
{inspection.bounds
|
|
204
|
+
? `${inspection.bounds.x.toFixed(2)} × ${inspection.bounds.y.toFixed(2)} × ${inspection.bounds.z.toFixed(2)}`
|
|
205
|
+
: 'None'}
|
|
206
|
+
</Readout>
|
|
207
|
+
</FieldRow>
|
|
208
|
+
<FieldRow label="GPU estimate">
|
|
209
|
+
<Readout>{(inspection.memoryBytes / 1_048_576).toFixed(2)} MiB</Readout>
|
|
210
|
+
</FieldRow>
|
|
211
|
+
<FieldRow label="Invalid values">
|
|
212
|
+
<Text tone={inspection.geometry.nonFiniteAttributeValues ? 'warning' : 'muted'}>
|
|
213
|
+
{inspection.geometry.nonFiniteAttributeValues.toLocaleString()}
|
|
214
|
+
</Text>
|
|
215
|
+
</FieldRow>
|
|
216
|
+
<FieldRow label="LODs">
|
|
217
|
+
<Readout>
|
|
218
|
+
{inspection.geometry.lodGroups
|
|
219
|
+
? `${inspection.geometry.lodLevels} levels`
|
|
220
|
+
: 'Not authored'}
|
|
221
|
+
</Readout>
|
|
222
|
+
</FieldRow>
|
|
223
|
+
<FieldRow
|
|
224
|
+
label="Collision"
|
|
225
|
+
hint="Collision remains a source/import operation when no model metadata exists."
|
|
226
|
+
>
|
|
227
|
+
<Readout>Not authored</Readout>
|
|
228
|
+
</FieldRow>
|
|
229
|
+
</FieldGroup>
|
|
230
|
+
</>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function RigBody({ rig }: { readonly rig: ModelRigInspection }) {
|
|
235
|
+
const problems = [
|
|
236
|
+
['Non-normalized weights', rig.nonNormalizedVertices],
|
|
237
|
+
['Zero-weight vertices', rig.zeroWeightVertices],
|
|
238
|
+
['Invalid joint indices', rig.invalidJointIndices],
|
|
239
|
+
['Inverse-bind mismatches', rig.inverseBindMismatches],
|
|
240
|
+
['External bones', rig.externalBones],
|
|
241
|
+
['Non-finite animation values', rig.nonFiniteAnimationValues],
|
|
242
|
+
['Unresolved track targets', rig.unresolvedTrackTargets.length],
|
|
243
|
+
] as const;
|
|
244
|
+
return (
|
|
245
|
+
<>
|
|
246
|
+
{!rig.valid && (
|
|
247
|
+
<EditorBanner tone="warning" icon={<EditorIcon icon={editorIcons.status.warning} />}>
|
|
248
|
+
Review the non-zero integrity checks below.
|
|
249
|
+
</EditorBanner>
|
|
250
|
+
)}
|
|
251
|
+
<FieldGroup>
|
|
252
|
+
<FieldRow label="Deformation">
|
|
253
|
+
<Readout>{rig.kind === 'skinned' ? 'Skinned mesh' : 'Rigid attachments'}</Readout>
|
|
254
|
+
</FieldRow>
|
|
255
|
+
<FieldRow label="Bones">
|
|
256
|
+
<Readout>{rig.uniqueBones}</Readout>
|
|
257
|
+
</FieldRow>
|
|
258
|
+
<FieldRow label="Root bones">
|
|
259
|
+
<Readout>{rig.rootBones.join(', ') || 'None'}</Readout>
|
|
260
|
+
</FieldRow>
|
|
261
|
+
{rig.kind === 'skinned' ? (
|
|
262
|
+
<>
|
|
263
|
+
<FieldRow label="Skeletons">
|
|
264
|
+
<Readout>{rig.skeletons}</Readout>
|
|
265
|
+
</FieldRow>
|
|
266
|
+
<FieldRow label="Bind mode">
|
|
267
|
+
<Readout>{rig.bindModes.join(', ') || 'None'}</Readout>
|
|
268
|
+
</FieldRow>
|
|
269
|
+
<FieldRow label="Weighted vertices">
|
|
270
|
+
<Readout>{rig.weightedVertices.toLocaleString()}</Readout>
|
|
271
|
+
</FieldRow>
|
|
272
|
+
</>
|
|
273
|
+
) : null}
|
|
274
|
+
{problems
|
|
275
|
+
.filter(([, count]) => count > 0)
|
|
276
|
+
.map(([label, count]) => (
|
|
277
|
+
<FieldRow key={label} label={label}>
|
|
278
|
+
<Text tone="warning">{count.toLocaleString()}</Text>
|
|
279
|
+
</FieldRow>
|
|
280
|
+
))}
|
|
281
|
+
{rig.unresolvedTrackTargets.length ? (
|
|
282
|
+
<FieldRow label="Missing targets">
|
|
283
|
+
<Readout>{rig.unresolvedTrackTargets.join(', ')}</Readout>
|
|
284
|
+
</FieldRow>
|
|
285
|
+
) : null}
|
|
286
|
+
</FieldGroup>
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function MaterialsBody({
|
|
292
|
+
adapter,
|
|
293
|
+
inspection,
|
|
294
|
+
}: {
|
|
295
|
+
readonly adapter: SourceObject3DAuthoringAdapter;
|
|
296
|
+
readonly inspection: ModelInspection | null;
|
|
297
|
+
}) {
|
|
298
|
+
const [query, setQuery] = useState('');
|
|
299
|
+
const root = adapter.documentRootObject;
|
|
300
|
+
const materials = (inspection?.materialSlots ?? [])
|
|
301
|
+
.map((material, sourceIndex) => ({
|
|
302
|
+
material,
|
|
303
|
+
// A loaded graph can contain distinct material objects carrying the same
|
|
304
|
+
// serialized UUID. The UUID remains the selection identity, while the
|
|
305
|
+
// traversal slot disambiguates React rows without changing that meaning.
|
|
306
|
+
rowKey: `${material.id}:${sourceIndex}`,
|
|
307
|
+
}))
|
|
308
|
+
.filter(({ material }) =>
|
|
309
|
+
`${material.name} ${material.type} ${material.textureSlots.join(' ')}`
|
|
310
|
+
.toLowerCase()
|
|
311
|
+
.includes(query.trim().toLowerCase()),
|
|
312
|
+
);
|
|
313
|
+
const inspectMaterialUser = (materialId: string) => {
|
|
314
|
+
if (!root) return;
|
|
315
|
+
const firstUser = modelSelectionObjects(root, { kind: 'material', id: materialId })[0];
|
|
316
|
+
const firstUserId = firstUser ? adapter.hierarchy.idForObject3D?.(firstUser) : null;
|
|
317
|
+
if (firstUserId) setAuthoringSelection(adapter, [firstUserId]);
|
|
318
|
+
};
|
|
319
|
+
return (
|
|
320
|
+
<>
|
|
321
|
+
<FieldGroup>
|
|
322
|
+
<FieldRow label="Filter" htmlFor="model-material-filter">
|
|
323
|
+
<TextInput
|
|
324
|
+
id="model-material-filter"
|
|
325
|
+
type="search"
|
|
326
|
+
value={query}
|
|
327
|
+
placeholder="Name, type, or map"
|
|
328
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
329
|
+
/>
|
|
330
|
+
</FieldRow>
|
|
331
|
+
</FieldGroup>
|
|
332
|
+
<div className="vgai-model-inspector-list">
|
|
333
|
+
{materials.map(({ material, rowKey }) => (
|
|
334
|
+
<EditorSurface key={rowKey} variant="raised" border className="vgai-model-inspector-row">
|
|
335
|
+
<span
|
|
336
|
+
className="vgai-model-material-swatch"
|
|
337
|
+
style={{ background: material.color ?? undefined }}
|
|
338
|
+
aria-hidden="true"
|
|
339
|
+
/>
|
|
340
|
+
<div>
|
|
341
|
+
<Text as="strong" variant="label" truncate>
|
|
342
|
+
{material.name}
|
|
343
|
+
</Text>
|
|
344
|
+
<Text variant="caption" tone="muted" truncate>
|
|
345
|
+
{material.type} · {material.usageCount} object{material.usageCount === 1 ? '' : 's'}{' '}
|
|
346
|
+
· {material.textureSlots.join(', ') || 'no maps'}
|
|
347
|
+
{material.transparent ? ' · transparent' : ''}
|
|
348
|
+
</Text>
|
|
349
|
+
</div>
|
|
350
|
+
<Button
|
|
351
|
+
type="button"
|
|
352
|
+
variant="ghost"
|
|
353
|
+
size="compact"
|
|
354
|
+
onClick={() => inspectMaterialUser(material.id)}
|
|
355
|
+
>
|
|
356
|
+
Inspect
|
|
357
|
+
</Button>
|
|
358
|
+
</EditorSurface>
|
|
359
|
+
))}
|
|
360
|
+
{materials.length === 0 ? <StateSurface compact title="No matching materials" /> : null}
|
|
361
|
+
</div>
|
|
362
|
+
</>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function SourceBody({
|
|
367
|
+
adapter,
|
|
368
|
+
provenance,
|
|
369
|
+
provenanceLoaded,
|
|
370
|
+
}: {
|
|
371
|
+
readonly adapter: SourceObject3DAuthoringAdapter;
|
|
372
|
+
readonly provenance: ProjectOutputProvenance | null;
|
|
373
|
+
readonly provenanceLoaded: boolean;
|
|
374
|
+
}) {
|
|
375
|
+
const store = useEditorStore();
|
|
376
|
+
useSyncExternalStore(subscribeToolContributions, getGlobalToolContributions);
|
|
377
|
+
const sourceContribution = provenance
|
|
378
|
+
? getDocumentToolContributions().find(
|
|
379
|
+
(contribution) => contribution.tool?.name === provenance.operationName,
|
|
380
|
+
)
|
|
381
|
+
: undefined;
|
|
382
|
+
return (
|
|
383
|
+
<>
|
|
384
|
+
{sourceContribution ? (
|
|
385
|
+
<div className="vgai-model-inspector-actions">
|
|
386
|
+
<Button
|
|
387
|
+
type="button"
|
|
388
|
+
variant="ghost"
|
|
389
|
+
size="compact"
|
|
390
|
+
onClick={() => openToolDocument(store, sourceContribution.id)}
|
|
391
|
+
>
|
|
392
|
+
Open Source
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
) : null}
|
|
396
|
+
<FieldGroup>
|
|
397
|
+
<FieldRow label="Path">
|
|
398
|
+
<Readout code>{adapter.options.sourcePath}</Readout>
|
|
399
|
+
</FieldRow>
|
|
400
|
+
<FieldRow label="Truth">
|
|
401
|
+
<Readout>{adapter.provenance.label}</Readout>
|
|
402
|
+
</FieldRow>
|
|
403
|
+
<FieldRow label="Ownership" hint={adapter.provenance.detail}>
|
|
404
|
+
<Readout>{adapter.provenance.source}</Readout>
|
|
405
|
+
</FieldRow>
|
|
406
|
+
{!provenanceLoaded ? (
|
|
407
|
+
<FieldRow label="Provenance">
|
|
408
|
+
<Readout>Loading…</Readout>
|
|
409
|
+
</FieldRow>
|
|
410
|
+
) : null}
|
|
411
|
+
{provenance ? (
|
|
412
|
+
<>
|
|
413
|
+
<FieldRow label="Generated by">
|
|
414
|
+
<Readout>{provenance.operationName}</Readout>
|
|
415
|
+
</FieldRow>
|
|
416
|
+
<FieldRow label="Tool">
|
|
417
|
+
<Readout code>{provenance.operationSource ?? 'Not recorded'}</Readout>
|
|
418
|
+
</FieldRow>
|
|
419
|
+
<FieldRow label="Generated">
|
|
420
|
+
<Readout>{new Date(provenance.createdAt).toLocaleString()}</Readout>
|
|
421
|
+
</FieldRow>
|
|
422
|
+
<FieldRow label="Output hash">
|
|
423
|
+
<Readout code>{provenance.output.sha256}</Readout>
|
|
424
|
+
</FieldRow>
|
|
425
|
+
</>
|
|
426
|
+
) : provenanceLoaded ? (
|
|
427
|
+
<FieldRow label="Provenance">
|
|
428
|
+
<Readout>Not recorded</Readout>
|
|
429
|
+
</FieldRow>
|
|
430
|
+
) : null}
|
|
431
|
+
</FieldGroup>
|
|
432
|
+
</>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Cached per ROOT OBJECT (`null` = this root has no summary to tell), because
|
|
437
|
+
* the composer reads a section's description on every inspector render and
|
|
438
|
+
* `inspectModel` walks the whole graph. Same freshness the body already
|
|
439
|
+
* assumes — its `useMemo` is keyed on the root too. */
|
|
440
|
+
const modelSummaryByRoot = new WeakMap<THREE.Object3D, string | null>();
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The model SUMMARY a document's section carries as its description ("1 meshes
|
|
444
|
+
* · 12 triangles") — the line the body's own header used to print beside a
|
|
445
|
+
* second copy of the section's title.
|
|
446
|
+
*
|
|
447
|
+
* A Gaussian splat document has no mesh/triangle story to tell (its own
|
|
448
|
+
* `Gaussian Splat` block carries the numbers), so it gets no description
|
|
449
|
+
* rather than a fabricated "1 meshes · 0 triangles".
|
|
450
|
+
*/
|
|
451
|
+
function modelDocumentDescription(adapter: AuthoringAdapter): string | undefined {
|
|
452
|
+
const root = (adapter as SourceObject3DAuthoringAdapter).documentRootObject ?? null;
|
|
453
|
+
if (!root) return undefined;
|
|
454
|
+
const cached = modelSummaryByRoot.get(root);
|
|
455
|
+
if (cached !== undefined) return cached ?? undefined;
|
|
456
|
+
const inspection = findSplat(root) ? null : inspectModel(root);
|
|
457
|
+
const summary = inspection
|
|
458
|
+
? `${inspection.meshes} meshes · ${inspection.triangles.toLocaleString()} triangles`
|
|
459
|
+
: null;
|
|
460
|
+
modelSummaryByRoot.set(root, summary);
|
|
461
|
+
return summary ?? undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The Source block's async half — asset PROVENANCE, which is a fetch. It is
|
|
466
|
+
* its own component so the Source section's body owns its loading state
|
|
467
|
+
* without any other section re-rendering for it.
|
|
468
|
+
*/
|
|
469
|
+
export function ModelSourceSection({ adapter }: { adapter: AuthoringAdapter }) {
|
|
470
|
+
const modelAdapter = adapter as SourceObject3DAuthoringAdapter;
|
|
471
|
+
const [provenance, setProvenance] = useState<ProjectOutputProvenance | null>(null);
|
|
472
|
+
const [provenanceLoaded, setProvenanceLoaded] = useState(false);
|
|
473
|
+
const modelSource = modelAdapter.options.modelSource;
|
|
474
|
+
const assetPath = modelSource?.kind === 'project-file' ? modelSource.path : null;
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
let cancelled = false;
|
|
478
|
+
setProvenanceLoaded(false);
|
|
479
|
+
if (!assetPath) {
|
|
480
|
+
setProvenance(null);
|
|
481
|
+
setProvenanceLoaded(true);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
void provenanceForProjectAsset(assetPath).then(
|
|
485
|
+
(resolved) => {
|
|
486
|
+
if (!cancelled) {
|
|
487
|
+
setProvenance(resolved);
|
|
488
|
+
setProvenanceLoaded(true);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
() => {
|
|
492
|
+
if (!cancelled) {
|
|
493
|
+
setProvenance(null);
|
|
494
|
+
setProvenanceLoaded(true);
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
return () => {
|
|
499
|
+
cancelled = true;
|
|
500
|
+
};
|
|
501
|
+
}, [assetPath]);
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<div data-testid="model-asset-inspector">
|
|
505
|
+
<SourceBody
|
|
506
|
+
adapter={modelAdapter}
|
|
507
|
+
provenance={provenance}
|
|
508
|
+
provenanceLoaded={provenanceLoaded}
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function NativeObjectBody({ inspection }: { readonly inspection: Object3DNodeInspection }) {
|
|
515
|
+
return (
|
|
516
|
+
<FieldGroup>
|
|
517
|
+
<FieldRow label="Native type">
|
|
518
|
+
<Readout>{inspection.type}</Readout>
|
|
519
|
+
</FieldRow>
|
|
520
|
+
<FieldRow label="Visible">
|
|
521
|
+
<Readout>{inspection.visible ? 'Yes' : 'No'}</Readout>
|
|
522
|
+
</FieldRow>
|
|
523
|
+
<FieldRow label="Children">
|
|
524
|
+
<Readout>{inspection.children}</Readout>
|
|
525
|
+
</FieldRow>
|
|
526
|
+
<FieldRow label="Descendants">
|
|
527
|
+
<Readout>{inspection.descendants}</Readout>
|
|
528
|
+
</FieldRow>
|
|
529
|
+
<FieldRow label="Render order">
|
|
530
|
+
<Readout>{inspection.renderOrder}</Readout>
|
|
531
|
+
</FieldRow>
|
|
532
|
+
<FieldRow label="Layer mask">
|
|
533
|
+
<Readout code>0x{inspection.layers.toString(16)}</Readout>
|
|
534
|
+
</FieldRow>
|
|
535
|
+
</FieldGroup>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function NativeGeometryBody({
|
|
540
|
+
geometry,
|
|
541
|
+
}: {
|
|
542
|
+
readonly geometry: NonNullable<Object3DNodeInspection['geometry']>;
|
|
543
|
+
}) {
|
|
544
|
+
return (
|
|
545
|
+
<>
|
|
546
|
+
{geometry.nonFiniteAttributeValues ? (
|
|
547
|
+
<EditorBanner tone="warning">
|
|
548
|
+
{geometry.nonFiniteAttributeValues.toLocaleString()} non-finite buffer values can break
|
|
549
|
+
bounds, picking, and selection outlines.
|
|
550
|
+
</EditorBanner>
|
|
551
|
+
) : null}
|
|
552
|
+
<FieldGroup>
|
|
553
|
+
<FieldRow label="Vertices">
|
|
554
|
+
<Readout>{geometry.vertices.toLocaleString()}</Readout>
|
|
555
|
+
</FieldRow>
|
|
556
|
+
<FieldRow label="Triangles">
|
|
557
|
+
<Readout>{geometry.triangles.toLocaleString()}</Readout>
|
|
558
|
+
</FieldRow>
|
|
559
|
+
<FieldRow label="Index buffer">
|
|
560
|
+
<Readout>{geometry.indexed ? 'Indexed' : 'Non-indexed'}</Readout>
|
|
561
|
+
</FieldRow>
|
|
562
|
+
<FieldRow label="Attributes">
|
|
563
|
+
<Readout>{geometry.attributes.join(', ')}</Readout>
|
|
564
|
+
</FieldRow>
|
|
565
|
+
<FieldRow label="UV channels">
|
|
566
|
+
<Readout>{geometry.uvChannels.join(', ') || 'None'}</Readout>
|
|
567
|
+
</FieldRow>
|
|
568
|
+
<FieldRow label="Morph targets">
|
|
569
|
+
<Readout>{geometry.morphTargets}</Readout>
|
|
570
|
+
</FieldRow>
|
|
571
|
+
<FieldRow label="Invalid values">
|
|
572
|
+
<Text tone={geometry.nonFiniteAttributeValues ? 'warning' : 'muted'}>
|
|
573
|
+
{geometry.nonFiniteAttributeValues.toLocaleString()}
|
|
574
|
+
</Text>
|
|
575
|
+
</FieldRow>
|
|
576
|
+
</FieldGroup>
|
|
577
|
+
</>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function NativeMaterialsBody({
|
|
582
|
+
materials,
|
|
583
|
+
}: {
|
|
584
|
+
readonly materials: Object3DNodeInspection['materials'];
|
|
585
|
+
}) {
|
|
586
|
+
return (
|
|
587
|
+
<div className="vgai-model-inspector-list">
|
|
588
|
+
{materials.map((material) => (
|
|
589
|
+
<EditorSurface
|
|
590
|
+
key={material.id}
|
|
591
|
+
variant="raised"
|
|
592
|
+
border
|
|
593
|
+
className="vgai-model-inspector-row"
|
|
594
|
+
>
|
|
595
|
+
<span
|
|
596
|
+
className="vgai-model-material-swatch"
|
|
597
|
+
style={{ background: material.color ?? undefined }}
|
|
598
|
+
aria-hidden="true"
|
|
599
|
+
/>
|
|
600
|
+
<div>
|
|
601
|
+
<Text as="strong" variant="label" truncate>
|
|
602
|
+
{material.name}
|
|
603
|
+
</Text>
|
|
604
|
+
<Text variant="caption" tone="muted" truncate>
|
|
605
|
+
{material.type} · {material.textureSlots.join(', ') || 'no maps'}
|
|
606
|
+
</Text>
|
|
607
|
+
</div>
|
|
608
|
+
</EditorSurface>
|
|
609
|
+
))}
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function NativeSkinBody({ skin }: { readonly skin: NonNullable<Object3DNodeInspection['skin']> }) {
|
|
615
|
+
return (
|
|
616
|
+
<FieldGroup>
|
|
617
|
+
<FieldRow label="Bones">
|
|
618
|
+
<Readout>{skin.bones}</Readout>
|
|
619
|
+
</FieldRow>
|
|
620
|
+
<FieldRow label="Root bones">
|
|
621
|
+
<Readout>{skin.rootBones.join(', ') || 'None'}</Readout>
|
|
622
|
+
</FieldRow>
|
|
623
|
+
<FieldRow label="Bind mode">
|
|
624
|
+
<Readout>{skin.bindMode}</Readout>
|
|
625
|
+
</FieldRow>
|
|
626
|
+
</FieldGroup>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function NativeBoneBody({ bone }: { readonly bone: NonNullable<Object3DNodeInspection['bone']> }) {
|
|
631
|
+
return (
|
|
632
|
+
<FieldGroup>
|
|
633
|
+
<FieldRow label="Parent">
|
|
634
|
+
<Readout>{bone.parent ?? 'Skeleton root'}</Readout>
|
|
635
|
+
</FieldRow>
|
|
636
|
+
<FieldRow label="Children">
|
|
637
|
+
<Readout>{bone.children.join(', ') || 'None'}</Readout>
|
|
638
|
+
</FieldRow>
|
|
639
|
+
</FieldGroup>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** One block of an asset document's inspection, as an identified section.
|
|
644
|
+
* The orders are spaced on the model's own scale so the whole set sits after
|
|
645
|
+
* the built-ins and keeps a stable, meaningful reading order. */
|
|
646
|
+
const MODEL_SECTION_ORDER = {
|
|
647
|
+
splat: CONTRIBUTED_SECTION_ORDER,
|
|
648
|
+
geometry: CONTRIBUTED_SECTION_ORDER + 10,
|
|
649
|
+
rig: CONTRIBUTED_SECTION_ORDER + 20,
|
|
650
|
+
// +25 is the RAGDOLL's, and it is `@volter/editor-game`'s
|
|
651
|
+
// (`contributions/ragdoll.inspector.tsx`, `export const order = 25`) — a
|
|
652
|
+
// contributed section's order is this same base plus what it declares, so
|
|
653
|
+
// the slot after Rig is held from there.
|
|
654
|
+
materials: CONTRIBUTED_SECTION_ORDER + 40,
|
|
655
|
+
source: CONTRIBUTED_SECTION_ORDER + 50,
|
|
656
|
+
} as const;
|
|
657
|
+
|
|
658
|
+
/** Node inspections are cheap but not free, and the composer runs on every
|
|
659
|
+
* inspector render. Cached by the root/object it describes, so a rebuilt
|
|
660
|
+
* graph is a fresh entry. */
|
|
661
|
+
const modelByRoot = new WeakMap<THREE.Object3D, ModelInspection>();
|
|
662
|
+
const rigByRoot = new WeakMap<THREE.Object3D, ModelRigInspection>();
|
|
663
|
+
const nodeByObject = new WeakMap<THREE.Object3D, Object3DNodeInspection>();
|
|
664
|
+
|
|
665
|
+
function cached<T>(store: WeakMap<THREE.Object3D, T>, key: THREE.Object3D, make: () => T): T {
|
|
666
|
+
const hit = store.get(key);
|
|
667
|
+
if (hit !== undefined) return hit;
|
|
668
|
+
const made = make();
|
|
669
|
+
store.set(key, made);
|
|
670
|
+
return made;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* An asset document's own sections — Rig, Animation, Materials, Geometry,
|
|
675
|
+
* Source — each identified, ordered and headed like every other section in
|
|
676
|
+
* the box.
|
|
677
|
+
*
|
|
678
|
+
* They used to be one section called "Model" whose body printed all of them
|
|
679
|
+
* under private sub-headers, which meant the card had a single "Model" tab
|
|
680
|
+
* hiding six blocks while a scene node got a tab each. "Everything I click on
|
|
681
|
+
* in the asset lab should basically have the same inspector as if I clicked
|
|
682
|
+
* on it in the scene" (owner, 2026-08-07) — so they are sections, and the
|
|
683
|
+
* word only survives where it is a TYPE: the document root's `typeLabel`
|
|
684
|
+
* ("Rigged model") on the identity row.
|
|
685
|
+
*
|
|
686
|
+
* The mesh/triangle summary lives on GEOMETRY's description, which is the
|
|
687
|
+
* honest home: meshes and triangles are geometry facts, and the row above
|
|
688
|
+
* already says what kind of thing the document is.
|
|
689
|
+
*/
|
|
690
|
+
function modelDocumentSections(adapter: AuthoringAdapter): InspectionSection[] {
|
|
691
|
+
const modelAdapter = adapter as SourceObject3DAuthoringAdapter;
|
|
692
|
+
const root = modelAdapter.documentRootObject;
|
|
693
|
+
const sections: InspectionSection[] = [];
|
|
694
|
+
const splat = findSplat(root);
|
|
695
|
+
if (root && splat) {
|
|
696
|
+
const metadata = getUserData(splat, 'gaussianSplat')!;
|
|
697
|
+
sections.push({
|
|
698
|
+
id: 'splat',
|
|
699
|
+
title: 'Gaussian Splat',
|
|
700
|
+
icon: faCircleNodes,
|
|
701
|
+
order: MODEL_SECTION_ORDER.splat,
|
|
702
|
+
description: `${metadata.numSplats.toLocaleString()} Gaussians · native SPZ`,
|
|
703
|
+
body: { kind: 'custom', render: () => <SplatBody splat={splat} /> },
|
|
704
|
+
});
|
|
705
|
+
} else if (root) {
|
|
706
|
+
const inspection = cached(modelByRoot, root, () => inspectModel(root));
|
|
707
|
+
const rig = cached(rigByRoot, root, () => inspectModelRig(root));
|
|
708
|
+
const description = modelDocumentDescription(adapter);
|
|
709
|
+
sections.push({
|
|
710
|
+
// Claims the descriptor channel: geometry IS this document root's
|
|
711
|
+
// properties, presented richly.
|
|
712
|
+
id: PROPERTIES_SECTION_ID,
|
|
713
|
+
title: 'Geometry',
|
|
714
|
+
icon: faCube,
|
|
715
|
+
order: MODEL_SECTION_ORDER.geometry,
|
|
716
|
+
// Omitted rather than written as `undefined` (the model is
|
|
717
|
+
// `exactOptionalPropertyTypes`).
|
|
718
|
+
...(description ? { description } : {}),
|
|
719
|
+
body: { kind: 'custom', render: () => <GeometryBody inspection={inspection} rig={rig} /> },
|
|
720
|
+
});
|
|
721
|
+
if (rig.skinnedMeshes > 0 || rig.uniqueBones > 0) {
|
|
722
|
+
sections.push({
|
|
723
|
+
id: 'rig',
|
|
724
|
+
title: 'Rig',
|
|
725
|
+
icon: faPersonRunning,
|
|
726
|
+
order: MODEL_SECTION_ORDER.rig,
|
|
727
|
+
description:
|
|
728
|
+
rig.kind === 'skinned'
|
|
729
|
+
? `${rig.uniqueBones} bones · ${rig.skinnedMeshes} skinned meshes`
|
|
730
|
+
: `${rig.uniqueBones} bones · rigid hierarchy`,
|
|
731
|
+
body: { kind: 'custom', render: () => <RigBody rig={rig} /> },
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
sections.push({
|
|
735
|
+
id: 'materials',
|
|
736
|
+
title: 'Materials',
|
|
737
|
+
icon: faPalette,
|
|
738
|
+
order: MODEL_SECTION_ORDER.materials,
|
|
739
|
+
description: `${inspection.materials} materials`,
|
|
740
|
+
body: {
|
|
741
|
+
kind: 'custom',
|
|
742
|
+
render: () => <MaterialsBody adapter={modelAdapter} inspection={inspection} />,
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
sections.push({
|
|
747
|
+
id: 'source',
|
|
748
|
+
title: 'Source',
|
|
749
|
+
icon: faFileLines,
|
|
750
|
+
order: MODEL_SECTION_ORDER.source,
|
|
751
|
+
description: modelAdapter.provenance.label,
|
|
752
|
+
defaultOpen: false,
|
|
753
|
+
body: { kind: 'custom', render: () => <ModelSourceSection adapter={adapter} /> },
|
|
754
|
+
});
|
|
755
|
+
// A document that declined the import audit (`audit: false` — the mesh
|
|
756
|
+
// document, which carries its own Data and Modifiers panels) keeps its
|
|
757
|
+
// materials and rig readouts and drops the Geometry block and
|
|
758
|
+
// the Source row: Blender's Object Data tab is the mesh's own, not an
|
|
759
|
+
// importer's report (owner-sighted 2026-09-04: "half filled with Geometry").
|
|
760
|
+
if (modelAdapter.options.audit === false) {
|
|
761
|
+
return sections.filter(
|
|
762
|
+
(section) => section.id !== PROPERTIES_SECTION_ID && section.id !== 'source',
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
return sections;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** A PART inside an asset document — a mesh, a bone, a group. Its native
|
|
769
|
+
* readouts are sections for the same reason the document's are: what you
|
|
770
|
+
* clicked decides the data, and each block of that data is its own named,
|
|
771
|
+
* glyphed thing. */
|
|
772
|
+
function object3dNodeSections(node: EditorNode | null, adapter: AuthoringAdapter) {
|
|
773
|
+
const object = node ? adapter.hierarchy.object3D?.(node.id) : null;
|
|
774
|
+
if (!object) return [];
|
|
775
|
+
// A native subject proxy already describes itself through the adapter's
|
|
776
|
+
// ordinary field currency. Treating its visualization mesh as model
|
|
777
|
+
// geometry would replace those native fields with irrelevant triangle and
|
|
778
|
+
// material readouts.
|
|
779
|
+
if (object3DAuthoringSubjectOf(object)) return [];
|
|
780
|
+
const inspection = cached(nodeByObject, object, () => inspectObject3DNode(object));
|
|
781
|
+
const sections: InspectionSection[] = [
|
|
782
|
+
{
|
|
783
|
+
// The rich presentation of this node's own properties.
|
|
784
|
+
id: PROPERTIES_SECTION_ID,
|
|
785
|
+
title: 'Object',
|
|
786
|
+
icon: faDiagramProject,
|
|
787
|
+
order: MODEL_SECTION_ORDER.geometry,
|
|
788
|
+
description: inspection.type,
|
|
789
|
+
body: { kind: 'custom', render: () => <NativeObjectBody inspection={inspection} /> },
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
const geometry = inspection.geometry;
|
|
793
|
+
if (geometry) {
|
|
794
|
+
sections.push({
|
|
795
|
+
id: 'geometry',
|
|
796
|
+
title: 'Geometry',
|
|
797
|
+
icon: faCube,
|
|
798
|
+
order: MODEL_SECTION_ORDER.geometry + 1,
|
|
799
|
+
description: `${geometry.triangles.toLocaleString()} triangles`,
|
|
800
|
+
body: { kind: 'custom', render: () => <NativeGeometryBody geometry={geometry} /> },
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (inspection.materials.length > 0) {
|
|
804
|
+
sections.push({
|
|
805
|
+
id: 'materials',
|
|
806
|
+
title: 'Materials',
|
|
807
|
+
icon: faPalette,
|
|
808
|
+
order: MODEL_SECTION_ORDER.materials,
|
|
809
|
+
description: `${inspection.materials.length} slots`,
|
|
810
|
+
body: {
|
|
811
|
+
kind: 'custom',
|
|
812
|
+
render: () => <NativeMaterialsBody materials={inspection.materials} />,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const skin = inspection.skin;
|
|
817
|
+
if (skin) {
|
|
818
|
+
sections.push({
|
|
819
|
+
id: 'skin',
|
|
820
|
+
title: 'Skin',
|
|
821
|
+
icon: faPersonRunning,
|
|
822
|
+
order: MODEL_SECTION_ORDER.rig,
|
|
823
|
+
description: `${skin.bones} bones`,
|
|
824
|
+
body: { kind: 'custom', render: () => <NativeSkinBody skin={skin} /> },
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const bone = inspection.bone;
|
|
828
|
+
if (bone) {
|
|
829
|
+
sections.push({
|
|
830
|
+
id: 'bone',
|
|
831
|
+
title: 'Bone',
|
|
832
|
+
icon: faBone,
|
|
833
|
+
order: MODEL_SECTION_ORDER.rig + 1,
|
|
834
|
+
description: bone.parent ?? 'Skeleton root',
|
|
835
|
+
body: { kind: 'custom', render: () => <NativeBoneBody bone={bone} /> },
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return sections;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const registrationGroup = createHmrRegistrationGroup(
|
|
842
|
+
import.meta.hot,
|
|
843
|
+
'model-asset-inspector-sections',
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Install this module's two section PRODUCERS. Called by
|
|
848
|
+
* `contributions/model-asset-sections.service.ts`: the `selection.inspector`
|
|
849
|
+
* point carries ONE component, and these register the multi-section producer
|
|
850
|
+
* shape (`InspectorSectionsProducer`) instead — one for the Object3D model
|
|
851
|
+
* document's whole rail, one for a node inside it. HMR-safe by the same group
|
|
852
|
+
* that held the module-scope registration.
|
|
853
|
+
*/
|
|
854
|
+
export function ensureModelAssetSectionsRegistered(): void {
|
|
855
|
+
registrationGroup.ensure((track) => {
|
|
856
|
+
track(
|
|
857
|
+
registerInspectorSections({
|
|
858
|
+
match: isObject3DModelDocument,
|
|
859
|
+
sections: (_node, adapter) => modelDocumentSections(adapter),
|
|
860
|
+
}),
|
|
861
|
+
);
|
|
862
|
+
track(
|
|
863
|
+
registerInspectorSections({
|
|
864
|
+
match: isObject3DModelNode,
|
|
865
|
+
sections: object3dNodeSections,
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
}
|