@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,76 @@
|
|
|
1
|
+
import { useEditorStore } from '@volter/editor-core/editor-runtime';
|
|
2
|
+
import type { InspectorSectionProps } from '@volter/editor-core/inspector-section-registry';
|
|
3
|
+
import type { AuthoringAdapter, EditorNode } from '@volter/editor-project/adapter';
|
|
4
|
+
import { type ReflectionProbeSnapshot, reflectionProbeOf } from '@volter/threejs-runtime/adapter/reflection-probe';
|
|
5
|
+
import { Button, EditorBanner, FieldGroup, FieldRow, Text } from '@volter/editor-sdk/widgets';
|
|
6
|
+
import { useSyncExternalStore } from 'react';
|
|
7
|
+
|
|
8
|
+
export function matches(node: EditorNode | null, adapter: AuthoringAdapter): boolean {
|
|
9
|
+
void adapter;
|
|
10
|
+
return node?.kind === 'reflection-probe';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STATUS_LABEL: Record<ReflectionProbeSnapshot['status'], string> = {
|
|
14
|
+
idle: 'Not captured',
|
|
15
|
+
queued: 'Capture queued',
|
|
16
|
+
capturing: 'Capturing…',
|
|
17
|
+
ready: 'Ready',
|
|
18
|
+
error: 'Capture failed',
|
|
19
|
+
};
|
|
20
|
+
const EMPTY_SNAPSHOT: ReflectionProbeSnapshot = { status: 'idle', lastCapturedAt: null };
|
|
21
|
+
|
|
22
|
+
export function ReflectionProbeCaptureSection({ adapter, nodeId }: InspectorSectionProps) {
|
|
23
|
+
const store = useEditorStore();
|
|
24
|
+
const object = nodeId
|
|
25
|
+
? (store.objectMap.get(nodeId) ?? adapter.hierarchy.object3D?.(nodeId))
|
|
26
|
+
: null;
|
|
27
|
+
const probe = reflectionProbeOf(object);
|
|
28
|
+
const snapshot = useSyncExternalStore(
|
|
29
|
+
(listener) => probe?.subscribe(listener) ?? (() => {}),
|
|
30
|
+
() => probe?.getSnapshot() ?? EMPTY_SNAPSHOT,
|
|
31
|
+
);
|
|
32
|
+
if (!probe) return null;
|
|
33
|
+
const mode =
|
|
34
|
+
probe.config.captureMode === 'on-change'
|
|
35
|
+
? 'On change'
|
|
36
|
+
: probe.config.captureMode === 'realtime'
|
|
37
|
+
? 'Realtime'
|
|
38
|
+
: 'Manual';
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{snapshot.status === 'error' && snapshot.message ? (
|
|
42
|
+
<EditorBanner tone="error">{snapshot.message}</EditorBanner>
|
|
43
|
+
) : null}
|
|
44
|
+
<FieldGroup>
|
|
45
|
+
<FieldRow label="Status">
|
|
46
|
+
<Text
|
|
47
|
+
tone={
|
|
48
|
+
snapshot.status === 'error'
|
|
49
|
+
? 'danger'
|
|
50
|
+
: snapshot.status === 'ready'
|
|
51
|
+
? 'success'
|
|
52
|
+
: 'muted'
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
{STATUS_LABEL[snapshot.status]}
|
|
56
|
+
</Text>
|
|
57
|
+
</FieldRow>
|
|
58
|
+
<FieldRow label="Update">
|
|
59
|
+
<Text tone="muted">{mode}</Text>
|
|
60
|
+
</FieldRow>
|
|
61
|
+
<FieldRow label="Cubemap">
|
|
62
|
+
<Text tone="muted">{`${probe.config.resolution} × ${probe.config.resolution}`}</Text>
|
|
63
|
+
</FieldRow>
|
|
64
|
+
<FieldRow label="Capture">
|
|
65
|
+
<Button
|
|
66
|
+
size="compact"
|
|
67
|
+
disabled={snapshot.status === 'capturing'}
|
|
68
|
+
onClick={() => probe.recapture()}
|
|
69
|
+
>
|
|
70
|
+
Recapture
|
|
71
|
+
</Button>
|
|
72
|
+
</FieldRow>
|
|
73
|
+
</FieldGroup>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positional-audio projection onto the generic spatial-handle seam.
|
|
3
|
+
*
|
|
4
|
+
* Web Audio owns every value and Three owns the live object. This module only
|
|
5
|
+
* translates those native values into world-space guide geometry and turns a
|
|
6
|
+
* world-space drag point back into one native value. It stores nothing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SpatialHandleLayer, SpatialPoint3 } from '@volter/editor-project/adapter';
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import {
|
|
12
|
+
applyTypedField,
|
|
13
|
+
isPositionalAudioObject,
|
|
14
|
+
type TypedThreeField,
|
|
15
|
+
typedFieldForPath,
|
|
16
|
+
} from './typed-three-inspector';
|
|
17
|
+
|
|
18
|
+
const REF_COLOR = '#42c8ff';
|
|
19
|
+
const MAX_COLOR = '#287fb8';
|
|
20
|
+
const INNER_COLOR = '#ffd166';
|
|
21
|
+
const OUTER_COLOR = '#ff9f43';
|
|
22
|
+
const ROUND_TO = 100;
|
|
23
|
+
|
|
24
|
+
export type AudioHandleId =
|
|
25
|
+
| 'audio-ref-distance'
|
|
26
|
+
| 'audio-max-distance'
|
|
27
|
+
| 'audio-inner-cone'
|
|
28
|
+
| 'audio-outer-cone';
|
|
29
|
+
|
|
30
|
+
export interface AudioHandleEdit {
|
|
31
|
+
readonly field: TypedThreeField;
|
|
32
|
+
readonly value: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tuple(value: THREE.Vector3): SpatialPoint3 {
|
|
36
|
+
return [value.x, value.y, value.z];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function worldFrame(object: THREE.Object3D): {
|
|
40
|
+
origin: THREE.Vector3;
|
|
41
|
+
forward: THREE.Vector3;
|
|
42
|
+
right: THREE.Vector3;
|
|
43
|
+
} {
|
|
44
|
+
object.updateWorldMatrix(true, false);
|
|
45
|
+
const origin = object.getWorldPosition(new THREE.Vector3());
|
|
46
|
+
const rotation = object.getWorldQuaternion(new THREE.Quaternion());
|
|
47
|
+
return {
|
|
48
|
+
origin,
|
|
49
|
+
// THREE.PositionalAudio writes this same local +Z orientation into its
|
|
50
|
+
// PannerNode in updateMatrixWorld().
|
|
51
|
+
forward: new THREE.Vector3(0, 0, 1).applyQuaternion(rotation).normalize(),
|
|
52
|
+
right: new THREE.Vector3(1, 0, 0).applyQuaternion(rotation).normalize(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pointOnCone(
|
|
57
|
+
origin: THREE.Vector3,
|
|
58
|
+
forward: THREE.Vector3,
|
|
59
|
+
right: THREE.Vector3,
|
|
60
|
+
length: number,
|
|
61
|
+
fullAngleDegrees: number,
|
|
62
|
+
): THREE.Vector3 {
|
|
63
|
+
const halfAngle = THREE.MathUtils.degToRad(fullAngleDegrees / 2);
|
|
64
|
+
return origin
|
|
65
|
+
.clone()
|
|
66
|
+
.addScaledVector(forward, Math.cos(halfAngle) * length)
|
|
67
|
+
.addScaledVector(right, Math.sin(halfAngle) * length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Selected-source attenuation and direction, with handles only where the
|
|
71
|
+
* native parameter currently has visible meaning. Web Audio ignores
|
|
72
|
+
* maxDistance outside the linear model, so no ring is drawn there. */
|
|
73
|
+
export function positionalAudioHandleLayer(
|
|
74
|
+
object: THREE.Object3D,
|
|
75
|
+
writable: (path: string) => boolean,
|
|
76
|
+
): SpatialHandleLayer | null {
|
|
77
|
+
if (!isPositionalAudioObject(object)) return null;
|
|
78
|
+
const { origin, forward, right } = worldFrame(object);
|
|
79
|
+
const refDistance = object.getRefDistance();
|
|
80
|
+
const maxDistance = object.getMaxDistance();
|
|
81
|
+
const distanceModel = object.getDistanceModel();
|
|
82
|
+
const innerAngle = object.panner.coneInnerAngle;
|
|
83
|
+
const outerAngle = object.panner.coneOuterAngle;
|
|
84
|
+
const directional = outerAngle < 359.5;
|
|
85
|
+
const coneLength = Math.max(1, Math.min(8, refDistance * 2.5));
|
|
86
|
+
|
|
87
|
+
const guides: SpatialHandleLayer['guides'][number][] = [
|
|
88
|
+
{
|
|
89
|
+
kind: 'sphere',
|
|
90
|
+
center: tuple(origin),
|
|
91
|
+
radius: refDistance,
|
|
92
|
+
color: REF_COLOR,
|
|
93
|
+
opacity: 0.75,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
const handles: SpatialHandleLayer['handles'][number][] = [
|
|
97
|
+
{
|
|
98
|
+
id: 'audio-ref-distance',
|
|
99
|
+
label: 'Reference distance',
|
|
100
|
+
position: tuple(origin.clone().addScaledVector(right, refDistance)),
|
|
101
|
+
color: REF_COLOR,
|
|
102
|
+
writable: writable('audio.refDistance'),
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (distanceModel === 'linear') {
|
|
107
|
+
guides.push({
|
|
108
|
+
kind: 'sphere',
|
|
109
|
+
center: tuple(origin),
|
|
110
|
+
radius: maxDistance,
|
|
111
|
+
color: MAX_COLOR,
|
|
112
|
+
opacity: 0.42,
|
|
113
|
+
dashed: true,
|
|
114
|
+
});
|
|
115
|
+
handles.push({
|
|
116
|
+
id: 'audio-max-distance',
|
|
117
|
+
label: 'Maximum distance',
|
|
118
|
+
position: tuple(origin.clone().addScaledVector(right, maxDistance)),
|
|
119
|
+
color: MAX_COLOR,
|
|
120
|
+
writable: writable('audio.maxDistance'),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (directional) {
|
|
125
|
+
guides.push({
|
|
126
|
+
kind: 'cone',
|
|
127
|
+
origin: tuple(origin),
|
|
128
|
+
direction: tuple(forward),
|
|
129
|
+
length: coneLength,
|
|
130
|
+
angle: outerAngle,
|
|
131
|
+
color: OUTER_COLOR,
|
|
132
|
+
opacity: 0.72,
|
|
133
|
+
});
|
|
134
|
+
handles.push({
|
|
135
|
+
id: 'audio-outer-cone',
|
|
136
|
+
label: 'Outer cone angle',
|
|
137
|
+
position: tuple(pointOnCone(origin, forward, right, coneLength, outerAngle)),
|
|
138
|
+
color: OUTER_COLOR,
|
|
139
|
+
writable: writable('audio.coneOuterAngle'),
|
|
140
|
+
});
|
|
141
|
+
if (innerAngle < 359.5) {
|
|
142
|
+
guides.push({
|
|
143
|
+
kind: 'cone',
|
|
144
|
+
origin: tuple(origin),
|
|
145
|
+
direction: tuple(forward),
|
|
146
|
+
length: coneLength,
|
|
147
|
+
angle: innerAngle,
|
|
148
|
+
color: INNER_COLOR,
|
|
149
|
+
opacity: 0.58,
|
|
150
|
+
dashed: true,
|
|
151
|
+
});
|
|
152
|
+
handles.push({
|
|
153
|
+
id: 'audio-inner-cone',
|
|
154
|
+
label: 'Inner cone angle',
|
|
155
|
+
position: tuple(pointOnCone(origin, forward, right, coneLength, innerAngle)),
|
|
156
|
+
color: INNER_COLOR,
|
|
157
|
+
writable: writable('audio.coneInnerAngle'),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { id: 'positional-audio', category: 'audio', guides, handles };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function rounded(value: number): number {
|
|
166
|
+
return Math.round(value * ROUND_TO) / ROUND_TO;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Translate a drag point into the one inspector/source field it edits. */
|
|
170
|
+
export function positionalAudioEditFromWorld(
|
|
171
|
+
object: THREE.Object3D,
|
|
172
|
+
sourceTag: string | undefined,
|
|
173
|
+
handleId: string,
|
|
174
|
+
worldPosition: SpatialPoint3,
|
|
175
|
+
): AudioHandleEdit | null {
|
|
176
|
+
if (!isPositionalAudioObject(object)) return null;
|
|
177
|
+
const { origin, forward } = worldFrame(object);
|
|
178
|
+
const point = new THREE.Vector3(...worldPosition);
|
|
179
|
+
const delta = point.sub(origin);
|
|
180
|
+
let path: string;
|
|
181
|
+
let value: number;
|
|
182
|
+
|
|
183
|
+
switch (handleId as AudioHandleId) {
|
|
184
|
+
case 'audio-ref-distance':
|
|
185
|
+
path = 'audio.refDistance';
|
|
186
|
+
value = Math.max(0.01, rounded(delta.length()));
|
|
187
|
+
break;
|
|
188
|
+
case 'audio-max-distance':
|
|
189
|
+
path = 'audio.maxDistance';
|
|
190
|
+
value = Math.max(object.getRefDistance() + 0.01, rounded(delta.length()));
|
|
191
|
+
break;
|
|
192
|
+
case 'audio-inner-cone':
|
|
193
|
+
case 'audio-outer-cone': {
|
|
194
|
+
path = handleId === 'audio-inner-cone' ? 'audio.coneInnerAngle' : 'audio.coneOuterAngle';
|
|
195
|
+
// Keep the signed axial component: angles above 180 degrees place the
|
|
196
|
+
// cone rim behind the source, and Web Audio legitimately accepts the
|
|
197
|
+
// full 0..360 range.
|
|
198
|
+
const axial = delta.dot(forward);
|
|
199
|
+
const radial = Math.sqrt(Math.max(0, delta.lengthSq() - axial * axial));
|
|
200
|
+
value = rounded(THREE.MathUtils.radToDeg(Math.atan2(radial, axial)) * 2);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const field = typedFieldForPath(object, null, path, sourceTag);
|
|
208
|
+
return field ? { field, value } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Apply only to the live native object; source persistence is the adapter's
|
|
212
|
+
* commit step and happens once at pointer-up. */
|
|
213
|
+
export function previewPositionalAudioEdit(object: THREE.Object3D, edit: AudioHandleEdit): void {
|
|
214
|
+
applyTypedField(object, null, edit.field, edit.value);
|
|
215
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** Native Rapier primitive colliders projected onto the generic viewport seam. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PhysicsColliderShape,
|
|
5
|
+
PhysicsColliderSnapshot,
|
|
6
|
+
SpatialHandleLayer,
|
|
7
|
+
SpatialPoint3,
|
|
8
|
+
} from '@volter/editor-project/adapter';
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
|
|
11
|
+
const COLLIDER_COLOR = '#00e58a';
|
|
12
|
+
const SENSOR_COLOR = '#c77dff';
|
|
13
|
+
const MIN_DIMENSION = 0.01;
|
|
14
|
+
const ROUND_TO = 1000;
|
|
15
|
+
|
|
16
|
+
export interface ColliderSourceBinding {
|
|
17
|
+
readonly sourceOid: string;
|
|
18
|
+
readonly sourceTag: 'CuboidCollider' | 'BallCollider' | 'CapsuleCollider';
|
|
19
|
+
readonly snapshot: PhysicsColliderSnapshot;
|
|
20
|
+
readonly writable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ColliderHandleEdit {
|
|
24
|
+
readonly sourceOid: string;
|
|
25
|
+
readonly args: readonly number[];
|
|
26
|
+
readonly shape: PhysicsColliderShape;
|
|
27
|
+
readonly colliderId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rounded(value: number): number {
|
|
31
|
+
return Math.round(value * ROUND_TO) / ROUND_TO;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tuple(value: THREE.Vector3): SpatialPoint3 {
|
|
35
|
+
return [value.x, value.y, value.z];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function frame(snapshot: PhysicsColliderSnapshot): {
|
|
39
|
+
center: THREE.Vector3;
|
|
40
|
+
rotation: THREE.Quaternion;
|
|
41
|
+
} {
|
|
42
|
+
return {
|
|
43
|
+
center: new THREE.Vector3(...snapshot.position),
|
|
44
|
+
rotation: new THREE.Quaternion(...snapshot.rotation),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function worldPoint(
|
|
49
|
+
snapshot: PhysicsColliderSnapshot,
|
|
50
|
+
local: readonly [number, number, number],
|
|
51
|
+
): SpatialPoint3 {
|
|
52
|
+
const { center, rotation } = frame(snapshot);
|
|
53
|
+
return tuple(new THREE.Vector3(...local).applyQuaternion(rotation).add(center));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleId(binding: ColliderSourceBinding, part: string): string {
|
|
57
|
+
return `collider:${encodeURIComponent(binding.sourceOid)}:${part}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function colliderHandleLayer(binding: ColliderSourceBinding): SpatialHandleLayer {
|
|
61
|
+
const { snapshot } = binding;
|
|
62
|
+
const color = snapshot.sensor ? SENSOR_COLOR : COLLIDER_COLOR;
|
|
63
|
+
const common = { color, writable: binding.writable };
|
|
64
|
+
const guides: SpatialHandleLayer['guides'][number][] = [];
|
|
65
|
+
const handles: SpatialHandleLayer['handles'][number][] = [];
|
|
66
|
+
|
|
67
|
+
switch (snapshot.shape.type) {
|
|
68
|
+
case 'cuboid': {
|
|
69
|
+
const halfExtents = snapshot.shape.halfExtents;
|
|
70
|
+
guides.push({
|
|
71
|
+
kind: 'box',
|
|
72
|
+
center: snapshot.position,
|
|
73
|
+
rotation: snapshot.rotation,
|
|
74
|
+
halfExtents,
|
|
75
|
+
color,
|
|
76
|
+
opacity: 0.78,
|
|
77
|
+
});
|
|
78
|
+
for (const [axis, label] of [
|
|
79
|
+
[0, 'X'],
|
|
80
|
+
[1, 'Y'],
|
|
81
|
+
[2, 'Z'],
|
|
82
|
+
] as const) {
|
|
83
|
+
for (const sign of [-1, 1] as const) {
|
|
84
|
+
const local: [number, number, number] = [0, 0, 0];
|
|
85
|
+
local[axis] = halfExtents[axis] * sign;
|
|
86
|
+
handles.push({
|
|
87
|
+
id: handleId(binding, `${axis}:${sign}`),
|
|
88
|
+
label: `${label} ${sign > 0 ? 'positive' : 'negative'} half extent`,
|
|
89
|
+
position: worldPoint(snapshot, local),
|
|
90
|
+
...common,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'ball':
|
|
97
|
+
guides.push({
|
|
98
|
+
kind: 'sphere',
|
|
99
|
+
center: snapshot.position,
|
|
100
|
+
radius: snapshot.shape.radius,
|
|
101
|
+
color,
|
|
102
|
+
opacity: 0.78,
|
|
103
|
+
});
|
|
104
|
+
handles.push({
|
|
105
|
+
id: handleId(binding, 'radius'),
|
|
106
|
+
label: 'Collider radius',
|
|
107
|
+
position: worldPoint(snapshot, [snapshot.shape.radius, 0, 0]),
|
|
108
|
+
...common,
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case 'capsule':
|
|
112
|
+
guides.push({
|
|
113
|
+
kind: 'capsule',
|
|
114
|
+
center: snapshot.position,
|
|
115
|
+
rotation: snapshot.rotation,
|
|
116
|
+
radius: snapshot.shape.radius,
|
|
117
|
+
halfHeight: snapshot.shape.halfHeight,
|
|
118
|
+
color,
|
|
119
|
+
opacity: 0.78,
|
|
120
|
+
});
|
|
121
|
+
handles.push(
|
|
122
|
+
{
|
|
123
|
+
id: handleId(binding, 'radius'),
|
|
124
|
+
label: 'Collider radius',
|
|
125
|
+
position: worldPoint(snapshot, [snapshot.shape.radius, 0, 0]),
|
|
126
|
+
...common,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: handleId(binding, 'half-height'),
|
|
130
|
+
label: 'Collider half height',
|
|
131
|
+
position: worldPoint(snapshot, [0, snapshot.shape.halfHeight + snapshot.shape.radius, 0]),
|
|
132
|
+
...common,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
// A shape the physics seam does not project (trimesh, heightfield, hull,
|
|
137
|
+
// cylinder, cone, …). It gets no guide and no handles because we do not
|
|
138
|
+
// know its geometry — but it is NOT dropped upstream any more, so a body
|
|
139
|
+
// carrying one still reports having physics rather than reporting none.
|
|
140
|
+
case 'unsupported':
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
id: `collider:${binding.sourceOid}`,
|
|
146
|
+
category: 'colliders',
|
|
147
|
+
guides,
|
|
148
|
+
handles,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseHandleId(handle: string): { oid: string; part: string } | null {
|
|
153
|
+
if (!handle.startsWith('collider:')) return null;
|
|
154
|
+
const rest = handle.slice('collider:'.length);
|
|
155
|
+
const separator = rest.indexOf(':');
|
|
156
|
+
if (separator < 1) return null;
|
|
157
|
+
try {
|
|
158
|
+
return { oid: decodeURIComponent(rest.slice(0, separator)), part: rest.slice(separator + 1) };
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function safeScale(value: number): number {
|
|
165
|
+
return value > 1e-8 ? value : 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Convert one world-space drag point into @react-three/rapier's native args
|
|
169
|
+
* and the live scaled Rapier shape used for immediate preview. */
|
|
170
|
+
export function colliderEditFromWorld(
|
|
171
|
+
bindings: readonly ColliderSourceBinding[],
|
|
172
|
+
handle: string,
|
|
173
|
+
worldPosition: SpatialPoint3,
|
|
174
|
+
): ColliderHandleEdit | null {
|
|
175
|
+
const parsed = parseHandleId(handle);
|
|
176
|
+
const binding = parsed ? bindings.find((candidate) => candidate.sourceOid === parsed.oid) : null;
|
|
177
|
+
if (!binding || !parsed || !binding.writable) return null;
|
|
178
|
+
const { center, rotation } = frame(binding.snapshot);
|
|
179
|
+
const local = new THREE.Vector3(...worldPosition)
|
|
180
|
+
.sub(center)
|
|
181
|
+
.applyQuaternion(rotation.clone().invert());
|
|
182
|
+
const shape = binding.snapshot.shape;
|
|
183
|
+
const scale = binding.snapshot.scale.map(safeScale) as [number, number, number];
|
|
184
|
+
|
|
185
|
+
if (shape.type === 'cuboid' && /^[012]:(?:-1|1)$/.test(parsed.part)) {
|
|
186
|
+
const axis = Number(parsed.part[0]) as 0 | 1 | 2;
|
|
187
|
+
const halfExtents: [number, number, number] = [...shape.halfExtents];
|
|
188
|
+
halfExtents[axis] = Math.max(MIN_DIMENSION, rounded(Math.abs(local.getComponent(axis))));
|
|
189
|
+
return {
|
|
190
|
+
sourceOid: binding.sourceOid,
|
|
191
|
+
colliderId: binding.snapshot.id,
|
|
192
|
+
shape: { type: 'cuboid', halfExtents },
|
|
193
|
+
args: halfExtents.map((value, index) => rounded(value / scale[index]!)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (shape.type === 'ball' && parsed.part === 'radius') {
|
|
198
|
+
const radius = Math.max(MIN_DIMENSION, rounded(local.length()));
|
|
199
|
+
return {
|
|
200
|
+
sourceOid: binding.sourceOid,
|
|
201
|
+
colliderId: binding.snapshot.id,
|
|
202
|
+
shape: { type: 'ball', radius },
|
|
203
|
+
args: [rounded(radius / scale[0])],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (shape.type === 'capsule') {
|
|
208
|
+
if (parsed.part === 'radius') {
|
|
209
|
+
const radius = Math.max(MIN_DIMENSION, rounded(Math.hypot(local.x, local.z)));
|
|
210
|
+
return {
|
|
211
|
+
sourceOid: binding.sourceOid,
|
|
212
|
+
colliderId: binding.snapshot.id,
|
|
213
|
+
shape: { ...shape, radius },
|
|
214
|
+
args: [rounded(shape.halfHeight / scale[0]), rounded(radius / scale[1])],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (parsed.part === 'half-height') {
|
|
218
|
+
const halfHeight = Math.max(MIN_DIMENSION, rounded(Math.abs(local.y) - shape.radius));
|
|
219
|
+
return {
|
|
220
|
+
sourceOid: binding.sourceOid,
|
|
221
|
+
colliderId: binding.snapshot.id,
|
|
222
|
+
shape: { ...shape, halfHeight },
|
|
223
|
+
args: [rounded(halfHeight / scale[0]), rounded(shape.radius / scale[1])],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Native Rapier impulse-joint anchors projected onto generic viewport guides. */
|
|
2
|
+
|
|
3
|
+
import type { R3fJointBinding, R3fJointLiteral } from '@volter/editor-core/ui-source/r3f-joint-binding';
|
|
4
|
+
import type { PhysicsJointSnapshot, SpatialHandleLayer, SpatialPoint3 } from '@volter/editor-project/adapter';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
|
|
7
|
+
const JOINT_COLOR = '#ffb020';
|
|
8
|
+
const ROUND_TO = 1000;
|
|
9
|
+
|
|
10
|
+
export interface JointSourceBinding {
|
|
11
|
+
readonly sourceOid: string;
|
|
12
|
+
readonly sourceFile: string;
|
|
13
|
+
readonly source: R3fJointBinding;
|
|
14
|
+
readonly snapshot: PhysicsJointSnapshot;
|
|
15
|
+
readonly writable: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface JointAnchorEdit {
|
|
19
|
+
readonly binding: JointSourceBinding;
|
|
20
|
+
readonly endpoint: 0 | 1;
|
|
21
|
+
readonly anchor: readonly [number, number, number];
|
|
22
|
+
readonly params: readonly R3fJointLiteral[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function key(binding: JointSourceBinding): string {
|
|
26
|
+
return `${binding.source.line}:${binding.source.col}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleId(binding: JointSourceBinding, endpoint: 0 | 1): string {
|
|
30
|
+
return `joint:${key(binding)}:${endpoint}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function jointHandleLayer(binding: JointSourceBinding): SpatialHandleLayer {
|
|
34
|
+
const snapshot = binding.snapshot;
|
|
35
|
+
const guides: SpatialHandleLayer['guides'][number][] = [
|
|
36
|
+
{
|
|
37
|
+
kind: 'line',
|
|
38
|
+
points: [snapshot.worldAnchor1, snapshot.worldAnchor2],
|
|
39
|
+
color: JOINT_COLOR,
|
|
40
|
+
opacity: 0.9,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
if (snapshot.worldAxis) {
|
|
44
|
+
const origin = new THREE.Vector3(...snapshot.worldAnchor1);
|
|
45
|
+
const axis = new THREE.Vector3(...snapshot.worldAxis).normalize();
|
|
46
|
+
guides.push({
|
|
47
|
+
kind: 'line',
|
|
48
|
+
points: [
|
|
49
|
+
[origin.x - axis.x, origin.y - axis.y, origin.z - axis.z],
|
|
50
|
+
[origin.x + axis.x, origin.y + axis.y, origin.z + axis.z],
|
|
51
|
+
],
|
|
52
|
+
color: '#ffd166',
|
|
53
|
+
opacity: 0.82,
|
|
54
|
+
dashed: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
id: `joint:${key(binding)}`,
|
|
59
|
+
category: 'joints',
|
|
60
|
+
guides,
|
|
61
|
+
handles: [
|
|
62
|
+
{
|
|
63
|
+
id: handleId(binding, 0),
|
|
64
|
+
label: 'Joint anchor A',
|
|
65
|
+
position: snapshot.worldAnchor1,
|
|
66
|
+
color: JOINT_COLOR,
|
|
67
|
+
writable: binding.writable,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: handleId(binding, 1),
|
|
71
|
+
label: 'Joint anchor B',
|
|
72
|
+
position: snapshot.worldAnchor2,
|
|
73
|
+
color: JOINT_COLOR,
|
|
74
|
+
writable: binding.writable,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rounded(value: number): number {
|
|
81
|
+
return Math.round(value * ROUND_TO) / ROUND_TO;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parsedHandle(handle: string): { key: string; endpoint: 0 | 1 } | null {
|
|
85
|
+
const match = /^joint:(\d+:\d+):([01])$/.exec(handle);
|
|
86
|
+
return match ? { key: match[1]!, endpoint: Number(match[2]) as 0 | 1 } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function anchorParamIndex(binding: JointSourceBinding, endpoint: 0 | 1): number {
|
|
90
|
+
return binding.source.hook === 'useFixedJoint' && endpoint === 1 ? 2 : endpoint;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Convert a world drag point back to the native hook's body-local anchor. */
|
|
94
|
+
export function jointAnchorEditFromWorld(
|
|
95
|
+
bindings: readonly JointSourceBinding[],
|
|
96
|
+
handle: string,
|
|
97
|
+
worldPosition: SpatialPoint3,
|
|
98
|
+
): JointAnchorEdit | null {
|
|
99
|
+
const parsed = parsedHandle(handle);
|
|
100
|
+
const binding = parsed ? bindings.find((candidate) => key(candidate) === parsed.key) : undefined;
|
|
101
|
+
if (!binding || !parsed || !binding.writable || !binding.source.params) return null;
|
|
102
|
+
const snapshot = binding.snapshot;
|
|
103
|
+
const position = parsed.endpoint === 0 ? snapshot.body1Position : snapshot.body2Position;
|
|
104
|
+
const rotation = parsed.endpoint === 0 ? snapshot.body1Rotation : snapshot.body2Rotation;
|
|
105
|
+
const local = new THREE.Vector3(...worldPosition)
|
|
106
|
+
.sub(new THREE.Vector3(...position))
|
|
107
|
+
.applyQuaternion(new THREE.Quaternion(...rotation).invert());
|
|
108
|
+
const anchor: [number, number, number] = [rounded(local.x), rounded(local.y), rounded(local.z)];
|
|
109
|
+
const params = binding.source.params.map((value) =>
|
|
110
|
+
Array.isArray(value) ? [...value] : value,
|
|
111
|
+
) as R3fJointLiteral[];
|
|
112
|
+
params[anchorParamIndex(binding, parsed.endpoint)] = anchor;
|
|
113
|
+
return { binding, endpoint: parsed.endpoint, anchor, params };
|
|
114
|
+
}
|