@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.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. 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
+ }