@volter/editor-game 0.5.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Native Three light projection onto the format-neutral spatial-handle seam.
3
+ *
4
+ * The live THREE.Light remains the value owner and the R3F adapter remains the
5
+ * persistence owner. This module only converts native distance/angle values to
6
+ * world-space geometry and a dragged world point back to the same native prop.
7
+ */
8
+
9
+ import type { SpatialHandleLayer, SpatialPoint3 } from '@volter/editor-project/adapter';
10
+ import * as THREE from 'three';
11
+ import { applyTypedField, type TypedThreeField, typedFieldForPath } from './typed-three-inspector';
12
+
13
+ const RANGE_COLOR = '#ffd166';
14
+ const ANGLE_COLOR = '#ff9f43';
15
+ const ROUND_TO = 1000;
16
+
17
+ export type LightHandleId = 'light-range' | 'light-angle';
18
+
19
+ export interface LightHandleEdit {
20
+ readonly field: TypedThreeField;
21
+ readonly value: number;
22
+ }
23
+
24
+ function tuple(value: THREE.Vector3): SpatialPoint3 {
25
+ return [value.x, value.y, value.z];
26
+ }
27
+
28
+ function rounded(value: number): number {
29
+ return Math.round(value * ROUND_TO) / ROUND_TO;
30
+ }
31
+
32
+ function lightFrame(object: THREE.Object3D): {
33
+ origin: THREE.Vector3;
34
+ rangeAxis: THREE.Vector3;
35
+ } {
36
+ object.updateWorldMatrix(true, false);
37
+ const rotation = object.getWorldQuaternion(new THREE.Quaternion());
38
+ return {
39
+ origin: object.getWorldPosition(new THREE.Vector3()),
40
+ rangeAxis: new THREE.Vector3(1, 0, 0).applyQuaternion(rotation).normalize(),
41
+ };
42
+ }
43
+
44
+ function spotFrame(light: THREE.SpotLight): {
45
+ origin: THREE.Vector3;
46
+ direction: THREE.Vector3;
47
+ radial: THREE.Vector3;
48
+ } {
49
+ light.updateWorldMatrix(true, false);
50
+ light.target.updateWorldMatrix(true, false);
51
+ const origin = light.getWorldPosition(new THREE.Vector3());
52
+ const target = light.target.getWorldPosition(new THREE.Vector3());
53
+ const direction = target.sub(origin).normalize();
54
+ if (direction.lengthSq() < 1e-8) {
55
+ direction.set(0, 0, -1).applyQuaternion(light.getWorldQuaternion(new THREE.Quaternion()));
56
+ }
57
+ const reference =
58
+ Math.abs(direction.y) > 0.98 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0);
59
+ const radial = new THREE.Vector3().crossVectors(direction, reference).normalize();
60
+ return { origin, direction, radial };
61
+ }
62
+
63
+ function pointOnSpotCone(
64
+ origin: THREE.Vector3,
65
+ direction: THREE.Vector3,
66
+ radial: THREE.Vector3,
67
+ length: number,
68
+ halfAngle: number,
69
+ ): THREE.Vector3 {
70
+ return origin
71
+ .clone()
72
+ .addScaledVector(direction, Math.cos(halfAngle) * length)
73
+ .addScaledVector(radial, Math.sin(halfAngle) * length);
74
+ }
75
+
76
+ /** Point/spot range and spot cone geometry. A zero Three distance means
77
+ * unbounded, so it deliberately has no finite range ring or drag point. */
78
+ export function lightHandleLayer(
79
+ object: THREE.Object3D,
80
+ writable: (path: string) => boolean,
81
+ ): SpatialHandleLayer | null {
82
+ const light = object as THREE.Object3D &
83
+ Partial<THREE.PointLight> & {
84
+ isPointLight?: boolean;
85
+ isSpotLight?: boolean;
86
+ };
87
+ if (!light.isPointLight && !light.isSpotLight) return null;
88
+
89
+ const { origin, rangeAxis } = lightFrame(object);
90
+ const distance = typeof light.distance === 'number' ? light.distance : 0;
91
+ const guides: SpatialHandleLayer['guides'][number][] = [];
92
+ const handles: SpatialHandleLayer['handles'][number][] = [];
93
+
94
+ if (distance > 0) {
95
+ guides.push({
96
+ kind: 'sphere',
97
+ center: tuple(origin),
98
+ radius: distance,
99
+ color: RANGE_COLOR,
100
+ opacity: 0.62,
101
+ });
102
+ handles.push({
103
+ id: 'light-range',
104
+ label: 'Light range',
105
+ position: tuple(origin.clone().addScaledVector(rangeAxis, distance)),
106
+ color: RANGE_COLOR,
107
+ writable: writable('light.distance'),
108
+ });
109
+ }
110
+
111
+ if (light.isSpotLight) {
112
+ const spot = object as THREE.SpotLight;
113
+ const { direction, radial } = spotFrame(spot);
114
+ // With an unbounded spot, target distance supplies only a readable guide
115
+ // length; it never becomes an authored light value.
116
+ const targetDistance = origin.distanceTo(spot.target.getWorldPosition(new THREE.Vector3()));
117
+ const length = distance > 0 ? distance : Math.max(1, targetDistance);
118
+ const fullAngle = THREE.MathUtils.radToDeg(spot.angle * 2);
119
+ guides.push({
120
+ kind: 'cone',
121
+ origin: tuple(origin),
122
+ direction: tuple(direction),
123
+ length,
124
+ angle: fullAngle,
125
+ color: ANGLE_COLOR,
126
+ opacity: 0.75,
127
+ });
128
+ handles.push({
129
+ id: 'light-angle',
130
+ label: 'Spot angle',
131
+ position: tuple(pointOnSpotCone(origin, direction, radial, length, spot.angle)),
132
+ color: ANGLE_COLOR,
133
+ writable: writable('light.angle'),
134
+ });
135
+ }
136
+
137
+ return guides.length > 0 || handles.length > 0
138
+ ? { id: 'light', category: 'lights', guides, handles }
139
+ : null;
140
+ }
141
+
142
+ /** Translate a drag point into the exact native/source field it edits. */
143
+ export function lightEditFromWorld(
144
+ object: THREE.Object3D,
145
+ sourceTag: string | undefined,
146
+ handleId: string,
147
+ worldPosition: SpatialPoint3,
148
+ ): LightHandleEdit | null {
149
+ const light = object as THREE.Object3D &
150
+ Partial<THREE.PointLight> & {
151
+ isPointLight?: boolean;
152
+ isSpotLight?: boolean;
153
+ };
154
+ const point = new THREE.Vector3(...worldPosition);
155
+ let path: string;
156
+ let value: number;
157
+
158
+ if (handleId === 'light-range' && (light.isPointLight || light.isSpotLight)) {
159
+ path = 'light.distance';
160
+ value = Math.max(0.01, rounded(point.distanceTo(lightFrame(object).origin)));
161
+ } else if (handleId === 'light-angle' && light.isSpotLight) {
162
+ path = 'light.angle';
163
+ const { origin, direction } = spotFrame(object as THREE.SpotLight);
164
+ const delta = point.sub(origin);
165
+ const axial = delta.dot(direction);
166
+ const radial = Math.sqrt(Math.max(0, delta.lengthSq() - axial * axial));
167
+ value = rounded(Math.max(0.01, Math.min(Math.PI / 2, Math.atan2(radial, axial))));
168
+ } else {
169
+ return null;
170
+ }
171
+
172
+ const field = typedFieldForPath(object, null, path, sourceTag);
173
+ return field ? { field, value } : null;
174
+ }
175
+
176
+ export function previewLightEdit(object: THREE.Object3D, edit: LightHandleEdit): void {
177
+ applyTypedField(object, null, edit.field, edit.value);
178
+ }
@@ -0,0 +1,93 @@
1
+ /** Native THREE.LOD distance thresholds projected onto generic viewport guides. */
2
+
3
+ import type { R3fLodBinding } from '@volter/editor-core/ui-source/r3f-lod-binding';
4
+ import type { SpatialHandleLayer, SpatialPoint3 } from '@volter/editor-project/adapter';
5
+ import * as THREE from 'three';
6
+
7
+ const COLORS = ['#7dd3fc', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc'];
8
+ const ROUND_TO = 1000;
9
+
10
+ export interface LodSourceBinding {
11
+ readonly key: string;
12
+ readonly sourceOid?: string;
13
+ readonly sourceFile?: string;
14
+ readonly source?: R3fLodBinding;
15
+ readonly lod: THREE.LOD;
16
+ readonly writableDistances: ReadonlySet<number>;
17
+ readonly writableHysteresis: boolean;
18
+ }
19
+
20
+ export interface LodDistanceEdit {
21
+ readonly binding: LodSourceBinding;
22
+ readonly level: number;
23
+ readonly value: number;
24
+ }
25
+
26
+ function centerOf(binding: LodSourceBinding): THREE.Vector3 {
27
+ binding.lod.updateWorldMatrix(true, false);
28
+ return binding.lod.getWorldPosition(new THREE.Vector3());
29
+ }
30
+
31
+ function color(index: number): string {
32
+ return COLORS[index % COLORS.length]!;
33
+ }
34
+
35
+ export function lodHandleLayer(binding: LodSourceBinding): SpatialHandleLayer {
36
+ const center = centerOf(binding);
37
+ const guides: SpatialHandleLayer['guides'][number][] = [];
38
+ const handles: SpatialHandleLayer['handles'][number][] = [];
39
+ binding.lod.levels.forEach((level, index) => {
40
+ if (!Number.isFinite(level.distance) || level.distance <= 0) return;
41
+ guides.push({
42
+ kind: 'sphere',
43
+ center: [center.x, center.y, center.z],
44
+ radius: level.distance,
45
+ color: color(index),
46
+ opacity: 0.56,
47
+ });
48
+ handles.push({
49
+ id: `lod:${encodeURIComponent(binding.key)}:${index}`,
50
+ label: `LOD ${index} distance`,
51
+ position: [center.x + level.distance, center.y, center.z],
52
+ color: color(index),
53
+ writable: binding.writableDistances.has(index),
54
+ });
55
+ });
56
+ return { id: `lod:${binding.key}`, category: 'lod', guides, handles };
57
+ }
58
+
59
+ function rounded(value: number): number {
60
+ return Math.round(value * ROUND_TO) / ROUND_TO;
61
+ }
62
+
63
+ export function normalizeLodDistance(
64
+ binding: LodSourceBinding,
65
+ level: number,
66
+ value: number,
67
+ ): number {
68
+ const previous = binding.lod.levels[level - 1]?.distance;
69
+ const next = binding.lod.levels[level + 1]?.distance;
70
+ const minimum = previous === undefined ? 0 : previous + 0.01;
71
+ const maximum = next === undefined ? Number.POSITIVE_INFINITY : Math.max(minimum, next - 0.01);
72
+ return rounded(THREE.MathUtils.clamp(value, minimum, maximum));
73
+ }
74
+
75
+ export function lodEditFromWorld(
76
+ bindings: readonly LodSourceBinding[],
77
+ handleId: string,
78
+ worldPosition: SpatialPoint3,
79
+ ): LodDistanceEdit | null {
80
+ const match = /^lod:([^:]+):(\d+)$/.exec(handleId);
81
+ if (!match) return null;
82
+ const key = decodeURIComponent(match[1]!);
83
+ const level = Number(match[2]);
84
+ const binding = bindings.find((candidate) => candidate.key === key);
85
+ if (!binding || !binding.writableDistances.has(level)) return null;
86
+ const value = new THREE.Vector3(...worldPosition).distanceTo(centerOf(binding));
87
+ return { binding, level, value: normalizeLodDistance(binding, level, value) };
88
+ }
89
+
90
+ export function previewLodDistance(edit: LodDistanceEdit): void {
91
+ const level = edit.binding.lod.levels[edit.level];
92
+ if (level) level.distance = edit.value;
93
+ }
@@ -0,0 +1,368 @@
1
+ /** Native three.quarks emitter shapes projected onto generic viewport guides. */
2
+
3
+ import type {
4
+ QuarksEmitterShape,
5
+ R3fParticleBinding,
6
+ } from '@volter/editor-core/ui-source/r3f-particle-binding';
7
+ import type { SpatialHandleLayer, SpatialPoint3 } from '@volter/editor-project/adapter';
8
+ import * as THREE from 'three';
9
+
10
+ const PARTICLE_COLOR = '#ff9f43';
11
+ const PARTICLE_SECONDARY = '#ffd166';
12
+ const ROUND_TO = 1000;
13
+
14
+ export interface NativeEmitterShape {
15
+ readonly type: QuarksEmitterShape;
16
+ radius?: number;
17
+ arc?: number;
18
+ thickness?: number;
19
+ angle?: number;
20
+ donutRadius?: number;
21
+ width?: number;
22
+ height?: number;
23
+ column?: number;
24
+ row?: number;
25
+ }
26
+
27
+ /**
28
+ * A three.quarks `ParticleSystem.emitter`, once the SOURCE has declared it.
29
+ *
30
+ * Deliberately carries no `type: 'ParticleEmitter'` discriminant. Which live
31
+ * object is an emitter is decided by the project's own source — the
32
+ * `<primitive object={system.emitter}>` element the OID index stamps with a
33
+ * `particleBinding` — never by string-matching a third-party library's
34
+ * internal `Object3D.type`. See `particleBindingsOf` in
35
+ * `r3f-source-authoring-adapter.ts` for the one place that decision is made.
36
+ */
37
+ export interface NativeParticleEmitter extends THREE.Object3D {
38
+ readonly system: { readonly emitterShape: NativeEmitterShape };
39
+ }
40
+
41
+ /**
42
+ * The native emitter shape of an object the source already declared to be an
43
+ * emitter, or `null` when the live object does not carry one.
44
+ *
45
+ * This reads the declared object's OWN three.quarks state; it is not a test of
46
+ * whether an arbitrary object is an emitter, and must never be used as one.
47
+ */
48
+ export function nativeEmitterShapeOf(object: THREE.Object3D): NativeEmitterShape | null {
49
+ const shape = (object as Partial<NativeParticleEmitter>).system?.emitterShape;
50
+ return shape && typeof shape.type === 'string' ? shape : null;
51
+ }
52
+
53
+ export interface ParticleSourceBinding {
54
+ readonly sourceOid: string;
55
+ readonly sourceFile: string;
56
+ readonly source: R3fParticleBinding;
57
+ readonly emitter: NativeParticleEmitter;
58
+ readonly shape: NativeEmitterShape;
59
+ readonly writableFields: ReadonlySet<string>;
60
+ }
61
+
62
+ export interface ParticleFieldEdit {
63
+ readonly binding: ParticleSourceBinding;
64
+ readonly field: string;
65
+ readonly value: number;
66
+ }
67
+
68
+ function key(binding: ParticleSourceBinding): string {
69
+ return encodeURIComponent(binding.sourceOid);
70
+ }
71
+
72
+ function point(value: THREE.Vector3): SpatialPoint3 {
73
+ return [value.x, value.y, value.z];
74
+ }
75
+
76
+ function world(binding: ParticleSourceBinding, local: THREE.Vector3): SpatialPoint3 {
77
+ binding.emitter.updateWorldMatrix(true, false);
78
+ return point(local.clone().applyMatrix4(binding.emitter.matrixWorld));
79
+ }
80
+
81
+ function line(
82
+ binding: ParticleSourceBinding,
83
+ points: readonly THREE.Vector3[],
84
+ opacity = 0.72,
85
+ ): SpatialHandleLayer['guides'][number] {
86
+ binding.emitter.updateWorldMatrix(true, false);
87
+ const matrix = binding.emitter.matrixWorld;
88
+ return {
89
+ kind: 'line',
90
+ points: points.map((candidate) => point(candidate.clone().applyMatrix4(matrix))),
91
+ color: PARTICLE_COLOR,
92
+ opacity,
93
+ };
94
+ }
95
+
96
+ function ring(
97
+ radius: number,
98
+ plane: 'xy' | 'xz' | 'yz' = 'xy',
99
+ segments = 48,
100
+ arc = Math.PI * 2,
101
+ ): THREE.Vector3[] {
102
+ const points: THREE.Vector3[] = [];
103
+ for (let index = 0; index <= segments; index += 1) {
104
+ const angle = (index / segments) * arc;
105
+ const a = Math.cos(angle) * radius;
106
+ const b = Math.sin(angle) * radius;
107
+ points.push(
108
+ plane === 'xy'
109
+ ? new THREE.Vector3(a, b, 0)
110
+ : plane === 'xz'
111
+ ? new THREE.Vector3(a, 0, b)
112
+ : new THREE.Vector3(0, a, b),
113
+ );
114
+ }
115
+ return points;
116
+ }
117
+
118
+ function rotateAroundZ(points: readonly THREE.Vector3[], angle: number): THREE.Vector3[] {
119
+ return points.map((candidate) =>
120
+ candidate.clone().applyAxisAngle(new THREE.Vector3(0, 0, 1), angle),
121
+ );
122
+ }
123
+
124
+ function upperArc(radius: number, plane: 'xz' | 'yz', segments = 24): THREE.Vector3[] {
125
+ const points: THREE.Vector3[] = [];
126
+ for (let index = 0; index <= segments; index += 1) {
127
+ const angle = (index / segments) * Math.PI;
128
+ const radial = Math.cos(angle) * radius;
129
+ const z = Math.sin(angle) * radius;
130
+ points.push(plane === 'xz' ? new THREE.Vector3(radial, 0, z) : new THREE.Vector3(0, radial, z));
131
+ }
132
+ return points;
133
+ }
134
+
135
+ function field(
136
+ shape: NativeEmitterShape,
137
+ name: keyof NativeEmitterShape,
138
+ fallback: number,
139
+ ): number {
140
+ const value = shape[name];
141
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
142
+ }
143
+
144
+ function outline(binding: ParticleSourceBinding): SpatialHandleLayer['guides'][number][] {
145
+ const shape = binding.shape;
146
+ const radius = field(shape, 'radius', 10);
147
+ const arc = THREE.MathUtils.clamp(field(shape, 'arc', Math.PI * 2), 0, Math.PI * 2);
148
+ const fullArc = Math.abs(arc - Math.PI * 2) < 0.001;
149
+ switch (shape.type) {
150
+ case 'point':
151
+ return [
152
+ line(binding, [new THREE.Vector3(-0.2, 0, 0), new THREE.Vector3(0.2, 0, 0)]),
153
+ line(binding, [new THREE.Vector3(0, -0.2, 0), new THREE.Vector3(0, 0.2, 0)]),
154
+ line(binding, [new THREE.Vector3(0, 0, -0.2), new THREE.Vector3(0, 0, 0.2)]),
155
+ ];
156
+ case 'sphere':
157
+ return [
158
+ line(binding, ring(radius, 'xy', 48, arc)),
159
+ line(binding, ring(radius, 'xz'), 0.5),
160
+ line(binding, rotateAroundZ(ring(radius, 'xz'), fullArc ? Math.PI / 2 : arc), 0.5),
161
+ ];
162
+ case 'hemisphere':
163
+ return [
164
+ line(binding, ring(radius, 'xy', 48, arc)),
165
+ line(binding, upperArc(radius, 'xz'), 0.55),
166
+ line(binding, rotateAroundZ(upperArc(radius, 'xz'), fullArc ? Math.PI / 2 : arc), 0.55),
167
+ ];
168
+ case 'cone': {
169
+ const angle = field(shape, 'angle', Math.PI / 6);
170
+ const length = Math.max(radius, 1);
171
+ const guides = [line(binding, ring(radius, 'xy', 48, arc))];
172
+ const rays = fullArc
173
+ ? [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]
174
+ : [0, arc / 3, (arc * 2) / 3, arc];
175
+ for (const theta of rays) {
176
+ const start = new THREE.Vector3(Math.cos(theta) * radius, Math.sin(theta) * radius, 0);
177
+ const end = start
178
+ .clone()
179
+ .add(
180
+ new THREE.Vector3(
181
+ Math.cos(theta) * Math.sin(angle),
182
+ Math.sin(theta) * Math.sin(angle),
183
+ Math.cos(angle),
184
+ ).multiplyScalar(length),
185
+ );
186
+ guides.push(line(binding, [start, end], 0.58));
187
+ }
188
+ return guides;
189
+ }
190
+ case 'circle':
191
+ return [line(binding, ring(radius, 'xy', 48, arc))];
192
+ case 'donut': {
193
+ const tube = field(shape, 'donutRadius', radius * 0.2);
194
+ return [
195
+ line(binding, ring(Math.max(0, radius - tube), 'xy', 48, arc), 0.45),
196
+ line(binding, ring(radius, 'xy', 48, arc)),
197
+ line(binding, ring(radius + tube, 'xy', 48, arc), 0.45),
198
+ line(
199
+ binding,
200
+ ring(tube, 'xz').map((candidate) => candidate.add(new THREE.Vector3(radius, 0, 0))),
201
+ 0.45,
202
+ ),
203
+ ];
204
+ }
205
+ case 'rectangle':
206
+ case 'grid': {
207
+ const width = field(shape, 'width', shape.type === 'grid' ? 1 : 10);
208
+ const height = field(shape, 'height', shape.type === 'grid' ? 1 : 10);
209
+ const halfWidth = width / 2;
210
+ const halfHeight = height / 2;
211
+ const guides = [
212
+ line(binding, [
213
+ new THREE.Vector3(-halfWidth, -halfHeight, 0),
214
+ new THREE.Vector3(halfWidth, -halfHeight, 0),
215
+ new THREE.Vector3(halfWidth, halfHeight, 0),
216
+ new THREE.Vector3(-halfWidth, halfHeight, 0),
217
+ new THREE.Vector3(-halfWidth, -halfHeight, 0),
218
+ ]),
219
+ ];
220
+ if (shape.type === 'grid') {
221
+ const columns = Math.min(16, Math.max(1, Math.round(field(shape, 'column', 10))));
222
+ const rows = Math.min(16, Math.max(1, Math.round(field(shape, 'row', 10))));
223
+ for (let index = 1; index < columns; index += 1) {
224
+ const x = -halfWidth + (index / columns) * width;
225
+ guides.push(
226
+ line(
227
+ binding,
228
+ [new THREE.Vector3(x, -halfHeight, 0), new THREE.Vector3(x, halfHeight, 0)],
229
+ 0.24,
230
+ ),
231
+ );
232
+ }
233
+ for (let index = 1; index < rows; index += 1) {
234
+ const y = -halfHeight + (index / rows) * height;
235
+ guides.push(
236
+ line(
237
+ binding,
238
+ [new THREE.Vector3(-halfWidth, y, 0), new THREE.Vector3(halfWidth, y, 0)],
239
+ 0.24,
240
+ ),
241
+ );
242
+ }
243
+ }
244
+ return guides;
245
+ }
246
+ }
247
+ }
248
+
249
+ function handle(
250
+ binding: ParticleSourceBinding,
251
+ fieldName: string,
252
+ label: string,
253
+ local: THREE.Vector3,
254
+ ): SpatialHandleLayer['handles'][number] {
255
+ return {
256
+ id: `particle:${key(binding)}:${fieldName}`,
257
+ label,
258
+ position: world(binding, local),
259
+ color: fieldName === 'angle' ? PARTICLE_SECONDARY : PARTICLE_COLOR,
260
+ writable: binding.writableFields.has(fieldName),
261
+ };
262
+ }
263
+
264
+ export function particleHandleLayer(binding: ParticleSourceBinding): SpatialHandleLayer {
265
+ const shape = binding.shape;
266
+ const radius = field(shape, 'radius', 10);
267
+ const handles: SpatialHandleLayer['handles'][number][] = [];
268
+ if (['sphere', 'hemisphere', 'cone', 'circle', 'donut'].includes(shape.type)) {
269
+ handles.push(handle(binding, 'radius', 'Emitter radius', new THREE.Vector3(radius, 0, 0)));
270
+ }
271
+ if (shape.type === 'cone') {
272
+ const angle = field(shape, 'angle', Math.PI / 6);
273
+ const length = Math.max(radius, 1);
274
+ handles.push(
275
+ handle(
276
+ binding,
277
+ 'angle',
278
+ 'Cone angle',
279
+ new THREE.Vector3(radius + Math.sin(angle) * length, 0, Math.cos(angle) * length),
280
+ ),
281
+ );
282
+ }
283
+ if (shape.type === 'donut') {
284
+ handles.push(
285
+ handle(
286
+ binding,
287
+ 'donutRadius',
288
+ 'Donut tube radius',
289
+ new THREE.Vector3(radius, 0, field(shape, 'donutRadius', radius * 0.2)),
290
+ ),
291
+ );
292
+ }
293
+ if (shape.type === 'rectangle' || shape.type === 'grid') {
294
+ handles.push(
295
+ handle(
296
+ binding,
297
+ 'width',
298
+ 'Emitter width',
299
+ new THREE.Vector3(field(shape, 'width', 1) / 2, 0, 0),
300
+ ),
301
+ handle(
302
+ binding,
303
+ 'height',
304
+ 'Emitter height',
305
+ new THREE.Vector3(0, field(shape, 'height', 1) / 2, 0),
306
+ ),
307
+ );
308
+ }
309
+ return {
310
+ id: `particle:${key(binding)}`,
311
+ category: 'particles',
312
+ guides: outline(binding),
313
+ handles,
314
+ };
315
+ }
316
+
317
+ function rounded(value: number): number {
318
+ return Math.round(value * ROUND_TO) / ROUND_TO;
319
+ }
320
+
321
+ function clampParticleField(fieldName: string, value: number): number {
322
+ if (fieldName === 'angle') return rounded(THREE.MathUtils.clamp(value, 0.01, Math.PI / 2 - 0.01));
323
+ if (fieldName === 'arc') return rounded(THREE.MathUtils.clamp(value, 0, Math.PI * 2));
324
+ if (fieldName === 'thickness') return rounded(THREE.MathUtils.clamp(value, 0, 1));
325
+ if (fieldName === 'column' || fieldName === 'row') return Math.max(1, Math.round(value));
326
+ return rounded(Math.max(0.01, value));
327
+ }
328
+
329
+ export function normalizeParticleField(fieldName: string, value: number): number {
330
+ return clampParticleField(fieldName, value);
331
+ }
332
+
333
+ export function particleEditFromWorld(
334
+ bindings: readonly ParticleSourceBinding[],
335
+ handleId: string,
336
+ worldPosition: SpatialPoint3,
337
+ ): ParticleFieldEdit | null {
338
+ const match = /^particle:([^:]+):(radius|angle|donutRadius|width|height)$/.exec(handleId);
339
+ const binding = match ? bindings.find((candidate) => key(candidate) === match[1]) : undefined;
340
+ const fieldName = match?.[2];
341
+ if (!binding || !fieldName || !binding.writableFields.has(fieldName)) return null;
342
+ binding.emitter.updateWorldMatrix(true, false);
343
+ const local = binding.emitter.worldToLocal(new THREE.Vector3(...worldPosition));
344
+ const radius = field(binding.shape, 'radius', 10);
345
+ let value: number;
346
+ if (fieldName === 'radius') {
347
+ value =
348
+ binding.shape.type === 'sphere' || binding.shape.type === 'hemisphere'
349
+ ? local.length()
350
+ : Math.hypot(local.x, local.y);
351
+ } else if (fieldName === 'angle') {
352
+ value = Math.atan2(
353
+ Math.max(0, Math.hypot(local.x, local.y) - radius),
354
+ Math.abs(local.z) || 0.001,
355
+ );
356
+ } else if (fieldName === 'donutRadius') {
357
+ value = Math.abs(local.z);
358
+ } else if (fieldName === 'width') {
359
+ value = Math.abs(local.x) * 2;
360
+ } else {
361
+ value = Math.abs(local.y) * 2;
362
+ }
363
+ return { binding, field: fieldName, value: clampParticleField(fieldName, value) };
364
+ }
365
+
366
+ export function previewParticleEdit(edit: ParticleFieldEdit): void {
367
+ (edit.binding.shape as unknown as Record<string, unknown>)[edit.field] = edit.value;
368
+ }