@volter/editor-threejs 0.5.211 → 0.5.213
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/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +6 -11
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +192 -439
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +6 -17
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +5 -3
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +486 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structural identity — the deterministic id scheme (and the small material
|
|
3
|
-
* reflection helper that rides along with it) the editor's three authoring
|
|
4
|
-
* adapter uses to address the objects of a world whose source carries no
|
|
5
|
-
* serve-time identity stamps
|
|
6
|
-
* (`packages/editor-threejs/src/kit/projection/three.ts`, `structuralIdentity`).
|
|
7
|
-
*
|
|
8
|
-
* Identity: each object gets a **structural-path id** — deterministic from the
|
|
9
|
-
* scene's shape (position in the tree + three.js type + name), so the SAME id
|
|
10
|
-
* re-binds to the SAME object after the game rebuilds its scene within a
|
|
11
|
-
* session. The pure walk returns that identity beside the native objects; the
|
|
12
|
-
* editor keeps the reverse lookup in its authoring adapter rather than writing
|
|
13
|
-
* editor currency into a foreign graph.
|
|
14
|
-
*
|
|
15
|
-
* This module used to be the shared core of a per-game JSON sidecar
|
|
16
|
-
* persistence system, which was deleted outright (2026-08-02) — ingest edits
|
|
17
|
-
* are LIVE-ONLY now. The id scheme survives because it is what makes the live
|
|
18
|
-
* hierarchy/selection/inspector work at all; nothing here reads or writes a
|
|
19
|
-
* file.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import type * as THREE from 'three';
|
|
23
|
-
import { setUserData } from '../../ecs/user-data';
|
|
24
|
-
|
|
25
|
-
/** Fixed id for the captured render camera (not a scene child, so no structural path). */
|
|
26
|
-
export const CAMERA_ID = 'ingest:camera';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The editor parks its OWN objects — grid, its two lights, the particle
|
|
30
|
-
* BatchedRenderer, every TransformControls/gizmo helper — on layer 31
|
|
31
|
-
* (`@volter/editor-threejs/viewport/editor-layers`'s `EDITOR_LAYER`) so the game camera
|
|
32
|
-
* never sees them. On an ingest root those objects are added to the GAME'S OWN
|
|
33
|
-
* scene, which is the same tree this walk indexes.
|
|
34
|
-
*/
|
|
35
|
-
const EDITOR_ONLY_LAYER_MASK = 1 << 31;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Editor furniture, not game content. Excluded from the walk entirely — see
|
|
39
|
-
* {@link collectStructuralIds} for why that exclusion is what makes the id
|
|
40
|
-
* scheme's central promise true.
|
|
41
|
-
*/
|
|
42
|
-
function isEditorOnly(o: THREE.Object3D): boolean {
|
|
43
|
-
return (o.layers.mask & EDITOR_ONLY_LAYER_MASK) !== 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The material whose colour a mesh's swatch reflects, if it has one.
|
|
48
|
-
*
|
|
49
|
-
* A multi-material mesh reflects its FIRST slot — that is what a single swatch
|
|
50
|
-
* can honestly stand for, and it is also the slot a creation-site anchor for
|
|
51
|
-
* `material.color` resolves against. Returns `null` for anything with no
|
|
52
|
-
* `color` at all rather than fabricating one.
|
|
53
|
-
*/
|
|
54
|
-
export function colorMaterialOf(o: THREE.Object3D): THREE.MeshStandardMaterial | null {
|
|
55
|
-
const mat = (o as THREE.Mesh).material;
|
|
56
|
-
if (!mat) return null;
|
|
57
|
-
const m = (Array.isArray(mat) ? mat[0] : mat) as THREE.MeshStandardMaterial | undefined;
|
|
58
|
-
return m?.color ? m : null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Result of a structural-path walk: the id→object map plus a few reflection stats. */
|
|
62
|
-
export interface StructuralIdWalk {
|
|
63
|
-
byId: Map<string, THREE.Object3D>;
|
|
64
|
-
count: number;
|
|
65
|
-
meshes: number;
|
|
66
|
-
lights: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* (Re)collect structural-path ids for every object under `scene` (and, if
|
|
71
|
-
* given, the separately-captured render `camera`, under the fixed
|
|
72
|
-
* {@link CAMERA_ID}). Idempotent and deterministic from scene structure
|
|
73
|
-
* (position + type + name) — so a re-walk after the game rebuilds part of its
|
|
74
|
-
* tree re-binds the same ids to the same objects.
|
|
75
|
-
*
|
|
76
|
-
* EDITOR FURNITURE IS EXCLUDED, and that exclusion is load-bearing rather than
|
|
77
|
-
* cosmetic. On an ingest root the editor adds its grid, lights, particle
|
|
78
|
-
* BatchedRenderer and TransformControls gizmo to the GAME'S OWN scene, and
|
|
79
|
-
* they arrive asynchronously — some before the walk, some after. Indexing them
|
|
80
|
-
* made a game object's path depend on how much editor furniture happened to be
|
|
81
|
-
* attached at walk time, so the same mesh changed id across a re-walk (observed
|
|
82
|
-
* in the games-fps ingest: an object addressed as `ingest:106/0/0/0:…` came
|
|
83
|
-
* back as `ingest:102/0:Mesh:Cube004` — four root slots earlier, exactly the
|
|
84
|
-
* grid + two editor lights + BatchedRenderer).
|
|
85
|
-
*
|
|
86
|
-
* Skipping the furniture entirely — not merely declining to give it an id —
|
|
87
|
-
* is what fixes that: the game's own children occupy 0..N-1 whatever else is
|
|
88
|
-
* parented alongside them. It also keeps gizmo handles and the grid out of the
|
|
89
|
-
* hierarchy/inspector projections built on this walk, where they were
|
|
90
|
-
* selectable and colorable as if they were game content.
|
|
91
|
-
*/
|
|
92
|
-
export function collectStructuralIds(
|
|
93
|
-
scene: THREE.Object3D,
|
|
94
|
-
camera?: THREE.Object3D | undefined,
|
|
95
|
-
): StructuralIdWalk {
|
|
96
|
-
const byId = new Map<string, THREE.Object3D>();
|
|
97
|
-
let count = 0;
|
|
98
|
-
let meshes = 0;
|
|
99
|
-
let lights = 0;
|
|
100
|
-
const visit = (
|
|
101
|
-
o: THREE.Object3D & { isMesh?: boolean; isLight?: boolean },
|
|
102
|
-
path: string,
|
|
103
|
-
): void => {
|
|
104
|
-
const id = `ingest:${path}:${o.type}:${o.name || ''}`;
|
|
105
|
-
byId.set(id, o);
|
|
106
|
-
count++;
|
|
107
|
-
if (o.isLight) lights++;
|
|
108
|
-
else if (o.isMesh) meshes++;
|
|
109
|
-
visitChildren(o, path);
|
|
110
|
-
};
|
|
111
|
-
const visitChildren = (o: THREE.Object3D, path: string): void => {
|
|
112
|
-
let i = 0;
|
|
113
|
-
for (const c of o.children) {
|
|
114
|
-
if (isEditorOnly(c)) continue;
|
|
115
|
-
visit(c, path ? `${path}/${i}` : `${i}`);
|
|
116
|
-
i++;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
visitChildren(scene, '');
|
|
120
|
-
if (camera) {
|
|
121
|
-
byId.set(CAMERA_ID, camera);
|
|
122
|
-
count++;
|
|
123
|
-
}
|
|
124
|
-
return { byId, count, meshes, lights };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Compatibility entry point for first-party callers that explicitly want
|
|
129
|
-
* structural ids stamped into their own graph. Foreign ingest authoring uses
|
|
130
|
-
* {@link collectStructuralIds} and never calls this mutating form.
|
|
131
|
-
*/
|
|
132
|
-
export function assignStructuralIds(
|
|
133
|
-
scene: THREE.Object3D,
|
|
134
|
-
camera?: THREE.Object3D | undefined,
|
|
135
|
-
): StructuralIdWalk {
|
|
136
|
-
const walk = collectStructuralIds(scene, camera);
|
|
137
|
-
for (const [id, object] of walk.byId) setUserData(object, 'entityId', id);
|
|
138
|
-
return walk;
|
|
139
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The capture window is a budget of VISIBLE time, not of wall-clock time.
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS MODULE EXISTS. An ingest mount waits for the game to render its
|
|
5
|
-
* first frame and fails by name when that never happens
|
|
6
|
-
* (`scene-capture.ts`'s `waitForCapture`). That wait used to be a plain
|
|
7
|
-
* `setTimeout`, i.e. a wall-clock deadline started AT BOOT — and a game cannot
|
|
8
|
-
* render while its tab is hidden, because the browser parks `requestAnimation
|
|
9
|
-
* Frame` for a backgrounded document. The two facts together make one
|
|
10
|
-
* deterministic failure: a tab that opens in the BACKGROUND (the normal human
|
|
11
|
-
* path — the editor's `edit` command auto-opens a tab that routinely lands behind the current
|
|
12
|
-
* window) burns its whole capture window unable to draw, the deadline fires,
|
|
13
|
-
* the mount dies terminally, and foregrounding the tab later changes nothing.
|
|
14
|
-
* Agents, whose tabs happen to be visible, never saw it.
|
|
15
|
-
*
|
|
16
|
-
* So the clock only runs while the document is VISIBLE. Hidden time is not
|
|
17
|
-
* spent, and while a tab is hidden the wait PARKS rather than expiring: the
|
|
18
|
-
* capture trap stays installed, the game's modules stay live, and the first
|
|
19
|
-
* frame the tab draws after the human brings it forward is trapped exactly as
|
|
20
|
-
* it would have been at boot. That is the retry — no second mount, no
|
|
21
|
-
* re-running `load()` (which would re-construct module-level state: the
|
|
22
|
-
* ARCHITECTURE-CORE "LOADING CONSTRUCTS, once" contract), and no forcing of a
|
|
23
|
-
* frame: the game's own loop resumes on its own when the browser un-parks it.
|
|
24
|
-
*
|
|
25
|
-
* There is deliberately no wall-clock cap on the parked state. An unspent
|
|
26
|
-
* budget is not a failure — a human returning to the tab is what spends it —
|
|
27
|
-
* and a cap would be exactly the boot-time clock this module exists to
|
|
28
|
-
* remove. The parked wait is reported instead of being silent: see
|
|
29
|
-
* `CaptureWaitObserver` in `scene-capture.ts` and the editor's `status` command's
|
|
30
|
-
* `ingestCaptureWait`.
|
|
31
|
-
*
|
|
32
|
-
* The state machine is a pure function of (banked segments, now, hidden) so
|
|
33
|
-
* the whole accrual rule is testable with no browser and no timers
|
|
34
|
-
* (`packages/engine/test/visible-capture-window.test.ts`); the runtime half
|
|
35
|
-
* below is only the timer arm/disarm around it.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Everything the budget reads about the outside world. Injected so the pure
|
|
40
|
-
* core stays pure and the runtime half is drivable by a fake in tests — and,
|
|
41
|
-
* as a side effect, so this module never hard-depends on `document` existing
|
|
42
|
-
* (unit runners, SSR).
|
|
43
|
-
*/
|
|
44
|
-
export interface VisibilityClock {
|
|
45
|
-
/** Milliseconds, monotonic-ish; `performance.now()`/`Date.now()` both fit. */
|
|
46
|
-
now(): number;
|
|
47
|
-
/** True while the document is hidden (no rAF, so no frame can be captured). */
|
|
48
|
-
hidden(): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* True while the browser is not presenting frames. This is wider than
|
|
51
|
-
* `hidden()`: WebKit also stops rAF for an unfocused window (and can suspend
|
|
52
|
-
* the page entirely) while `document.hidden` still reads false.
|
|
53
|
-
*
|
|
54
|
-
* Optional for compatibility with injected clocks; absent means `hidden()`.
|
|
55
|
-
*/
|
|
56
|
-
suspended?(): boolean;
|
|
57
|
-
/** Why {@link suspended} is true, when the clock can say. */
|
|
58
|
-
suspensionReason?(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
59
|
-
/** Subscribe to visibility transitions; returns the unsubscribe. */
|
|
60
|
-
subscribe(onChange: () => void): () => void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The real one: `document.visibilityState` + `visibilitychange`. Falls back
|
|
64
|
-
* to permanently-visible where there is no `document` at all, which is the
|
|
65
|
-
* honest answer for a headless caller — it has no tab to background. */
|
|
66
|
-
export function documentVisibilityClock(): VisibilityClock {
|
|
67
|
-
const doc = typeof document === 'undefined' ? null : document;
|
|
68
|
-
const win = doc?.defaultView ?? null;
|
|
69
|
-
let pageSuspended = false;
|
|
70
|
-
const reason = (): 'hidden' | 'unfocused' | 'page-suspended' | null => {
|
|
71
|
-
if (pageSuspended) return 'page-suspended';
|
|
72
|
-
if (doc?.hidden === true) return 'hidden';
|
|
73
|
-
// WebKit stops rAF when the browser window becomes inactive, including
|
|
74
|
-
// when another app is in front. Page Visibility can still say `visible`
|
|
75
|
-
// in that state. Focus is a platform fact, not a Safari/UA sniff, and in
|
|
76
|
-
// browsers that keep rendering while unfocused this merely parks the
|
|
77
|
-
// expiry clock until either a frame arrives or focus returns.
|
|
78
|
-
if (doc && typeof doc.hasFocus === 'function' && !doc.hasFocus()) return 'unfocused';
|
|
79
|
-
return null;
|
|
80
|
-
};
|
|
81
|
-
return {
|
|
82
|
-
now: () => (typeof performance === 'undefined' ? Date.now() : performance.now()),
|
|
83
|
-
hidden: () => doc?.hidden === true,
|
|
84
|
-
suspended: () => reason() !== null,
|
|
85
|
-
suspensionReason: reason,
|
|
86
|
-
subscribe(onChange) {
|
|
87
|
-
if (!doc) return () => {};
|
|
88
|
-
const onPageHide = () => {
|
|
89
|
-
pageSuspended = true;
|
|
90
|
-
onChange();
|
|
91
|
-
};
|
|
92
|
-
const onPageShow = () => {
|
|
93
|
-
pageSuspended = false;
|
|
94
|
-
onChange();
|
|
95
|
-
};
|
|
96
|
-
doc.addEventListener('visibilitychange', onChange);
|
|
97
|
-
win?.addEventListener('blur', onChange);
|
|
98
|
-
win?.addEventListener('focus', onChange);
|
|
99
|
-
win?.addEventListener('pagehide', onPageHide);
|
|
100
|
-
win?.addEventListener('pageshow', onPageShow);
|
|
101
|
-
return () => {
|
|
102
|
-
doc.removeEventListener('visibilitychange', onChange);
|
|
103
|
-
win?.removeEventListener('blur', onChange);
|
|
104
|
-
win?.removeEventListener('focus', onChange);
|
|
105
|
-
win?.removeEventListener('pagehide', onPageHide);
|
|
106
|
-
win?.removeEventListener('pageshow', onPageShow);
|
|
107
|
-
};
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The banked halves of the wait plus the segment currently running. Immutable:
|
|
114
|
-
* every transition returns a new value, so a caller can hold one and compare.
|
|
115
|
-
*/
|
|
116
|
-
export interface VisibleBudgetState {
|
|
117
|
-
/** The window, in VISIBLE milliseconds. */
|
|
118
|
-
readonly budgetMs: number;
|
|
119
|
-
/** Visible time banked from completed segments. */
|
|
120
|
-
readonly visibleMs: number;
|
|
121
|
-
/** Hidden time banked from completed segments (reported, never spent). */
|
|
122
|
-
readonly hiddenMs: number;
|
|
123
|
-
/** When the current segment started. */
|
|
124
|
-
readonly since: number;
|
|
125
|
-
/** Whether the current segment is a hidden one. */
|
|
126
|
-
readonly hidden: boolean;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Open the window at `now`, in whichever visibility the document is in. */
|
|
130
|
-
export function beginVisibleBudget(
|
|
131
|
-
budgetMs: number,
|
|
132
|
-
now: number,
|
|
133
|
-
hidden: boolean,
|
|
134
|
-
): VisibleBudgetState {
|
|
135
|
-
return { budgetMs, visibleMs: 0, hiddenMs: 0, since: now, hidden };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Apply the document's current visibility at `now`: bank the segment that just
|
|
140
|
-
* ended into its own bucket and start the next one. A call that does not change
|
|
141
|
-
* visibility is a no-op *by value* (same accrual, same segment start), so a
|
|
142
|
-
* duplicate `visibilitychange` can never bank a zero-length segment twice or
|
|
143
|
-
* restart the clock.
|
|
144
|
-
*/
|
|
145
|
-
export function applyVisibility(
|
|
146
|
-
state: VisibleBudgetState,
|
|
147
|
-
now: number,
|
|
148
|
-
hidden: boolean,
|
|
149
|
-
): VisibleBudgetState {
|
|
150
|
-
if (hidden === state.hidden) return state;
|
|
151
|
-
const elapsed = Math.max(0, now - state.since);
|
|
152
|
-
return {
|
|
153
|
-
budgetMs: state.budgetMs,
|
|
154
|
-
visibleMs: state.hidden ? state.visibleMs : state.visibleMs + elapsed,
|
|
155
|
-
hiddenMs: state.hidden ? state.hiddenMs + elapsed : state.hiddenMs,
|
|
156
|
-
since: now,
|
|
157
|
-
hidden,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Visible time spent so far, including the segment in flight. */
|
|
162
|
-
export function visibleElapsedMs(state: VisibleBudgetState, now: number): number {
|
|
163
|
-
return state.visibleMs + (state.hidden ? 0 : Math.max(0, now - state.since));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Hidden time so far, including the segment in flight. Never spent — it is
|
|
167
|
-
* reported so a failure message can say what the window did NOT count. */
|
|
168
|
-
export function hiddenElapsedMs(state: VisibleBudgetState, now: number): number {
|
|
169
|
-
return state.hiddenMs + (state.hidden ? Math.max(0, now - state.since) : 0);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Visible time left in the window; `0` once it is spent. */
|
|
173
|
-
export function visibleRemainingMs(state: VisibleBudgetState, now: number): number {
|
|
174
|
-
return Math.max(0, state.budgetMs - visibleElapsedMs(state, now));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** A live view of one running window, for the status wire and the failure message. */
|
|
178
|
-
export interface VisibleCaptureWindow {
|
|
179
|
-
/** The window's size, in visible milliseconds. */
|
|
180
|
-
readonly budgetMs: number;
|
|
181
|
-
/** Visible milliseconds spent so far. */
|
|
182
|
-
elapsedVisibleMs(): number;
|
|
183
|
-
/** Milliseconds this window has spent browser-suspended (not counted). */
|
|
184
|
-
elapsedHiddenMs(): number;
|
|
185
|
-
/**
|
|
186
|
-
* Whether the document itself is hidden. Kept distinct from browser
|
|
187
|
-
* suspension so status never calls an unfocused, still-visible window hidden.
|
|
188
|
-
*/
|
|
189
|
-
isHidden(): boolean;
|
|
190
|
-
/** Whether the expiry budget is parked because the browser is not presenting frames. */
|
|
191
|
-
isSuspended(): boolean;
|
|
192
|
-
/** The browser condition parking the budget, when observable. */
|
|
193
|
-
suspensionReason(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
194
|
-
/** Stop the timer and drop the visibility listener. Idempotent. */
|
|
195
|
-
cancel(): void;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* How a caller configures ONE such wait — the options both `waitForCapture`s
|
|
200
|
-
* take (`adapter/ingest/scene-capture.ts` on the three surface,
|
|
201
|
-
* `pixi/scene-capture.ts` on the canvas surface). Passing a bare number
|
|
202
|
-
* instead is `{ timeoutMs }`, the shape every existing caller uses.
|
|
203
|
-
*
|
|
204
|
-
* It lives HERE, with the window, rather than once per lane, because every
|
|
205
|
-
* field is a parameter of the mechanism above and none of them is a parameter
|
|
206
|
-
* of a surface: `timeoutMs` is {@link startVisibleCaptureWindow}'s `budgetMs`,
|
|
207
|
-
* `visibility` is its {@link VisibilityClock}, and `onWait` hands out the
|
|
208
|
-
* {@link VisibleCaptureWindow} it returns. The two lanes had identical copies
|
|
209
|
-
* of this — which is what a shape with no lane-specific field looks like when
|
|
210
|
-
* it is declared per lane — while both already imported those three names from
|
|
211
|
-
* this module. Genuinely per-surface shapes (`CapturedRuntime` vs
|
|
212
|
-
* `CapturedRuntime2D`: a three scene/renderer against a Pixi stage/app) stay in
|
|
213
|
-
* their own lanes and keep their own names.
|
|
214
|
-
*/
|
|
215
|
-
export interface CaptureWaitOptions {
|
|
216
|
-
/** The capture window, in VISIBLE milliseconds (default 10s). */
|
|
217
|
-
timeoutMs?: number | undefined;
|
|
218
|
-
/** Injected in tests; defaults to the document's own visibility. */
|
|
219
|
-
visibility?: VisibilityClock | undefined;
|
|
220
|
-
/**
|
|
221
|
-
* Called with a LIVE view of the wait when it begins, and with `null` the
|
|
222
|
-
* moment it ends (captured, expired, or the window was cancelled).
|
|
223
|
-
*
|
|
224
|
-
* A wait parked on a hidden tab is otherwise indistinguishable from a hung
|
|
225
|
-
* mount: nothing renders, nothing fails, and every door reports silence.
|
|
226
|
-
* This is the seam the editor publishes to the editor's `status` command so the answer is
|
|
227
|
-
* "waiting for the first visible frame — the tab is hidden", not a countdown
|
|
228
|
-
* that is not running.
|
|
229
|
-
*/
|
|
230
|
-
onWait?: ((wait: VisibleCaptureWindow | null) => void) | undefined;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Start a window that calls `onExpire` after `budgetMs` of VISIBLE time.
|
|
235
|
-
* While browser frame presentation is suspended the timer is disarmed entirely
|
|
236
|
-
* (so a throttled background timer cannot fire it late either) and re-armed
|
|
237
|
-
* with the remaining budget when frame presentation resumes.
|
|
238
|
-
*/
|
|
239
|
-
export function startVisibleCaptureWindow(opts: {
|
|
240
|
-
budgetMs: number;
|
|
241
|
-
onExpire: () => void;
|
|
242
|
-
/** Defaults to the document's own visibility. */
|
|
243
|
-
clock?: VisibilityClock;
|
|
244
|
-
/** Defaults to `setTimeout`/`clearTimeout`. */
|
|
245
|
-
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
246
|
-
clearTimer?: (handle: unknown) => void;
|
|
247
|
-
}): VisibleCaptureWindow {
|
|
248
|
-
const clock = opts.clock ?? documentVisibilityClock();
|
|
249
|
-
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
250
|
-
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
|
|
251
|
-
|
|
252
|
-
const suspended = () => clock.suspended?.() ?? clock.hidden();
|
|
253
|
-
let state = beginVisibleBudget(opts.budgetMs, clock.now(), suspended());
|
|
254
|
-
let timer: unknown = null;
|
|
255
|
-
let done = false;
|
|
256
|
-
|
|
257
|
-
function disarm(): void {
|
|
258
|
-
if (timer !== null) {
|
|
259
|
-
clearTimer(timer);
|
|
260
|
-
timer = null;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function arm(): void {
|
|
265
|
-
disarm();
|
|
266
|
-
if (done || state.hidden) return;
|
|
267
|
-
timer = setTimer(
|
|
268
|
-
() => {
|
|
269
|
-
timer = null;
|
|
270
|
-
if (done) return;
|
|
271
|
-
done = true;
|
|
272
|
-
unsubscribe();
|
|
273
|
-
opts.onExpire();
|
|
274
|
-
},
|
|
275
|
-
visibleRemainingMs(state, clock.now()),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const unsubscribe = clock.subscribe(() => {
|
|
280
|
-
if (done) return;
|
|
281
|
-
state = applyVisibility(state, clock.now(), suspended());
|
|
282
|
-
arm();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
arm();
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
budgetMs: opts.budgetMs,
|
|
289
|
-
elapsedVisibleMs: () => visibleElapsedMs(state, clock.now()),
|
|
290
|
-
elapsedHiddenMs: () => hiddenElapsedMs(state, clock.now()),
|
|
291
|
-
isHidden: () => clock.hidden(),
|
|
292
|
-
isSuspended: () => state.hidden,
|
|
293
|
-
suspensionReason: () =>
|
|
294
|
-
state.hidden ? (clock.suspensionReason?.() ?? (clock.hidden() ? 'hidden' : null)) : null,
|
|
295
|
-
cancel() {
|
|
296
|
-
if (done) return;
|
|
297
|
-
done = true;
|
|
298
|
-
disarm();
|
|
299
|
-
unsubscribe();
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `applyWorldRendererConfig` — the three.js half of the renderer-config seam.
|
|
3
|
-
*
|
|
4
|
-
* The SHAPE is the project contract's (`@volter/project/adapter/renderer-config`,
|
|
5
|
-
* whose header states the rule and the three load-bearing properties); this
|
|
6
|
-
* file is what writes it onto a live `WebGLRenderer` and hands back the
|
|
7
|
-
* restore. A mounted three root declares it (`MountedThreeRoot.rendererConfig`)
|
|
8
|
-
* and the editor's world-root stage applies it.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type {
|
|
12
|
-
WorldOutputColorSpace,
|
|
13
|
-
WorldRendererConfig,
|
|
14
|
-
WorldShadowMapType,
|
|
15
|
-
WorldToneMapping,
|
|
16
|
-
} from '@volter/project/adapter/renderer-config';
|
|
17
|
-
import type * as THREE from 'three';
|
|
18
|
-
|
|
19
|
-
export type {
|
|
20
|
-
WorldOutputColorSpace,
|
|
21
|
-
WorldRendererConfig,
|
|
22
|
-
WorldShadowMapType,
|
|
23
|
-
WorldToneMapping,
|
|
24
|
-
} from '@volter/project/adapter/renderer-config';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Apply `config` to `renderer`, returning the restore function that puts back what was there.
|
|
28
|
-
*
|
|
29
|
-
* `three` is passed in rather than imported for values so the enum constants come from the HOST's
|
|
30
|
-
* three instance — the same identity rule `r3f-root.tsx` follows for the scene and camera.
|
|
31
|
-
*/
|
|
32
|
-
export function applyWorldRendererConfig(
|
|
33
|
-
three: typeof THREE,
|
|
34
|
-
renderer: THREE.WebGLRenderer,
|
|
35
|
-
config: WorldRendererConfig,
|
|
36
|
-
): () => void {
|
|
37
|
-
// A host that mounts a world WITHOUT rasterizing it hands the adapter a duck-typed renderer —
|
|
38
|
-
// the editor's design session (`createDesignTimeRenderer`: four members, deliberately never
|
|
39
|
-
// widened) and jsdom test harnesses both do. Such a surface has no colour pipeline to configure:
|
|
40
|
-
// the frame the user sees is drawn by a DIFFERENT renderer (the editor's own), so applying the
|
|
41
|
-
// world's config there is meaningless — and calling `getClearColor` on it is a TypeError that
|
|
42
|
-
// unmounts the whole world at edit time (measured: every Godot port's edit viewport blanked with
|
|
43
|
-
// '"world" failed to mount — renderer.getClearColor is not a function'). Detect the real
|
|
44
|
-
// `WebGLRenderer` surface by the one method this function must call, and no-op otherwise.
|
|
45
|
-
if (typeof renderer.getClearColor !== 'function') {
|
|
46
|
-
return () => {};
|
|
47
|
-
}
|
|
48
|
-
const toneMappings: Record<WorldToneMapping, THREE.ToneMapping> = {
|
|
49
|
-
none: three.NoToneMapping,
|
|
50
|
-
linear: three.LinearToneMapping,
|
|
51
|
-
reinhard: three.ReinhardToneMapping,
|
|
52
|
-
cineon: three.CineonToneMapping,
|
|
53
|
-
aces: three.ACESFilmicToneMapping,
|
|
54
|
-
agx: three.AgXToneMapping,
|
|
55
|
-
neutral: three.NeutralToneMapping,
|
|
56
|
-
};
|
|
57
|
-
const colorSpaces: Record<WorldOutputColorSpace, THREE.ColorSpace> = {
|
|
58
|
-
srgb: three.SRGBColorSpace,
|
|
59
|
-
'srgb-linear': three.LinearSRGBColorSpace,
|
|
60
|
-
};
|
|
61
|
-
const shadowMapTypes: Record<WorldShadowMapType, THREE.ShadowMapType> = {
|
|
62
|
-
basic: three.BasicShadowMap,
|
|
63
|
-
pcf: three.PCFShadowMap,
|
|
64
|
-
'pcf-soft': three.PCFSoftShadowMap,
|
|
65
|
-
vsm: three.VSMShadowMap,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const restores: (() => void)[] = [];
|
|
69
|
-
|
|
70
|
-
if (config.toneMapping !== undefined) {
|
|
71
|
-
const previous = renderer.toneMapping;
|
|
72
|
-
renderer.toneMapping = toneMappings[config.toneMapping];
|
|
73
|
-
restores.push(() => {
|
|
74
|
-
renderer.toneMapping = previous;
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (config.toneMappingExposure !== undefined) {
|
|
78
|
-
const previous = renderer.toneMappingExposure;
|
|
79
|
-
renderer.toneMappingExposure = config.toneMappingExposure;
|
|
80
|
-
restores.push(() => {
|
|
81
|
-
renderer.toneMappingExposure = previous;
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (config.outputColorSpace !== undefined) {
|
|
85
|
-
const previous = renderer.outputColorSpace;
|
|
86
|
-
renderer.outputColorSpace = colorSpaces[config.outputColorSpace];
|
|
87
|
-
restores.push(() => {
|
|
88
|
-
renderer.outputColorSpace = previous;
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (config.shadowMapType !== undefined && renderer.shadowMap !== undefined) {
|
|
92
|
-
const previous = renderer.shadowMap.type;
|
|
93
|
-
renderer.shadowMap.type = shadowMapTypes[config.shadowMapType];
|
|
94
|
-
renderer.shadowMap.needsUpdate = true;
|
|
95
|
-
restores.push(() => {
|
|
96
|
-
renderer.shadowMap.type = previous;
|
|
97
|
-
renderer.shadowMap.needsUpdate = true;
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (config.clearColor !== undefined) {
|
|
101
|
-
const previousColor = new three.Color();
|
|
102
|
-
renderer.getClearColor(previousColor);
|
|
103
|
-
// Alpha is READ BACK and re-passed, never assumed: see `clearColor`'s doc above.
|
|
104
|
-
const alpha = renderer.getClearAlpha();
|
|
105
|
-
renderer.setClearColor(new three.Color(config.clearColor), alpha);
|
|
106
|
-
restores.push(() => {
|
|
107
|
-
renderer.setClearColor(previousColor, alpha);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
// Reverse order, so a field written twice (it cannot be, today) unwinds correctly.
|
|
113
|
-
for (let i = restores.length - 1; i >= 0; i--) restores[i]?.();
|
|
114
|
-
};
|
|
115
|
-
}
|
package/src/asset-parse-error.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The GENERIC fetched-asset parse error.
|
|
3
|
-
*
|
|
4
|
-
* Every authored asset format the engine validates at fetch time throws this
|
|
5
|
-
* when Zod rejects the payload, naming the offending file (T4.6). It lives
|
|
6
|
-
* here — beside `loader.ts`/`assets.ts` — rather than inside a format module so
|
|
7
|
-
* every caller (currently the input-map loader in `input/input-manager.ts`)
|
|
8
|
-
* does not have to reach into an unrelated subsystem for it.
|
|
9
|
-
*
|
|
10
|
-
* The name says ASSET, not scene: an `.inputmap.json` failure is not a scene
|
|
11
|
-
* failure, and the class covers every structured fetched asset.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ZodIssue } from 'zod';
|
|
15
|
-
|
|
16
|
-
export class AssetParseError extends Error {
|
|
17
|
-
readonly issues: ZodIssue[];
|
|
18
|
-
/** The offending file's path/URL, when the caller knows it (T4.6). */
|
|
19
|
-
readonly filePath?: string | undefined;
|
|
20
|
-
|
|
21
|
-
constructor(issues: ZodIssue[], filePath?: string) {
|
|
22
|
-
const msg = issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
23
|
-
const header = filePath ? `Asset validation failed (${filePath}):` : 'Asset validation failed:';
|
|
24
|
-
super(`${header}\n${msg}`);
|
|
25
|
-
this.name = 'AssetParseError';
|
|
26
|
-
this.issues = issues;
|
|
27
|
-
this.filePath = filePath;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The live objects behind the hierarchy's node ids, as a media
|
|
3
|
-
* integration provides them: the Three integration answers from the session store's Three half,
|
|
4
|
-
* and a lane that reads a scene's objects (the navmesh bake, Play's camera flight) asks here.
|
|
5
|
-
* With none registered there are no objects.
|
|
6
|
-
*/
|
|
7
|
-
import type * as THREE from 'three';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The world node IS the entity, so a contribution inspecting a live behavior resolves the
|
|
11
|
-
* hierarchy's node id to the object itself. The map is the authored viewport's in Edit and the
|
|
12
|
-
* adopted live scene's in Play; a reader must not hold an object across the hierarchy's
|
|
13
|
-
* `subscribe` firings.
|
|
14
|
-
*/
|
|
15
|
-
export interface HostHierarchyObjects {
|
|
16
|
-
object(id: string): THREE.Object3D | null;
|
|
17
|
-
objects(): ReadonlyMap<string, THREE.Object3D>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let registered: HostHierarchyObjects | null = null;
|
|
21
|
-
|
|
22
|
-
export function registerHostHierarchyObjects(provider: HostHierarchyObjects): () => void {
|
|
23
|
-
registered = provider;
|
|
24
|
-
return () => {
|
|
25
|
-
if (registered === provider) registered = null;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function hostHierarchyObjects(): HostHierarchyObjects | null {
|
|
30
|
-
return registered;
|
|
31
|
-
}
|