@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,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed inspector fields for the three common three.js objects a scene author
|
|
3
|
+
* edits by hand — a LIGHT, a mesh's MATERIAL, a CAMERA, or positional AUDIO — as pure functions
|
|
4
|
+
* over the LIVE object.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately NOT a descriptor store, a schema, or a scene-document
|
|
7
|
+
* type. It is a lens: given a live `Object3D` (and, for a mesh, its material),
|
|
8
|
+
* it names the handful of high-value typed properties that object ACTUALLY has
|
|
9
|
+
* right now, reads their current values, and applies an edit back onto the live
|
|
10
|
+
* object. The R3F source authoring adapter wraps these into `PropertyDescriptor`
|
|
11
|
+
* rows and routes the write through its EXISTING source-writeback path (the same
|
|
12
|
+
* `writeProp` a Transform edit or a `material.color` edit already uses) — the
|
|
13
|
+
* three root's source stays the single truth (§Roots), and nothing here invents
|
|
14
|
+
* a second one.
|
|
15
|
+
*
|
|
16
|
+
* Honest absence is the whole point: `typedThreeFields` returns ONLY the fields
|
|
17
|
+
* the given object exposes. A `MeshStandardMaterial` yields `roughness`/
|
|
18
|
+
* `metalness`; a `MeshBasicMaterial` yields neither (it has no such property),
|
|
19
|
+
* so those rows never appear. A `THREE.PointLight` yields `distance`/`decay`
|
|
20
|
+
* but not `angle`/`penumbra` (those are spotlight-only). No selected object of
|
|
21
|
+
* the wrong kind ever shows a row it cannot honor.
|
|
22
|
+
*
|
|
23
|
+
* The property NAMES are the three.js / react-three-fiber intrinsic prop names
|
|
24
|
+
* verbatim (`intensity`, `roughness`, `fov`, …), so a written attribute reads
|
|
25
|
+
* as idiomatic R3F source (`<pointLight intensity={5} />`) and the value the
|
|
26
|
+
* inspector shows is the one the JSX prop would set.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { PropertyDescriptor } from '@volter/editor-project/adapter';
|
|
30
|
+
import * as THREE from 'three';
|
|
31
|
+
|
|
32
|
+
/** The section a typed field renders under (a titled `PropertyDescriptor.group`). */
|
|
33
|
+
export type TypedThreeGroup = 'Light' | 'Material' | 'Camera' | 'Audio';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One typed inspector field: its three.js prop name (also the source attribute
|
|
37
|
+
* name and the inspector path suffix), its widget type, the section it groups
|
|
38
|
+
* under, and WHICH source tag owns its attribute — the object's own JSX element
|
|
39
|
+
* (`self`) or its material's child element (`material`).
|
|
40
|
+
*/
|
|
41
|
+
export interface TypedThreeField {
|
|
42
|
+
readonly prop: string;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly type: Extract<
|
|
45
|
+
PropertyDescriptor['type'],
|
|
46
|
+
'number' | 'color' | 'boolean' | 'enum' | 'asset'
|
|
47
|
+
>;
|
|
48
|
+
readonly options?: readonly string[];
|
|
49
|
+
readonly group: TypedThreeGroup;
|
|
50
|
+
readonly target: 'self' | 'material';
|
|
51
|
+
/** Full inspector path — `${group-prefix}.${prop}`, e.g. `light.intensity`. */
|
|
52
|
+
readonly path: string;
|
|
53
|
+
/**
|
|
54
|
+
* For an `asset` slot only: how the texture's bytes must be INTERPRETED.
|
|
55
|
+
* A slot the shader reads as COLOUR (`map`, `emissiveMap`) is authored in
|
|
56
|
+
* sRGB and must be decoded; a slot it reads as DATA (`normalMap`,
|
|
57
|
+
* `roughnessMap`, `metalnessMap`, `aoMap`) carries raw numbers and must not
|
|
58
|
+
* be. Getting this wrong is not subtle — a decoded normal map bends light in
|
|
59
|
+
* the wrong direction — and three.js cannot infer it from the file, which is
|
|
60
|
+
* why the slot declares it here rather than the loader guessing.
|
|
61
|
+
*/
|
|
62
|
+
readonly colorSpace?: 'srgb' | 'linear';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Path prefix per group, so `get`/`set` can dispatch on the path alone. */
|
|
66
|
+
const PREFIX: Record<TypedThreeGroup, string> = {
|
|
67
|
+
Light: 'light',
|
|
68
|
+
Material: 'material',
|
|
69
|
+
Camera: 'camera',
|
|
70
|
+
Audio: 'audio',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function field(
|
|
74
|
+
group: TypedThreeGroup,
|
|
75
|
+
target: TypedThreeField['target'],
|
|
76
|
+
prop: string,
|
|
77
|
+
label: string,
|
|
78
|
+
type: TypedThreeField['type'],
|
|
79
|
+
path = `${PREFIX[group]}.${prop}`,
|
|
80
|
+
options?: readonly string[],
|
|
81
|
+
): TypedThreeField {
|
|
82
|
+
return { prop, label, type, group, target, path, ...(options ? { options } : {}) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** An `asset` slot on a material — a texture map, with the colour space its
|
|
86
|
+
* shader input demands (see {@link TypedThreeField.colorSpace}). */
|
|
87
|
+
function mapField(prop: string, label: string, colorSpace: 'srgb' | 'linear'): TypedThreeField {
|
|
88
|
+
return {
|
|
89
|
+
prop,
|
|
90
|
+
label,
|
|
91
|
+
type: 'asset',
|
|
92
|
+
group: 'Material',
|
|
93
|
+
target: 'material',
|
|
94
|
+
path: `${PREFIX.Material}.${prop}`,
|
|
95
|
+
colorSpace,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The texture slots this lens offers, in the order a material author reaches
|
|
101
|
+
* for them. Each row appears only when the CONCRETE material class carries
|
|
102
|
+
* that property — a `MeshBasicMaterial` has `map` and `aoMap` but no
|
|
103
|
+
* `normalMap`/`roughnessMap`/`metalnessMap`/`emissiveMap`, so those four never
|
|
104
|
+
* render for one. Honest absence, exactly as the scalar rows above.
|
|
105
|
+
*/
|
|
106
|
+
const MATERIAL_MAP_SLOTS: readonly TypedThreeField[] = [
|
|
107
|
+
mapField('map', 'Base Color Map', 'srgb'),
|
|
108
|
+
mapField('normalMap', 'Normal Map', 'linear'),
|
|
109
|
+
mapField('roughnessMap', 'Roughness Map', 'linear'),
|
|
110
|
+
mapField('metalnessMap', 'Metalness Map', 'linear'),
|
|
111
|
+
mapField('emissiveMap', 'Emissive Map', 'srgb'),
|
|
112
|
+
mapField('aoMap', 'Ambient Occlusion Map', 'linear'),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** `#rrggbb` view of a three color property, or undefined when absent. */
|
|
116
|
+
function readColor(value: unknown): string | undefined {
|
|
117
|
+
const color = value as THREE.Color | undefined;
|
|
118
|
+
return color && (color as { isColor?: boolean }).isColor ? `#${color.getHexString()}` : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ------------------------------------------------------------------ textures
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `userData` key carrying the project path an assigned texture came FROM.
|
|
125
|
+
*
|
|
126
|
+
* A `THREE.Texture` does not remember its origin in any form the inspector can
|
|
127
|
+
* show: `image.src` is an absolute URL the browser resolved, and a texture that
|
|
128
|
+
* arrived inside a `.glb` has no URL at all. Stamping the path we were handed
|
|
129
|
+
* is what lets the slot read back the same string the picker offered.
|
|
130
|
+
*/
|
|
131
|
+
const ASSET_PATH_KEY = 'vgaiAssetPath';
|
|
132
|
+
|
|
133
|
+
/** Textures THIS module loaded. Only these are disposed on reassign/clear —
|
|
134
|
+
* a texture that came from a GLB or the game's own code may be shared by
|
|
135
|
+
* other materials, and disposing it would blank them too. */
|
|
136
|
+
const ownedTextures = new WeakSet<THREE.Texture>();
|
|
137
|
+
|
|
138
|
+
/** One loader for the session — three's own loader manager caches per URL. */
|
|
139
|
+
const textureLoader = new THREE.TextureLoader();
|
|
140
|
+
|
|
141
|
+
/** How an assigned texture reads back in the inspector: the project path it
|
|
142
|
+
* was assigned from, else whatever identity the texture already carried. */
|
|
143
|
+
function readTextureRef(value: unknown): string {
|
|
144
|
+
const texture = value as THREE.Texture | null | undefined;
|
|
145
|
+
if (!texture || !(texture as { isTexture?: boolean }).isTexture) return '';
|
|
146
|
+
const stamped = (texture.userData as Record<string, unknown> | undefined)?.[ASSET_PATH_KEY];
|
|
147
|
+
if (typeof stamped === 'string' && stamped) return stamped;
|
|
148
|
+
if (texture.name) return texture.name;
|
|
149
|
+
const src = (texture.image as { src?: string } | undefined)?.src;
|
|
150
|
+
return typeof src === 'string' ? src : '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The served URL for a project asset path. Public-root-relative paths are
|
|
154
|
+
* what the asset seams speak (`textures/brick.png`); anything already
|
|
155
|
+
* absolute or inline is passed through untouched. */
|
|
156
|
+
function assetUrlFor(path: string): string {
|
|
157
|
+
return /^(?:[a-z]+:|\/)/i.test(path) ? path : `/${path}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Assign (or clear) a texture slot on the LIVE material.
|
|
162
|
+
*
|
|
163
|
+
* `TextureLoader.load` hands back the `Texture` object synchronously and fills
|
|
164
|
+
* its image in later, setting `needsUpdate` itself — so the assignment is a
|
|
165
|
+
* plain property write here and the viewport picks the pixels up on the frame
|
|
166
|
+
* they arrive. Returns the path to persist, `''` for a cleared slot, or `null`
|
|
167
|
+
* when there is no material to assign to.
|
|
168
|
+
*/
|
|
169
|
+
function applyAssetField(
|
|
170
|
+
material: THREE.Material | null,
|
|
171
|
+
field: TypedThreeField,
|
|
172
|
+
value: unknown,
|
|
173
|
+
): string | null {
|
|
174
|
+
if (!material) return null;
|
|
175
|
+
const owner = material as unknown as Record<string, unknown>;
|
|
176
|
+
const previous = owner[field.prop] as THREE.Texture | null | undefined;
|
|
177
|
+
const path = value === null || value === undefined ? '' : String(value);
|
|
178
|
+
if (previous && ownedTextures.has(previous) && readTextureRef(previous) !== path) {
|
|
179
|
+
previous.dispose();
|
|
180
|
+
}
|
|
181
|
+
if (!path) {
|
|
182
|
+
owner[field.prop] = null;
|
|
183
|
+
material.needsUpdate = true;
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
const texture = textureLoader.load(assetUrlFor(path));
|
|
187
|
+
texture.colorSpace = field.colorSpace === 'srgb' ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
188
|
+
texture.userData[ASSET_PATH_KEY] = path;
|
|
189
|
+
texture.name = path;
|
|
190
|
+
ownedTextures.add(texture);
|
|
191
|
+
owner[field.prop] = texture;
|
|
192
|
+
// Adding or removing a map changes which shader three compiles for this
|
|
193
|
+
// material, so the program must be rebuilt — a bare property write is not
|
|
194
|
+
// enough for texture slots the way it is for a scalar.
|
|
195
|
+
material.needsUpdate = true;
|
|
196
|
+
return path;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------- descriptors
|
|
200
|
+
|
|
201
|
+
/** Typed fields a `THREE.Light` exposes — common to every light, then the
|
|
202
|
+
* point/spot falloff and the spot cone, each guarded by the concrete subtype. */
|
|
203
|
+
export function lightTypedFields(light: THREE.Light): readonly TypedThreeField[] {
|
|
204
|
+
const l = light as THREE.Light & {
|
|
205
|
+
isDirectionalLight?: boolean;
|
|
206
|
+
isPointLight?: boolean;
|
|
207
|
+
isSpotLight?: boolean;
|
|
208
|
+
};
|
|
209
|
+
const fields: TypedThreeField[] = [
|
|
210
|
+
field('Light', 'self', 'color', 'Color', 'color'),
|
|
211
|
+
field('Light', 'self', 'intensity', 'Intensity', 'number'),
|
|
212
|
+
];
|
|
213
|
+
if (l.isPointLight || l.isSpotLight) {
|
|
214
|
+
fields.push(
|
|
215
|
+
field('Light', 'self', 'distance', 'Distance', 'number'),
|
|
216
|
+
field('Light', 'self', 'decay', 'Decay', 'number'),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (l.isSpotLight) {
|
|
220
|
+
fields.push(
|
|
221
|
+
field('Light', 'self', 'angle', 'Angle', 'number'),
|
|
222
|
+
field('Light', 'self', 'penumbra', 'Penumbra', 'number'),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return fields;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Shadow casting is an Object3D flag rather than a Light member in three.js,
|
|
229
|
+
* so it remains a separate lens from {@link lightTypedFields}. Only the three
|
|
230
|
+
* light classes whose render path can honor it expose the row. */
|
|
231
|
+
export function lightShadowTypedField(object: THREE.Object3D): TypedThreeField | undefined {
|
|
232
|
+
const light = object as THREE.Light & {
|
|
233
|
+
isDirectionalLight?: boolean;
|
|
234
|
+
isPointLight?: boolean;
|
|
235
|
+
isSpotLight?: boolean;
|
|
236
|
+
};
|
|
237
|
+
return light.isDirectionalLight || light.isPointLight || light.isSpotLight
|
|
238
|
+
? field('Light', 'self', 'castShadow', 'Cast Shadow', 'boolean', 'shadow.cast')
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Typed fields a material exposes — each guarded by whether the concrete
|
|
243
|
+
* material class actually carries that property (a `MeshBasicMaterial` has no
|
|
244
|
+
* `roughness`/`metalness`/`emissive`, so none appear). */
|
|
245
|
+
export function materialTypedFields(material: THREE.Material): readonly TypedThreeField[] {
|
|
246
|
+
const m = material as THREE.Material & {
|
|
247
|
+
color?: THREE.Color;
|
|
248
|
+
roughness?: number;
|
|
249
|
+
metalness?: number;
|
|
250
|
+
emissive?: THREE.Color;
|
|
251
|
+
wireframe?: boolean;
|
|
252
|
+
};
|
|
253
|
+
const fields: TypedThreeField[] = [];
|
|
254
|
+
if ((m.color as { isColor?: boolean } | undefined)?.isColor)
|
|
255
|
+
fields.push(field('Material', 'material', 'color', 'Color', 'color'));
|
|
256
|
+
if (typeof m.roughness === 'number')
|
|
257
|
+
fields.push(field('Material', 'material', 'roughness', 'Roughness', 'number'));
|
|
258
|
+
if (typeof m.metalness === 'number')
|
|
259
|
+
fields.push(field('Material', 'material', 'metalness', 'Metalness', 'number'));
|
|
260
|
+
if ((m.emissive as { isColor?: boolean } | undefined)?.isColor)
|
|
261
|
+
fields.push(field('Material', 'material', 'emissive', 'Emissive', 'color'));
|
|
262
|
+
// `opacity`/`transparent` live on the base `Material` — every material has them.
|
|
263
|
+
fields.push(
|
|
264
|
+
field('Material', 'material', 'opacity', 'Opacity', 'number'),
|
|
265
|
+
field('Material', 'material', 'transparent', 'Transparent', 'boolean'),
|
|
266
|
+
);
|
|
267
|
+
if (typeof m.wireframe === 'boolean')
|
|
268
|
+
fields.push(field('Material', 'material', 'wireframe', 'Wireframe', 'boolean'));
|
|
269
|
+
// Texture slots last: they are the tallest rows, and reading the scalars
|
|
270
|
+
// first is the order every material inspector uses.
|
|
271
|
+
const owner = material as unknown as Record<string, unknown>;
|
|
272
|
+
for (const slot of MATERIAL_MAP_SLOTS) if (slot.prop in owner) fields.push(slot);
|
|
273
|
+
return fields;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Typed fields a camera exposes — the perspective lens, or the orthographic
|
|
277
|
+
* zoom + frustum, keyed off the concrete camera subtype. */
|
|
278
|
+
export function cameraTypedFields(camera: THREE.Camera): readonly TypedThreeField[] {
|
|
279
|
+
const c = camera as THREE.Camera & {
|
|
280
|
+
isPerspectiveCamera?: boolean;
|
|
281
|
+
isOrthographicCamera?: boolean;
|
|
282
|
+
};
|
|
283
|
+
if (c.isPerspectiveCamera) {
|
|
284
|
+
return [
|
|
285
|
+
field('Camera', 'self', 'fov', 'FOV', 'number'),
|
|
286
|
+
field('Camera', 'self', 'near', 'Near', 'number'),
|
|
287
|
+
field('Camera', 'self', 'far', 'Far', 'number'),
|
|
288
|
+
field('Camera', 'self', 'zoom', 'Zoom', 'number'),
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
if (c.isOrthographicCamera) {
|
|
292
|
+
return [
|
|
293
|
+
field('Camera', 'self', 'zoom', 'Zoom', 'number'),
|
|
294
|
+
field('Camera', 'self', 'near', 'Near', 'number'),
|
|
295
|
+
field('Camera', 'self', 'far', 'Far', 'number'),
|
|
296
|
+
field('Camera', 'self', 'left', 'Left', 'number'),
|
|
297
|
+
field('Camera', 'self', 'right', 'Right', 'number'),
|
|
298
|
+
field('Camera', 'self', 'top', 'Top', 'number'),
|
|
299
|
+
field('Camera', 'self', 'bottom', 'Bottom', 'number'),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Structural rather than `instanceof`: ingested worlds can carry a different
|
|
306
|
+
* three constructor while still exposing the native PositionalAudio contract. */
|
|
307
|
+
export function isPositionalAudioObject(object: THREE.Object3D): object is THREE.PositionalAudio {
|
|
308
|
+
const candidate = object as THREE.Object3D & Partial<THREE.PositionalAudio>;
|
|
309
|
+
return (
|
|
310
|
+
typeof candidate.getRefDistance === 'function' &&
|
|
311
|
+
typeof candidate.setRefDistance === 'function' &&
|
|
312
|
+
!!candidate.panner
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Native positional-audio fields, expressed with the source props their R3F
|
|
318
|
+
* owner actually accepts. Drei's `<PositionalAudio>` names reference distance
|
|
319
|
+
* `distance`; the native `<positionalAudio>` intrinsic reaches the PannerNode
|
|
320
|
+
* through R3F's ordinary dash-piercing syntax.
|
|
321
|
+
*/
|
|
322
|
+
export function audioTypedFields(sourceTag = 'PositionalAudio'): readonly TypedThreeField[] {
|
|
323
|
+
const refProp = sourceTag === 'PositionalAudio' ? 'distance' : 'panner-refDistance';
|
|
324
|
+
return [
|
|
325
|
+
field('Audio', 'self', refProp, 'Reference Distance', 'number', 'audio.refDistance'),
|
|
326
|
+
field(
|
|
327
|
+
'Audio',
|
|
328
|
+
'self',
|
|
329
|
+
'panner-distanceModel',
|
|
330
|
+
'Distance Model',
|
|
331
|
+
'enum',
|
|
332
|
+
'audio.distanceModel',
|
|
333
|
+
['inverse', 'linear', 'exponential'],
|
|
334
|
+
),
|
|
335
|
+
field('Audio', 'self', 'panner-rolloffFactor', 'Rolloff', 'number', 'audio.rolloffFactor'),
|
|
336
|
+
field('Audio', 'self', 'panner-maxDistance', 'Maximum Distance', 'number', 'audio.maxDistance'),
|
|
337
|
+
field('Audio', 'self', 'panner-coneInnerAngle', 'Inner Cone', 'number', 'audio.coneInnerAngle'),
|
|
338
|
+
field('Audio', 'self', 'panner-coneOuterAngle', 'Outer Cone', 'number', 'audio.coneOuterAngle'),
|
|
339
|
+
field('Audio', 'self', 'panner-coneOuterGain', 'Outside Gain', 'number', 'audio.coneOuterGain'),
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Every typed field the given object exposes right now: its light fields when it
|
|
345
|
+
* is a light, its camera fields when it is a camera, and its material's fields
|
|
346
|
+
* when a material is supplied. An object that is none of these yields `[]`.
|
|
347
|
+
*/
|
|
348
|
+
export function typedThreeFields(
|
|
349
|
+
object: THREE.Object3D,
|
|
350
|
+
material: THREE.Material | null,
|
|
351
|
+
sourceTag?: string,
|
|
352
|
+
): readonly TypedThreeField[] {
|
|
353
|
+
const o = object as THREE.Object3D & { isLight?: boolean; isCamera?: boolean };
|
|
354
|
+
const fields: TypedThreeField[] = [];
|
|
355
|
+
if (o.isLight) fields.push(...lightTypedFields(object as unknown as THREE.Light));
|
|
356
|
+
if (o.isCamera) fields.push(...cameraTypedFields(object as THREE.Camera));
|
|
357
|
+
if (isPositionalAudioObject(object)) fields.push(...audioTypedFields(sourceTag));
|
|
358
|
+
if (material) fields.push(...materialTypedFields(material));
|
|
359
|
+
return fields;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The typed field a path names for this object, or undefined when the object
|
|
363
|
+
* has no such field (honest absence — a stale/foreign path reads as nothing). */
|
|
364
|
+
export function typedFieldForPath(
|
|
365
|
+
object: THREE.Object3D,
|
|
366
|
+
material: THREE.Material | null,
|
|
367
|
+
path: string,
|
|
368
|
+
sourceTag?: string,
|
|
369
|
+
): TypedThreeField | undefined {
|
|
370
|
+
const shadow = lightShadowTypedField(object);
|
|
371
|
+
return (
|
|
372
|
+
typedThreeFields(object, material, sourceTag).find((f) => f.path === path) ??
|
|
373
|
+
(shadow?.path === path ? shadow : undefined)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --------------------------------------------------------------- read / apply
|
|
378
|
+
|
|
379
|
+
/** The property owner a field targets: the object itself, or its material. */
|
|
380
|
+
function ownerOf(
|
|
381
|
+
object: THREE.Object3D,
|
|
382
|
+
material: THREE.Material | null,
|
|
383
|
+
field: TypedThreeField,
|
|
384
|
+
): Record<string, unknown> | null {
|
|
385
|
+
return (field.target === 'material' ? material : object) as Record<string, unknown> | null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The inspector value for a typed field, read from the LIVE object. Color
|
|
389
|
+
* fields read as `#rrggbb`; number/boolean fields read their raw value. */
|
|
390
|
+
export function readTypedField(
|
|
391
|
+
object: THREE.Object3D,
|
|
392
|
+
material: THREE.Material | null,
|
|
393
|
+
field: TypedThreeField,
|
|
394
|
+
): unknown {
|
|
395
|
+
if (field.group === 'Audio' && isPositionalAudioObject(object)) {
|
|
396
|
+
const panner = object.panner;
|
|
397
|
+
switch (field.path) {
|
|
398
|
+
case 'audio.refDistance':
|
|
399
|
+
return object.getRefDistance();
|
|
400
|
+
case 'audio.distanceModel':
|
|
401
|
+
return object.getDistanceModel();
|
|
402
|
+
case 'audio.rolloffFactor':
|
|
403
|
+
return object.getRolloffFactor();
|
|
404
|
+
case 'audio.maxDistance':
|
|
405
|
+
return object.getMaxDistance();
|
|
406
|
+
case 'audio.coneInnerAngle':
|
|
407
|
+
return panner.coneInnerAngle;
|
|
408
|
+
case 'audio.coneOuterAngle':
|
|
409
|
+
return panner.coneOuterAngle;
|
|
410
|
+
case 'audio.coneOuterGain':
|
|
411
|
+
return panner.coneOuterGain;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const owner = ownerOf(object, material, field);
|
|
415
|
+
if (!owner) return undefined;
|
|
416
|
+
const raw = owner[field.prop];
|
|
417
|
+
if (field.type === 'color') return readColor(raw);
|
|
418
|
+
if (field.type === 'asset') return readTextureRef(raw);
|
|
419
|
+
return raw;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function applyAudioField(
|
|
423
|
+
object: THREE.PositionalAudio,
|
|
424
|
+
field: TypedThreeField,
|
|
425
|
+
value: unknown,
|
|
426
|
+
): string | number | null {
|
|
427
|
+
if (field.path === 'audio.distanceModel') {
|
|
428
|
+
const model = String(value);
|
|
429
|
+
if (model !== 'inverse' && model !== 'linear' && model !== 'exponential') return null;
|
|
430
|
+
object.setDistanceModel(model);
|
|
431
|
+
return model;
|
|
432
|
+
}
|
|
433
|
+
const num = Number(value);
|
|
434
|
+
if (!Number.isFinite(num)) return null;
|
|
435
|
+
switch (field.path) {
|
|
436
|
+
case 'audio.refDistance':
|
|
437
|
+
object.setRefDistance(Math.max(0.01, num));
|
|
438
|
+
return Math.max(0.01, num);
|
|
439
|
+
case 'audio.rolloffFactor':
|
|
440
|
+
object.setRolloffFactor(Math.max(0, num));
|
|
441
|
+
return Math.max(0, num);
|
|
442
|
+
case 'audio.maxDistance':
|
|
443
|
+
object.setMaxDistance(Math.max(object.getRefDistance() + 0.01, num));
|
|
444
|
+
return Math.max(object.getRefDistance() + 0.01, num);
|
|
445
|
+
case 'audio.coneInnerAngle': {
|
|
446
|
+
const inner = Math.max(0, Math.min(object.panner.coneOuterAngle, num));
|
|
447
|
+
object.setDirectionalCone(inner, object.panner.coneOuterAngle, object.panner.coneOuterGain);
|
|
448
|
+
return inner;
|
|
449
|
+
}
|
|
450
|
+
case 'audio.coneOuterAngle': {
|
|
451
|
+
const outer = Math.max(object.panner.coneInnerAngle, Math.min(360, num));
|
|
452
|
+
object.setDirectionalCone(object.panner.coneInnerAngle, outer, object.panner.coneOuterGain);
|
|
453
|
+
return outer;
|
|
454
|
+
}
|
|
455
|
+
case 'audio.coneOuterGain': {
|
|
456
|
+
const gain = Math.max(0, Math.min(1, num));
|
|
457
|
+
object.setDirectionalCone(object.panner.coneInnerAngle, object.panner.coneOuterAngle, gain);
|
|
458
|
+
return gain;
|
|
459
|
+
}
|
|
460
|
+
default:
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Apply a typed edit onto the LIVE object (optimistic preview) and return the
|
|
467
|
+
* value to persist to source — a `#rrggbb` string for a color, the number for a
|
|
468
|
+
* number, the boolean for a boolean, the project asset path (or `''` for a
|
|
469
|
+
* cleared slot) for an `asset` — or `null` when the edit cannot be applied
|
|
470
|
+
* (no owner, or a non-finite number). The caller serializes and writes it back
|
|
471
|
+
* through the source-write backend; nothing here touches source.
|
|
472
|
+
*
|
|
473
|
+
* An `asset` value is NOT a JSX-attribute literal — a texture reaches R3F
|
|
474
|
+
* source through a loader hook, not a string prop — so the lane that owns
|
|
475
|
+
* source persistence refuses that half by name rather than writing a string
|
|
476
|
+
* where a `THREE.Texture` belongs. The live half above is real either way.
|
|
477
|
+
*/
|
|
478
|
+
export function applyTypedField(
|
|
479
|
+
object: THREE.Object3D,
|
|
480
|
+
material: THREE.Material | null,
|
|
481
|
+
field: TypedThreeField,
|
|
482
|
+
value: unknown,
|
|
483
|
+
): string | number | boolean | null {
|
|
484
|
+
if (field.group === 'Audio' && isPositionalAudioObject(object)) {
|
|
485
|
+
return applyAudioField(object, field, value);
|
|
486
|
+
}
|
|
487
|
+
if (field.type === 'asset') return applyAssetField(material, field, value);
|
|
488
|
+
const owner = ownerOf(object, material, field);
|
|
489
|
+
if (!owner) return null;
|
|
490
|
+
if (field.type === 'color') {
|
|
491
|
+
const hex = String(value);
|
|
492
|
+
const color = owner[field.prop] as THREE.Color | undefined;
|
|
493
|
+
color?.set(hex);
|
|
494
|
+
return hex;
|
|
495
|
+
}
|
|
496
|
+
if (field.type === 'boolean') {
|
|
497
|
+
const bool = Boolean(value);
|
|
498
|
+
owner[field.prop] = bool;
|
|
499
|
+
markDirty(object, material, field);
|
|
500
|
+
return bool;
|
|
501
|
+
}
|
|
502
|
+
const num = Number(value);
|
|
503
|
+
if (!Number.isFinite(num)) return null;
|
|
504
|
+
owner[field.prop] = num;
|
|
505
|
+
markDirty(object, material, field);
|
|
506
|
+
return num;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Re-derive state that three.js does not recompute on a bare property write:
|
|
510
|
+
* a camera's projection matrix, and a material's `needsUpdate` for flags that
|
|
511
|
+
* change how it compiles (`transparent`/`wireframe`). */
|
|
512
|
+
function markDirty(
|
|
513
|
+
object: THREE.Object3D,
|
|
514
|
+
material: THREE.Material | null,
|
|
515
|
+
field: TypedThreeField,
|
|
516
|
+
): void {
|
|
517
|
+
if (field.group === 'Camera') {
|
|
518
|
+
const cam = object as THREE.Camera & { updateProjectionMatrix?: () => void };
|
|
519
|
+
cam.updateProjectionMatrix?.();
|
|
520
|
+
}
|
|
521
|
+
if (
|
|
522
|
+
field.target === 'material' &&
|
|
523
|
+
material &&
|
|
524
|
+
(field.prop === 'transparent' || field.prop === 'wireframe')
|
|
525
|
+
) {
|
|
526
|
+
material.needsUpdate = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Extract Component…" — the native row's structural extraction, registered
|
|
3
|
+
* through the host's hierarchy contribution seam.
|
|
4
|
+
*
|
|
5
|
+
* The fork menu's mirror (`fork-menu.ts`), reusing the host's locator
|
|
6
|
+
* resolution for the same reason it does: the extract accessors are plain
|
|
7
|
+
* methods on the R3F adapter rather than routed contract members, so ownership
|
|
8
|
+
* is resolved through the composite's public `ownerOf` walk and the owning
|
|
9
|
+
* child is probed directly. The applicability rule and every sentence are pure
|
|
10
|
+
* and live in `@editor/instance-extract-actions`; the source rewrite is pure
|
|
11
|
+
* and lives server-side in the host's `ui-source/plan-extract-component.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
|
|
15
|
+
import { instanceSourceLocatorFor } from '@volter/editor-core/authoring/instance-source-menu';
|
|
16
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
17
|
+
import { registerHierarchyMenuItems } from '@volter/editor-core/hierarchy-menu-registry';
|
|
18
|
+
import {
|
|
19
|
+
canExtractNode,
|
|
20
|
+
EXTRACT_COMPONENT_LABEL,
|
|
21
|
+
type InstanceExtractSource,
|
|
22
|
+
} from '../../host/instance-extract-actions';
|
|
23
|
+
import { showTransientHint } from '@volter/editor-core/transient-hint';
|
|
24
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
25
|
+
|
|
26
|
+
/** The extract surface for `nodeId`, or `null` — the host's own owner walk. */
|
|
27
|
+
export function instanceExtractSourceFor(
|
|
28
|
+
adapter: AuthoringAdapter,
|
|
29
|
+
nodeId: string,
|
|
30
|
+
): InstanceExtractSource | null {
|
|
31
|
+
return instanceSourceLocatorFor(adapter, nodeId) as InstanceExtractSource | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractable(store: EditorShellStore, nodeId: string): boolean {
|
|
35
|
+
const adapter = getActiveAuthoring(store);
|
|
36
|
+
return canExtractNode(
|
|
37
|
+
adapter.hierarchy.node(nodeId),
|
|
38
|
+
instanceExtractSourceFor(adapter, nodeId),
|
|
39
|
+
nodeId,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let unregister: (() => void) | null = null;
|
|
44
|
+
|
|
45
|
+
/** Idempotent install of the extract item (see `component-verbs.service.ts`). */
|
|
46
|
+
export function ensureInstanceExtractMenuRegistered(): void {
|
|
47
|
+
if (unregister) return;
|
|
48
|
+
unregister = registerHierarchyMenuItems(
|
|
49
|
+
({ nodeId, store }) => extractable(store, nodeId),
|
|
50
|
+
({ nodeId, store }) => {
|
|
51
|
+
const source = instanceExtractSourceFor(getActiveAuthoring(store), nodeId);
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
label: EXTRACT_COMPONENT_LABEL,
|
|
55
|
+
action: () => {
|
|
56
|
+
// Every outcome — created, refused by the plan, refused by
|
|
57
|
+
// transport, or created-but-not-replaced — comes back as a
|
|
58
|
+
// sentence from the adapter. No click is silent.
|
|
59
|
+
void source?.extractComponent?.(nodeId).then(showTransientHint);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Drop the registration — the service contribution's stop. */
|
|
68
|
+
export function unregisterInstanceExtractMenu(): void {
|
|
69
|
+
unregister?.();
|
|
70
|
+
unregister = null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Fork Component…" — the instance row's definition copy, registered through
|
|
3
|
+
* the host's hierarchy contribution seam.
|
|
4
|
+
*
|
|
5
|
+
* Same split as `extract-menu.ts`, and the SAME structural locator probe:
|
|
6
|
+
* `GameHierarchy` stays contract-only and never learns that a three world has
|
|
7
|
+
* a component definition to copy; THIS module resolves the owning child
|
|
8
|
+
* adapter (through the composite's `ownerOf`, not by decoding the id) and
|
|
9
|
+
* probes it for the fork accessors. The applicability rule and every sentence
|
|
10
|
+
* are pure and live in `@editor/instance-fork-actions`; the source rewrite
|
|
11
|
+
* itself is pure and lives server-side in the host's
|
|
12
|
+
* `ui-source/plan-fork-component.ts`.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT REUSES THE SOURCE MENU's LOCATOR RESOLUTION rather than the
|
|
15
|
+
* composite's routing (which `internals-menu.ts` can use): the fork accessors
|
|
16
|
+
* are plain methods on the R3F adapter rather than routed contract members, so
|
|
17
|
+
* asking the composite directly would ask the wrong object.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
|
|
21
|
+
import { instanceSourceLocatorFor } from '@volter/editor-core/authoring/instance-source-menu';
|
|
22
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
23
|
+
import { registerHierarchyMenuItems } from '@volter/editor-core/hierarchy-menu-registry';
|
|
24
|
+
import {
|
|
25
|
+
canForkInstance,
|
|
26
|
+
FORK_COMPONENT_LABEL,
|
|
27
|
+
type InstanceForkSource,
|
|
28
|
+
} from '../../host/instance-fork-actions';
|
|
29
|
+
import { showTransientHint } from '@volter/editor-core/transient-hint';
|
|
30
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
31
|
+
|
|
32
|
+
/** The fork surface for `nodeId`, or `null` — the host's own owner walk. */
|
|
33
|
+
export function instanceForkSourceFor(
|
|
34
|
+
adapter: AuthoringAdapter,
|
|
35
|
+
nodeId: string,
|
|
36
|
+
): InstanceForkSource | null {
|
|
37
|
+
return instanceSourceLocatorFor(adapter, nodeId) as InstanceForkSource | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function forkable(store: EditorShellStore, nodeId: string): boolean {
|
|
41
|
+
const adapter = getActiveAuthoring(store);
|
|
42
|
+
return canForkInstance(
|
|
43
|
+
adapter.hierarchy.node(nodeId),
|
|
44
|
+
instanceForkSourceFor(adapter, nodeId),
|
|
45
|
+
nodeId,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let unregister: (() => void) | null = null;
|
|
50
|
+
|
|
51
|
+
/** Idempotent install of the fork item (see `component-verbs.service.ts`). */
|
|
52
|
+
export function ensureInstanceForkMenuRegistered(): void {
|
|
53
|
+
if (unregister) return;
|
|
54
|
+
unregister = registerHierarchyMenuItems(
|
|
55
|
+
({ nodeId, store }) => forkable(store, nodeId),
|
|
56
|
+
({ nodeId, store }) => {
|
|
57
|
+
const source = instanceForkSourceFor(getActiveAuthoring(store), nodeId);
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
label: FORK_COMPONENT_LABEL,
|
|
61
|
+
action: () => {
|
|
62
|
+
// Every outcome — created, refused by the plan, refused by
|
|
63
|
+
// transport, or created-but-not-retargeted — comes back as a
|
|
64
|
+
// sentence from the adapter. Nothing here invents wording, and no
|
|
65
|
+
// click is silent.
|
|
66
|
+
void source?.forkComponent?.(nodeId).then(showTransientHint);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Drop the registration — the service contribution's stop. */
|
|
75
|
+
export function unregisterInstanceForkMenu(): void {
|
|
76
|
+
unregister?.();
|
|
77
|
+
unregister = null;
|
|
78
|
+
}
|