@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 3D board's FRAMING MATH — pure, three-free, and the sibling of
|
|
3
|
+
* `board-layout.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Layout answers *where each exhibit stands* at true scale. This module
|
|
6
|
+
* answers *what the default camera should fit*. Those are different
|
|
7
|
+
* questions the moment one exhibit carries a helper volume — a light-beam
|
|
8
|
+
* cone, an unbounded ground plane, a skybox-scale mesh — whose authored
|
|
9
|
+
* AABB is orders of magnitude larger than the body a reader is trying to
|
|
10
|
+
* identify. Framing the union then zooms the camera out until every
|
|
11
|
+
* exhibit is a few pixels in a mostly-black frame.
|
|
12
|
+
*
|
|
13
|
+
* True scale is inviolable: nothing here scales an exhibit, and a helper
|
|
14
|
+
* volume is still in the scene at its authored size. What changes is
|
|
15
|
+
* which box the DEFAULT view (and the Frame button / view presets) fits:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Per-exhibit presence.** Each exhibit's node boxes are reduced to
|
|
18
|
+
* the body worth standing next to its neighbours. A named helper —
|
|
19
|
+
* an authored overlay volume, a BackSide enclosure, or an unbounded
|
|
20
|
+
* plane — is dropped from that body when a body remains. A
|
|
21
|
+
* single-node exhibit is never trimmed: it *is* the exhibit, even
|
|
22
|
+
* when that exhibit is a 28 m cone or a 120 m tower.
|
|
23
|
+
* 2. **Layout uses presence**, so a helper cannot reserve a floor slot
|
|
24
|
+
* the size of itself and push the next district out to its tip. The
|
|
25
|
+
* helper still extends through space at true scale.
|
|
26
|
+
* 3. **The default frame is the union of placed presence boxes**, not
|
|
27
|
+
* the union of every authored vertex. Frame-selection still frames
|
|
28
|
+
* whatever is picked (the cone, if that is what was clicked).
|
|
29
|
+
*
|
|
30
|
+
* Size is the wrong axis for "volume vs body." An 8× sliver cut missed
|
|
31
|
+
* the reported 16 × 16 × 28 m cone (1.75×). A relative-to-own-median cut
|
|
32
|
+
* ate the subject of any exhibit that is one large mesh plus small
|
|
33
|
+
* details (a hull with rivets; a building with a door). Mass distribution
|
|
34
|
+
* cannot save it either: the same cone is 91 % of the lighthouse's mesh
|
|
35
|
+
* surface (so a 90 % area box IS the cone) and still sits in the 90 %
|
|
36
|
+
* triangle-count set. The discriminator that exists in the data is the
|
|
37
|
+
* material flag the author set because the thing is a volume:
|
|
38
|
+
* `depthWrite === false` (the cone: MeshBasicMaterial, transparent,
|
|
39
|
+
* opacity 0.16, depthWrite false; every tower / hull / rivet mesh:
|
|
40
|
+
* opaque standard material, depthWrite true). That flag arrives here as
|
|
41
|
+
* {@link BoardNode.overlay}. Emissive intensity is not a volume mark —
|
|
42
|
+
* the lantern house is emissive and is body.
|
|
43
|
+
*
|
|
44
|
+
* A 0.2 m prop next to a 30 m building is two compact exhibits, and the
|
|
45
|
+
* union of those bodies is exactly the true-scale comparison the board
|
|
46
|
+
* exists to show.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
type BoardBounds,
|
|
51
|
+
type BoardItemPlacement,
|
|
52
|
+
type BoardLayout,
|
|
53
|
+
type BoardLayoutItem,
|
|
54
|
+
layoutThreeBoard,
|
|
55
|
+
} from './board-layout';
|
|
56
|
+
|
|
57
|
+
export type { BoardBounds };
|
|
58
|
+
|
|
59
|
+
/** Why a node box was kept out of an exhibit's presence / default frame. */
|
|
60
|
+
export type BoardHelperKind = 'volume' | 'unbounded-plane' | 'environment-scale';
|
|
61
|
+
|
|
62
|
+
export interface BoardHelperVolume {
|
|
63
|
+
readonly kind: BoardHelperKind;
|
|
64
|
+
readonly bounds: BoardBounds;
|
|
65
|
+
readonly size: readonly [number, number, number];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One drawing node of an exhibit. The two flags are authored material
|
|
70
|
+
* marks collected by `collectContentNodeRecords` — this module never
|
|
71
|
+
* looks at a mesh.
|
|
72
|
+
*
|
|
73
|
+
* - `overlay`: `depthWrite === false` (a light cone, trigger, AoE)
|
|
74
|
+
* - `enclosure`: `side === BackSide` (a skybox)
|
|
75
|
+
*/
|
|
76
|
+
export interface BoardNode {
|
|
77
|
+
readonly bounds: BoardBounds;
|
|
78
|
+
readonly overlay?: boolean;
|
|
79
|
+
readonly enclosure?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ExhibitPresence {
|
|
83
|
+
/** Union of every finite node box — the authored extent, true scale. */
|
|
84
|
+
readonly full: BoardBounds;
|
|
85
|
+
/** The body the board stands and frames. Equal to {@link full} when
|
|
86
|
+
* nothing was defensible to drop. */
|
|
87
|
+
readonly presence: BoardBounds;
|
|
88
|
+
readonly helpers: readonly BoardHelperVolume[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BoardExhibitMeasure {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly groupKey: string;
|
|
94
|
+
readonly nodes: readonly (BoardBounds | BoardNode)[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BoardFramedExhibit {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly groupKey: string;
|
|
100
|
+
readonly full: BoardBounds;
|
|
101
|
+
readonly presence: BoardBounds;
|
|
102
|
+
readonly helpers: readonly BoardHelperVolume[];
|
|
103
|
+
readonly placement: BoardItemPlacement;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BoardFrame {
|
|
107
|
+
readonly layout: BoardLayout;
|
|
108
|
+
readonly exhibits: readonly BoardFramedExhibit[];
|
|
109
|
+
/** Union of placed presence boxes — what the default camera fits. */
|
|
110
|
+
readonly frame: BoardBounds;
|
|
111
|
+
/** Union of placed FULL boxes — what a naive frame-all would fit. */
|
|
112
|
+
readonly fullFrame: BoardBounds;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ZERO_BOUNDS: BoardBounds = { min: [0, 0, 0], max: [0, 0, 0] };
|
|
116
|
+
|
|
117
|
+
/** A plane is thinner than this on one axis, and larger than
|
|
118
|
+
* {@link PLANE_SPAN_METRES} on the other two. A shape of the box itself
|
|
119
|
+
* — not a comparison to its siblings — so a building with handles cannot
|
|
120
|
+
* classify as a plane. */
|
|
121
|
+
const PLANE_THIN_METRES = 0.35;
|
|
122
|
+
const PLANE_SPAN_METRES = 20;
|
|
123
|
+
|
|
124
|
+
function isFiniteBounds(bounds: BoardBounds): boolean {
|
|
125
|
+
return [...bounds.min, ...bounds.max].every((value) => Number.isFinite(value));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeBounds(bounds: BoardBounds): BoardBounds {
|
|
129
|
+
if (!isFiniteBounds(bounds)) return ZERO_BOUNDS;
|
|
130
|
+
return {
|
|
131
|
+
min: [
|
|
132
|
+
Math.min(bounds.min[0], bounds.max[0]),
|
|
133
|
+
Math.min(bounds.min[1], bounds.max[1]),
|
|
134
|
+
Math.min(bounds.min[2], bounds.max[2]),
|
|
135
|
+
],
|
|
136
|
+
max: [
|
|
137
|
+
Math.max(bounds.min[0], bounds.max[0]),
|
|
138
|
+
Math.max(bounds.min[1], bounds.max[1]),
|
|
139
|
+
Math.max(bounds.min[2], bounds.max[2]),
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function boundsSize(bounds: BoardBounds): readonly [number, number, number] {
|
|
145
|
+
const normalized = normalizeBounds(bounds);
|
|
146
|
+
return [
|
|
147
|
+
normalized.max[0] - normalized.min[0],
|
|
148
|
+
normalized.max[1] - normalized.min[1],
|
|
149
|
+
normalized.max[2] - normalized.min[2],
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function boundsSpan(bounds: BoardBounds): number {
|
|
154
|
+
return Math.max(...boundsSize(bounds));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function unionBounds(boxes: readonly BoardBounds[]): BoardBounds {
|
|
158
|
+
const finite = boxes.map(normalizeBounds).filter((box) => isFiniteBounds(box));
|
|
159
|
+
if (finite.length === 0) return ZERO_BOUNDS;
|
|
160
|
+
const first = finite[0]!;
|
|
161
|
+
const min: [number, number, number] = [first.min[0], first.min[1], first.min[2]];
|
|
162
|
+
const max: [number, number, number] = [first.max[0], first.max[1], first.max[2]];
|
|
163
|
+
for (let i = 1; i < finite.length; i++) {
|
|
164
|
+
const box = finite[i]!;
|
|
165
|
+
min[0] = Math.min(min[0], box.min[0]);
|
|
166
|
+
min[1] = Math.min(min[1], box.min[1]);
|
|
167
|
+
min[2] = Math.min(min[2], box.min[2]);
|
|
168
|
+
max[0] = Math.max(max[0], box.max[0]);
|
|
169
|
+
max[1] = Math.max(max[1], box.max[1]);
|
|
170
|
+
max[2] = Math.max(max[2], box.max[2]);
|
|
171
|
+
}
|
|
172
|
+
return { min, max };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function translateBounds(
|
|
176
|
+
bounds: BoardBounds,
|
|
177
|
+
translation: readonly [number, number, number],
|
|
178
|
+
): BoardBounds {
|
|
179
|
+
const normalized = normalizeBounds(bounds);
|
|
180
|
+
return {
|
|
181
|
+
min: [
|
|
182
|
+
normalized.min[0] + translation[0],
|
|
183
|
+
normalized.min[1] + translation[1],
|
|
184
|
+
normalized.min[2] + translation[2],
|
|
185
|
+
],
|
|
186
|
+
max: [
|
|
187
|
+
normalized.max[0] + translation[0],
|
|
188
|
+
normalized.max[1] + translation[1],
|
|
189
|
+
normalized.max[2] + translation[2],
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Accept either a raw box or a {@link BoardNode} so layout ghosts (boxes
|
|
195
|
+
* only) and collected exhibits (boxes + overlay) share one call. */
|
|
196
|
+
export function asBoardNode(input: BoardBounds | BoardNode): BoardNode {
|
|
197
|
+
if ('min' in input) return { bounds: input };
|
|
198
|
+
return {
|
|
199
|
+
bounds: input.bounds,
|
|
200
|
+
...(input.overlay === true ? { overlay: true as const } : {}),
|
|
201
|
+
...(input.enclosure === true ? { enclosure: true as const } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Named kind for a helper volume, or `null` when the box is the exhibit's body.
|
|
206
|
+
*
|
|
207
|
+
* `overlay` / `enclosure` are authored material marks. Size never names a
|
|
208
|
+
* volume or a skybox: a sibling ratio is what ate the hull-with-rivets
|
|
209
|
+
* and the building-with-handles. */
|
|
210
|
+
export function classifyHelper(
|
|
211
|
+
bounds: BoardBounds,
|
|
212
|
+
flags: { overlay?: boolean; enclosure?: boolean } = {},
|
|
213
|
+
): BoardHelperKind | null {
|
|
214
|
+
if (flags.overlay) return 'volume';
|
|
215
|
+
if (flags.enclosure) return 'environment-scale';
|
|
216
|
+
const size = boundsSize(bounds);
|
|
217
|
+
const rankedSize = [...size].sort((a, b) => b - a);
|
|
218
|
+
const longest = rankedSize[0] ?? 0;
|
|
219
|
+
const mid = rankedSize[1] ?? 0;
|
|
220
|
+
const shortest = rankedSize[2] ?? 0;
|
|
221
|
+
if (shortest < PLANE_THIN_METRES && mid >= PLANE_SPAN_METRES && longest >= PLANE_SPAN_METRES) {
|
|
222
|
+
return 'unbounded-plane';
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The body of one exhibit, given one box per content node.
|
|
229
|
+
*
|
|
230
|
+
* A node is dropped only when it has a named helper kind — overlay volume,
|
|
231
|
+
* enclosure / skybox, or unbounded plane — AND at least one body node
|
|
232
|
+
* remains. A stacked building (drum + crown) is all body. A hull with
|
|
233
|
+
* rivets is all body. A single-node exhibit is never trimmed: it *is* the
|
|
234
|
+
* exhibit, even if that exhibit is a plane or a cone.
|
|
235
|
+
*/
|
|
236
|
+
export function exhibitPresence(nodes: readonly (BoardBounds | BoardNode)[]): ExhibitPresence {
|
|
237
|
+
const content = nodes
|
|
238
|
+
.map(asBoardNode)
|
|
239
|
+
.map((node) => ({
|
|
240
|
+
bounds: normalizeBounds(node.bounds),
|
|
241
|
+
overlay: node.overlay === true,
|
|
242
|
+
enclosure: node.enclosure === true,
|
|
243
|
+
}))
|
|
244
|
+
.filter((node) => isFiniteBounds(node.bounds));
|
|
245
|
+
const boxes = content.map((node) => node.bounds);
|
|
246
|
+
const full = unionBounds(boxes);
|
|
247
|
+
if (content.length < 2) return { full, presence: full, helpers: [] };
|
|
248
|
+
|
|
249
|
+
const kinds = content.map((node) =>
|
|
250
|
+
classifyHelper(node.bounds, { overlay: node.overlay, enclosure: node.enclosure }),
|
|
251
|
+
);
|
|
252
|
+
const body = content.filter((_, index) => kinds[index] === null).map((node) => node.bounds);
|
|
253
|
+
if (body.length === 0) return { full, presence: full, helpers: [] };
|
|
254
|
+
|
|
255
|
+
const helpers: BoardHelperVolume[] = [];
|
|
256
|
+
for (let i = 0; i < content.length; i++) {
|
|
257
|
+
const kind = kinds[i] ?? null;
|
|
258
|
+
if (kind === null) continue;
|
|
259
|
+
helpers.push({ kind, bounds: content[i]!.bounds, size: boundsSize(content[i]!.bounds) });
|
|
260
|
+
}
|
|
261
|
+
return { full, presence: unionBounds(body), helpers };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Lay every exhibit out by its PRESENCE box and compute the default frame.
|
|
266
|
+
*
|
|
267
|
+
* `fullFrame` is carried so a test (and a doctor) can name the camera the
|
|
268
|
+
* old union would have produced without reconstructing it.
|
|
269
|
+
*/
|
|
270
|
+
export function frameThreeBoard(exhibits: readonly BoardExhibitMeasure[]): BoardFrame {
|
|
271
|
+
const measured = exhibits.map((exhibit) => ({
|
|
272
|
+
exhibit,
|
|
273
|
+
presence: exhibitPresence(exhibit.nodes),
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const layoutItems: BoardLayoutItem[] = measured.map(({ exhibit, presence }) => ({
|
|
277
|
+
id: exhibit.id,
|
|
278
|
+
groupKey: exhibit.groupKey,
|
|
279
|
+
bounds: presence.presence,
|
|
280
|
+
}));
|
|
281
|
+
const layout = layoutThreeBoard(layoutItems);
|
|
282
|
+
const placementById = new Map<string, BoardItemPlacement>();
|
|
283
|
+
for (const district of layout.districts) {
|
|
284
|
+
for (const item of district.items) placementById.set(item.id, item);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const framed: BoardFramedExhibit[] = [];
|
|
288
|
+
const placedPresence: BoardBounds[] = [];
|
|
289
|
+
const placedFull: BoardBounds[] = [];
|
|
290
|
+
for (const { exhibit, presence } of measured) {
|
|
291
|
+
const placement = placementById.get(exhibit.id);
|
|
292
|
+
if (!placement) continue;
|
|
293
|
+
framed.push({
|
|
294
|
+
id: exhibit.id,
|
|
295
|
+
groupKey: exhibit.groupKey,
|
|
296
|
+
full: presence.full,
|
|
297
|
+
presence: presence.presence,
|
|
298
|
+
helpers: presence.helpers,
|
|
299
|
+
placement,
|
|
300
|
+
});
|
|
301
|
+
placedPresence.push(translateBounds(presence.presence, placement.translation));
|
|
302
|
+
placedFull.push(translateBounds(presence.full, placement.translation));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
layout,
|
|
307
|
+
exhibits: framed,
|
|
308
|
+
frame: unionBounds(placedPresence),
|
|
309
|
+
fullFrame: unionBounds(placedFull),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface BoardCameraSpec {
|
|
314
|
+
readonly fovDeg: number;
|
|
315
|
+
readonly aspect: number;
|
|
316
|
+
readonly padding?: number;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface BoardCameraPose {
|
|
320
|
+
readonly target: readonly [number, number, number];
|
|
321
|
+
readonly position: readonly [number, number, number];
|
|
322
|
+
readonly distance: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Perspective fit of `frame` along `direction` (centre → camera). Same
|
|
327
|
+
* geometry as `perspectiveDistanceToFitBox`, kept three-free so the unit
|
|
328
|
+
* test is bounds-in / pose-out.
|
|
329
|
+
*/
|
|
330
|
+
export function boardCameraPose(
|
|
331
|
+
frame: BoardBounds,
|
|
332
|
+
direction: readonly [number, number, number],
|
|
333
|
+
camera: BoardCameraSpec,
|
|
334
|
+
): BoardCameraPose {
|
|
335
|
+
const normalized = normalizeBounds(frame);
|
|
336
|
+
const target = [
|
|
337
|
+
(normalized.min[0] + normalized.max[0]) / 2,
|
|
338
|
+
(normalized.min[1] + normalized.max[1]) / 2,
|
|
339
|
+
(normalized.min[2] + normalized.max[2]) / 2,
|
|
340
|
+
] as const;
|
|
341
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]) || 1;
|
|
342
|
+
const view: readonly [number, number, number] = [
|
|
343
|
+
direction[0] / length,
|
|
344
|
+
direction[1] / length,
|
|
345
|
+
direction[2] / length,
|
|
346
|
+
];
|
|
347
|
+
const fallbackUp: readonly [number, number, number] =
|
|
348
|
+
Math.abs(view[1]) > 0.999 ? [0, 0, 1] : [0, 1, 0];
|
|
349
|
+
const rightRaw: readonly [number, number, number] = [
|
|
350
|
+
fallbackUp[1] * view[2] - fallbackUp[2] * view[1],
|
|
351
|
+
fallbackUp[2] * view[0] - fallbackUp[0] * view[2],
|
|
352
|
+
fallbackUp[0] * view[1] - fallbackUp[1] * view[0],
|
|
353
|
+
];
|
|
354
|
+
const rightLen = Math.hypot(...rightRaw) || 1;
|
|
355
|
+
const right: readonly [number, number, number] = [
|
|
356
|
+
rightRaw[0] / rightLen,
|
|
357
|
+
rightRaw[1] / rightLen,
|
|
358
|
+
rightRaw[2] / rightLen,
|
|
359
|
+
];
|
|
360
|
+
const up: readonly [number, number, number] = [
|
|
361
|
+
view[1] * right[2] - view[2] * right[1],
|
|
362
|
+
view[2] * right[0] - view[0] * right[2],
|
|
363
|
+
view[0] * right[1] - view[1] * right[0],
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
let halfDepth = 0;
|
|
367
|
+
let halfHeight = 0;
|
|
368
|
+
let halfWidth = 0;
|
|
369
|
+
for (const x of [normalized.min[0], normalized.max[0]]) {
|
|
370
|
+
for (const y of [normalized.min[1], normalized.max[1]]) {
|
|
371
|
+
for (const z of [normalized.min[2], normalized.max[2]]) {
|
|
372
|
+
const ox = x - target[0];
|
|
373
|
+
const oy = y - target[1];
|
|
374
|
+
const oz = z - target[2];
|
|
375
|
+
halfWidth = Math.max(halfWidth, Math.abs(ox * right[0] + oy * right[1] + oz * right[2]));
|
|
376
|
+
halfHeight = Math.max(halfHeight, Math.abs(ox * up[0] + oy * up[1] + oz * up[2]));
|
|
377
|
+
halfDepth = Math.max(halfDepth, Math.abs(ox * view[0] + oy * view[1] + oz * view[2]));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const padding = camera.padding ?? 1.15;
|
|
383
|
+
const verticalHalfFov = (camera.fovDeg * 0.5 * Math.PI) / 180;
|
|
384
|
+
const horizontalHalfFov = Math.atan(Math.tan(verticalHalfFov) * Math.max(camera.aspect, 0.01));
|
|
385
|
+
const fitDistance = Math.max(
|
|
386
|
+
halfHeight / Math.tan(verticalHalfFov),
|
|
387
|
+
halfWidth / Math.tan(horizontalHalfFov),
|
|
388
|
+
);
|
|
389
|
+
const distance = Math.max(fitDistance * padding + halfDepth, 3);
|
|
390
|
+
return {
|
|
391
|
+
target,
|
|
392
|
+
distance,
|
|
393
|
+
position: [
|
|
394
|
+
target[0] + view[0] * distance,
|
|
395
|
+
target[1] + view[1] * distance,
|
|
396
|
+
target[2] + view[2] * distance,
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Approximate on-screen height of a world-space span at `distance`, as a
|
|
402
|
+
* fraction of the viewport. Used by tests to pin "identifiable", not by
|
|
403
|
+
* the board itself. */
|
|
404
|
+
export function viewportHeightFraction(
|
|
405
|
+
worldHeight: number,
|
|
406
|
+
distance: number,
|
|
407
|
+
fovDeg: number,
|
|
408
|
+
): number {
|
|
409
|
+
if (!(distance > 0)) return 0;
|
|
410
|
+
const half = Math.tan((fovDeg * 0.5 * Math.PI) / 180) * distance;
|
|
411
|
+
return worldHeight / (2 * half);
|
|
412
|
+
}
|