@volter/editor-threejs 0.5.210 → 0.5.212
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 +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- 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 +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- 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 +1 -1
- 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 +476 -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/kit/viewport-surface-status.tsx +44 -0
- 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,14 +1,14 @@
|
|
|
1
1
|
import type { SparkRenderer } from '@sparkjsdev/spark';
|
|
2
|
-
import { invalidateStages } from '@volter/sdk/
|
|
2
|
+
import { invalidateStages } from '@volter/sdk/stage-invalidation';
|
|
3
3
|
import { StagePresentationRig } from './components/standard-viewport-dressing';
|
|
4
4
|
import {
|
|
5
5
|
bindViewPresentation,
|
|
6
6
|
subscribeViewportPresentation,
|
|
7
7
|
type ViewportOverlays,
|
|
8
8
|
viewPresentation,
|
|
9
|
-
} from '@volter/sdk/
|
|
9
|
+
} from '@volter/sdk/viewport-presentation';
|
|
10
10
|
import { themeVars, zIndex } from '@volter/sdk/widgets';
|
|
11
|
-
import type {
|
|
11
|
+
import type { AuthoringAdapter } from '@volter/project/adapter';
|
|
12
12
|
import {
|
|
13
13
|
EDITOR_CAMERA_FAR,
|
|
14
14
|
EDITOR_CAMERA_NEAR,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import * as THREE from 'three';
|
|
34
34
|
import { axisViewName, isQuarterTurnUp } from './asset-workflow/model-inspection';
|
|
35
35
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
36
|
-
import { TransformControls } from 'three/addons/controls/TransformControls.js';
|
|
36
|
+
import { TransformControls, type TransformControlsGizmo } from 'three/addons/controls/TransformControls.js';
|
|
37
37
|
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
|
38
38
|
import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
|
|
39
39
|
import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
|
|
@@ -41,18 +41,16 @@ import { BatchedRenderer } from 'three.quarks';
|
|
|
41
41
|
import {
|
|
42
42
|
applyAuthoringTransform,
|
|
43
43
|
beginAuthoringTransformEdit,
|
|
44
|
-
dropAuthoringAsset,
|
|
45
44
|
endAuthoringTransformEdit,
|
|
46
45
|
setAuthoringSelection,
|
|
47
46
|
spatialHandlesForAdapter,
|
|
48
|
-
} from '@volter/sdk/
|
|
49
|
-
import { beginLiveGesture, endLiveGesture } from '
|
|
50
|
-
import { drillIntoSelectionScope, pickAcrossScopeExit } from '@volter/sdk/
|
|
47
|
+
} from '@volter/sdk/authoring/consumer-actions';
|
|
48
|
+
import { beginLiveGesture, endLiveGesture } from './live-gesture-lock';
|
|
49
|
+
import { drillIntoSelectionScope, pickAcrossScopeExit } from '@volter/sdk/authoring/selection-scope';
|
|
51
50
|
import { setViewportPickContext } from './authoring/viewport-pick-context';
|
|
52
|
-
import { isRootHidden } from '@volter/sdk/
|
|
53
|
-
import type { CameraViewMode } from './camera-authoring';
|
|
51
|
+
import { isRootHidden } from '@volter/sdk/authoring/world-session-state';
|
|
54
52
|
import { type ConstraintControl, ConstraintHelper } from './constraint-helper';
|
|
55
|
-
import { editorConsole } from '@volter/sdk/
|
|
53
|
+
import { editorConsole } from '@volter/sdk/editor-console';
|
|
56
54
|
import type { EditorShellStore } from './editor-shell-store';
|
|
57
55
|
import { findEntityLod } from './entity-lod';
|
|
58
56
|
import { entityIdOf, entityObject3D } from './entity-object';
|
|
@@ -69,8 +67,7 @@ import {
|
|
|
69
67
|
nativeViewportSelectionBox,
|
|
70
68
|
nativeViewportWire,
|
|
71
69
|
subscribeNativeSelectionTheme,
|
|
72
|
-
} from '@volter/sdk/
|
|
73
|
-
import { presentationRegionBasis } from '@volter/sdk/kit/presentation-surface';
|
|
70
|
+
} from '@volter/sdk/native-selection-style';
|
|
74
71
|
import { ReflectionProbeHelper } from './reflection-probe-helper';
|
|
75
72
|
import { frameableContentBounds, seededViewShowsWorld, viewFromGameCamera } from './scene-framing';
|
|
76
73
|
import {
|
|
@@ -86,22 +83,21 @@ import { perspectiveDistanceToFitBox } from './three-viewport/camera-fit';
|
|
|
86
83
|
import { SelectionBrackets } from './three-viewport/selection-brackets';
|
|
87
84
|
import { collectThreeSelectionOutlineTargets } from './three-viewport/selection-outline';
|
|
88
85
|
import { toneMappedSourceColor } from './three-viewport/source-color';
|
|
89
|
-
import type { ThreeViewportProjection } from '
|
|
90
|
-
import { showTransientHint } from '@volter/sdk/
|
|
91
|
-
import {
|
|
86
|
+
import type { ThreeViewportProjection } from './three-viewport-presentation';
|
|
87
|
+
import { showTransientHint } from '@volter/sdk/transient-hint';
|
|
88
|
+
import { editorHost } from '@volter/sdk/host';
|
|
92
89
|
import { TriggerVolumeHelper } from './trigger-volume-helper';
|
|
93
90
|
import { viewportAuthoringPolicy } from './viewport-authoring-policy';
|
|
94
91
|
import { ensureThreeIntegration } from './three-integration';
|
|
95
92
|
|
|
96
|
-
/** A bounds box or LineSegments wireframe tagged with its source entity object. */
|
|
97
93
|
/** A selected mesh's wireframe (the view's `selection.wire` overlay, Unity's Selection Wire):
|
|
98
94
|
* a stage-owned line set that copies the mesh's world matrix each frame, never a child of it. */
|
|
99
95
|
interface WireHelper extends THREE.LineSegments {
|
|
100
96
|
_wireOf?: THREE.Object3D;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
/** The source tag on a diagnostic wire cube; other helper kinds have no tag. */
|
|
100
|
+
interface TaggedHelper {
|
|
105
101
|
_entityObj?: THREE.Object3D;
|
|
106
102
|
}
|
|
107
103
|
|
|
@@ -132,30 +128,11 @@ export function topmostPublishedObjects(
|
|
|
132
128
|
return roots;
|
|
133
129
|
}
|
|
134
130
|
|
|
135
|
-
/** Topmost live objects among an exact add/remove delta; removed ids contribute nothing. */
|
|
136
|
-
export function topmostChangedObjects(
|
|
137
|
-
objectMap: ReadonlyMap<string, THREE.Object3D>,
|
|
138
|
-
changedIds: ReadonlySet<string>,
|
|
139
|
-
): THREE.Object3D[] {
|
|
140
|
-
const changed = new Set<THREE.Object3D>();
|
|
141
|
-
for (const id of changedIds) {
|
|
142
|
-
const object = objectMap.get(id);
|
|
143
|
-
if (object) changed.add(object);
|
|
144
|
-
}
|
|
145
|
-
return [...changed].filter((object) => {
|
|
146
|
-
for (let parent = object.parent; parent !== null; parent = parent.parent) {
|
|
147
|
-
if (changed.has(parent)) return false;
|
|
148
|
-
}
|
|
149
|
-
return true;
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
131
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* preview, re-add on `setScene`, refresh each frame, dispose on rebuild.
|
|
132
|
+
* The Bounds diagnostic's boxes and degenerate-entity wire cubes, selected
|
|
133
|
+
* mesh wires, and selection brackets. They share visibility, scene transfer
|
|
134
|
+
* and disposal; each frame updates bounds and brackets, copies mesh-wire
|
|
135
|
+
* matrices, or follows a tagged cube's source position.
|
|
159
136
|
*/
|
|
160
137
|
type BoundsHelper = ContentBoundsHelper | THREE.LineSegments | SelectionBrackets;
|
|
161
138
|
|
|
@@ -174,16 +151,10 @@ const DEGENERATE_HELPER_SIZE = 0.5;
|
|
|
174
151
|
* streaming itself in is reached within a second. */
|
|
175
152
|
const CONTENT_BOUNDS_MAX_AGE_MS = 1000;
|
|
176
153
|
|
|
177
|
-
/** The two ground-plane normals a region's declared basis selects between.
|
|
178
|
-
* Shared constants: `Plane.set` copies the normal, so nothing here is handed a
|
|
179
|
-
* reference it could mutate. */
|
|
180
|
-
const UP_Y = new THREE.Vector3(0, 1, 0);
|
|
181
|
-
const UP_Z = new THREE.Vector3(0, 0, 1);
|
|
182
|
-
|
|
183
154
|
/** Scratch for the clip-plane fit — one per module, same reason as
|
|
184
155
|
* `content-bounds.ts`'s `nodeBox`. */
|
|
185
156
|
const _clipSphere = new THREE.Sphere();
|
|
186
|
-
/** Gizmo-anchor scratch — see the two pivot branches in `
|
|
157
|
+
/** Gizmo-anchor scratch — see the two pivot branches in `syncFromStore`. */
|
|
187
158
|
const _medianTmp = new THREE.Vector3();
|
|
188
159
|
const _pivotPos = new THREE.Vector3();
|
|
189
160
|
const _pivotQuat = new THREE.Quaternion();
|
|
@@ -205,8 +176,7 @@ const VC_DIRS: THREE.Vector3[] = [
|
|
|
205
176
|
/**
|
|
206
177
|
* THE EDITOR'S OWN TRANSFORM-GIZMO AXIS COLOURS (X/Y/Z), used when the look names no
|
|
207
178
|
* `color.gizmo` group. Their values are Godot's editor axis colours (`theme_modern.cpp`,
|
|
208
|
-
* `axis_x_color`…).
|
|
209
|
-
* `new THREE.Color(...)`, so nothing here can be mutated in place.
|
|
179
|
+
* `axis_x_color`…). Plain sRGB triples become fresh Color values through `_gizmoAxisColor`.
|
|
210
180
|
*/
|
|
211
181
|
const KIT_GIZMO_AXIS_RGB: readonly [number, number, number][] = [
|
|
212
182
|
[0.96, 0.2, 0.32], // X
|
|
@@ -232,9 +202,8 @@ const KIT_GIZMO_AXIS_RGB: readonly [number, number, number][] = [
|
|
|
232
202
|
* spellings, and the matrix is the authority — see
|
|
233
203
|
* `blender-runtime-view.ts`'s constructor.
|
|
234
204
|
*
|
|
235
|
-
* Chosen by the
|
|
236
|
-
*
|
|
237
|
-
* which program's viewport this is.
|
|
205
|
+
* Chosen by the view presentation's world frame through `setStageFunction`.
|
|
206
|
+
* The look supplies the gizmos' paint and dimensions.
|
|
238
207
|
*/
|
|
239
208
|
type StageAxisFrame = readonly (readonly [axis: number, sign: 1 | -1])[];
|
|
240
209
|
const AXIS_FRAME_Y_UP: StageAxisFrame = [
|
|
@@ -257,46 +226,9 @@ const AXIS_FRAME_Z_UP_LEFT: StageAxisFrame = [
|
|
|
257
226
|
/** The label a source axis index carries on the navigation gizmo. */
|
|
258
227
|
const AXIS_LETTER = ['X', 'Y', 'Z'] as const;
|
|
259
228
|
|
|
260
|
-
/**
|
|
261
|
-
* An ONLINE asset dropped on the viewport — the asset browser's drag payload,
|
|
262
|
-
* verbatim. An `url` means the file to fetch is already known; without one the
|
|
263
|
-
* source has to be asked for its file list first.
|
|
264
|
-
*/
|
|
265
|
-
export interface OnlineAssetDrop {
|
|
266
|
-
readonly source: string;
|
|
267
|
-
readonly id: string;
|
|
268
|
-
readonly name: string;
|
|
269
|
-
readonly url?: string;
|
|
270
|
-
readonly format?: string;
|
|
271
|
-
readonly includes?: { relativePath: string; url: string; size: number }[];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* THE VIEWPORT'S ONE DOOR TO THE ONLINE ASSET LIBRARY — a collaborator the
|
|
276
|
-
* SHELL installs, never a transport the viewport imports (ARCHITECTURE-CORE
|
|
277
|
-
* §Editor chrome, "the viewport stack is separable").
|
|
278
|
-
*
|
|
279
|
-
* The viewport contributes what only it knows: the world point under the
|
|
280
|
-
* cursor, and the adapter that owns what a dropped path MEANS. Everything the
|
|
281
|
-
* download needs — the asset cache, the source's file list, the project's
|
|
282
|
-
* download history, and the console report when it fails — is the shell's
|
|
283
|
-
* (`world-root-stage.ts` installs it over `editor-api.ts` +
|
|
284
|
-
* `asset-editor-persistence.ts`, which together weigh 64 files the gizmo
|
|
285
|
-
* viewport otherwise carries to place a chair).
|
|
286
|
-
*
|
|
287
|
-
* Returns the PROJECT-LOCAL path to hand the adapter's `assetDrop`, or `null`
|
|
288
|
-
* when the asset could not be resolved — in which case the resolver has
|
|
289
|
-
* already SAID SO on the console, so the viewport stays quiet. A rejection is
|
|
290
|
-
* the resolver failing to report at all, and the viewport reports that itself.
|
|
291
|
-
*/
|
|
292
|
-
export type OnlineAssetResolver = (asset: OnlineAssetDrop) => Promise<string | null>;
|
|
293
|
-
|
|
294
229
|
export interface EditorViewportOptions {
|
|
295
230
|
/** Native renderer for renderer-coupled Three extensions such as Spark. */
|
|
296
231
|
readonly renderer?: THREE.WebGLRenderer;
|
|
297
|
-
/** How a dropped ONLINE asset becomes a project path. Omit and an online
|
|
298
|
-
* drop is REFUSED BY NAME on the console — see {@link OnlineAssetResolver}. */
|
|
299
|
-
readonly onlineAssetResolver?: OnlineAssetResolver;
|
|
300
232
|
/** Document-local adapter. Omit for the editor's active project adapter. */
|
|
301
233
|
readonly authoring?: () => AuthoringAdapter;
|
|
302
234
|
/** Document-local picking when this viewport is not the project viewport. */
|
|
@@ -304,48 +236,15 @@ export interface EditorViewportOptions {
|
|
|
304
236
|
/** The camera the stage draws with, when that is not the viewport's own (a document session's
|
|
305
237
|
* orthographic or camera view): screen tests such as the box select project through it. */
|
|
306
238
|
readonly drawCamera?: () => THREE.Camera;
|
|
307
|
-
/** Only the one project viewport publishes the
|
|
239
|
+
/** Only the one project viewport publishes the global pick context. */
|
|
308
240
|
readonly publishPickContext?: boolean;
|
|
309
241
|
readonly onProjectionChange?: (projection: ThreeViewportProjection) => void;
|
|
310
|
-
/**
|
|
311
|
-
* How far the canvas BLEEDS past the box a person can see, in CSS px.
|
|
312
|
-
*
|
|
313
|
-
* A document stage inflates its canvas (`StageHost.tsx`, `inset: -12`) so
|
|
314
|
-
* the image fills the dock panel edge to edge, and gives the DOM furniture
|
|
315
|
-
* that bleed back (`inset: 12`) so the chrome stays inside the panel. The
|
|
316
|
-
* ORIENTATION GIZMO is drawn on the canvas, not in the DOM, so it never got
|
|
317
|
-
* that correction: measured on the Model stage, the canvas runs to page x
|
|
318
|
-
* 1437.5 where the panel ends at 1419, and the gizmo — placed 5 px from the
|
|
319
|
-
* CANVAS edge — had its −Z ball cut off by the Inspector. That is the same
|
|
320
|
-
* defect the furniture's own docblock records fixing for the navigation
|
|
321
|
-
* cluster. One value, set from the one expression that positions the DOM
|
|
322
|
-
* furniture; every other host leaves it 0.
|
|
323
|
-
*/
|
|
242
|
+
/** Canvas bleed in CSS pixels. Inset the canvas-rendered compass by the same amount that positions the DOM furniture inside the visible panel. */
|
|
324
243
|
readonly chromeInsetPx?: number;
|
|
325
|
-
/**
|
|
326
|
-
* The editor's own DESIGN-TIME LIGHT RIG (the ambient and the directional
|
|
327
|
-
* this class adds to every scene it is given). Default on. `false` is a
|
|
328
|
-
* host saying the content lights itself — which is the same thing a
|
|
329
|
-
* document says by turning the standard dressing's key light off, and the
|
|
330
|
-
* rig has to hear it. MEASURED: the rig's lights sit on `EDITOR_LAYER`,
|
|
331
|
-
* but three filters lights by the CAMERA's layers
|
|
332
|
-
* (`WebGLRenderer.projectObject`), and the camera that draws a document
|
|
333
|
-
* stage enables that layer so it can see the grid and the gizmo — so the
|
|
334
|
-
* layer scopes the rig away from nobody, and a document lit by its own
|
|
335
|
-
* view-locked studio was being lit by this as well.
|
|
336
|
-
*/
|
|
244
|
+
/** Enable the editor light rig by default. Hosts whose content supplies its own lighting disable it explicitly. */
|
|
337
245
|
readonly lightRig?: boolean;
|
|
338
246
|
}
|
|
339
247
|
|
|
340
|
-
/**
|
|
341
|
-
* The floor grid as a shader over one plane (`extent` metres square, y = 0).
|
|
342
|
-
* Lines are `fwidth`-antialiased so they keep one on-screen width at every
|
|
343
|
-
* distance; the 1 m level fades out where its cells fall under a few pixels
|
|
344
|
-
* (Blender's adaptive grid never lets lines merge), the 10 m level stays; the
|
|
345
|
-
* whole floor fades to its edge. `uColor` is the palette's grid colour
|
|
346
|
-
* (`_applyViewportLook`). Depth-tested, never written: content standing on
|
|
347
|
-
* the floor occludes it, and it occludes nothing.
|
|
348
|
-
*/
|
|
349
248
|
/**
|
|
350
249
|
* THE STAGE'S FIELD OF VIEW, as the view states it (`ViewportCamera.fov`): an angle held on one
|
|
351
250
|
* side of the region — vertical, horizontal, the larger or the smaller — so a panel that widens
|
|
@@ -404,23 +303,7 @@ function hsvToColor(h: number, s: number, v: number, out: THREE.Color): THREE.Co
|
|
|
404
303
|
}
|
|
405
304
|
|
|
406
305
|
|
|
407
|
-
/**
|
|
408
|
-
* One ball of the navigation gizmo: filled with its letter (a positive axis),
|
|
409
|
-
* or the ring Blender draws for a negative one.
|
|
410
|
-
*
|
|
411
|
-
* BOTH FORMS MUST REACH THE SAME EDGE, and they did not. The disc was filled
|
|
412
|
-
* to `size/2 − 5` while the ring's 8 px stroke straddled that radius and
|
|
413
|
-
* reached `size/2 − 1`, so a POSITIVE ball drew 0.844 of its sprite and a
|
|
414
|
-
* negative one 0.969 — measured live on the Model stage, +X came out 11.8 CSS
|
|
415
|
-
* beside −X's 15.5, the filled ball SMALLER than the hollow one beside it.
|
|
416
|
-
* Blender's are one size (its variation is depth, which the caller applies).
|
|
417
|
-
* One outer radius here, the ring's stroke laid inside it.
|
|
418
|
-
*
|
|
419
|
-
* THE LETTER is not black. Measured on Blender's +X ball: glyph ink
|
|
420
|
-
* (88,19,29) over a (245,54,81) fill — 0.359 / 0.352 / 0.358 of the ball's
|
|
421
|
-
* own colour, one ratio in all three channels, so it is the axis colour
|
|
422
|
-
* darkened rather than a neutral. Cap height 7.5 CSS on a 17.5 CSS ball.
|
|
423
|
-
*/
|
|
306
|
+
/** Draw positive-axis balls filled with dark letters and negative-axis balls as rings, with equal outer extents. */
|
|
424
307
|
const COMPASS_LETTER_INK = 0.356;
|
|
425
308
|
const COMPASS_LETTER_CAP_FRACTION = 7.5 / 17.5;
|
|
426
309
|
|
|
@@ -502,44 +385,7 @@ function compassGlyphTexture(text: string): THREE.CanvasTexture {
|
|
|
502
385
|
* three-quarter view (`ViewportCamera.opening`). */
|
|
503
386
|
const STAGE_OPENING_DIRECTION = new THREE.Vector3(1, 0.72, 1).normalize();
|
|
504
387
|
|
|
505
|
-
/**
|
|
506
|
-
* THE COMPASS'S BOX, AND THE GIZMO'S OWN SIZES — re-measured 2026-09-19 from
|
|
507
|
-
* `modeling-edit-none.png` and `modeling-object-none.png` at their native 2x,
|
|
508
|
-
* and the numbers they replace were not small errors.
|
|
509
|
-
*
|
|
510
|
-
* THE METHOD, because a ball-centre distance read straight off the frame is
|
|
511
|
-
* the wrong number: every axis is FORESHORTENED by the view, so the one thing
|
|
512
|
-
* that can be measured is the projection. The main camera's own basis comes
|
|
513
|
-
* out of the floor calibration (see {@link GRAZING_FADE_GLSL}); an axis `a`
|
|
514
|
-
* lands at `R · (a·right, −a·up)` from the gizmo's centre, and the three
|
|
515
|
-
* NEGATIVE balls (no stalk to contaminate a centroid) then give three
|
|
516
|
-
* equations for `R` and the centre. Solved: R = 63.9 device px = **31.96 CSS**
|
|
517
|
-
* with the centre at (2734.2, 257.3) device, cross-checked by the third ball
|
|
518
|
-
* to within one device pixel and by the ink bbox (2·R·0.9155 + ball = 150
|
|
519
|
-
* device against 149 measured). The previous `28` came from reading a
|
|
520
|
-
* foreshortened distance as if it were the radius.
|
|
521
|
-
*
|
|
522
|
-
* BALL DIAMETER is not one number either: Blender scales it with DEPTH, which
|
|
523
|
-
* an orthographic gizmo camera does not do for free. Six balls, against
|
|
524
|
-
* `facing` = (axis·view + 1)/2 — the same quantity
|
|
525
|
-
* `_syncOrientationGizmoDepth` already fades with:
|
|
526
|
-
*
|
|
527
|
-
* facing 0.09 0.28 0.32 0.68 0.72 0.91
|
|
528
|
-
* d (CSS) 15.25 15.75 16.12 17.00 16.75 17.50
|
|
529
|
-
*
|
|
530
|
-
* — a straight line, `d = 15.0 + 2.75·facing`, residual ≤ 0.25 CSS on all six.
|
|
531
|
-
*
|
|
532
|
-
* PLACEMENT, measured identically in both frames: the gizmo's coloured ink
|
|
533
|
-
* spans 74.5 × 73.5 CSS centred on that solved centre, its right edge 8.5 CSS
|
|
534
|
-
* inside the region's right edge and its top 38 CSS below the region's top.
|
|
535
|
-
* (The old `14` and `39.5` were the same reading taken against a smaller
|
|
536
|
-
* drawn circle.) The navigation cluster's first glyph sits 16 CSS under the
|
|
537
|
-
* ink, its glyphs 16 CSS on a 30 CSS pitch — which ours already draws
|
|
538
|
-
* exactly, so only where the cluster STARTS moves.
|
|
539
|
-
*
|
|
540
|
-
* The box is the scissored square the gizmo draws into; the geometry reaches
|
|
541
|
-
* `stalk + ball/2` = 40.2 px from the centre, so 90 still contains it.
|
|
542
|
-
*/
|
|
388
|
+
/** Navigation-gizmo dimensions in CSS pixels, shared by sprite geometry and its canvas box. */
|
|
543
389
|
export const COMPASS_BOX_PX = 90;
|
|
544
390
|
/** Blender's gizmo sphere: the distance from the centre to a ball's centre. */
|
|
545
391
|
const COMPASS_STALK_PX = 32;
|
|
@@ -558,59 +404,11 @@ const COMPASS_MARGIN_TOP_PX = 38 + COMPASS_INK_HEIGHT_PX / 2 - COMPASS_BOX_PX /
|
|
|
558
404
|
* `stage.chrome.viewName` `gizmo`). */
|
|
559
405
|
export const COMPASS_CENTER_RIGHT_PX = COMPASS_MARGIN_RIGHT_PX + COMPASS_BOX_PX / 2;
|
|
560
406
|
export const COMPASS_INK_BOTTOM_PX = COMPASS_MARGIN_TOP_PX + COMPASS_BOX_PX / 2 + COMPASS_INK_HEIGHT_PX / 2;
|
|
561
|
-
/**
|
|
562
|
-
* Where the navigation cluster starts. Blender's first GLYPH sits 16 CSS
|
|
563
|
-
* under the gizmo's ink; the cluster component pads 14.5 CSS above its own
|
|
564
|
-
* first glyph (measured on our frame), so the constant is that gap less the
|
|
565
|
-
* padding.
|
|
566
|
-
*/
|
|
407
|
+
/** Navigation-cluster top offset after subtracting the component’s own padding from the desired gap. */
|
|
567
408
|
export const COMPASS_CLUSTER_TOP_PX =
|
|
568
409
|
COMPASS_MARGIN_TOP_PX + COMPASS_BOX_PX / 2 + COMPASS_INK_HEIGHT_PX / 2 + 16 - 14.5;
|
|
569
410
|
|
|
570
|
-
/**
|
|
571
|
-
* THE GRAZING FADE — the floor's depth, transcribed from Blender's own frames.
|
|
572
|
-
*
|
|
573
|
-
* The radial band below keeps a finite grid off a hard rim; it says nothing
|
|
574
|
-
* about DEPTH, and at a document camera standing 11 m from a 1 m subject the
|
|
575
|
-
* band (110–200 m) never engages inside the visible floor. Measured on our
|
|
576
|
-
* Model stage: the 1 m line reads 84 and the X axis `203,41,63` at EVERY
|
|
577
|
-
* depth, from the front of the frame to the horizon — a flat lattice, which
|
|
578
|
-
* is what a fresh viewer names first.
|
|
579
|
-
*
|
|
580
|
-
* WHAT BLENDER DOES, measured (not read out of its source): calibrate
|
|
581
|
-
* `modeling-edit-none.png` from its own floor — fit the two axis lines, index
|
|
582
|
-
* the 1 m grid crossings along each (a 1D projective map, residual <0.05 of a
|
|
583
|
-
* cell), and solve the camera from the two vanishing points with the
|
|
584
|
-
* principal point at the region centre. That gives f = 1976 device px
|
|
585
|
-
* (Blender's viewport lens: 50 mm on its DEFAULT_SENSOR_WIDTH of 72 mm),
|
|
586
|
-
* camera 17.96 m out at elevation 26.46°, azimuth −23.82° — which reproduces
|
|
587
|
-
* the elevation and azimuth Blender's opening direction was independently
|
|
588
|
-
* solved from (`@volter/editor-blender`'s presentation), so the calibration is
|
|
589
|
-
* checked against something already in this repo. Sampling the X axis at thirteen known floor points, its ink over
|
|
590
|
-
* the `#3f3f3f` floor, IN LINEAR LIGHT (the blend happens before the sRGB
|
|
591
|
-
* encode — read as sRGB levels the profile fits nothing):
|
|
592
|
-
*
|
|
593
|
-
* camera dist 12.3 15.6 22.2 28.6 45.9 70.5 115.2 (metres)
|
|
594
|
-
* alpha 0.943 0.963 0.872 0.766 0.572 0.426 0.258
|
|
595
|
-
* 1-(1-h/r)^4 0.985 0.944 0.832 0.731 0.535 0.383 0.250
|
|
596
|
-
* ratio 0.957 1.020 1.048 1.048 1.068 1.113 1.033
|
|
597
|
-
*
|
|
598
|
-
* So the fade is `1 - (1 - |V·n|)^4` over the unit fragment→camera vector and
|
|
599
|
-
* the floor normal — the GRAZING ANGLE, flat to ±6% across a 9x range of
|
|
600
|
-
* distance. Two frames of the same scene at different view distances
|
|
601
|
-
* (`modeling-near.png`, `modeling-far.png`) put the axis at the same PIXELS
|
|
602
|
-
* with ink within 3–5%, which is the same statement: the profile follows the
|
|
603
|
-
* angle, not the metres.
|
|
604
|
-
*
|
|
605
|
-
* WHERE THE EVIDENCE STOPS: Blender also carries a far-distance term, and
|
|
606
|
-
* these frames cannot size it — at 115 m from an 18 m camera the residual is
|
|
607
|
-
* still flat, so whatever its onset is, it is past everything photographed.
|
|
608
|
-
* Not transcribed; the band below is what ends our finite floor.
|
|
609
|
-
*
|
|
610
|
-
* Scale-free by construction: the fade is a ratio of camera height to
|
|
611
|
-
* distance, so a 1 m subject framed like Blender's 2 m one fades identically
|
|
612
|
-
* at the same pixels.
|
|
613
|
-
*/
|
|
411
|
+
/** Fade the floor by the ratio of camera height to distance, independent of subject scale. */
|
|
614
412
|
export const GRAZING_FADE_GLSL = /* glsl */ `
|
|
615
413
|
float volterGrazingFade(vec3 worldPosition, vec3 eye, vec3 planeNormal, float amount) {
|
|
616
414
|
vec3 toEye = eye - worldPosition;
|
|
@@ -628,26 +426,7 @@ const AXIS_SEGMENT_METRES = 2;
|
|
|
628
426
|
/** Segments per axis (both directions from the origin). */
|
|
629
427
|
const AXIS_SEGMENTS_PER_AXIS = (AXIS_HALF_LENGTH * 2) / AXIS_SEGMENT_METRES;
|
|
630
428
|
|
|
631
|
-
/**
|
|
632
|
-
* The X and Z axis lines as a CHAIN of short segments rather than one segment
|
|
633
|
-
* each, so the grazing fade below can read a per-fragment world position.
|
|
634
|
-
*
|
|
635
|
-
* WHY, measured: a line through the origin always runs BEHIND the camera in
|
|
636
|
-
* one direction, and `LineMaterial` trims such a segment against the camera
|
|
637
|
-
* plane while building its screen-space quad (`trimSegment`). A varying
|
|
638
|
-
* carried from the UNTRIMMED endpoints then interpolates across a quad whose
|
|
639
|
-
* clip `w` went through zero, and the reconstruction is meaningless: probed
|
|
640
|
-
* on the live Model stage with the fragment's own `|Δy|/|Δ|` written out as
|
|
641
|
-
* grey, a 400 m segment read 0.027–0.045 down the whole visible line where
|
|
642
|
-
* the true grazing sine runs 0.20–0.70 — i.e. pinned near the far endpoint's
|
|
643
|
-
* value, and the axis faded flat to a tenth of its colour everywhere. Over a
|
|
644
|
-
* 2 m segment the interpolation is exact wherever the segment is in front of
|
|
645
|
-
* the camera, and the one segment that straddles the camera plane is 2 m of
|
|
646
|
-
* line at the edge of the frame. (three's own answer to this is the
|
|
647
|
-
* `worldUnits` path, which carries BOTH endpoints as constant varyings and
|
|
648
|
-
* solves for the fragment — but that path also makes `linewidth` a world
|
|
649
|
-
* measure, and this line's 2 device px is measured.)
|
|
650
|
-
*/
|
|
429
|
+
/** Build axis lines from short segments so the grazing fade interpolates positions across clipped screen-space quads. */
|
|
651
430
|
function axisSegmentPositions(threeAxis: number): number[] {
|
|
652
431
|
const positions: number[] = [];
|
|
653
432
|
for (let i = 0; i < AXIS_SEGMENTS_PER_AXIS; i++) {
|
|
@@ -663,7 +442,7 @@ function axisSegmentPositions(threeAxis: number): number[] {
|
|
|
663
442
|
|
|
664
443
|
/**
|
|
665
444
|
* Give the floor AXES the same grazing fade the floor grid takes, through the
|
|
666
|
-
* same 0..1 amount object. It starts at 0
|
|
445
|
+
* same 0..1 amount object. It starts at 0 (no fade), and
|
|
667
446
|
* `_applyViewportLook` raises it for a look that paints the viewport.
|
|
668
447
|
*/
|
|
669
448
|
function applyAxisGrazingFade(
|
|
@@ -698,6 +477,15 @@ const _gridQuat = new THREE.Quaternion();
|
|
|
698
477
|
const _gridEye = new THREE.Vector3();
|
|
699
478
|
const _gridUp = new THREE.Vector3(0, 1, 0);
|
|
700
479
|
|
|
480
|
+
/**
|
|
481
|
+
* The floor grid as a shader over one plane (`extent` metres square, y = 0).
|
|
482
|
+
* Lines are `fwidth`-antialiased so they keep one on-screen width at every
|
|
483
|
+
* distance; the 1 m level fades out where its cells fall under a few pixels
|
|
484
|
+
* (Blender's adaptive grid never lets lines merge), the 10 m level stays; the
|
|
485
|
+
* whole floor fades to its edge. `uColor` is the palette's grid colour
|
|
486
|
+
* (`_applyViewportLook`). Depth-tested, never written: content standing on
|
|
487
|
+
* the floor occludes it, and it occludes nothing.
|
|
488
|
+
*/
|
|
701
489
|
function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial> {
|
|
702
490
|
const geometry = new THREE.PlaneGeometry(extent, extent);
|
|
703
491
|
geometry.rotateX(-Math.PI / 2);
|
|
@@ -715,7 +503,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
715
503
|
uFadeEnd: { value: extent / 2 },
|
|
716
504
|
// The grazing fade's amount, 0..1 — set by `_applyViewportLook` from
|
|
717
505
|
// the one predicate that already decides whether a look paints the
|
|
718
|
-
// viewport at all.
|
|
506
|
+
// viewport at all. {@link GRAZING_FADE_GLSL} computes its angle profile.
|
|
719
507
|
uGrazingFade: { value: 0 },
|
|
720
508
|
// The plane the grid lies in: the floor, or an axis-aligned orthographic view's own plane
|
|
721
509
|
// (`EditorViewport.alignGridToView`). A unit axis.
|
|
@@ -756,12 +544,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
756
544
|
uniform float uReach;
|
|
757
545
|
varying vec3 vWorld;
|
|
758
546
|
${GRAZING_FADE_GLSL}
|
|
759
|
-
//
|
|
760
|
-
// anti-aliased edge — so its core is FULL, the way Blender's is. The
|
|
761
|
-
// earlier form (1 - distance/width) was a triangle with no plateau:
|
|
762
|
-
// whether a line reached its own colour depended on where the nearest
|
|
763
|
-
// fragment centre happened to fall, and the measured peak wandered
|
|
764
|
-
// between 0.67 and 1 of it.
|
|
547
|
+
// Device-pixel line coverage with a full-strength core and a one-pixel antialiased edge.
|
|
765
548
|
float gridLine(vec2 coord, float width) {
|
|
766
549
|
vec2 d = max(fwidth(coord), vec2(1e-6));
|
|
767
550
|
vec2 dist = abs(fract(coord - 0.5) - 0.5) / d;
|
|
@@ -791,12 +574,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
791
574
|
float pixel = max(fwidth(world).x, fwidth(world).y);
|
|
792
575
|
float bottomAlpha = uMinorFade * (1.0 - smoothstep(uUnit * 0.015625, uUnit * 0.25, pixel));
|
|
793
576
|
float emphasis = top > 0.5 ? 1.0 : middle > 0.5 ? uMinorFade : 0.0;
|
|
794
|
-
//
|
|
795
|
-
// two colours here were fitted to Blender's PERSPECTIVE frames, where four additive
|
|
796
|
-
// passes (1 + 1/2 + 1/4 + 1/8) take that alpha to about 0.94. Drawn once, as an
|
|
797
|
-
// aligned view draws it, the grid colour keeps 0.502 / 0.941 of that. (Measured: the
|
|
798
|
-
// 10 cm lines +5 over the ground at 198 px per metre, predicted +5.4; the 1 m lines
|
|
799
|
-
// 89 against 88. The 0.94 is inferred from the passes, not read off a frame.)
|
|
577
|
+
// Normalize the theme grid alpha for a single aligned pass. Emphasis uses full opacity.
|
|
800
578
|
float levelAlpha = (top > 0.5 || middle > 0.5 ? 1.0 : bottom * bottomAlpha) * mix(0.533, 1.0, emphasis);
|
|
801
579
|
float edge = 1.0 - smoothstep(uFadeStart * uReach, uFadeEnd * uReach, length(world - uCenter));
|
|
802
580
|
float alignedAlpha = levelAlpha * edge * uOpacity;
|
|
@@ -807,37 +585,17 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
807
585
|
}
|
|
808
586
|
vec2 pixelsPerMetre = 1.0 / max(fwidth(p), vec2(1e-6));
|
|
809
587
|
float minorVisible = smoothstep(4.0, 14.0, min(pixelsPerMetre.x, pixelsPerMetre.y)) * uMinorFade;
|
|
810
|
-
//
|
|
811
|
-
// do: the 1 m line is measured at exactly the palette's grid colour
|
|
812
|
-
// and the 10 m line brighter than it (see uMajorColor). The minor
|
|
813
|
-
// level's only attenuation is its own drop-out as cells shrink.
|
|
588
|
+
// Fade minor lines as their cells approach pixel size.
|
|
814
589
|
float minor = gridLine(p, uLineWidth) * minorVisible;
|
|
815
|
-
//
|
|
816
|
-
// reach full coverage — and so its full colour. Measured across one
|
|
817
|
-
// scanline of modeling-object-none.png at device resolution: the 1 m
|
|
818
|
-
// line is 4 px at half rise and plateaus at 83, the 10 m line is 6 px
|
|
819
|
-
// and plateaus at 101. At one shared width ours drew a 10 m line that
|
|
820
|
-
// never exceeded 0.75 coverage and so measured 91, not 102.
|
|
590
|
+
// Major lines use their own width and color.
|
|
821
591
|
float major = gridLine(p / uMajorEvery, uMajorWidth);
|
|
822
592
|
float line = max(minor, major);
|
|
823
593
|
float fade = 1.0 - smoothstep(uFadeStart * uReach, uFadeEnd * uReach, length(world - uCenter));
|
|
824
|
-
//
|
|
825
|
-
// Blender's own grazing-angle profile, transcribed from its frames
|
|
826
|
-
// (see GRAZING_FADE_GLSL above). Without it this lattice reads the
|
|
827
|
-
// same ink at the front of the frame and at the horizon.
|
|
594
|
+
// Apply the floor’s grazing-angle attenuation independently of its distance fade.
|
|
828
595
|
fade *= volterGrazingFade(vWorld, cameraPosition, uPlaneNormal, uGrazingFade);
|
|
829
596
|
float alpha = line * fade * uOpacity;
|
|
830
597
|
if (alpha <= 0.002) discard;
|
|
831
|
-
//
|
|
832
|
-
// coverage being drawn, never a comparison of the two and never the
|
|
833
|
-
// major's raw coverage. Every 10 m line is also a 1 m line, so there
|
|
834
|
-
// the two are equal: a step() between them compares values equal to
|
|
835
|
-
// within rounding and flips per fragment (photographed with the major
|
|
836
|
-
// forced white: the 10 m lines came out DOTTED), and mixing by the raw
|
|
837
|
-
// coverage dilutes the colour by the line's own anti-aliasing (the
|
|
838
|
-
// same lines then measured 91 where Blender's is 102). As a share it
|
|
839
|
-
// is exactly 1 wherever the major draws — core and skirt alike, and in
|
|
840
|
-
// the far field where the minor has dropped out — and 0 on a 1 m line.
|
|
598
|
+
// Mix colors by the major line’s share of coverage, including antialiased skirts.
|
|
841
599
|
float majorShare = clamp(major / max(line, 1e-6), 0.0, 1.0);
|
|
842
600
|
gl_FragColor = vec4(mix(uColor, uMajorColor, majorShare), alpha);
|
|
843
601
|
#include <colorspace_fragment>
|
|
@@ -890,6 +648,8 @@ export class EditorViewport {
|
|
|
890
648
|
readonly camera: THREE.PerspectiveCamera;
|
|
891
649
|
readonly orthographicCamera: THREE.OrthographicCamera;
|
|
892
650
|
readonly orbitControls: OrbitControls;
|
|
651
|
+
/** Fit clip planes only for the editor-owned camera; a live project camera retains its own projection. */
|
|
652
|
+
readonly projectionOwner = { owned: false };
|
|
893
653
|
readonly transformControls: TransformControls;
|
|
894
654
|
/** The rotate/scale halves of the 'combined' gizmo (Unity's Transform
|
|
895
655
|
* tool). Constructed always, shown only in combined mode — three's
|
|
@@ -906,16 +666,14 @@ export class EditorViewport {
|
|
|
906
666
|
})();
|
|
907
667
|
/** The floor: a shader grid (`createFloorGrid`), not line geometry. */
|
|
908
668
|
readonly grid: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>;
|
|
909
|
-
/** Keyed `bounds:<id
|
|
910
|
-
*
|
|
911
|
-
* Nothing reads a key back; the map exists so a rebuild can dispose exactly
|
|
912
|
-
* what it made. */
|
|
669
|
+
/** Keyed `bounds:<id>`, `selection:<id>` or `wire:<id>:<index>` for diagnostic boxes,
|
|
670
|
+
* selection brackets and mesh wires. A rebuild disposes every stored helper. */
|
|
913
671
|
private _boxHelpers = new Map<string, BoundsHelper>();
|
|
914
|
-
/** The view's selection marks (
|
|
672
|
+
/** The view's selection marks (`@volter/sdk/viewport-presentation` overlays.selection): the native
|
|
915
673
|
* outline is the stage's own switch; `box` brackets every selected object, not only those
|
|
916
674
|
* without geometry (Godot); `wire` draws the selected meshes' wireframes (Unity). */
|
|
917
|
-
private _selectionMarks = {
|
|
918
|
-
private _unsubscribeSelectionTheme: () => void
|
|
675
|
+
private _selectionMarks = { wire: false, box: false };
|
|
676
|
+
private _unsubscribeSelectionTheme: () => void;
|
|
919
677
|
private _constraintHelpers = new Map<string, ConstraintHelper>();
|
|
920
678
|
private _constraintControlRaycaster = (() => {
|
|
921
679
|
const raycaster = new THREE.Raycaster();
|
|
@@ -929,7 +687,7 @@ export class EditorViewport {
|
|
|
929
687
|
private _cameraHelpers = new Map<string, { source: THREE.Camera; helper: THREE.CameraHelper }>();
|
|
930
688
|
private _lightHelpers = new Map<
|
|
931
689
|
string,
|
|
932
|
-
{ source: THREE.Light; helper: THREE.Object3D & { update
|
|
690
|
+
{ source: THREE.Light; helper: THREE.Object3D & { update(): void; dispose(): void } }
|
|
933
691
|
>();
|
|
934
692
|
private _audioHelpers = new Map<string, { source: THREE.Object3D; helper: THREE.Group }>();
|
|
935
693
|
private _spatialHandleRoots: THREE.Object3D[] = [];
|
|
@@ -944,8 +702,8 @@ export class EditorViewport {
|
|
|
944
702
|
private _spatialHandleDragPlane = new THREE.Plane();
|
|
945
703
|
private _spatialHandleDragOffset = new THREE.Vector3();
|
|
946
704
|
private _spatialHandleDragPoint = new THREE.Vector3();
|
|
947
|
-
private _orbitEnabledBeforeHandleDrag
|
|
948
|
-
private _transformEnabledBeforeHandleDrag
|
|
705
|
+
private _orbitEnabledBeforeHandleDrag!: boolean;
|
|
706
|
+
private _transformEnabledBeforeHandleDrag!: boolean;
|
|
949
707
|
private _gizmoHelper: THREE.Object3D;
|
|
950
708
|
private _editorAmbient: THREE.AmbientLight;
|
|
951
709
|
private _editorDirLight: THREE.DirectionalLight;
|
|
@@ -953,53 +711,50 @@ export class EditorViewport {
|
|
|
953
711
|
private readonly _lightRig: boolean;
|
|
954
712
|
|
|
955
713
|
private _scene: THREE.Scene;
|
|
956
|
-
/**
|
|
957
|
-
* measuring it walks the whole graph (2k+ nodes for an ingested game).
|
|
958
|
-
* Empty until the first refresh; see {@link _contentBounds}. */
|
|
714
|
+
/** Cached world AABB of non-editor content; empty until the first _contentBounds refresh. */
|
|
959
715
|
private readonly _contentBoundsBox = new THREE.Box3();
|
|
960
716
|
/** `performance.now()` of the last content-bounds measurement; 0 invalidates
|
|
961
717
|
* it (a scene swap), so the next reader re-measures immediately. */
|
|
962
718
|
private _contentBoundsAt = 0;
|
|
963
|
-
private _renderer: THREE.WebGLRenderer | undefined;
|
|
719
|
+
private readonly _renderer: THREE.WebGLRenderer | undefined;
|
|
964
720
|
private _sparkRenderer: SparkRenderer | null = null;
|
|
965
721
|
private _sparkRendererLoading = false;
|
|
966
722
|
private _sparkRendererFailed = false;
|
|
967
723
|
private _lastSparkDiscoveryAt = Number.NEGATIVE_INFINITY;
|
|
968
724
|
private _disposed = false;
|
|
969
725
|
/** Editor infrastructure objects that must move with the active scene. */
|
|
970
|
-
private _editorObjects: THREE.Object3D[]
|
|
726
|
+
private _editorObjects: THREE.Object3D[];
|
|
971
727
|
/** The view presentation this stage is lit by, when bound (`bindPresentation`). */
|
|
972
728
|
private _presentation: { readonly rig: StagePresentationRig; readonly stop: () => void } | null = null;
|
|
973
729
|
/** The world's axis lines: WHICH show is the view's (`overlays.axes`, {@link setAxisLines}),
|
|
974
730
|
* their colours and width the look's (`color.viewport.axisX/Y/Z`, `axisLineWidth`). */
|
|
975
|
-
private _axisLines: LineSegments2 | null
|
|
731
|
+
private _axisLines: LineSegments2 | null;
|
|
976
732
|
/** The world's vertical axis line, a child of {@link _axisLines} (see `_rebuildAxisLines`). */
|
|
977
733
|
private _verticalAxisLine: LineSegments2 | null = null;
|
|
978
734
|
/** The look's axis-line width in device pixels (null: the editor's own), which an
|
|
979
735
|
* axis-aligned view sets aside for one pixel. */
|
|
980
|
-
private _axisLineWidth
|
|
736
|
+
private _axisLineWidth!: number | null;
|
|
981
737
|
/** A device-pixel width as `LineMaterial` takes it: CSS pixels, three keeping its viewport in
|
|
982
738
|
* CSS units. The editor's own is 2 CSS. */
|
|
983
739
|
private _axisLineCss(device: number | null): number {
|
|
984
740
|
return device === null ? 2 : device / (this._renderer?.getPixelRatio() ?? 1);
|
|
985
741
|
}
|
|
986
|
-
private _axesWanted
|
|
742
|
+
private _axesWanted!: boolean;
|
|
987
743
|
private _stageAxes: ViewportOverlays['axes'] = 'floor';
|
|
988
744
|
/** The look's axis colours by WORLD axis (X, Y, Z), each `null` for the gizmo's own. */
|
|
989
|
-
private _lookAxisHexes
|
|
745
|
+
private _lookAxisHexes!: readonly [number | null, number | null, number | null];
|
|
990
746
|
/** The palette's grid hex, kept for the same reason the axis hexes are. */
|
|
991
|
-
private _lookGridHex
|
|
992
|
-
/** The palette background
|
|
993
|
-
*
|
|
994
|
-
* theme or pipeline change replaces OUR colour and never a host's own. */
|
|
747
|
+
private _lookGridHex!: number;
|
|
748
|
+
/** The palette's background hex, used to recognize the viewport's own background
|
|
749
|
+
* and preserve a host-supplied background. */
|
|
995
750
|
private _lookBackgroundHex: number | null = null;
|
|
996
751
|
private _lookToneMapping: THREE.ToneMapping | null = null;
|
|
997
|
-
private _lookExposure
|
|
752
|
+
private _lookExposure!: number;
|
|
998
753
|
/** The active look's `stage.gizmoSize`, px per Blender gizmo unit
|
|
999
754
|
* — null under every look that names none. Cached off the theme token
|
|
1000
755
|
* rather than read per frame; {@link _applyGizmoSize} is what turns it into
|
|
1001
756
|
* three's `size`. */
|
|
1002
|
-
private _lookGizmoSizePx
|
|
757
|
+
private _lookGizmoSizePx!: number | null;
|
|
1003
758
|
/** The stage's world up axis (its presentation's `world.upAxis`, `setStageFunction`), as
|
|
1004
759
|
* the table every gizmo label and colour reads (see {@link StageAxisFrame}). Defaults to
|
|
1005
760
|
* three's own frame. */
|
|
@@ -1010,23 +765,8 @@ export class EditorViewport {
|
|
|
1010
765
|
* makes a delta enough. */
|
|
1011
766
|
private _appliedAxisSigns: readonly (1 | -1)[] = [1, 1, 1];
|
|
1012
767
|
/** The look's gizmo colours and highlight ({@link nativeGizmoLook}); all-null keeps the
|
|
1013
|
-
* editor's own
|
|
1014
|
-
private _gizmoLook
|
|
1015
|
-
axes: null,
|
|
1016
|
-
navigation: null,
|
|
1017
|
-
hover: null,
|
|
1018
|
-
drag: null,
|
|
1019
|
-
opacity: null,
|
|
1020
|
-
arrowLength: null,
|
|
1021
|
-
arrowHead: null,
|
|
1022
|
-
ringWidth: null,
|
|
1023
|
-
navigationSize: null,
|
|
1024
|
-
navigationForm: 'balls',
|
|
1025
|
-
background: null,
|
|
1026
|
-
navigationCorner: 'top-right',
|
|
1027
|
-
highlightSaturation: null,
|
|
1028
|
-
highlightValue: null,
|
|
1029
|
-
};
|
|
768
|
+
* editor's own axis colours, three's yellow highlight and each handle's native opacity. */
|
|
769
|
+
private _gizmoLook!: NativeGizmoLook;
|
|
1030
770
|
/** The stage's box-select rule (its presentation's `interaction.boxSelect`) — `'touch'` is
|
|
1031
771
|
* Blender's Select Box, `'contain'` (and null) the editor's own. Read by the marquee at the
|
|
1032
772
|
* moment it resolves, never cached into the rectangle. */
|
|
@@ -1048,24 +788,19 @@ export class EditorViewport {
|
|
|
1048
788
|
/** Camera input belongs to the whole viewport surface, including authoring
|
|
1049
789
|
* overlays rendered above the WebGL canvas. */
|
|
1050
790
|
private _interactionElement: HTMLElement;
|
|
1051
|
-
/**
|
|
1052
|
-
private
|
|
1053
|
-
/** The interaction-element dblclick relay — same targeting story as the
|
|
1054
|
-
* pointerup relay above (OrbitControls' pointer capture retargets the
|
|
1055
|
-
* clicks a `dblclick` is synthesized from). */
|
|
1056
|
-
private _onInteractionDblClick: ((e: MouseEvent) => void) | undefined;
|
|
791
|
+
/** Relay double-clicks from the element OrbitControls captures. */
|
|
792
|
+
private readonly _onInteractionDblClick: (e: MouseEvent) => void;
|
|
1057
793
|
private readonly _authoring: () => AuthoringAdapter;
|
|
1058
794
|
private readonly _pick: ((clientX: number, clientY: number) => string | null) | undefined;
|
|
1059
795
|
private readonly _drawCamera: (() => THREE.Camera) | undefined;
|
|
1060
796
|
/** The camera on screen: the stage's drawing camera where it states one, else the viewport's.
|
|
1061
797
|
* Every screen test asks it (picks, the box select, vertex snap, handles, the ground plane);
|
|
1062
|
-
* fly mode
|
|
798
|
+
* fly mode moves the viewport's camera; grid alignment takes its caller's camera. */
|
|
1063
799
|
private get _screenCamera(): THREE.Camera {
|
|
1064
800
|
return this._drawCamera?.() ?? this.renderCamera;
|
|
1065
801
|
}
|
|
1066
802
|
private readonly _standaloneAuthoring: boolean;
|
|
1067
803
|
private readonly _publishPickContext: boolean;
|
|
1068
|
-
private readonly _onlineAssetResolver: OnlineAssetResolver | undefined;
|
|
1069
804
|
private readonly _onProjectionChange: ((projection: ThreeViewportProjection) => void) | undefined;
|
|
1070
805
|
private _pointerDownPos = new THREE.Vector2();
|
|
1071
806
|
private _skipNextSync = false;
|
|
@@ -1075,14 +810,14 @@ export class EditorViewport {
|
|
|
1075
810
|
/** True between a canvas pointerdown and its pointerup — see `_onPointerUp`. */
|
|
1076
811
|
private _pointerSessionActive = false;
|
|
1077
812
|
private _marqueeDiv: HTMLDivElement | null = null;
|
|
1078
|
-
private _pointerDownButton
|
|
813
|
+
private _pointerDownButton!: number;
|
|
1079
814
|
private _altDragOrbit = false;
|
|
1080
815
|
|
|
1081
816
|
// -- Orientation gizmo --
|
|
1082
817
|
private _vcScene = new THREE.Scene();
|
|
1083
818
|
private _vcCamera: THREE.OrthographicCamera;
|
|
1084
819
|
private _vcClickTargets: THREE.Mesh[] = [];
|
|
1085
|
-
/** The gizmo's six balls and
|
|
820
|
+
/** The gizmo's six balls and six stalks, kept for the per-frame depth
|
|
1086
821
|
* cue (`_syncOrientationGizmoDepth`). */
|
|
1087
822
|
private _vcBalls: Array<{
|
|
1088
823
|
readonly fill: THREE.Sprite;
|
|
@@ -1100,7 +835,7 @@ export class EditorViewport {
|
|
|
1100
835
|
readonly positive: boolean;
|
|
1101
836
|
readonly color: THREE.Color;
|
|
1102
837
|
}> = [];
|
|
1103
|
-
/**
|
|
838
|
+
/** Cone heads and triad lines, depth-sorted without dimming. */
|
|
1104
839
|
private _vcSolids: Array<{ readonly mesh: THREE.Mesh; readonly direction: THREE.Vector3 }> = [];
|
|
1105
840
|
private _vcSize = COMPASS_BOX_PX;
|
|
1106
841
|
/** The view's navigation gizmo (`overlays.navigation`): clicked, only drawn, or neither. */
|
|
@@ -1112,54 +847,24 @@ export class EditorViewport {
|
|
|
1112
847
|
private _vcMarginRight = COMPASS_MARGIN_RIGHT_PX;
|
|
1113
848
|
private _vcMarginTop = COMPASS_MARGIN_TOP_PX;
|
|
1114
849
|
/** See {@link EditorViewportOptions.chromeInsetPx}. */
|
|
1115
|
-
private _chromeInsetPx
|
|
850
|
+
private readonly _chromeInsetPx: number;
|
|
1116
851
|
/** Scratch dir vector reused each frame by renderViewCube (avoids per-frame alloc). */
|
|
1117
852
|
private _vcDir = new THREE.Vector3();
|
|
1118
|
-
/** Scratch color reused by renderCameraPreview (avoids per-frame alloc). */
|
|
1119
|
-
private _previewClearColor = new THREE.Color();
|
|
1120
|
-
/** Transient camera view/pilot subject. The native camera remains owned by
|
|
1121
|
-
* the active adapter; this reference changes only viewport presentation. */
|
|
1122
|
-
private _cameraView: THREE.Camera | null = null;
|
|
1123
853
|
private _projection: ThreeViewportProjection = 'perspective';
|
|
1124
|
-
private
|
|
1125
|
-
private _viewportAspect = 1;
|
|
854
|
+
private _viewportAspect: number;
|
|
1126
855
|
/** The view's field of view (`ViewportCamera.fov`), held on its side as the region reshapes. */
|
|
1127
856
|
private _fieldOfView: StageFieldOfView = KIT_FIELD_OF_VIEW;
|
|
1128
|
-
private _orthographicHeight
|
|
1129
|
-
|
|
1130
|
-
private _orbitEnabledBeforeCameraView = true;
|
|
1131
|
-
private _orbitTargetBeforeCameraView = new THREE.Vector3();
|
|
1132
|
-
/** Last objectMap + visibility signature applied by syncFromStore's gizmo/helper
|
|
1133
|
-
* passes — lets those O(scene) loops skip when their inputs are unchanged (e.g.
|
|
1134
|
-
* during a transform drag, which notifies per frame but changes none of them). */
|
|
857
|
+
private _orthographicHeight!: number;
|
|
858
|
+
/** Inputs last reconciled by syncFromStore's helper membership and visibility work. */
|
|
1135
859
|
private _lastGizmoObjectMap: Map<string, THREE.Object3D> | null = null;
|
|
1136
|
-
private _lastGizmoSig
|
|
1137
|
-
private _lastGizmoEpoch
|
|
1138
|
-
/**
|
|
1139
|
-
/**
|
|
1140
|
-
* Spec 28 — recomputed from surface visibility AND selection ownership on every
|
|
1141
|
-
* `syncFromStore`; read by the per-frame render hooks (`renderViewCube`,
|
|
1142
|
-
* `renderCameraPreview`) and the view-cube click path, which run between
|
|
1143
|
-
* notifies and must not re-walk the composite 60×/s.
|
|
1144
|
-
*/
|
|
860
|
+
private _lastGizmoSig!: string;
|
|
861
|
+
private _lastGizmoEpoch!: number;
|
|
862
|
+
/** Selection ownership and surface visibility cached for helper, handle, gizmo and input consumers. */
|
|
1145
863
|
private _threejsToolContextActive = false;
|
|
1146
864
|
/** A three stage is painted here (selection or not) — the floor's and the compass's gate. */
|
|
1147
865
|
private _threeSurfaceShowing = false;
|
|
1148
866
|
private _vcRaycaster = new THREE.Raycaster();
|
|
1149
867
|
|
|
1150
|
-
// -- Asset drop --
|
|
1151
|
-
private _dropOverlay: HTMLDivElement | null = null;
|
|
1152
|
-
/**
|
|
1153
|
-
* The ground the viewport drops assets and probe points onto. NOT a constant:
|
|
1154
|
-
* it is re-read from the ACTIVE region's declared basis
|
|
1155
|
-
* (`presentation-surface.ts`'s `presentationRegionBasis`) on every raycast,
|
|
1156
|
-
* because a Z-up
|
|
1157
|
-
* game's ground is not `y = 0` and the editor may not guess which it is
|
|
1158
|
-
* (ARCHITECTURE-CORE §The editor protocol, "Zero inference"). Mutated in
|
|
1159
|
-
* place rather than reallocated — this sits on a drag path, not a frame path.
|
|
1160
|
-
*/
|
|
1161
|
-
private _groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
1162
|
-
|
|
1163
868
|
// -- Multi-drag: track primary object's transform at drag start --
|
|
1164
869
|
private _dragStartPos = new THREE.Vector3();
|
|
1165
870
|
private _dragStartQuat = new THREE.Quaternion();
|
|
@@ -1218,30 +923,28 @@ export class EditorViewport {
|
|
|
1218
923
|
* debug mesh, … — scene-root objects toggled by the Helpers menu. */
|
|
1219
924
|
private _helpers = new Map<string, THREE.Object3D>();
|
|
1220
925
|
|
|
1221
|
-
//
|
|
1222
|
-
|
|
1223
|
-
// -- Snap-to-view animation --
|
|
926
|
+
// Snap-to-view animation.
|
|
1224
927
|
private _snapAnimating = false;
|
|
1225
|
-
private _snapStartTime
|
|
928
|
+
private _snapStartTime!: number;
|
|
1226
929
|
private _snapDuration = 400;
|
|
1227
930
|
private _snapQ1 = new THREE.Quaternion();
|
|
1228
931
|
private _snapQ2 = new THREE.Quaternion();
|
|
1229
|
-
private _snapDist
|
|
932
|
+
private _snapDist!: number;
|
|
1230
933
|
|
|
1231
934
|
// -- Fly camera (right-click + WASD) --
|
|
1232
935
|
private _flyActive = false;
|
|
1233
936
|
private _snapHold = false;
|
|
1234
937
|
private _flyKeys = new Set<string>();
|
|
1235
938
|
private _flySpeed = 5;
|
|
1236
|
-
private _flyEuler = new THREE.Euler(
|
|
939
|
+
private _flyEuler = new THREE.Euler();
|
|
1237
940
|
private _rightMouseDown = false;
|
|
1238
941
|
|
|
1239
942
|
constructor(
|
|
1240
943
|
canvas: HTMLCanvasElement,
|
|
1241
944
|
scene: THREE.Scene,
|
|
1242
945
|
store: EditorShellStore,
|
|
1243
|
-
interactionElement: HTMLElement
|
|
1244
|
-
options: EditorViewportOptions
|
|
946
|
+
interactionElement: HTMLElement,
|
|
947
|
+
options: EditorViewportOptions,
|
|
1245
948
|
) {
|
|
1246
949
|
// The integration's registrations (the authoring policy this viewport asks, its verbs, the
|
|
1247
950
|
// view-state persistence) exist before any viewport does; idempotent.
|
|
@@ -1258,7 +961,6 @@ export class EditorViewport {
|
|
|
1258
961
|
this._standaloneAuthoring = options.authoring !== undefined;
|
|
1259
962
|
this._publishPickContext = options.publishPickContext ?? true;
|
|
1260
963
|
this._chromeInsetPx = options.chromeInsetPx ?? 0;
|
|
1261
|
-
this._onlineAssetResolver = options.onlineAssetResolver;
|
|
1262
964
|
this._onProjectionChange = options.onProjectionChange;
|
|
1263
965
|
|
|
1264
966
|
// Camera — enableAll so editor sees both game content (layer 0) and editor objects (layer 31)
|
|
@@ -1283,63 +985,35 @@ export class EditorViewport {
|
|
|
1283
985
|
this.orthographicCamera.layers.enableAll();
|
|
1284
986
|
this._syncOrthographicFromPerspective();
|
|
1285
987
|
|
|
1286
|
-
//
|
|
1287
|
-
// grid treatment), so it never ends on a hard rim or degenerates into
|
|
1288
|
-
// convergence-line artifacts at the horizon.
|
|
1289
|
-
// The floor is a SHADER grid, Blender's way: anti-aliased lines of a
|
|
1290
|
-
// constant on-screen width (a line helper's one device pixel halved into
|
|
1291
|
-
// the floor on every downsample — measured), a minor 1 m level that fades
|
|
1292
|
-
// out where its cells get small so distance never merges it into a slab,
|
|
1293
|
-
// a major 10 m level that stays, the palette's colour, and a fade to the
|
|
1294
|
-
// floor's own edge so it never ends on a rim.
|
|
988
|
+
// Draw anti-aliased screen-width floor lines, adaptively fading fine levels while retaining the major grid and radial edge fade.
|
|
1295
989
|
this.grid = createFloorGrid(400);
|
|
1296
990
|
this.grid.layers.set(EDITOR_LAYER);
|
|
1297
991
|
scene.add(this.grid);
|
|
1298
|
-
//
|
|
1299
|
-
// screen-space two-pixel lines: a one-pixel line halves into the floor
|
|
1300
|
-
// when a frame is downsampled (measured), Blender's are two.
|
|
992
|
+
// Draw world axes as screen-space lines sized by the viewport look and projection.
|
|
1301
993
|
const axisGeometry = new LineSegmentsGeometry();
|
|
1302
|
-
axisGeometry.setPositions(axisSegmentPositions(0));
|
|
1303
|
-
axisGeometry.setColors(new Array(AXIS_SEGMENTS_PER_AXIS * 2 * 3).fill(1));
|
|
1304
994
|
this._axisLines = new LineSegments2(
|
|
1305
995
|
axisGeometry,
|
|
1306
996
|
new LineMaterial({
|
|
1307
997
|
vertexColors: true,
|
|
1308
|
-
|
|
1309
|
-
// Blended for draw order over the floor, but at FULL strength:
|
|
1310
|
-
// Blender's axis plateaus at its flat theme colour (`#cb293f`,
|
|
1311
|
-
// `#69aa15`, measured across the whole width of
|
|
1312
|
-
// `modeling-object-none.png`), and an opacity here would make the
|
|
1313
|
-
// stage's axis colour depend on whatever it happens to cross.
|
|
998
|
+
// Keep axis colours at full strength while blending their draw order over the floor.
|
|
1314
999
|
transparent: true,
|
|
1315
1000
|
opacity: 1,
|
|
1316
1001
|
depthWrite: false,
|
|
1317
1002
|
toneMapped: false,
|
|
1318
1003
|
}),
|
|
1319
1004
|
);
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
// one `fade`, and its X axis measures 0.96 of its colour at the front of
|
|
1323
|
-
// the frame against 0.26 near the horizon. `LineMaterial` interpolates
|
|
1324
|
-
// per-segment endpoints with the clip `w` intact, so a world-position
|
|
1325
|
-
// varying taken from the segment's own ends is perspective-correct along
|
|
1326
|
-
// the line — which is all a 400 m two-segment cross needs.
|
|
1005
|
+
// Share the floor's grazing fade. LineMaterial interpolates the segment endpoints with
|
|
1006
|
+
// clip w intact, keeping the world-position varying perspective-correct along each axis.
|
|
1327
1007
|
applyAxisGrazingFade(
|
|
1328
1008
|
this._axisLines.material as LineMaterial,
|
|
1329
1009
|
this.grid.material.uniforms['uGrazingFade'] as { value: number },
|
|
1330
1010
|
this.grid.material.uniforms['uPlaneNormal'] as { value: THREE.Vector3 },
|
|
1331
1011
|
);
|
|
1332
|
-
this._axisLines.visible = false;
|
|
1333
1012
|
this._axisLines.layers.set(EDITOR_LAYER);
|
|
1334
1013
|
scene.add(this._axisLines);
|
|
1335
1014
|
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
// `subscribeNativeSelectionTheme` calls its listener SYNCHRONOUSLY once
|
|
1339
|
-
// (native-selection-style.ts), and from higher in this constructor that
|
|
1340
|
-
// first call read `this.grid.geometry` off an undefined grid — the whole
|
|
1341
|
-
// viewport construction threw, and every 3D document died with
|
|
1342
|
-
// "Cannot read properties of undefined (reading 'geometry')".
|
|
1015
|
+
// Subscribe after constructing the grid and axis lines: subscribeNativeSelectionTheme
|
|
1016
|
+
// invokes its listener synchronously, and that listener applies their viewport look.
|
|
1343
1017
|
this._unsubscribeSelectionTheme = subscribeNativeSelectionTheme(canvas, () => {
|
|
1344
1018
|
invalidateStages();
|
|
1345
1019
|
// The selection marks carry their look whole (colour, box form and frame, wire colour and
|
|
@@ -1361,14 +1035,13 @@ export class EditorViewport {
|
|
|
1361
1035
|
this._lookGizmoSizePx = nativeViewportGizmoSize(canvas);
|
|
1362
1036
|
this._applyGizmoSize();
|
|
1363
1037
|
this._applyGizmoArrows();
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
this._readLookStage(canvas);
|
|
1038
|
+
// Refresh the gizmos for the current look and presentation frame.
|
|
1039
|
+
// The initial callback precedes transform-control construction.
|
|
1040
|
+
this._readLookStage();
|
|
1368
1041
|
});
|
|
1369
1042
|
|
|
1370
1043
|
// Editor lighting (on EDITOR_LAYER so game camera doesn't see them)
|
|
1371
|
-
this._lightRig = options
|
|
1044
|
+
this._lightRig = options.lightRig !== false;
|
|
1372
1045
|
this._editorAmbient = new THREE.AmbientLight(0xffffff, 0.5);
|
|
1373
1046
|
this._editorAmbient.layers.set(EDITOR_LAYER);
|
|
1374
1047
|
scene.add(this._editorAmbient);
|
|
@@ -1376,7 +1049,7 @@ export class EditorViewport {
|
|
|
1376
1049
|
this._editorDirLight.position.set(10, 20, 10);
|
|
1377
1050
|
this._editorDirLight.layers.set(EDITOR_LAYER);
|
|
1378
1051
|
scene.add(this._editorDirLight);
|
|
1379
|
-
// `
|
|
1052
|
+
// `setScene` is where the rig's visibility is decided for every scene
|
|
1380
1053
|
// AFTER this one; the first scene arrives here, so it is decided here too.
|
|
1381
1054
|
this._editorAmbient.visible = this._lightRig;
|
|
1382
1055
|
this._editorDirLight.visible = this._lightRig;
|
|
@@ -1386,15 +1059,7 @@ export class EditorViewport {
|
|
|
1386
1059
|
this.batchedRenderer.layers.set(EDITOR_LAYER);
|
|
1387
1060
|
scene.add(this.batchedRenderer);
|
|
1388
1061
|
|
|
1389
|
-
//
|
|
1390
|
-
// run in registration order, so this must be first to win the wheel.
|
|
1391
|
-
// A trackpad has no middle button (pan was unreachable) and its two-finger
|
|
1392
|
-
// scroll arrived as plain zoom — a 20-minute human build session on one
|
|
1393
|
-
// called moving the camera "nearly impossible" (runhuman pass 12,
|
|
1394
|
-
// 2026-08-28). Semantics: pinch (ctrl+wheel) and pure-vertical wheel stay
|
|
1395
|
-
// ZOOM (mouse wheels are pure-vertical, so mice are untouched); a wheel
|
|
1396
|
-
// with a horizontal component — only trackpads produce one — or Shift
|
|
1397
|
-
// held PANS in screen space, Figma-fashion.
|
|
1062
|
+
// Register trackpad navigation before OrbitControls so it owns the wheel: horizontal or Shift wheel pans in screen space.
|
|
1398
1063
|
interactionElement.addEventListener('wheel', this._onTrackpadWheel, { passive: false });
|
|
1399
1064
|
|
|
1400
1065
|
// OrbitControls: the editor's own mouse is right-drag to orbit, middle-drag to pan and the
|
|
@@ -1403,24 +1068,21 @@ export class EditorViewport {
|
|
|
1403
1068
|
// (its mouse look) the browser refuses capture with an uncaught InvalidStateError on each
|
|
1404
1069
|
// click. Capture is meaningless under a lock (every move already comes to the locked
|
|
1405
1070
|
// element), so it is skipped then.
|
|
1406
|
-
|
|
1071
|
+
// Look up capture on the element's prototype at each call so editor.document.click can
|
|
1072
|
+
// suppress capture while dispatching a synthetic pointer, which has no browser pointer id.
|
|
1073
|
+
const realm = (interactionElement.ownerDocument.defaultView ?? window) as Window & typeof globalThis;
|
|
1407
1074
|
interactionElement.setPointerCapture = (pointerId: number) => {
|
|
1408
1075
|
if (interactionElement.ownerDocument.pointerLockElement) return;
|
|
1409
|
-
|
|
1076
|
+
realm.Element.prototype.setPointerCapture.call(interactionElement, pointerId);
|
|
1410
1077
|
};
|
|
1411
1078
|
this.orbitControls = new OrbitControls(this.camera, interactionElement);
|
|
1412
1079
|
this.orbitControls.enableDamping = true;
|
|
1413
1080
|
this.orbitControls.dampingFactor = 0.1;
|
|
1414
1081
|
this.applyKeymapNavigation();
|
|
1415
|
-
this._unsubscribeKeymap =
|
|
1082
|
+
this._unsubscribeKeymap = editorHost().keyboard.subscribe(() => this.applyKeymapNavigation());
|
|
1416
1083
|
this._installTurntable();
|
|
1417
1084
|
|
|
1418
|
-
//
|
|
1419
|
-
// Construction order IS pointer priority (each instance registers its own
|
|
1420
|
-
// canvas listeners): translate, then SCALE, then rotate — the screen-space
|
|
1421
|
-
// rotate ring's band can cross a scale cube's projected spot, and a
|
|
1422
|
-
// pointer on that small explicit target means scale (measured: the ring
|
|
1423
|
-
// stole the X-cube drag when rotate registered first).
|
|
1085
|
+
// Register translate, scale and rotate controls in pointer-priority order; combined mode shows all three.
|
|
1424
1086
|
this.transformControls = new TransformControls(this.camera, canvas);
|
|
1425
1087
|
this._auxScaleControls = new TransformControls(this.camera, canvas);
|
|
1426
1088
|
this._auxScaleControls.setMode('scale');
|
|
@@ -1428,12 +1090,7 @@ export class EditorViewport {
|
|
|
1428
1090
|
this._auxRotateControls.setMode('rotate');
|
|
1429
1091
|
this._activeGizmo = this.transformControls;
|
|
1430
1092
|
const wireGizmo = (gizmoControls: TransformControls): void => {
|
|
1431
|
-
//
|
|
1432
|
-
// turns to the hovered axis only when the helper next updates its matrices, a render away,
|
|
1433
|
-
// and `pointerDown` reads it as it stands; a press within a frame of its hover (a quick
|
|
1434
|
-
// flick onto an arrow, a touch, whose hover runs inside the press) started on the plane of
|
|
1435
|
-
// no axis, which faces the camera, and the object moved by that plane's foreshortened
|
|
1436
|
-
// offset: measured on a fresh model, 1.24 m where the cursor asked for 2.78 m.
|
|
1093
|
+
// Refresh the hovered drag plane before pointerDown reads it, including a hover first established by a touch press.
|
|
1437
1094
|
const pressed = gizmoControls.pointerDown.bind(gizmoControls);
|
|
1438
1095
|
gizmoControls.pointerDown = (pointer) => {
|
|
1439
1096
|
gizmoControls.getHelper().updateMatrixWorld(true);
|
|
@@ -1443,8 +1100,7 @@ export class EditorViewport {
|
|
|
1443
1100
|
gizmoControls.getRaycaster().layers.enableAll();
|
|
1444
1101
|
gizmoControls.addEventListener('dragging-changed', (event) => {
|
|
1445
1102
|
const dragging = (event as unknown as { value: boolean }).value;
|
|
1446
|
-
//
|
|
1447
|
-
// (live-gesture-lock.ts) — a swap mid-drag snapped moved objects back.
|
|
1103
|
+
// Hold the design-session remount while a transform drag owns the live objects.
|
|
1448
1104
|
if (dragging) beginLiveGesture();
|
|
1449
1105
|
else endLiveGesture();
|
|
1450
1106
|
this.orbitControls.enabled = !dragging;
|
|
@@ -1454,7 +1110,7 @@ export class EditorViewport {
|
|
|
1454
1110
|
for (const other of this._allGizmos()) {
|
|
1455
1111
|
if (other !== gizmoControls) other.enabled = false;
|
|
1456
1112
|
}
|
|
1457
|
-
const gizmoObj = gizmoControls.object
|
|
1113
|
+
const gizmoObj = gizmoControls.object!;
|
|
1458
1114
|
const channel =
|
|
1459
1115
|
gizmoControls.mode === 'translate'
|
|
1460
1116
|
? 'position'
|
|
@@ -1464,63 +1120,61 @@ export class EditorViewport {
|
|
|
1464
1120
|
const writable = (id: string): boolean =>
|
|
1465
1121
|
this._authoring().transforms?.editability?.(id, channel).writable ?? true;
|
|
1466
1122
|
const primaryDragId =
|
|
1467
|
-
this._attachedAuthoringId ??
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1123
|
+
this._attachedAuthoringId ?? this._authoringIdForObject(gizmoObj) ?? '';
|
|
1124
|
+
this._dragStartPos.copy(gizmoObj.position);
|
|
1125
|
+
this._dragStartQuat.copy(gizmoObj.quaternion);
|
|
1126
|
+
this._dragStartScale.copy(gizmoObj.scale);
|
|
1127
|
+
this._otherDragObjects = [];
|
|
1128
|
+
this._allDragObjects = [];
|
|
1129
|
+
this._pivotPos.copy(gizmoObj.position);
|
|
1130
|
+
const primaryId =
|
|
1131
|
+
gizmoObj === this._pivotDummy ? null : this._authoringIdForObject(gizmoObj);
|
|
1132
|
+
if (!primaryId) {
|
|
1133
|
+
// Without a semantic primary ID, derive drag subjects from the writable selection.
|
|
1134
|
+
for (const id of this._store.shell.selectedEntityIds) {
|
|
1135
|
+
if (!writable(id)) continue;
|
|
1136
|
+
const obj = this._objectForAuthoringId(id);
|
|
1137
|
+
if (!obj) continue;
|
|
1138
|
+
this._allDragObjects.push({
|
|
1139
|
+
id,
|
|
1140
|
+
obj,
|
|
1141
|
+
startPos: obj.position.clone(),
|
|
1142
|
+
startQuat: obj.quaternion.clone(),
|
|
1143
|
+
startScale: obj.scale.clone(),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
for (const id of this._store.shell.selectedEntityIds) {
|
|
1148
|
+
if (id === primaryDragId) continue;
|
|
1149
|
+
if (!writable(id)) continue;
|
|
1150
|
+
const obj = this._objectForAuthoringId(id);
|
|
1151
|
+
if (!obj) continue;
|
|
1152
|
+
this._otherDragObjects.push({
|
|
1153
|
+
id,
|
|
1154
|
+
obj,
|
|
1155
|
+
startPos: obj.position.clone(),
|
|
1156
|
+
startQuat: obj.quaternion.clone(),
|
|
1157
|
+
startScale: obj.scale.clone(),
|
|
1158
|
+
});
|
|
1505
1159
|
}
|
|
1506
|
-
const editIds =
|
|
1507
|
-
gizmoObj === this._pivotDummy
|
|
1508
|
-
? this._allDragObjects.map(({ id }) => id)
|
|
1509
|
-
: [primaryDragId, ...this._otherDragObjects.map(({ id }) => id)];
|
|
1510
|
-
this._preservedChildObjects = this._preparePreservedChildren(editIds.filter(Boolean));
|
|
1511
|
-
this._activeTransformEditIds = [
|
|
1512
|
-
...new Set([
|
|
1513
|
-
...editIds.filter(Boolean),
|
|
1514
|
-
...this._preservedChildObjects.map(({ id }) => id),
|
|
1515
|
-
]),
|
|
1516
|
-
];
|
|
1517
|
-
// Begin every semantic source edit before TransformControls mutates
|
|
1518
|
-
// any native object. Source-backed adapters need the pre-drag pose of
|
|
1519
|
-
// secondary selections too; physics-backed adapters freeze each body.
|
|
1520
|
-
const authoring = this._authoring();
|
|
1521
|
-
for (const id of this._activeTransformEditIds)
|
|
1522
|
-
beginAuthoringTransformEdit(authoring, id);
|
|
1523
1160
|
}
|
|
1161
|
+
const editIds =
|
|
1162
|
+
gizmoObj === this._pivotDummy
|
|
1163
|
+
? this._allDragObjects.map(({ id }) => id)
|
|
1164
|
+
: [primaryDragId, ...this._otherDragObjects.map(({ id }) => id)];
|
|
1165
|
+
this._preservedChildObjects = this._preparePreservedChildren(editIds.filter(Boolean));
|
|
1166
|
+
this._activeTransformEditIds = [
|
|
1167
|
+
...new Set([
|
|
1168
|
+
...editIds.filter(Boolean),
|
|
1169
|
+
...this._preservedChildObjects.map(({ id }) => id),
|
|
1170
|
+
]),
|
|
1171
|
+
];
|
|
1172
|
+
// Begin every semantic source edit before TransformControls mutates
|
|
1173
|
+
// any native object. Source-backed adapters need the pre-drag pose of
|
|
1174
|
+
// secondary selections too; physics-backed adapters freeze each body.
|
|
1175
|
+
const authoring = this._authoring();
|
|
1176
|
+
for (const id of this._activeTransformEditIds)
|
|
1177
|
+
beginAuthoringTransformEdit(authoring, id);
|
|
1524
1178
|
} else {
|
|
1525
1179
|
// Hide snap indicators when drag ends
|
|
1526
1180
|
this._surfaceSnapIndicator.visible = false;
|
|
@@ -1530,8 +1184,7 @@ export class EditorViewport {
|
|
|
1530
1184
|
}
|
|
1531
1185
|
});
|
|
1532
1186
|
gizmoControls.addEventListener('objectChange', () => {
|
|
1533
|
-
const gizmoObj = gizmoControls.object
|
|
1534
|
-
if (!gizmoObj) return;
|
|
1187
|
+
const gizmoObj = gizmoControls.object!;
|
|
1535
1188
|
const mode = gizmoControls.mode;
|
|
1536
1189
|
// A CENTRE-anchored gizmo takes the individual-origins math on purpose:
|
|
1537
1190
|
// its anchor is where the handles are DRAWN, not a pivot the edit is
|
|
@@ -1599,9 +1252,8 @@ export class EditorViewport {
|
|
|
1599
1252
|
}
|
|
1600
1253
|
this._restorePreservedChildren();
|
|
1601
1254
|
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
// LineSegments) does not, hence the guard.
|
|
1255
|
+
// Bounds helpers and selection brackets update mid-drag; mesh wires and
|
|
1256
|
+
// diagnostic wire cubes are LineSegments without an update method.
|
|
1605
1257
|
for (const helper of this._boxHelpers.values()) {
|
|
1606
1258
|
if ('update' in helper) helper.update();
|
|
1607
1259
|
}
|
|
@@ -1650,9 +1302,7 @@ export class EditorViewport {
|
|
|
1650
1302
|
this._vertexSnapIndicator.layers.set(EDITOR_LAYER);
|
|
1651
1303
|
scene.add(this._vertexSnapIndicator);
|
|
1652
1304
|
|
|
1653
|
-
//
|
|
1654
|
-
this._gizmoLook = nativeGizmoLook(canvas);
|
|
1655
|
-
this._rebuildAxisLines();
|
|
1305
|
+
// Install handle highlights and align the gizmo geometry.
|
|
1656
1306
|
for (const controls of this._allGizmos()) this._installGizmoHighlight(controls);
|
|
1657
1307
|
this._patchGizmo(this._gizmoHelper);
|
|
1658
1308
|
this._patchGizmo(this._auxRotateControls.getHelper());
|
|
@@ -1661,22 +1311,14 @@ export class EditorViewport {
|
|
|
1661
1311
|
this._dropNegativeAxisHandles(this._auxScaleControls.getHelper(), ['scale']);
|
|
1662
1312
|
this._patchCombinedScaleGizmo(this._auxScaleControls.getHelper());
|
|
1663
1313
|
this._applyGizmoArrows();
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
// synchronous call found none (see {@link _applyGizmoSize}).
|
|
1667
|
-
this._lookGizmoSizePx = nativeViewportGizmoSize(canvas);
|
|
1314
|
+
// Apply the look’s gizmo size once the controls exist; the theme subscription may run before
|
|
1315
|
+
// their construction.
|
|
1668
1316
|
this._applyGizmoSize();
|
|
1669
|
-
//
|
|
1670
|
-
// mirror is a difference over the geometry they produced, so it is the
|
|
1671
|
-
// last word on where a handle is drawn.
|
|
1672
|
-
// The stage's AXIS FRAME (its world's up axis, `setStageFunction`), after the three patches
|
|
1673
|
-
// above have run: the mirror is a difference over the geometry they produced, so it is the
|
|
1674
|
-
// last word on where a handle is drawn.
|
|
1317
|
+
// Apply the stage's axis frame after constructing and patching its gizmo geometry.
|
|
1675
1318
|
this._applyGizmoAxisFrame();
|
|
1676
1319
|
|
|
1677
|
-
//
|
|
1320
|
+
// The theme subscription builds the orientation gizmo; this camera draws it.
|
|
1678
1321
|
this._vcCamera = new THREE.OrthographicCamera(-1.5, 1.5, 1.5, -1.5, 0.1, 10);
|
|
1679
|
-
this._initOrientationGizmo();
|
|
1680
1322
|
|
|
1681
1323
|
// -- Fly camera input (right-click + WASD) --
|
|
1682
1324
|
window.addEventListener('keydown', this._onFlyKeyDown);
|
|
@@ -1688,58 +1330,22 @@ export class EditorViewport {
|
|
|
1688
1330
|
|
|
1689
1331
|
// Alt+Left-drag = orbit (Unity-style), via capture phase so it fires before OrbitControls
|
|
1690
1332
|
canvas.addEventListener('pointerdown', this._onAltOrbitStart, { capture: true });
|
|
1691
|
-
//
|
|
1692
|
-
//
|
|
1693
|
-
// later left-drag orbited the camera and the marquee never drew (a human
|
|
1694
|
-
// tester lost rectangle-select entirely: "it's like a playing view" —
|
|
1695
|
-
// runhuman pass 27). `pointercancel` covers an interrupted drag the same
|
|
1696
|
-
// way. Registered in _onAltOrbitRelease so dispose can remove them.
|
|
1333
|
+
// Listen on window so an Alt+drag released outside the canvas restores the controls.
|
|
1334
|
+
// pointercancel handles interrupted drags; dispose removes both listeners.
|
|
1697
1335
|
window.addEventListener('pointerup', this._onAltOrbitRelease, { capture: true });
|
|
1698
1336
|
window.addEventListener('pointercancel', this._onAltOrbitRelease, { capture: true });
|
|
1337
|
+
// A handle or constraint-control drag whose release never arrives (the pointer cancelled, the
|
|
1338
|
+
// window lost focus mid-drag) ends here: its flags would otherwise keep `interacting()` true
|
|
1339
|
+
// and the stage redrawing every frame (`StageHost`).
|
|
1340
|
+
window.addEventListener('pointercancel', this._onInteractionDragLost, { capture: true });
|
|
1341
|
+
window.addEventListener('blur', this._onInteractionDragLost);
|
|
1699
1342
|
window.addEventListener('keydown', this._onSnapHoldKey);
|
|
1700
1343
|
window.addEventListener('keyup', this._onSnapHoldKey);
|
|
1701
1344
|
|
|
1702
|
-
//
|
|
1703
|
-
// is only reproducible with REAL pointer input — synthetic `dispatchEvent`
|
|
1704
|
-
// still works, so unit coverage cannot see it:
|
|
1705
|
-
// OrbitControls (constructed over `interactionElement` above) calls
|
|
1706
|
-
// `setPointerCapture` on its domElement on EVERY pointerdown — including
|
|
1707
|
-
// the disabled LEFT button — so all subsequent pointermove/pointerup are
|
|
1708
|
-
// RETARGETED to the interaction element and canvas-bound listeners
|
|
1709
|
-
// silently die. Because the interaction element is NOT the canvas
|
|
1710
|
-
// (right-drag-over-overlays orbit needs it off the canvas), canvas-bound
|
|
1711
|
-
// move/up alone would kill click-select, marquee, and the alt-orbit
|
|
1712
|
-
// release above.
|
|
1713
|
-
//
|
|
1714
|
-
// The combined mechanism:
|
|
1715
|
-
// - `pointerdown` binds to the CANVAS: a down anywhere else is not a
|
|
1716
|
-
// viewport gesture. It opens `_pointerSessionActive`, and the session
|
|
1717
|
-
// guards in `_onPointerUp` AND `_onPointerMove` keep gestures that
|
|
1718
|
-
// started on container chrome (toolbar buttons, overlay children)
|
|
1719
|
-
// from running selection/marquee against a stale down-pos.
|
|
1720
|
-
// - `pointermove` binds to the INTERACTION ELEMENT so marquee updates
|
|
1721
|
-
// keep firing mid-drag: once OrbitControls owns the capture, a
|
|
1722
|
-
// canvas-bound move never fires. (The DOM `RootSelectionOverlay` has
|
|
1723
|
-
// its own marquee via `world-overlay-gestures`, but the viewport's
|
|
1724
|
-
// must work bare too.)
|
|
1725
|
-
// - `pointerup` binds to the interaction element via the stored
|
|
1726
|
-
// `_onInteractionPointerUp` wrapper (removed in `dispose`). Its
|
|
1727
|
-
// target guard keeps overlay-OWNED ups (the overlay stopPropagation's
|
|
1728
|
-
// its own pointerdowns before OrbitControls can capture, so their ups
|
|
1729
|
-
// land on overlay children) routed exclusively through the overlay's
|
|
1730
|
-
// own selection logic. The `_pointerSessionActive` disjunct still
|
|
1731
|
-
// admits a CANVAS-origin gesture whose down was never captured
|
|
1732
|
-
// (handle drags stopPropagation the down too) and whose up therefore
|
|
1733
|
-
// lands on overlay chrome — dropping those would leave the
|
|
1734
|
-
// session/handle-drag flags stuck; `_onPointerUp`'s own guards make
|
|
1735
|
-
// such ups end the drag, never select.
|
|
1345
|
+
// Bind pointer selection to the interaction element that OrbitControls captures, and handle pointerup there so real drags release their session flags.
|
|
1736
1346
|
canvas.addEventListener('pointerdown', this._onPointerDown);
|
|
1737
1347
|
interactionElement.addEventListener('pointermove', this._onPointerMove);
|
|
1738
|
-
|
|
1739
|
-
if (e.target === canvas || e.target === interactionElement || this._pointerSessionActive)
|
|
1740
|
-
this._onPointerUp(e);
|
|
1741
|
-
};
|
|
1742
|
-
interactionElement.addEventListener('pointerup', this._onInteractionPointerUp);
|
|
1348
|
+
interactionElement.addEventListener('pointerup', this._onPointerUp);
|
|
1743
1349
|
|
|
1744
1350
|
// Double-click = DRILL IN (see `_onDoubleClick`). Bound on the interaction
|
|
1745
1351
|
// element for the same reason `pointerup` is: OrbitControls captures the
|
|
@@ -1751,15 +1357,10 @@ export class EditorViewport {
|
|
|
1751
1357
|
};
|
|
1752
1358
|
interactionElement.addEventListener('dblclick', this._onInteractionDblClick);
|
|
1753
1359
|
|
|
1754
|
-
// Asset drag-and-drop onto viewport
|
|
1755
|
-
canvas.addEventListener('dragover', this._onDragOver);
|
|
1756
|
-
canvas.addEventListener('dragleave', this._onDragLeave);
|
|
1757
|
-
canvas.addEventListener('drop', this._onDrop);
|
|
1758
|
-
|
|
1759
1360
|
// Collect editor infrastructure objects so they can be moved between scenes
|
|
1760
1361
|
this._editorObjects = [
|
|
1761
1362
|
this.grid,
|
|
1762
|
-
|
|
1363
|
+
this._axisLines,
|
|
1763
1364
|
this._editorAmbient,
|
|
1764
1365
|
this._editorDirLight,
|
|
1765
1366
|
this.batchedRenderer,
|
|
@@ -1771,11 +1372,9 @@ export class EditorViewport {
|
|
|
1771
1372
|
this._vertexSnapIndicator,
|
|
1772
1373
|
];
|
|
1773
1374
|
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
|
|
1777
|
-
this._installPickContext(this.camera);
|
|
1778
|
-
}
|
|
1375
|
+
// Publish this viewport's camera, canvas and editor controls for adapter raycasting.
|
|
1376
|
+
// Cleared in dispose().
|
|
1377
|
+
this._installPickContext(this.camera);
|
|
1779
1378
|
}
|
|
1780
1379
|
|
|
1781
1380
|
/** Swap the scene the viewport renders. Moves editor infrastructure objects to the new scene. */
|
|
@@ -1813,14 +1412,8 @@ export class EditorViewport {
|
|
|
1813
1412
|
}
|
|
1814
1413
|
this._scene = newScene;
|
|
1815
1414
|
this._contentBoundsAt = 0; // different content → re-measure before the next draw
|
|
1816
|
-
//
|
|
1817
|
-
|
|
1818
|
-
// session) owns its lighting completely — the editor camera sees
|
|
1819
|
-
// EDITOR_LAYER lights, so the rig would add a white ambient+directional
|
|
1820
|
-
// the game camera never sees, and the mismatch pops as a color/brightness
|
|
1821
|
-
// shift the instant play takes over the draw. WYSIWYG for adopted scenes.
|
|
1822
|
-
// A stage bound to a view presentation is lit by it instead (`bindPresentation`).
|
|
1823
|
-
const rigOn = this._lightRig && !this._store.isAdoptedSceneActive && this._presentation === null;
|
|
1415
|
+
// Use the editor rig only when enabled and no view presentation supplies the lighting.
|
|
1416
|
+
const rigOn = this._lightRig && this._presentation === null;
|
|
1824
1417
|
this._editorAmbient.visible = rigOn;
|
|
1825
1418
|
this._editorDirLight.visible = rigOn;
|
|
1826
1419
|
}
|
|
@@ -1829,11 +1422,9 @@ export class EditorViewport {
|
|
|
1829
1422
|
return this._scene;
|
|
1830
1423
|
}
|
|
1831
1424
|
|
|
1832
|
-
/** Camera that
|
|
1833
|
-
* free editor camera; view-through/pilot temporarily returns the authored
|
|
1834
|
-
* native camera without changing its ownership. */
|
|
1425
|
+
/** Camera that paints the Scene document: the free editor camera. */
|
|
1835
1426
|
get renderCamera(): THREE.Camera {
|
|
1836
|
-
return this.
|
|
1427
|
+
return this.freeCamera;
|
|
1837
1428
|
}
|
|
1838
1429
|
|
|
1839
1430
|
get freeCamera(): THREE.PerspectiveCamera | THREE.OrthographicCamera {
|
|
@@ -1859,10 +1450,6 @@ export class EditorViewport {
|
|
|
1859
1450
|
}
|
|
1860
1451
|
|
|
1861
1452
|
setProjection(projection: ThreeViewportProjection): void {
|
|
1862
|
-
if (this._cameraView) {
|
|
1863
|
-
this._pendingProjection = projection;
|
|
1864
|
-
return;
|
|
1865
|
-
}
|
|
1866
1453
|
if (this._projection === projection) return;
|
|
1867
1454
|
if (projection === 'orthographic') this._syncOrthographicFromPerspective();
|
|
1868
1455
|
else this._syncPerspectiveFromOrthographic();
|
|
@@ -1936,7 +1523,7 @@ export class EditorViewport {
|
|
|
1936
1523
|
* the right button then does nothing here, as in Blender, where it is the context menu's.
|
|
1937
1524
|
*/
|
|
1938
1525
|
private applyKeymapNavigation(): void {
|
|
1939
|
-
const { orbit, turntable } =
|
|
1526
|
+
const { orbit, turntable } = editorHost().keyboard.navigation();
|
|
1940
1527
|
// Only a turntable keeps a roll: under any other orbit the view comes back level (a Top
|
|
1941
1528
|
// view's screen up, which is no roll, stands).
|
|
1942
1529
|
if (!turntable && this.camera.up.y < 0.9999 && Math.abs(this.camera.up.z) < 0.9999) this.camera.up.set(0, 1, 0);
|
|
@@ -1947,7 +1534,7 @@ export class EditorViewport {
|
|
|
1947
1534
|
RIGHT: orbit === 'middle' ? (-1 as THREE.MOUSE) : THREE.MOUSE.ROTATE,
|
|
1948
1535
|
};
|
|
1949
1536
|
}
|
|
1950
|
-
private _unsubscribeKeymap: () => void
|
|
1537
|
+
private _unsubscribeKeymap: () => void;
|
|
1951
1538
|
|
|
1952
1539
|
/**
|
|
1953
1540
|
* A KEYMAP'S TURNTABLE (`KeymapNavigation.turntable`) in place of three's spherical orbit, on
|
|
@@ -1977,17 +1564,17 @@ export class EditorViewport {
|
|
|
1977
1564
|
controls._handleMouseDownDolly = (event) => {
|
|
1978
1565
|
dollyDown(event);
|
|
1979
1566
|
dolly = null;
|
|
1980
|
-
if (
|
|
1567
|
+
if (editorHost().keyboard.navigation().zoom?.drag !== 'dolly') return;
|
|
1981
1568
|
const top = this._canvas.getBoundingClientRect().top;
|
|
1982
1569
|
const camera = this.orbitControls.object;
|
|
1983
1570
|
dolly = {
|
|
1984
1571
|
len0: Math.max(5 + event.clientY - top, 1),
|
|
1985
1572
|
offset: camera.position.clone().sub(this.orbitControls.target),
|
|
1986
|
-
zoom0: (camera as THREE.OrthographicCamera).zoom
|
|
1573
|
+
zoom0: (camera as THREE.PerspectiveCamera | THREE.OrthographicCamera).zoom,
|
|
1987
1574
|
};
|
|
1988
1575
|
};
|
|
1989
1576
|
controls._handleMouseMoveDolly = (event) => {
|
|
1990
|
-
if (
|
|
1577
|
+
if (editorHost().keyboard.navigation().zoom?.drag !== 'dolly' || !dolly) {
|
|
1991
1578
|
dollyMove(event);
|
|
1992
1579
|
return;
|
|
1993
1580
|
}
|
|
@@ -2017,13 +1604,13 @@ export class EditorViewport {
|
|
|
2017
1604
|
down(event);
|
|
2018
1605
|
drag = null;
|
|
2019
1606
|
for (const listener of [...this._rotateStartListeners]) listener();
|
|
2020
|
-
if (!
|
|
1607
|
+
if (!editorHost().keyboard.navigation().turntable) return;
|
|
2021
1608
|
const camera = this.orbitControls.object;
|
|
2022
1609
|
const viewUp = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
|
|
2023
1610
|
drag = { x: event.clientX, y: event.clientY, reverse: viewUp.y < 0 ? -1 : 1 };
|
|
2024
1611
|
};
|
|
2025
1612
|
controls._handleMouseMoveRotate = (event) => {
|
|
2026
|
-
const turntable =
|
|
1613
|
+
const turntable = editorHost().keyboard.navigation().turntable;
|
|
2027
1614
|
if (!turntable || !drag) {
|
|
2028
1615
|
move(event);
|
|
2029
1616
|
return;
|
|
@@ -2040,7 +1627,7 @@ export class EditorViewport {
|
|
|
2040
1627
|
// Auto Perspective on a stage that draws with the viewport's own projection (a document's
|
|
2041
1628
|
// session keeps its own and answers for it).
|
|
2042
1629
|
() => {
|
|
2043
|
-
if (!
|
|
1630
|
+
if (!editorHost().keyboard.navigation().autoPerspective || this._projection !== 'orthographic') return;
|
|
2044
1631
|
const offset = this.camera.position.clone().sub(this.orbitControls.target);
|
|
2045
1632
|
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(this.camera.quaternion);
|
|
2046
1633
|
if (axisViewName(offset, up) !== null) this.setProjection('perspective');
|
|
@@ -2054,7 +1641,7 @@ export class EditorViewport {
|
|
|
2054
1641
|
*/
|
|
2055
1642
|
private readonly _axisViewListeners: (() => boolean)[] = [
|
|
2056
1643
|
() => {
|
|
2057
|
-
if (
|
|
1644
|
+
if (editorHost().keyboard.navigation().autoPerspective) this.setProjection('orthographic');
|
|
2058
1645
|
return true;
|
|
2059
1646
|
},
|
|
2060
1647
|
];
|
|
@@ -2155,7 +1742,7 @@ export class EditorViewport {
|
|
|
2155
1742
|
* orbit's pivot, as a whole gesture (a camera lock writes it).
|
|
2156
1743
|
*/
|
|
2157
1744
|
zoomStep(direction: 1 | -1): void {
|
|
2158
|
-
const step =
|
|
1745
|
+
const step = editorHost().keyboard.navigation().zoom?.step ?? 1.2;
|
|
2159
1746
|
const factor = direction > 0 ? 1 / step : step;
|
|
2160
1747
|
const camera = this.orbitControls.object;
|
|
2161
1748
|
this.orbitControls.dispatchEvent({ type: 'start' });
|
|
@@ -2202,54 +1789,6 @@ export class EditorViewport {
|
|
|
2202
1789
|
this.orbitControls.update();
|
|
2203
1790
|
}
|
|
2204
1791
|
|
|
2205
|
-
get cameraViewMode(): CameraViewMode | null {
|
|
2206
|
-
return this._cameraViewMode;
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
/** Enter an exact native camera view. Pilot additionally hands OrbitControls
|
|
2210
|
-
* the authored camera; the caller owns begin/apply/end persistence. */
|
|
2211
|
-
setCameraView(camera: THREE.Camera, mode: CameraViewMode): void {
|
|
2212
|
-
this.clearCameraView();
|
|
2213
|
-
this._cameraView = camera;
|
|
2214
|
-
this._cameraViewMode = mode;
|
|
2215
|
-
this._orbitEnabledBeforeCameraView = this.orbitControls.enabled;
|
|
2216
|
-
this._orbitTargetBeforeCameraView.copy(this.orbitControls.target);
|
|
2217
|
-
if (mode === 'pilot') {
|
|
2218
|
-
const position = camera.getWorldPosition(new THREE.Vector3());
|
|
2219
|
-
const forward = camera.getWorldDirection(new THREE.Vector3());
|
|
2220
|
-
const distance = Math.max(1, this.freeCamera.position.distanceTo(this.orbitControls.target));
|
|
2221
|
-
this.orbitControls.object = camera;
|
|
2222
|
-
this.orbitControls.target.copy(position).add(forward.multiplyScalar(distance));
|
|
2223
|
-
this.orbitControls.enabled = true;
|
|
2224
|
-
this.orbitControls.update();
|
|
2225
|
-
} else {
|
|
2226
|
-
this.orbitControls.enabled = false;
|
|
2227
|
-
}
|
|
2228
|
-
if (this._publishPickContext) {
|
|
2229
|
-
this._installPickContext(camera);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
/** Restore the free editor camera exactly where it was before view-through. */
|
|
2234
|
-
clearCameraView(): void {
|
|
2235
|
-
if (!this._cameraView) return;
|
|
2236
|
-
if (this._flyActive) this._exitFlyMode();
|
|
2237
|
-
this.orbitControls.object = this.freeCamera;
|
|
2238
|
-
this.orbitControls.target.copy(this._orbitTargetBeforeCameraView);
|
|
2239
|
-
this.orbitControls.enabled = this._orbitEnabledBeforeCameraView;
|
|
2240
|
-
this.orbitControls.update();
|
|
2241
|
-
this._cameraView = null;
|
|
2242
|
-
this._cameraViewMode = null;
|
|
2243
|
-
const pendingProjection = this._pendingProjection;
|
|
2244
|
-
this._pendingProjection = null;
|
|
2245
|
-
if (pendingProjection && pendingProjection !== this._projection) {
|
|
2246
|
-
this.setProjection(pendingProjection);
|
|
2247
|
-
}
|
|
2248
|
-
if (this._publishPickContext) {
|
|
2249
|
-
this._installPickContext(this.freeCamera);
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
1792
|
get objectMap(): Map<string, THREE.Object3D> {
|
|
2254
1793
|
return this._objectMap;
|
|
2255
1794
|
}
|
|
@@ -2286,7 +1825,7 @@ export class EditorViewport {
|
|
|
2286
1825
|
}
|
|
2287
1826
|
|
|
2288
1827
|
private _authoringIdForObject(object: THREE.Object3D): string | null {
|
|
2289
|
-
return this._authoring().hierarchy.
|
|
1828
|
+
return this._authoring().hierarchy.idForNativeObject?.(object) ?? entityIdOf(object) ?? null;
|
|
2290
1829
|
}
|
|
2291
1830
|
|
|
2292
1831
|
/**
|
|
@@ -2315,22 +1854,20 @@ export class EditorViewport {
|
|
|
2315
1854
|
color: new THREE.Color(nativeSelectionColors(this._canvas).visible),
|
|
2316
1855
|
toneMapped: false,
|
|
2317
1856
|
});
|
|
2318
|
-
const wire = new THREE.LineSegments(edges, mat) as
|
|
1857
|
+
const wire = new THREE.LineSegments(edges, mat) as THREE.LineSegments & TaggedHelper;
|
|
2319
1858
|
obj.getWorldPosition(wire.position);
|
|
2320
|
-
//
|
|
2321
|
-
wire.isBoxHelper = true;
|
|
1859
|
+
// Retain the source so the wire cube follows its world position each frame.
|
|
2322
1860
|
wire._entityObj = obj;
|
|
2323
1861
|
return wire;
|
|
2324
1862
|
}
|
|
2325
1863
|
|
|
2326
1864
|
/**
|
|
2327
|
-
* Rebuild
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
* painted by the native postprocessing silhouette instead.
|
|
1865
|
+
* Rebuild diagnostic bounds and the presentation's selection wires/brackets.
|
|
1866
|
+
* Non-renderable selections use brackets unless their document draws its own overlay;
|
|
1867
|
+
* geometric selections can use wires, boxes and the native silhouette.
|
|
2331
1868
|
*/
|
|
2332
1869
|
private _syncBoxHelpers(): void {
|
|
2333
|
-
//
|
|
1870
|
+
// Dispose the previous geometry and materials before rebuilding.
|
|
2334
1871
|
for (const helper of this._boxHelpers.values()) {
|
|
2335
1872
|
this._scene.remove(helper);
|
|
2336
1873
|
helper.geometry.dispose();
|
|
@@ -2362,7 +1899,7 @@ export class EditorViewport {
|
|
|
2362
1899
|
let index = 0;
|
|
2363
1900
|
obj.traverse((child) => {
|
|
2364
1901
|
const mesh = child as THREE.Mesh;
|
|
2365
|
-
if (!mesh.isMesh ||
|
|
1902
|
+
if (!mesh.isMesh || isInEditorOwnedSubtree(mesh)) return;
|
|
2366
1903
|
const look = nativeViewportWire(this._canvas);
|
|
2367
1904
|
const wire = new THREE.LineSegments(
|
|
2368
1905
|
new THREE.WireframeGeometry(mesh.geometry),
|
|
@@ -2399,7 +1936,7 @@ export class EditorViewport {
|
|
|
2399
1936
|
if (this._standaloneAuthoring && (obj as THREE.Bone).isBone) continue;
|
|
2400
1937
|
// Constraint owners, targets and poles already have a semantic native
|
|
2401
1938
|
// viewport presentation (lines plus a target/pole handle). Drawing the
|
|
2402
|
-
//
|
|
1939
|
+
// empty-transform corner brackets over that presentation creates
|
|
2403
1940
|
// two competing selection languages and obscures the draggable handle.
|
|
2404
1941
|
if ([...this._constraintHelpers.values()].some((helper) => helper.presents(obj))) continue;
|
|
2405
1942
|
// Nor over an object whose document draws its overlay itself, selection colour included
|
|
@@ -2420,12 +1957,12 @@ export class EditorViewport {
|
|
|
2420
1957
|
* The source component stays an empty, ordinary transform node; these
|
|
2421
1958
|
* helpers live on EDITOR_LAYER and therefore never enter Game view.
|
|
2422
1959
|
*/
|
|
2423
|
-
private _syncReflectionProbeHelpers(
|
|
2424
|
-
if (
|
|
1960
|
+
private _syncReflectionProbeHelpers(reconcileMembership: boolean): void {
|
|
1961
|
+
if (reconcileMembership) {
|
|
2425
1962
|
const current = new Map<string, THREE.Object3D>();
|
|
2426
1963
|
const admitted = new Set<THREE.Object3D>();
|
|
2427
1964
|
for (const [id, object] of this._objectMap) {
|
|
2428
|
-
if (
|
|
1965
|
+
if (admitted.has(object) || !reflectionProbeOf(object)) continue;
|
|
2429
1966
|
admitted.add(object);
|
|
2430
1967
|
current.set(id, object);
|
|
2431
1968
|
}
|
|
@@ -2435,28 +1972,6 @@ export class EditorViewport {
|
|
|
2435
1972
|
this._reflectionProbeHelpers.delete(id);
|
|
2436
1973
|
}
|
|
2437
1974
|
for (const [id, source] of current) this._ensureReflectionProbeHelper(id, source);
|
|
2438
|
-
} else {
|
|
2439
|
-
let requiresFullReconcile = false;
|
|
2440
|
-
for (const id of changedIds) {
|
|
2441
|
-
const source = this._objectMap.get(id);
|
|
2442
|
-
const next = source && reflectionProbeOf(source) ? source : null;
|
|
2443
|
-
const previous = this._reflectionProbeHelpers.get(id);
|
|
2444
|
-
if (previous && previous.source !== next) {
|
|
2445
|
-
if (next === null) requiresFullReconcile = true;
|
|
2446
|
-
previous.dispose();
|
|
2447
|
-
this._reflectionProbeHelpers.delete(id);
|
|
2448
|
-
}
|
|
2449
|
-
if (
|
|
2450
|
-
next &&
|
|
2451
|
-
![...this._reflectionProbeHelpers.values()].some((helper) => helper.source === next)
|
|
2452
|
-
) {
|
|
2453
|
-
this._ensureReflectionProbeHelper(id, next);
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
if (requiresFullReconcile) {
|
|
2457
|
-
this._syncReflectionProbeHelpers();
|
|
2458
|
-
return;
|
|
2459
|
-
}
|
|
2460
1975
|
}
|
|
2461
1976
|
for (const [id, helper] of this._reflectionProbeHelpers) {
|
|
2462
1977
|
helper.setSelected(this._store.shell.selectedEntityIds.has(id));
|
|
@@ -2475,21 +1990,13 @@ export class EditorViewport {
|
|
|
2475
1990
|
this._scene.add(helper);
|
|
2476
1991
|
}
|
|
2477
1992
|
|
|
2478
|
-
/**
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
* so the viewport showed nothing at all where it sits. The game must not draw
|
|
2482
|
-
* it: game source here is ecosystem-native and asks the runtime nothing about
|
|
2483
|
-
* being authored. So the editor draws it, in the shape `_syncLightHelpers`
|
|
2484
|
-
* already uses — editor-owned, on EDITOR_LAYER, never in the game's tree and
|
|
2485
|
-
* never in Play.
|
|
2486
|
-
*/
|
|
2487
|
-
private _syncTriggerVolumeHelpers(changedIds: ReadonlySet<string> | null = null): void {
|
|
2488
|
-
if (changedIds === null) {
|
|
1993
|
+
/** Draw editor-owned trigger-volume guides on EDITOR_LAYER without adding geometry to the project tree. */
|
|
1994
|
+
private _syncTriggerVolumeHelpers(reconcileMembership: boolean): void {
|
|
1995
|
+
if (reconcileMembership) {
|
|
2489
1996
|
const current = new Map<string, THREE.Object3D>();
|
|
2490
1997
|
const admitted = new Set<THREE.Object3D>();
|
|
2491
1998
|
for (const [id, object] of this._objectMap) {
|
|
2492
|
-
if (
|
|
1999
|
+
if (admitted.has(object) || !triggerVolumeOf(object)) continue;
|
|
2493
2000
|
admitted.add(object);
|
|
2494
2001
|
current.set(id, object);
|
|
2495
2002
|
}
|
|
@@ -2498,27 +2005,6 @@ export class EditorViewport {
|
|
|
2498
2005
|
this._disposeTriggerVolumeHelper(id, helper);
|
|
2499
2006
|
}
|
|
2500
2007
|
for (const [id, source] of current) this._ensureTriggerVolumeHelper(id, source);
|
|
2501
|
-
} else {
|
|
2502
|
-
let requiresFullReconcile = false;
|
|
2503
|
-
for (const id of changedIds) {
|
|
2504
|
-
const source = this._objectMap.get(id);
|
|
2505
|
-
const next = source && triggerVolumeOf(source) ? source : null;
|
|
2506
|
-
const previous = this._triggerVolumeHelpers.get(id);
|
|
2507
|
-
if (previous && previous.source !== next) {
|
|
2508
|
-
if (next === null) requiresFullReconcile = true;
|
|
2509
|
-
this._disposeTriggerVolumeHelper(id, previous);
|
|
2510
|
-
}
|
|
2511
|
-
if (
|
|
2512
|
-
next &&
|
|
2513
|
-
![...this._triggerVolumeHelpers.values()].some((helper) => helper.source === next)
|
|
2514
|
-
) {
|
|
2515
|
-
this._ensureTriggerVolumeHelper(id, next);
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
if (requiresFullReconcile) {
|
|
2519
|
-
this._syncTriggerVolumeHelpers();
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
2008
|
}
|
|
2523
2009
|
const visible =
|
|
2524
2010
|
this._threeSurfaceShowing &&
|
|
@@ -2544,12 +2030,12 @@ export class EditorViewport {
|
|
|
2544
2030
|
}
|
|
2545
2031
|
|
|
2546
2032
|
/** Keep one editor-owned projection beside every source-owned constraint owner. */
|
|
2547
|
-
private _syncConstraintHelpers(
|
|
2548
|
-
if (
|
|
2033
|
+
private _syncConstraintHelpers(reconcileMembership: boolean): void {
|
|
2034
|
+
if (reconcileMembership) {
|
|
2549
2035
|
const current = new Map<string, THREE.Object3D>();
|
|
2550
2036
|
const admitted = new Set<THREE.Object3D>();
|
|
2551
2037
|
for (const [id, object] of this._objectMap) {
|
|
2552
|
-
if (
|
|
2038
|
+
if (admitted.has(object) || constraintsOf(object).length === 0) continue;
|
|
2553
2039
|
admitted.add(object);
|
|
2554
2040
|
current.set(id, object);
|
|
2555
2041
|
}
|
|
@@ -2559,28 +2045,6 @@ export class EditorViewport {
|
|
|
2559
2045
|
this._constraintHelpers.delete(id);
|
|
2560
2046
|
}
|
|
2561
2047
|
for (const [id, source] of current) this._ensureConstraintHelper(id, source);
|
|
2562
|
-
} else {
|
|
2563
|
-
let requiresFullReconcile = false;
|
|
2564
|
-
for (const id of changedIds) {
|
|
2565
|
-
const source = this._objectMap.get(id);
|
|
2566
|
-
const next = source && constraintsOf(source).length > 0 ? source : null;
|
|
2567
|
-
const previous = this._constraintHelpers.get(id);
|
|
2568
|
-
if (previous && previous.source !== next) {
|
|
2569
|
-
if (next === null) requiresFullReconcile = true;
|
|
2570
|
-
this._disposeConstraintHelper(previous);
|
|
2571
|
-
this._constraintHelpers.delete(id);
|
|
2572
|
-
}
|
|
2573
|
-
if (
|
|
2574
|
-
next &&
|
|
2575
|
-
![...this._constraintHelpers.values()].some((helper) => helper.source === next)
|
|
2576
|
-
) {
|
|
2577
|
-
this._ensureConstraintHelper(id, next);
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
if (requiresFullReconcile) {
|
|
2581
|
-
this._syncConstraintHelpers();
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
2048
|
}
|
|
2585
2049
|
|
|
2586
2050
|
const selectedObjects = new Set<THREE.Object3D>();
|
|
@@ -2698,45 +2162,19 @@ export class EditorViewport {
|
|
|
2698
2162
|
}
|
|
2699
2163
|
|
|
2700
2164
|
/** Native camera frustums for the ordinary Helpers → Cameras overlay. */
|
|
2701
|
-
private _syncCameraHelpers(
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
let requiresFullReconcile = false;
|
|
2715
|
-
for (const id of changedIds) {
|
|
2716
|
-
const object = this._objectMap.get(id);
|
|
2717
|
-
const next = object ? this._nativeCamera(object) : null;
|
|
2718
|
-
const previous = this._cameraHelpers.get(id);
|
|
2719
|
-
if (previous && previous.source !== next) {
|
|
2720
|
-
if (next === null) requiresFullReconcile = true;
|
|
2721
|
-
this._disposeCameraHelper(id, previous);
|
|
2722
|
-
}
|
|
2723
|
-
if (next && ![...this._cameraHelpers.values()].some((entry) => entry.source === next)) {
|
|
2724
|
-
this._ensureCameraHelper(id, next);
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
if (requiresFullReconcile) {
|
|
2728
|
-
this._syncCameraHelpers();
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
// PERSISTENT, like every other engine's camera icon: Unity, Unreal, Godot
|
|
2733
|
-
// and Blender all draw a camera in the viewport whether or not it is
|
|
2734
|
-
// selected — that drawing is HOW you find it and click it. Gating it on a
|
|
2735
|
-
// three selection meant a scene with nothing selected showed no cameras at
|
|
2736
|
-
// all, and the only way to see one was to already have found it in the
|
|
2737
|
-
// hierarchy. The surface gate is the same one the grid uses ("Blender's
|
|
2738
|
-
// floor is persistent; only the gizmos need an owner"); a camera icon is
|
|
2739
|
-
// furniture, not a gizmo.
|
|
2165
|
+
private _syncCameraHelpers(): void {
|
|
2166
|
+
const current = new Map<string, THREE.Camera>();
|
|
2167
|
+
for (const [id, object] of this._objectMap) {
|
|
2168
|
+
const camera = this._nativeCamera(object);
|
|
2169
|
+
if (camera) current.set(id, camera);
|
|
2170
|
+
}
|
|
2171
|
+
for (const [id, entry] of this._cameraHelpers) {
|
|
2172
|
+
if (current.get(id) === entry.source) continue;
|
|
2173
|
+
this._disposeCameraHelper(id, entry);
|
|
2174
|
+
}
|
|
2175
|
+
for (const [id, camera] of current) this._ensureCameraHelper(id, camera);
|
|
2176
|
+
// Keep camera icons visible with the stage’s persistent furniture so unselected cameras can be
|
|
2177
|
+
// found and selected.
|
|
2740
2178
|
const visible =
|
|
2741
2179
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.cameras;
|
|
2742
2180
|
for (const entry of this._cameraHelpers.values()) {
|
|
@@ -2746,55 +2184,28 @@ export class EditorViewport {
|
|
|
2746
2184
|
}
|
|
2747
2185
|
|
|
2748
2186
|
/**
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
* Two testers asked for this in the same words: with a directional light
|
|
2754
|
-
* selected there was no way to tell which way it threw ("is it on the left
|
|
2755
|
-
* side, the right, downwards?" — runhuman passes 68/70), and a point light
|
|
2756
|
-
* showed only its transform. Same shape as `_syncCameraHelpers`: one
|
|
2757
|
-
* editor-owned overlay per native light, reconciled per notify, never in the
|
|
2758
|
-
* game's tree and never in Game view.
|
|
2187
|
+
* Reconcile one editor-owned three.js helper per native light when Helpers → Lights is enabled,
|
|
2188
|
+
* using the same overlay lifecycle as camera helpers. Helpers remain outside the authored tree
|
|
2189
|
+
* and hidden in Game view.
|
|
2759
2190
|
*/
|
|
2760
|
-
private _syncLightHelpers(
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
this._disposeLightHelper(id, entry);
|
|
2770
|
-
}
|
|
2771
|
-
for (const [id, light] of current) this._ensureLightHelper(id, light);
|
|
2772
|
-
} else {
|
|
2773
|
-
let requiresFullReconcile = false;
|
|
2774
|
-
for (const id of changedIds) {
|
|
2775
|
-
const object = this._objectMap.get(id);
|
|
2776
|
-
const next = object ? this._nativeLight(object) : null;
|
|
2777
|
-
const previous = this._lightHelpers.get(id);
|
|
2778
|
-
if (previous && previous.source !== next) {
|
|
2779
|
-
if (next === null) requiresFullReconcile = true;
|
|
2780
|
-
this._disposeLightHelper(id, previous);
|
|
2781
|
-
}
|
|
2782
|
-
if (next && ![...this._lightHelpers.values()].some((entry) => entry.source === next)) {
|
|
2783
|
-
this._ensureLightHelper(id, next);
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
if (requiresFullReconcile) {
|
|
2787
|
-
this._syncLightHelpers();
|
|
2788
|
-
return;
|
|
2789
|
-
}
|
|
2191
|
+
private _syncLightHelpers(): void {
|
|
2192
|
+
const current = new Map<string, THREE.Light>();
|
|
2193
|
+
for (const [id, object] of this._objectMap) {
|
|
2194
|
+
const light = this._nativeLight(object);
|
|
2195
|
+
if (light) current.set(id, light);
|
|
2196
|
+
}
|
|
2197
|
+
for (const [id, entry] of this._lightHelpers) {
|
|
2198
|
+
if (current.get(id) === entry.source) continue;
|
|
2199
|
+
this._disposeLightHelper(id, entry);
|
|
2790
2200
|
}
|
|
2201
|
+
for (const [id, light] of current) this._ensureLightHelper(id, light);
|
|
2791
2202
|
// Persistent for the same reason as the camera frustum above: a light you
|
|
2792
2203
|
// have not selected is exactly the light you are looking for.
|
|
2793
2204
|
const visible =
|
|
2794
2205
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.lights;
|
|
2795
2206
|
for (const entry of this._lightHelpers.values()) {
|
|
2796
2207
|
entry.helper.visible = visible;
|
|
2797
|
-
entry.helper.update
|
|
2208
|
+
entry.helper.update();
|
|
2798
2209
|
}
|
|
2799
2210
|
}
|
|
2800
2211
|
|
|
@@ -2807,20 +2218,18 @@ export class EditorViewport {
|
|
|
2807
2218
|
|
|
2808
2219
|
private _disposeLightHelper(
|
|
2809
2220
|
id: string,
|
|
2810
|
-
entry: {
|
|
2221
|
+
entry: { helper: THREE.Object3D & { dispose(): void } },
|
|
2811
2222
|
): void {
|
|
2812
2223
|
entry.helper.removeFromParent();
|
|
2813
|
-
entry.helper.dispose
|
|
2224
|
+
entry.helper.dispose();
|
|
2814
2225
|
this._lightHelpers.delete(id);
|
|
2815
2226
|
}
|
|
2816
2227
|
|
|
2817
2228
|
private _ensureLightHelper(id: string, light: THREE.Light): void {
|
|
2818
2229
|
if (this._lightHelpers.has(id)) return;
|
|
2819
|
-
//
|
|
2820
|
-
// light class this `three` does not know gets no overlay rather than a
|
|
2821
|
-
// wrong one — the same narrow refusal the camera overlay makes.
|
|
2230
|
+
// Draw helpers for supported light classes, including hemisphere lights; ambient lights have no helper.
|
|
2822
2231
|
const size = 0.5;
|
|
2823
|
-
let helper: (THREE.Object3D & { update
|
|
2232
|
+
let helper: (THREE.Object3D & { update(): void; dispose(): void }) | null = null;
|
|
2824
2233
|
const candidate = light as THREE.Light & {
|
|
2825
2234
|
isDirectionalLight?: boolean;
|
|
2826
2235
|
isPointLight?: boolean;
|
|
@@ -2844,26 +2253,13 @@ export class EditorViewport {
|
|
|
2844
2253
|
helper.name = `Light helper: ${light.name || id}`;
|
|
2845
2254
|
helper.userData['editorOnly'] = true;
|
|
2846
2255
|
helper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
2847
|
-
helper.layers.set(EDITOR_LAYER);
|
|
2848
2256
|
this._lightHelpers.set(id, { source: light, helper });
|
|
2849
2257
|
this._scene.add(helper);
|
|
2850
2258
|
}
|
|
2851
2259
|
|
|
2852
|
-
/**
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
*
|
|
2856
|
-
* A listener and a positional source have a place in the world and no
|
|
2857
|
-
* geometry, so without an overlay they are invisible in the viewport: the
|
|
2858
|
-
* hierarchy lists them and the scene shows nothing where they are. This
|
|
2859
|
-
* small marker locates each source. Selected-source distance and direction
|
|
2860
|
-
* guides belong to the adapter's spatial-handle provider, which understands
|
|
2861
|
-
* the native audio model and owns the corresponding edits. Markers follow
|
|
2862
|
-
* the same lifecycle as `_syncLightHelpers`: editor-owned, on the editor
|
|
2863
|
-
* layer, never in the game's tree and never in Game view.
|
|
2864
|
-
*/
|
|
2865
|
-
private _syncAudioHelpers(changedIds: ReadonlySet<string> | null = null): void {
|
|
2866
|
-
if (changedIds === null) {
|
|
2260
|
+
/** Locate native audio listeners/sources with editor-only markers. Adapter spatial handles own distance/direction edits. */
|
|
2261
|
+
private _syncAudioHelpers(reconcileMembership: boolean): void {
|
|
2262
|
+
if (reconcileMembership) {
|
|
2867
2263
|
const current = new Map<string, THREE.Object3D>();
|
|
2868
2264
|
for (const [id, object] of this._objectMap) {
|
|
2869
2265
|
const audio = this._nativeAudio(object);
|
|
@@ -2874,16 +2270,6 @@ export class EditorViewport {
|
|
|
2874
2270
|
this._disposeAudioHelper(id, entry);
|
|
2875
2271
|
}
|
|
2876
2272
|
for (const [id, audio] of current) this._ensureAudioHelper(id, audio);
|
|
2877
|
-
} else {
|
|
2878
|
-
for (const id of changedIds) {
|
|
2879
|
-
const object = this._objectMap.get(id);
|
|
2880
|
-
const next = object ? this._nativeAudio(object) : null;
|
|
2881
|
-
const previous = this._audioHelpers.get(id);
|
|
2882
|
-
if (previous && previous.source !== next) this._disposeAudioHelper(id, previous);
|
|
2883
|
-
if (next && ![...this._audioHelpers.values()].some((entry) => entry.source === next)) {
|
|
2884
|
-
this._ensureAudioHelper(id, next);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
2273
|
}
|
|
2888
2274
|
const visible =
|
|
2889
2275
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.audio;
|
|
@@ -2912,22 +2298,20 @@ export class EditorViewport {
|
|
|
2912
2298
|
helper.name = `Audio helper: ${source.name || id}`;
|
|
2913
2299
|
helper.userData['editorOnly'] = true;
|
|
2914
2300
|
helper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
2915
|
-
|
|
2916
|
-
// Parented to the source so it rides the same transform: a footstep
|
|
2917
|
-
// emitter on a walking character must not leave its ring behind.
|
|
2301
|
+
// Parent the marker to its source so it follows the source's transform.
|
|
2918
2302
|
source.add(helper);
|
|
2919
2303
|
this._audioHelpers.set(id, { source, helper });
|
|
2920
2304
|
}
|
|
2921
2305
|
|
|
2922
2306
|
private _disposeAudioHelper(
|
|
2923
2307
|
id: string,
|
|
2924
|
-
entry: {
|
|
2308
|
+
entry: { helper: THREE.Group },
|
|
2925
2309
|
): void {
|
|
2926
2310
|
entry.helper.traverse((child) => {
|
|
2927
2311
|
const mesh = child as THREE.Mesh;
|
|
2928
|
-
mesh.geometry?.dispose
|
|
2312
|
+
mesh.geometry?.dispose();
|
|
2929
2313
|
const material = mesh.material as THREE.Material | undefined;
|
|
2930
|
-
material?.dispose
|
|
2314
|
+
material?.dispose();
|
|
2931
2315
|
});
|
|
2932
2316
|
entry.helper.removeFromParent();
|
|
2933
2317
|
this._audioHelpers.delete(id);
|
|
@@ -2935,7 +2319,7 @@ export class EditorViewport {
|
|
|
2935
2319
|
|
|
2936
2320
|
private _disposeCameraHelper(
|
|
2937
2321
|
id: string,
|
|
2938
|
-
entry: {
|
|
2322
|
+
entry: { helper: THREE.CameraHelper },
|
|
2939
2323
|
): void {
|
|
2940
2324
|
entry.helper.removeFromParent();
|
|
2941
2325
|
entry.helper.dispose();
|
|
@@ -2944,9 +2328,7 @@ export class EditorViewport {
|
|
|
2944
2328
|
|
|
2945
2329
|
private _ensureCameraHelper(id: string, camera: THREE.Camera): void {
|
|
2946
2330
|
if (this._cameraHelpers.has(id)) return;
|
|
2947
|
-
//
|
|
2948
|
-
// its own, older `three`. The failure is the discriminator; every compatible camera keeps its
|
|
2949
|
-
// overlay, while an incompatible revision records the narrow refusal below.
|
|
2331
|
+
// Project camera helpers may come from a separate three instance. Keep compatible overlays and record a narrow refusal when CameraHelper cannot read the camera.
|
|
2950
2332
|
let helper: THREE.CameraHelper;
|
|
2951
2333
|
try {
|
|
2952
2334
|
helper = new THREE.CameraHelper(camera);
|
|
@@ -2968,12 +2350,7 @@ export class EditorViewport {
|
|
|
2968
2350
|
this._refusedCameraOverlays.add(id);
|
|
2969
2351
|
editorConsole.warn(
|
|
2970
2352
|
`Helpers → Cameras draws no frustum for "${camera.name || camera.type}": ` +
|
|
2971
|
-
`${error instanceof Error ? error.message : String(error)}.
|
|
2972
|
-
"constructed by the GAME's own copy of three, and `THREE.CameraHelper` is the editor's " +
|
|
2973
|
-
'class reading fields off it that that revision does not define. Every other camera ' +
|
|
2974
|
-
'capability (framing, selection, inspection) is unaffected. It closes when the game ' +
|
|
2975
|
-
"resolves `three` to the host's instance — a bare, un-rewritten `import 'three'` in its " +
|
|
2976
|
-
'own source.',
|
|
2353
|
+
`${error instanceof Error ? error.message : String(error)}. Framing, selection and inspection remain available.`,
|
|
2977
2354
|
'authoring',
|
|
2978
2355
|
);
|
|
2979
2356
|
}
|
|
@@ -3040,13 +2417,13 @@ export class EditorViewport {
|
|
|
3040
2417
|
}
|
|
3041
2418
|
|
|
3042
2419
|
private _finishSpatialHandleDrag(commit: boolean): void {
|
|
3043
|
-
const active = this._activeSpatialHandle
|
|
2420
|
+
const active = this._activeSpatialHandle!;
|
|
3044
2421
|
this._activeSpatialHandle = null;
|
|
3045
2422
|
this.orbitControls.enabled = this._orbitEnabledBeforeHandleDrag;
|
|
3046
2423
|
this.transformControls.enabled = this._transformEnabledBeforeHandleDrag;
|
|
3047
2424
|
this.syncFromStore();
|
|
3048
2425
|
this._canvas.style.cursor = '';
|
|
3049
|
-
if (commit
|
|
2426
|
+
if (commit) {
|
|
3050
2427
|
void spatialHandlesForAdapter(this._authoring())?.commit(active.nodeId, active.handle.id, [
|
|
3051
2428
|
this._spatialHandleDragPoint.x,
|
|
3052
2429
|
this._spatialHandleDragPoint.y,
|
|
@@ -3084,41 +2461,34 @@ export class EditorViewport {
|
|
|
3084
2461
|
}
|
|
3085
2462
|
setSpatialHandleHovered(hovered, true);
|
|
3086
2463
|
const binding = spatialHandleBinding(hovered);
|
|
3087
|
-
this._canvas.style.cursor = binding
|
|
2464
|
+
this._canvas.style.cursor = binding.handle.writable ? 'grab' : 'not-allowed';
|
|
3088
2465
|
}
|
|
3089
2466
|
|
|
3090
2467
|
private _updateEditorHandleInteraction(e: PointerEvent): boolean {
|
|
3091
2468
|
return this._updateSpatialHandleDrag(e) || this._updateConstraintControlHover(e);
|
|
3092
2469
|
}
|
|
3093
2470
|
|
|
3094
|
-
private
|
|
3095
|
-
return [
|
|
3096
|
-
this.grid,
|
|
3097
|
-
...(this._axisLines ? [this._axisLines] : []),
|
|
3098
|
-
this._gizmoHelper,
|
|
3099
|
-
this._auxRotateControls.getHelper(),
|
|
3100
|
-
this._auxScaleControls.getHelper(),
|
|
3101
|
-
...this._boxHelpers.values(),
|
|
3102
|
-
...this._constraintHelpers.values(),
|
|
3103
|
-
...this._reflectionProbeHelpers.values(),
|
|
3104
|
-
...this._triggerVolumeHelpers.values(),
|
|
3105
|
-
...[...this._cameraHelpers.values()].map(({ helper }) => helper),
|
|
3106
|
-
...this._spatialHandleRoots,
|
|
3107
|
-
];
|
|
2471
|
+
private _allGizmos(): TransformControls[] {
|
|
2472
|
+
return [this.transformControls, this._auxRotateControls, this._auxScaleControls];
|
|
3108
2473
|
}
|
|
3109
2474
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
);
|
|
3114
|
-
for (const [object] of visibility) object.visible = false;
|
|
3115
|
-
return () => {
|
|
3116
|
-
for (const [object, visible] of visibility) object.visible = visible;
|
|
3117
|
-
};
|
|
2475
|
+
/** A drag is under way: a transform gizmo, a spatial handle or a constraint control. Every
|
|
2476
|
+
* frame of it is a change of the picture (`StageHost`), whatever the drag signals. */
|
|
2477
|
+
interacting(): boolean {
|
|
2478
|
+
return this._anyGizmoDragging() || this._activeSpatialHandle !== null || this._constraintControlPointerSession;
|
|
3118
2479
|
}
|
|
3119
2480
|
|
|
3120
|
-
|
|
3121
|
-
|
|
2481
|
+
/**
|
|
2482
|
+
* WHAT INPUT CHANGES IN THE STAGE'S OWN FURNITURE: the hovered constraint control and spatial
|
|
2483
|
+
* handle, and each transform gizmo's shown state, highlighted axis and drag. A stage counts a
|
|
2484
|
+
* pointer move as a change of its picture only when this (or the camera) moves, so hovering
|
|
2485
|
+
* over a still view does not start its accumulation over (`StageHost`).
|
|
2486
|
+
*/
|
|
2487
|
+
interactionState(): string {
|
|
2488
|
+
const gizmos = this._allGizmos()
|
|
2489
|
+
.map((controls) => `${controls.getHelper().visible ? 1 : 0}${controls.enabled ? 1 : 0}${controls.axis ?? '-'}${controls.dragging ? 1 : 0}`)
|
|
2490
|
+
.join(',');
|
|
2491
|
+
return `${this._hoveredConstraintControl?.mesh.uuid ?? '-'}|${this._hoveredSpatialHandle?.uuid ?? '-'}|${gizmos}`;
|
|
3122
2492
|
}
|
|
3123
2493
|
|
|
3124
2494
|
private _anyGizmoDragging(): boolean {
|
|
@@ -3137,14 +2507,9 @@ export class EditorViewport {
|
|
|
3137
2507
|
|
|
3138
2508
|
/** Sync mode/space/snap from store. */
|
|
3139
2509
|
syncFromStore(): void {
|
|
3140
|
-
// Ensure all gizmo children stay on EDITOR_LAYER (TransformControls rebuilds internally)
|
|
3141
|
-
this._gizmoHelper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
3142
|
-
|
|
3143
2510
|
const mode = this._store.shell.transformMode;
|
|
3144
2511
|
const combined = mode === 'combined';
|
|
3145
|
-
//
|
|
3146
|
-
// note), so there is no handle family to put the primary controls in; it
|
|
3147
|
-
// keeps whatever it had and the attach block below detaches instead.
|
|
2512
|
+
// Select uses translate mode with the controls detached, so it draws no transform gizmo.
|
|
3148
2513
|
const primaryMode = combined || mode === 'select' ? 'translate' : mode;
|
|
3149
2514
|
if (this.transformControls.mode !== primaryMode) {
|
|
3150
2515
|
this.transformControls.setMode(primaryMode);
|
|
@@ -3152,18 +2517,14 @@ export class EditorViewport {
|
|
|
3152
2517
|
this._applySnapToGizmos();
|
|
3153
2518
|
for (const controls of this._allGizmos()) controls.setSpace(this._store.shell.transformSpace);
|
|
3154
2519
|
|
|
3155
|
-
//
|
|
3156
|
-
//
|
|
3157
|
-
// The grid, view cube and transform gizmo require BOTH.
|
|
3158
|
-
// A pinned camera preview remains visible without a selection.
|
|
3159
|
-
// Empty/organizational/cross-world selections deliberately leave only
|
|
3160
|
-
// shell chrome rather than advertising an arbitrary coordinate system.
|
|
2520
|
+
// Enable transform tools only for the visible three.js stage that owns this selection.
|
|
2521
|
+
// Camera previews and persistent furniture have their own visibility gates.
|
|
3161
2522
|
const policy = viewportAuthoringPolicy();
|
|
3162
2523
|
const authoring = this._authoring();
|
|
3163
2524
|
const toolOwner = policy.toolOwner(authoring, this._store.shell.selectedEntityIds);
|
|
3164
2525
|
this._threejsToolContextActive =
|
|
3165
2526
|
(this._standaloneAuthoring || policy.toolOwnerPainted(this._store.shell, toolOwner)) &&
|
|
3166
|
-
toolOwner?.kind === '
|
|
2527
|
+
toolOwner?.kind === 'stage';
|
|
3167
2528
|
|
|
3168
2529
|
// Grid visibility — the user's toggle, AND'd with a three stage showing at
|
|
3169
2530
|
// all (selection or not: Blender's floor is persistent; the gizmos above
|
|
@@ -3174,30 +2535,12 @@ export class EditorViewport {
|
|
|
3174
2535
|
this.grid.visible = this._presentationGrid && threeSurface;
|
|
3175
2536
|
if (this._axisLines) this._axisLines.visible = this.grid.visible && this._axesWanted;
|
|
3176
2537
|
|
|
3177
|
-
//
|
|
3178
|
-
//
|
|
3179
|
-
// membership and the visibility toggles — none of which change during a
|
|
3180
|
-
// transform drag/scrub (which still fires syncFromStore every frame). Gate
|
|
3181
|
-
// them on a cheap signature so a drag doesn't re-walk the whole graph 60×/s.
|
|
2538
|
+
// Reconcile helper membership, icon visibility and camera/light helpers when
|
|
2539
|
+
// the object map, gizmo epoch, helper settings or stage visibility changes.
|
|
3182
2540
|
const vis = this._store.shell.helperVisibility;
|
|
3183
|
-
// World-hidden eye (D9) folded into the SAME cheap signature/gate as the
|
|
3184
|
-
// gizmo/helper passes below — see `world-hidden-viewport.ts`'s doc
|
|
3185
|
-
// comment for why this must be recomputed from scratch on every rebuild
|
|
3186
|
-
// (never a one-shot flip) and why folding it in here (rather than a
|
|
3187
|
-
// separate always-on pass) is safe: the toggle itself
|
|
3188
|
-
// (`GameHierarchy.tsx`'s `toggleRootHidden` + `store.notifyIngestEdit()`)
|
|
3189
|
-
// changes `hiddenRootId`'s hidden state, which changes this signature,
|
|
3190
|
-
// so it re-applies on the very next `syncFromStore()` call — exactly the
|
|
3191
|
-
// same notify path that already re-triggers this whole method.
|
|
3192
2541
|
const hiddenRootId = this._standaloneAuthoring ? null : policy.threeViewportRootId(this._store.shell);
|
|
3193
2542
|
const hidden = hiddenRootId !== null && isRootHidden(hiddenRootId);
|
|
3194
|
-
//
|
|
3195
|
-
// SINGLE-entity rebuild (scene-sync `updateEntityPreview`) mutates the
|
|
3196
|
-
// existing objectMap in place — same instance, same size, so the gate
|
|
3197
|
-
// below never re-fires — while re-stamping the rebuilt object visible
|
|
3198
|
-
// from its descriptor. Cost is one map walk per notify only while a
|
|
3199
|
-
// world is actually hidden; the gated call below still handles the
|
|
3200
|
-
// hide→show transition (restoring per-entity visibility).
|
|
2543
|
+
// Reapply hidden state on each notification, including in-place object replacements.
|
|
3201
2544
|
if (hidden) policy.applyRootHiddenVisibility(this._objectMap, hiddenRootId);
|
|
3202
2545
|
// Scene-LEVEL environment (background/skybox/fog/IBL). Nothing here has to
|
|
3203
2546
|
// RESTORE it on the show edge — an adapter-backed world paints its own
|
|
@@ -3205,33 +2548,17 @@ export class EditorViewport {
|
|
|
3205
2548
|
// same rationale as the per-notify `applyRootHiddenVisibility` above.
|
|
3206
2549
|
if (hidden) policy.suppressRootEnvironment(this._scene);
|
|
3207
2550
|
const gizmoEpoch = this._store.gizmoEpoch;
|
|
3208
|
-
const gizmoSig = `${this._store.shell.showHelpers}|${
|
|
2551
|
+
const gizmoSig = `${this._store.shell.showHelpers}|${JSON.stringify(vis)}|${threeSurface}`;
|
|
3209
2552
|
const mapChanged = this._objectMap !== this._lastGizmoObjectMap;
|
|
3210
2553
|
const settingsChanged = gizmoSig !== this._lastGizmoSig;
|
|
3211
|
-
const
|
|
3212
|
-
|
|
3213
|
-
: this._store.ingestObjectMapMembershipChangesSince(this._lastGizmoEpoch);
|
|
3214
|
-
const changedIds =
|
|
3215
|
-
mapChanged || settingsChanged || membershipChanges === null
|
|
3216
|
-
? null
|
|
3217
|
-
: membershipChanges.changedIds;
|
|
3218
|
-
const reconcileMembership = changedIds === null || changedIds.size > 0;
|
|
2554
|
+
const reconcileMembership =
|
|
2555
|
+
mapChanged || settingsChanged || gizmoEpoch !== this._lastGizmoEpoch;
|
|
3219
2556
|
this._lastGizmoObjectMap = this._objectMap;
|
|
3220
2557
|
this._lastGizmoSig = gizmoSig;
|
|
3221
2558
|
this._lastGizmoEpoch = gizmoEpoch;
|
|
3222
2559
|
if (reconcileMembership) {
|
|
3223
|
-
// Threejs roots have no DOM layer to `display:none` (unlike react/pixi
|
|
3224
|
-
// — `design-time-layers.ts`'s `applySessionStyle`), so this is what
|
|
3225
|
-
// actually makes a hidden three world's group-row eye hide its
|
|
3226
|
-
// objects in THIS viewport. ABOVE the per-entity `visible` flag (ANDed
|
|
3227
|
-
// in, never overwritten) — see `world-hidden-viewport.ts`.
|
|
3228
|
-
policy.applyRootHiddenVisibility(this._objectMap, hiddenRootId);
|
|
3229
|
-
|
|
3230
2560
|
// Helper + icon visibility — per-type filtering
|
|
3231
|
-
const helperRoots =
|
|
3232
|
-
changedIds === null
|
|
3233
|
-
? topmostPublishedObjects(this._objectMap)
|
|
3234
|
-
: topmostChangedObjects(this._objectMap, changedIds);
|
|
2561
|
+
const helperRoots = topmostPublishedObjects(this._objectMap);
|
|
3235
2562
|
for (const obj of helperRoots) {
|
|
3236
2563
|
obj.traverse((child) => {
|
|
3237
2564
|
if (getUserData(child, 'editorIcon')) {
|
|
@@ -3261,16 +2588,16 @@ export class EditorViewport {
|
|
|
3261
2588
|
helper.visible =
|
|
3262
2589
|
this._store.shell.showHelpers && (!(kind in vis) || (vis as Record<string, boolean>)[kind]!);
|
|
3263
2590
|
}
|
|
3264
|
-
this._syncCameraHelpers(
|
|
3265
|
-
this._syncLightHelpers(
|
|
2591
|
+
this._syncCameraHelpers();
|
|
2592
|
+
this._syncLightHelpers();
|
|
3266
2593
|
}
|
|
3267
2594
|
|
|
3268
|
-
this._syncAudioHelpers(
|
|
3269
|
-
this._syncReflectionProbeHelpers(
|
|
3270
|
-
this._syncTriggerVolumeHelpers(
|
|
3271
|
-
this._syncConstraintHelpers(
|
|
2595
|
+
this._syncAudioHelpers(reconcileMembership);
|
|
2596
|
+
this._syncReflectionProbeHelpers(reconcileMembership);
|
|
2597
|
+
this._syncTriggerVolumeHelpers(reconcileMembership);
|
|
2598
|
+
this._syncConstraintHelpers(reconcileMembership);
|
|
3272
2599
|
|
|
3273
|
-
// Attach gizmo based on pivot mode.
|
|
2600
|
+
// Attach gizmo based on pivot mode. No gizmo without a visible
|
|
3274
2601
|
// three surface — this whole block re-derives per notify, so detaching
|
|
3275
2602
|
// here is self-healing (the un-hide notify re-attaches to the same
|
|
3276
2603
|
// selection with no extra bookkeeping).
|
|
@@ -3284,8 +2611,7 @@ export class EditorViewport {
|
|
|
3284
2611
|
// Combined mode filters/attaches on POSITION (its median-filter channel)
|
|
3285
2612
|
// and attaches when ANY channel is writable — a scale-locked prefab
|
|
3286
2613
|
// instance still gets move+rotate handles instead of no gizmo at all
|
|
3287
|
-
// (
|
|
3288
|
-
// an empty viewport with no visible refusal).
|
|
2614
|
+
// (an empty viewport with no visible refusal).
|
|
3289
2615
|
const activeTransformChannel =
|
|
3290
2616
|
mode === 'translate' || mode === 'combined'
|
|
3291
2617
|
? 'position'
|
|
@@ -3315,12 +2641,8 @@ export class EditorViewport {
|
|
|
3315
2641
|
}
|
|
3316
2642
|
const obj = this._objectForAuthoringId(id);
|
|
3317
2643
|
if (obj) {
|
|
3318
|
-
//
|
|
3319
|
-
//
|
|
3320
|
-
// wrong for any nested node, and it is wrong a second way for a
|
|
3321
|
-
// node whose driver owns its matrix (`authoring/
|
|
3322
|
-
// live-object-transform.ts`: `.position` is frozen at spawn while
|
|
3323
|
-
// the object renders wherever its matrix puts it).
|
|
2644
|
+
// Compute the scene-root pivot from world matrices. Local position fields do not
|
|
2645
|
+
// describe nested nodes or objects driven directly through their matrices.
|
|
3324
2646
|
median.add(obj.getWorldPosition(_medianTmp));
|
|
3325
2647
|
count++;
|
|
3326
2648
|
}
|
|
@@ -3355,8 +2677,8 @@ export class EditorViewport {
|
|
|
3355
2677
|
// `matrixWorld` (three's `getWorld*` decompose it), never from the
|
|
3356
2678
|
// vector fields: a node whose driver owns its matrix keeps
|
|
3357
2679
|
// `.position`/`.quaternion` frozen at spawn, so anchoring on those
|
|
3358
|
-
// put the gizmo where the object
|
|
3359
|
-
// where it is (`authoring/live-object-transform.ts`).
|
|
2680
|
+
// would put the gizmo where the object was at spawn while its bounds
|
|
2681
|
+
// draw where it is (`authoring/live-object-transform.ts`).
|
|
3360
2682
|
const pivotLocal = new THREE.Vector3(entityPivot[0], entityPivot[1], entityPivot[2]);
|
|
3361
2683
|
pivotLocal.applyQuaternion(obj.getWorldQuaternion(_pivotQuat));
|
|
3362
2684
|
this._pivotDummy.position.copy(obj.getWorldPosition(_pivotPos)).add(pivotLocal);
|
|
@@ -3381,13 +2703,6 @@ export class EditorViewport {
|
|
|
3381
2703
|
this._syncCombinedGizmoHalves();
|
|
3382
2704
|
}
|
|
3383
2705
|
|
|
3384
|
-
/**
|
|
3385
|
-
* The combined gizmo's halves: shown only in combined mode, each half only
|
|
3386
|
-
* where its channel is WRITABLE for the attached entity — a prefab instance
|
|
3387
|
-
* with a locked scale (the starter's Hero Box) shows move+rotate and
|
|
3388
|
-
* honestly no scale handles, instead of a silently dead gizmo. Runs AFTER
|
|
3389
|
-
* the attach block: it reads the attachment this same pass just made.
|
|
3390
|
-
*/
|
|
3391
2706
|
/** Snap values reach the gizmos from the store's toggle OR a held
|
|
3392
2707
|
* Ctrl/⌘ (the Unity/Blender hold-to-snap convention) — three's controls
|
|
3393
2708
|
* read the snap on every pointer move, so a mid-drag hold takes effect. */
|
|
@@ -3407,7 +2722,7 @@ export class EditorViewport {
|
|
|
3407
2722
|
private _modifiedZoom = false;
|
|
3408
2723
|
|
|
3409
2724
|
private readonly _onAltOrbitStart = (e: PointerEvent): void => {
|
|
3410
|
-
if (e.button === 1 && (e.ctrlKey || e.metaKey) &&
|
|
2725
|
+
if (e.button === 1 && (e.ctrlKey || e.metaKey) && editorHost().keyboard.navigation().orbit === 'middle') {
|
|
3411
2726
|
this._modifiedZoom = true;
|
|
3412
2727
|
this.orbitControls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY;
|
|
3413
2728
|
}
|
|
@@ -3417,9 +2732,9 @@ export class EditorViewport {
|
|
|
3417
2732
|
}
|
|
3418
2733
|
};
|
|
3419
2734
|
|
|
3420
|
-
private readonly _onAltOrbitRelease = (event
|
|
2735
|
+
private readonly _onAltOrbitRelease = (event: PointerEvent): void => {
|
|
3421
2736
|
// The zoom is the middle button's gesture, and ends with that button's release.
|
|
3422
|
-
if (this._modifiedZoom && (
|
|
2737
|
+
if (this._modifiedZoom && (event.type === 'pointercancel' || event.button === 1)) {
|
|
3423
2738
|
this._modifiedZoom = false;
|
|
3424
2739
|
this.applyKeymapNavigation();
|
|
3425
2740
|
}
|
|
@@ -3435,14 +2750,15 @@ export class EditorViewport {
|
|
|
3435
2750
|
if (hold === this._snapHold) return;
|
|
3436
2751
|
this._snapHold = hold;
|
|
3437
2752
|
this._applySnapToGizmos();
|
|
3438
|
-
//
|
|
3439
|
-
// snapping (human pass 10 said exactly that) — name it while it happens.
|
|
2753
|
+
// Show a hint when the modifier temporarily enables snapping during a drag.
|
|
3440
2754
|
if (hold && this._anyGizmoDragging() && !this._store.shell.snapEnabled) {
|
|
3441
2755
|
const { translate, rotate, scale } = this._store.shell.snapValues;
|
|
3442
2756
|
showTransientHint(`Snapping while held: ${translate} units · ${rotate}° · ×${scale}`);
|
|
3443
2757
|
}
|
|
3444
2758
|
};
|
|
3445
2759
|
|
|
2760
|
+
/** In combined mode, show each transform half only when its channel is
|
|
2761
|
+
* writable for the entity attached by the current sync pass. */
|
|
3446
2762
|
private _syncCombinedGizmoHalves(): void {
|
|
3447
2763
|
if (this._anyGizmoDragging()) return;
|
|
3448
2764
|
const combined = this._store.shell.transformMode === 'combined';
|
|
@@ -3463,17 +2779,9 @@ export class EditorViewport {
|
|
|
3463
2779
|
const scaleHelper = this._auxScaleControls.getHelper();
|
|
3464
2780
|
rotateHelper.visible = rotateOn;
|
|
3465
2781
|
scaleHelper.visible = scaleOn;
|
|
3466
|
-
for (const helper of [rotateHelper, scaleHelper]) {
|
|
3467
|
-
helper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
3468
|
-
}
|
|
3469
2782
|
this.transformControls.enabled = true;
|
|
3470
2783
|
}
|
|
3471
2784
|
|
|
3472
|
-
/**
|
|
3473
|
-
* Re-add editor infrastructure objects to the scene after play mode
|
|
3474
|
-
* has cleared all scene children. Call this after loadDocument restores
|
|
3475
|
-
* entities so the grid, lights, gizmos, and particle renderer are present.
|
|
3476
|
-
*/
|
|
3477
2785
|
/** Move all editor gizmos from the editor scene into a target scene (e.g. the live game scene). */
|
|
3478
2786
|
moveGizmosTo(target: THREE.Scene): void {
|
|
3479
2787
|
const objs = this._gizmoObjects();
|
|
@@ -3505,22 +2813,14 @@ export class EditorViewport {
|
|
|
3505
2813
|
}
|
|
3506
2814
|
|
|
3507
2815
|
/**
|
|
3508
|
-
*
|
|
3509
|
-
*
|
|
3510
|
-
*
|
|
3511
|
-
* A live game keeps building itself long after the editor adopts it (a
|
|
3512
|
-
* level-streaming game parses its whole world in seconds after first
|
|
3513
|
-
* capture), so this cannot be a one-shot measurement at scene swap —
|
|
3514
|
-
* but neither can it run per frame over thousands of nodes. Cheap readers
|
|
3515
|
-
* (the per-frame clip planes) take the cache; {@link focusOnScene} forces a
|
|
3516
|
-
* fresh measurement, because framing on a stale box is exactly the miss it
|
|
3517
|
-
* exists to prevent.
|
|
2816
|
+
* Cache the active scene’s content AABB for CONTENT_BOUNDS_MAX_AGE_MS as its live graph changes.
|
|
2817
|
+
* Per-frame clip planes use the cached extent; focusOnScene forces a fresh measurement.
|
|
3518
2818
|
*/
|
|
3519
|
-
private _contentBounds(
|
|
2819
|
+
private _contentBounds(): THREE.Box3 {
|
|
3520
2820
|
const now = performance.now();
|
|
3521
2821
|
const stale =
|
|
3522
2822
|
this._contentBoundsAt === 0 || now - this._contentBoundsAt > CONTENT_BOUNDS_MAX_AGE_MS;
|
|
3523
|
-
if (
|
|
2823
|
+
if (stale) {
|
|
3524
2824
|
this._contentBoundsBox.makeEmpty();
|
|
3525
2825
|
for (const child of this._scene.children) {
|
|
3526
2826
|
if (isInEditorOwnedSubtree(child)) continue;
|
|
@@ -3551,15 +2851,9 @@ export class EditorViewport {
|
|
|
3551
2851
|
return boxes;
|
|
3552
2852
|
}
|
|
3553
2853
|
|
|
3554
|
-
/**
|
|
3555
|
-
* Grow the camera's clip planes to reach the content, every frame.
|
|
3556
|
-
*
|
|
3557
|
-
* See `viewport-clip-planes.ts` for the measured defect this closes: a world
|
|
3558
|
-
* larger than the frozen `far = 1000` was clipped away to an empty viewport.
|
|
3559
|
-
* Cheap — the bounds are cached, so a frame costs one distance and, only when
|
|
3560
|
-
* the planes actually move, one `updateProjectionMatrix`.
|
|
3561
|
-
*/
|
|
2854
|
+
/** Grow clip planes to reach cached content bounds. Update the projection only when the required planes change. */
|
|
3562
2855
|
private _updateClipPlanes(): void {
|
|
2856
|
+
if (this.projectionOwner.owned) return;
|
|
3563
2857
|
const box = this._contentBounds();
|
|
3564
2858
|
let distance = 0;
|
|
3565
2859
|
let radius = 0;
|
|
@@ -3586,14 +2880,9 @@ export class EditorViewport {
|
|
|
3586
2880
|
* Returns false — camera untouched — when that view cannot be used: the
|
|
3587
2881
|
* world is behind it, on it, or PRESSED AGAINST IT (a camera placed inside
|
|
3588
2882
|
* the geometry it is about to drive through is a fine gameplay camera and a
|
|
3589
|
-
* useless first look — `seededViewShowsWorld` is the measurement
|
|
3590
|
-
* caller falls back to the framing, which always shows something).
|
|
2883
|
+
* useless first look — `seededViewShowsWorld` is the measurement).
|
|
3591
2884
|
*
|
|
3592
|
-
* A caller
|
|
3593
|
-
* itself asynchronously, and Cuberun's own `<PerspectiveCamera makeDefault>`
|
|
3594
|
-
* is not in the scene at all on the frame the editor captures it (its
|
|
3595
|
-
* `<Suspense>` content has not resolved), so a seed attempted only at mount
|
|
3596
|
-
* misses the flagship case.
|
|
2885
|
+
* A caller can retry as asynchronous scene content and its camera mount.
|
|
3597
2886
|
*/
|
|
3598
2887
|
seedFromGameCamera(gameCamera: THREE.Camera): boolean {
|
|
3599
2888
|
const box = frameableContentBounds(this._measureContentBounds());
|
|
@@ -3647,8 +2936,8 @@ export class EditorViewport {
|
|
|
3647
2936
|
box.getSize(new THREE.Vector3()).x / this._viewportAspect,
|
|
3648
2937
|
) * 1.15;
|
|
3649
2938
|
this._syncOrthographicFromPerspective(true);
|
|
3650
|
-
// Before any draw: the framing distance for a world-sized scene
|
|
3651
|
-
//
|
|
2939
|
+
// Before any draw: the framing distance for a world-sized scene can land
|
|
2940
|
+
// past a stale far plane.
|
|
3652
2941
|
this._updateClipPlanes();
|
|
3653
2942
|
return true;
|
|
3654
2943
|
}
|
|
@@ -3752,7 +3041,7 @@ export class EditorViewport {
|
|
|
3752
3041
|
|
|
3753
3042
|
/**
|
|
3754
3043
|
* Show (or clear with null) the editor-only helper of `kind` —
|
|
3755
|
-
*
|
|
3044
|
+
* The object joins the editor
|
|
3756
3045
|
* scene as a scene-root helper (`editorHelper`, `editorHelperType = kind`)
|
|
3757
3046
|
* and follows the Helpers menu's toggle for its kind.
|
|
3758
3047
|
*/
|
|
@@ -3885,32 +3174,20 @@ export class EditorViewport {
|
|
|
3885
3174
|
// every draw. Nothing here should shadow that with a second source.
|
|
3886
3175
|
}
|
|
3887
3176
|
|
|
3888
|
-
/**
|
|
3889
|
-
* The presentation rig's lighting source for what the scene now holds: a view whose lighting is
|
|
3890
|
-
* `auto` gives way to an adopted scene's own lights. Every draw runs it; a stage whose document
|
|
3891
|
-
* is hidden (the game world during Play, drawn by the Game view) runs it too, so the editor's
|
|
3892
|
-
* studio lights never light the game.
|
|
3893
|
-
*/
|
|
3177
|
+
/** Resolve the current presentation source without applying the rig's own lights. */
|
|
3894
3178
|
resolvePresentation(): void {
|
|
3895
|
-
this._presentation?.rig.resolveSource({ light:
|
|
3179
|
+
this._presentation?.rig.resolveSource({ light: false });
|
|
3896
3180
|
}
|
|
3897
3181
|
|
|
3898
|
-
update(dt
|
|
3182
|
+
update(dt: number): void {
|
|
3899
3183
|
this._ensureSparkRenderer();
|
|
3900
3184
|
this._syncLookBackground();
|
|
3901
|
-
//
|
|
3902
|
-
// three then errors EVERY FRAME ("must be a part of the scene graph" —
|
|
3903
|
-
// human pass 10 recorded seven while authoring). Detach the moment the
|
|
3904
|
-
// attachment goes stale; the store-driven sync re-attaches when the
|
|
3905
|
-
// selection re-resolves onto the fresh object.
|
|
3185
|
+
// Detach stale gizmo objects after a remount, then resolve the current selection again.
|
|
3906
3186
|
const attachedObject = this.transformControls.object;
|
|
3907
3187
|
if (attachedObject && attachedObject !== this._pivotDummy && !attachedObject.parent) {
|
|
3908
3188
|
this._detachGizmos();
|
|
3909
3189
|
this.syncFromStore();
|
|
3910
3190
|
}
|
|
3911
|
-
// Animate custom (shader) materials in edit mode — same tick the runtime
|
|
3912
|
-
// preRender phase runs, so authored shaders preview live in the viewport.
|
|
3913
|
-
|
|
3914
3191
|
// Fly camera movement (before orbit so it takes priority)
|
|
3915
3192
|
this._updateFlyCamera(dt);
|
|
3916
3193
|
|
|
@@ -3973,21 +3250,9 @@ export class EditorViewport {
|
|
|
3973
3250
|
}
|
|
3974
3251
|
|
|
3975
3252
|
/**
|
|
3976
|
-
*
|
|
3977
|
-
*
|
|
3978
|
-
*
|
|
3979
|
-
* time would silently un-pin — per-frame enforcement over a map that is
|
|
3980
|
-
* almost always empty is the robust (and cheap) form. 'Auto' removes the
|
|
3981
|
-
* map entry and restores `autoUpdate` (see EditorShellStore.setLodForcedLevel),
|
|
3982
|
-
* handing selection back to the renderer's own `LOD.update(camera)`.
|
|
3983
|
-
*
|
|
3984
|
-
* That last sentence is load-bearing and easy to disbelieve, because nothing
|
|
3985
|
-
* in this repo calls `LOD.update`: THREE does it itself. `WebGLRenderer`'s
|
|
3986
|
-
* `projectObject` runs `if (object.isLOD) { if (object.autoUpdate === true)
|
|
3987
|
-
* object.update(camera); }` for every visible LOD, every render. `autoUpdate`
|
|
3988
|
-
* is the ONE switch this whole feature turns, and pinning works by turning it
|
|
3989
|
-
* off. Do not add an engine-side per-frame LOD tick to "fix" the missing
|
|
3990
|
-
* call — it would duplicate the renderer's own work and fight the pin.
|
|
3253
|
+
* Apply pinned LOD levels each frame so asynchronous loads and rebuilt instances inherit the view
|
|
3254
|
+
* state. Auto removes the pin and restores autoUpdate; WebGLRenderer then calls
|
|
3255
|
+
* LOD.update(camera) during rendering.
|
|
3991
3256
|
*/
|
|
3992
3257
|
private _enforceLodForcedLevels(): void {
|
|
3993
3258
|
const projectedObjects = new Set(this._objectMap.values());
|
|
@@ -4011,22 +3276,19 @@ export class EditorViewport {
|
|
|
4011
3276
|
*/
|
|
4012
3277
|
renderWithInfrastructure(draw: () => void): void {
|
|
4013
3278
|
const spark = this._sparkRenderer;
|
|
4014
|
-
const restoreCameraView = this._cameraView ? this._suspendPreviewInfrastructure() : null;
|
|
4015
3279
|
if (spark) this._scene.add(spark);
|
|
4016
3280
|
try {
|
|
4017
3281
|
draw();
|
|
4018
3282
|
} finally {
|
|
4019
3283
|
spark?.removeFromParent();
|
|
4020
|
-
restoreCameraView?.();
|
|
4021
3284
|
}
|
|
4022
3285
|
}
|
|
4023
3286
|
|
|
4024
|
-
/** Render the standard orientation gizmo in the
|
|
3287
|
+
/** Render the standard orientation gizmo in the corner selected by the look. */
|
|
4025
3288
|
renderViewCube(renderer: THREE.WebGLRenderer): void {
|
|
4026
3289
|
if (!this._threeSurfaceShowing) return; // no three stage, no compass (Blender's is persistent on one)
|
|
4027
3290
|
if (this._navigation === 'hidden') return;
|
|
4028
3291
|
if (!this._store.shell.showGizmos) return; // the stage's Show Gizmo is off
|
|
4029
|
-
if (this._cameraView) return; // exact camera view owns the viewport
|
|
4030
3292
|
const w = renderer.domElement.clientWidth;
|
|
4031
3293
|
const h = renderer.domElement.clientHeight;
|
|
4032
3294
|
const size = this._vcSize;
|
|
@@ -4068,47 +3330,7 @@ export class EditorViewport {
|
|
|
4068
3330
|
}
|
|
4069
3331
|
}
|
|
4070
3332
|
|
|
4071
|
-
/**
|
|
4072
|
-
* The surrounding label/actions are React chrome; this reuses the one
|
|
4073
|
-
* renderer and WebGL context rather than mounting a second viewport. */
|
|
4074
|
-
renderCameraPreview(
|
|
4075
|
-
renderer: THREE.WebGLRenderer,
|
|
4076
|
-
entityCamera: THREE.Camera | null,
|
|
4077
|
-
previewElement: HTMLElement | null,
|
|
4078
|
-
): void {
|
|
4079
|
-
if (!entityCamera || !previewElement || this._cameraView) return;
|
|
4080
|
-
|
|
4081
|
-
const w = renderer.domElement.clientWidth;
|
|
4082
|
-
const h = renderer.domElement.clientHeight;
|
|
4083
|
-
const canvasRect = renderer.domElement.getBoundingClientRect();
|
|
4084
|
-
const previewRect = previewElement.getBoundingClientRect();
|
|
4085
|
-
const px = Math.round(previewRect.left - canvasRect.left);
|
|
4086
|
-
const py = Math.round(canvasRect.bottom - previewRect.bottom);
|
|
4087
|
-
const pw = Math.max(1, Math.round(previewRect.width));
|
|
4088
|
-
const ph = Math.max(1, Math.round(previewRect.height));
|
|
4089
|
-
if (px >= w || py >= h || px + pw <= 0 || py + ph <= 0) return;
|
|
4090
|
-
|
|
4091
|
-
const restoreInfrastructure = this._suspendPreviewInfrastructure();
|
|
4092
|
-
// Save renderer state: the preview is a late scissored draw over the main
|
|
4093
|
-
// frame and must leave the following frame's composer untouched.
|
|
4094
|
-
const prevClearColor = renderer.getClearColor(this._previewClearColor);
|
|
4095
|
-
const prevClearAlpha = renderer.getClearAlpha();
|
|
4096
|
-
try {
|
|
4097
|
-
renderer.setScissorTest(true);
|
|
4098
|
-
renderer.setViewport(px, py, pw, ph);
|
|
4099
|
-
renderer.setScissor(px, py, pw, ph);
|
|
4100
|
-
renderer.setClearColor(0x111214, 1);
|
|
4101
|
-
renderer.clear();
|
|
4102
|
-
renderer.render(this._scene, entityCamera);
|
|
4103
|
-
} finally {
|
|
4104
|
-
renderer.setScissorTest(false);
|
|
4105
|
-
renderer.setViewport(0, 0, w, h);
|
|
4106
|
-
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
4107
|
-
restoreInfrastructure();
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
|
|
4111
|
-
/** True when the fly camera is active (right-click + movement key held). */
|
|
3333
|
+
/** True while the fly-camera session is active. */
|
|
4112
3334
|
get isFlying(): boolean {
|
|
4113
3335
|
return this._flyActive;
|
|
4114
3336
|
}
|
|
@@ -4126,6 +3348,8 @@ export class EditorViewport {
|
|
|
4126
3348
|
this._canvas.removeEventListener('pointerdown', this._onAltOrbitStart, { capture: true });
|
|
4127
3349
|
window.removeEventListener('pointerup', this._onAltOrbitRelease, { capture: true });
|
|
4128
3350
|
window.removeEventListener('pointercancel', this._onAltOrbitRelease, { capture: true });
|
|
3351
|
+
window.removeEventListener('pointercancel', this._onInteractionDragLost, { capture: true });
|
|
3352
|
+
window.removeEventListener('blur', this._onInteractionDragLost);
|
|
4129
3353
|
window.removeEventListener('keydown', this._onSnapHoldKey);
|
|
4130
3354
|
window.removeEventListener('keyup', this._onSnapHoldKey);
|
|
4131
3355
|
this._interactionElement.removeEventListener('pointerup', this._onFlyPointerUp);
|
|
@@ -4134,15 +3358,8 @@ export class EditorViewport {
|
|
|
4134
3358
|
this._interactionElement.removeEventListener('wheel', this._onTrackpadWheel);
|
|
4135
3359
|
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
4136
3360
|
this._interactionElement.removeEventListener('pointermove', this._onPointerMove);
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
}
|
|
4140
|
-
if (this._onInteractionDblClick) {
|
|
4141
|
-
this._interactionElement.removeEventListener('dblclick', this._onInteractionDblClick);
|
|
4142
|
-
}
|
|
4143
|
-
this._canvas.removeEventListener('dragover', this._onDragOver);
|
|
4144
|
-
this._canvas.removeEventListener('dragleave', this._onDragLeave);
|
|
4145
|
-
this._canvas.removeEventListener('drop', this._onDrop);
|
|
3361
|
+
this._interactionElement.removeEventListener('pointerup', this._onPointerUp);
|
|
3362
|
+
this._interactionElement.removeEventListener('dblclick', this._onInteractionDblClick);
|
|
4146
3363
|
// Dispose controls now: deferred source cleanup must never disconnect
|
|
4147
3364
|
// the reused element after the replacement controls have connected.
|
|
4148
3365
|
this.orbitControls.dispose();
|
|
@@ -4150,9 +3367,9 @@ export class EditorViewport {
|
|
|
4150
3367
|
}
|
|
4151
3368
|
|
|
4152
3369
|
/**
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
3370
|
+
* Apply the palette's viewport group (`EditorTheme.color.viewport`): cache the grid
|
|
3371
|
+
* ink for `_applyGridLines`, paint axis-line colours and widths (the view owns visibility),
|
|
3372
|
+
* and a flat background when
|
|
4156
3373
|
* named — replacing only a background this viewport painted or none at all,
|
|
4157
3374
|
* never a host's own (a document's declared colour, a world radiance).
|
|
4158
3375
|
*/
|
|
@@ -4164,9 +3381,7 @@ export class EditorViewport {
|
|
|
4164
3381
|
// stage's colours or keeps none of them: Classic Graphite declares no
|
|
4165
3382
|
// group and keeps its floor without the grazing fade. Which axis lines
|
|
4166
3383
|
// show is the view's (`overlays.axes`), whatever the look.
|
|
4167
|
-
|
|
4168
|
-
if (grazing) grazing.value = look.background !== null ? 1 : 0;
|
|
4169
|
-
this._paintLookGrid();
|
|
3384
|
+
this.grid.material.uniforms['uGrazingFade']!.value = look.background !== null ? 1 : 0;
|
|
4170
3385
|
this._lookAxisHexes = [look.axisX, look.axisY, look.axisZ];
|
|
4171
3386
|
if (this._axisLines) {
|
|
4172
3387
|
(this._axisLines.material as LineMaterial).linewidth = this._axisLineCss(look.axisLineWidth);
|
|
@@ -4190,22 +3405,7 @@ export class EditorViewport {
|
|
|
4190
3405
|
}
|
|
4191
3406
|
|
|
4192
3407
|
/**
|
|
4193
|
-
*
|
|
4194
|
-
* the ONE stage surface whose material is tone-mapped (see
|
|
4195
|
-
* {@link toneMappedSourceColor}), so both levels are handed to it as the
|
|
4196
|
-
* colours that MAP to the palette's.
|
|
4197
|
-
*
|
|
4198
|
-
* Blender draws both levels from ONE grid colour at two alphas. Measured in
|
|
4199
|
-
* `modeling-object-none.png` over its #3f3f3f background: the 1 m line
|
|
4200
|
-
* composites to lum 84 — exactly the palette's `#545454` — and the 10 m
|
|
4201
|
-
* line to 102, a 1.86x contrast. The palette names the MINOR's finished
|
|
4202
|
-
* appearance, so the major is that colour carried the rest of the way by
|
|
4203
|
-
* the measured ratio — and the ratio is applied in the FINISHED sRGB space
|
|
4204
|
-
* the 84 and the 102 were read in, before the inversion, because ACES is
|
|
4205
|
-
* not linear and extrapolating on its far side lands somewhere else.
|
|
4206
|
-
*/
|
|
4207
|
-
/**
|
|
4208
|
-
* LIGHT AND DRESS THIS STAGE BY A VIEW'S PRESENTATION (`kit/viewport-presentation`): its studio
|
|
3408
|
+
* LIGHT AND DRESS THIS STAGE BY A VIEW'S PRESENTATION (`@volter/sdk/viewport-presentation`): its studio
|
|
4209
3409
|
* or preview lighting through a `StagePresentationRig` in this scene (in place of the
|
|
4210
3410
|
* viewport's own ambient and directional), its selection marks and its grid's major step.
|
|
4211
3411
|
* The stage's render pipeline keeps its tone mapping. The scene's own lighting is what an
|
|
@@ -4240,7 +3440,7 @@ export class EditorViewport {
|
|
|
4240
3440
|
rig.dispose();
|
|
4241
3441
|
this._editorObjects = this._editorObjects.filter((object) => !roots.includes(object));
|
|
4242
3442
|
if (this._presentation === binding) this._presentation = null;
|
|
4243
|
-
const rigOn = this._lightRig
|
|
3443
|
+
const rigOn = this._lightRig;
|
|
4244
3444
|
this._editorAmbient.visible = rigOn;
|
|
4245
3445
|
this._editorDirLight.visible = rigOn;
|
|
4246
3446
|
},
|
|
@@ -4252,11 +3452,11 @@ export class EditorViewport {
|
|
|
4252
3452
|
return binding.stop;
|
|
4253
3453
|
}
|
|
4254
3454
|
|
|
4255
|
-
/** The view's
|
|
4256
|
-
setSelectionMarks(marks: { readonly
|
|
3455
|
+
/** The view's wire and box marks; a change rebuilds them for the current selection. */
|
|
3456
|
+
setSelectionMarks(marks: { readonly wire: boolean; readonly box: boolean }): void {
|
|
4257
3457
|
const current = this._selectionMarks;
|
|
4258
|
-
if (current.
|
|
4259
|
-
this._selectionMarks = {
|
|
3458
|
+
if (current.wire === marks.wire && current.box === marks.box) return;
|
|
3459
|
+
this._selectionMarks = { wire: marks.wire, box: marks.box };
|
|
4260
3460
|
this._syncBoxHelpers();
|
|
4261
3461
|
}
|
|
4262
3462
|
|
|
@@ -4338,23 +3538,21 @@ export class EditorViewport {
|
|
|
4338
3538
|
|
|
4339
3539
|
/** Minor cells per major line (`overlays.grid.majorEvery`; Blender 10, Godot 8). */
|
|
4340
3540
|
setGridMajorEvery(every: number): void {
|
|
4341
|
-
|
|
4342
|
-
if (uniform && Number.isFinite(every) && every >= 1) uniform.value = every;
|
|
3541
|
+
if (Number.isFinite(every) && every >= 1) this.grid.material.uniforms['uMajorEvery']!.value = every;
|
|
4343
3542
|
}
|
|
4344
3543
|
|
|
4345
3544
|
/** The look's floor lines: widths in device pixels and the major level's contrast. */
|
|
4346
|
-
private _gridMajorContrast
|
|
3545
|
+
private _gridMajorContrast!: number;
|
|
4347
3546
|
private _applyGridLines(lines: ReturnType<typeof nativeViewportGrid>): void {
|
|
4348
3547
|
const uniforms = this.grid.material.uniforms;
|
|
4349
|
-
|
|
4350
|
-
|
|
3548
|
+
uniforms['uLineWidth']!.value = lines.lineWidth;
|
|
3549
|
+
uniforms['uMajorWidth']!.value = lines.majorWidth;
|
|
4351
3550
|
this._gridMajorContrast = lines.majorContrast;
|
|
4352
3551
|
this._paintLookGrid();
|
|
4353
3552
|
}
|
|
4354
3553
|
|
|
4355
3554
|
private _paintLookGrid(): void {
|
|
4356
3555
|
const hex = this._lookGridHex;
|
|
4357
|
-
if (hex === null) return;
|
|
4358
3556
|
const backgroundHex = this._lookBackgroundHex ?? 0x3f3f3f;
|
|
4359
3557
|
const channel = (shift: number) => {
|
|
4360
3558
|
const line = (hex >> shift) & 0xff;
|
|
@@ -4364,18 +3562,11 @@ export class EditorViewport {
|
|
|
4364
3562
|
const majorHex = (channel(16) << 16) | (channel(8) << 8) | channel(0);
|
|
4365
3563
|
const minor = toneMappedSourceColor(hex, this._renderer);
|
|
4366
3564
|
const major = toneMappedSourceColor(majorHex, this._renderer);
|
|
4367
|
-
(this.grid.material.uniforms['uColor']
|
|
4368
|
-
(this.grid.material.uniforms['uMajorColor']
|
|
3565
|
+
(this.grid.material.uniforms['uColor']!.value as THREE.Color).copy(minor);
|
|
3566
|
+
(this.grid.material.uniforms['uMajorColor']!.value as THREE.Color).copy(major);
|
|
4369
3567
|
}
|
|
4370
3568
|
|
|
4371
|
-
/**
|
|
4372
|
-
* THE WORLD'S AXIS LINES, rebuilt from the view's `overlays.axes`, the stage's axis frame and
|
|
4373
|
-
* the look's colours. `floor` is the two world axes three's X and Z carry; each shown world
|
|
4374
|
-
* axis is drawn along the three axis that carries it. The colours are RAW (`toneMapped: false`,
|
|
4375
|
-
* opaque): what is set is what the screen shows — Blender's X plateaus at `#cb293f` and Y at
|
|
4376
|
-
* `#69aa15` in `modeling-object-none.png`, which is what its palette names. An axis the look
|
|
4377
|
-
* gives no colour takes the gizmo's.
|
|
4378
|
-
*/
|
|
3569
|
+
/** Build shown world axes from overlays.axes and the stage axis frame. Use raw look colours, falling back to the gizmo palette for unspecified axes. */
|
|
4379
3570
|
private _rebuildAxisLines(): void {
|
|
4380
3571
|
const lines = this._axisLines;
|
|
4381
3572
|
if (!lines) return;
|
|
@@ -4389,16 +3580,14 @@ export class EditorViewport {
|
|
|
4389
3580
|
this._axesWanted = shown.length > 0;
|
|
4390
3581
|
lines.visible = this.grid.visible && this._axesWanted;
|
|
4391
3582
|
if (!this._axesWanted) return;
|
|
4392
|
-
//
|
|
4393
|
-
// its first draw (`_maxInstanceCount`), so growing one from two lines to three drew only
|
|
4394
|
-
// the first two.
|
|
3583
|
+
// Replace geometry on rebuild so its cached instance count matches the current axes.
|
|
4395
3584
|
const build = (threeAxes: readonly number[]): LineSegmentsGeometry => {
|
|
4396
3585
|
const positions: number[] = [];
|
|
4397
3586
|
const colors: number[] = [];
|
|
4398
3587
|
for (const threeAxis of threeAxes) {
|
|
4399
3588
|
const world = frame[threeAxis]![0];
|
|
4400
|
-
const hex = this._lookAxisHexes[world]
|
|
4401
|
-
const color = hex === null
|
|
3589
|
+
const hex = this._lookAxisHexes[world]!;
|
|
3590
|
+
const color = hex === null ? this._gizmoAxisColor(world) : new THREE.Color(hex);
|
|
4402
3591
|
positions.push(...axisSegmentPositions(threeAxis));
|
|
4403
3592
|
for (let i = 0; i < AXIS_SEGMENTS_PER_AXIS * 2; i++) colors.push(color.r, color.g, color.b);
|
|
4404
3593
|
}
|
|
@@ -4494,14 +3683,12 @@ export class EditorViewport {
|
|
|
4494
3683
|
(this._axisLines.material as THREE.Material).dispose();
|
|
4495
3684
|
this._axisLines = null;
|
|
4496
3685
|
}
|
|
4497
|
-
this.clearCameraView();
|
|
4498
3686
|
if (this._sparkRenderer) {
|
|
4499
3687
|
this._scene.remove(this._sparkRenderer);
|
|
4500
3688
|
disposeSparkRendererWhenIdle(this._sparkRenderer);
|
|
4501
3689
|
this._sparkRenderer = null;
|
|
4502
3690
|
}
|
|
4503
3691
|
this.disconnectInput();
|
|
4504
|
-
this._hideDropOverlay();
|
|
4505
3692
|
for (const helper of this._helpers.values()) this._scene.remove(helper);
|
|
4506
3693
|
this._helpers.clear();
|
|
4507
3694
|
for (const helper of this._boxHelpers.values()) {
|
|
@@ -4526,8 +3713,9 @@ export class EditorViewport {
|
|
|
4526
3713
|
// --- Private ---
|
|
4527
3714
|
|
|
4528
3715
|
private _ensureSparkRenderer(): void {
|
|
3716
|
+
const renderer = this._renderer;
|
|
4529
3717
|
if (
|
|
4530
|
-
!
|
|
3718
|
+
!renderer ||
|
|
4531
3719
|
this._sparkRenderer ||
|
|
4532
3720
|
this._sparkRendererLoading ||
|
|
4533
3721
|
this._sparkRendererFailed
|
|
@@ -4540,8 +3728,8 @@ export class EditorViewport {
|
|
|
4540
3728
|
this._sparkRendererLoading = true;
|
|
4541
3729
|
void import('@sparkjsdev/spark')
|
|
4542
3730
|
.then(({ SparkRenderer }) => {
|
|
4543
|
-
if (this._disposed
|
|
4544
|
-
const spark = new SparkRenderer({ renderer
|
|
3731
|
+
if (this._disposed) return;
|
|
3732
|
+
const spark = new SparkRenderer({ renderer, enableLod: false });
|
|
4545
3733
|
spark.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
4546
3734
|
this._sparkRenderer = spark;
|
|
4547
3735
|
invalidateStages();
|
|
@@ -4557,21 +3745,7 @@ export class EditorViewport {
|
|
|
4557
3745
|
}
|
|
4558
3746
|
|
|
4559
3747
|
/**
|
|
4560
|
-
*
|
|
4561
|
-
* rig. Measured in `modeling-object-none.png` at matched scale: a 72 CSS px
|
|
4562
|
-
* circle of 16 px balls on a 28 px radius; the three POSITIVE balls are
|
|
4563
|
-
* filled in their axis colour and carry a dark letter, the three negative
|
|
4564
|
-
* ones are a 2 px ring of the same colour over the stage; a 3 px stalk runs
|
|
4565
|
-
* from the centre to each positive ball only; and the whole thing fades
|
|
4566
|
-
* back-to-front with depth (`_syncOrientationGizmoDepth`, run per frame),
|
|
4567
|
-
* which is what makes the near balls read as near.
|
|
4568
|
-
*
|
|
4569
|
-
* The gizmo camera's frustum is +/-1.5, and the scissored box it draws into
|
|
4570
|
-
* is `COMPASS_BOX_PX`, so one unit is `COMPASS_BOX_PX / 3` screen px — every
|
|
4571
|
-
* size below is a measured pixel count divided by that.
|
|
4572
|
-
*/
|
|
4573
|
-
/**
|
|
4574
|
-
* BUILD (or REBUILD) THE NAVIGATION GIZMO in the look's axis frame.
|
|
3748
|
+
* BUILD (or REBUILD) THE NAVIGATION GIZMO in the presented world's axis frame.
|
|
4575
3749
|
*
|
|
4576
3750
|
* Blender's gizmo draws a stalk and a FILLED, LETTERED ball on each axis's
|
|
4577
3751
|
* POSITIVE side and a hollow one opposite, and the axis it calls Z is the
|
|
@@ -4586,16 +3760,15 @@ export class EditorViewport {
|
|
|
4586
3760
|
* a fact about the scene and not about what the axis is called.
|
|
4587
3761
|
*/
|
|
4588
3762
|
private _buildOrientationGizmo(): void {
|
|
4589
|
-
if (this._vcScene === undefined) return;
|
|
4590
3763
|
// Rebuilt on every look change, so what it held is released, textures included.
|
|
4591
3764
|
for (const held of [...this._vcScene.children]) {
|
|
4592
3765
|
this._vcScene.remove(held);
|
|
4593
3766
|
const drawn = held as THREE.Mesh | THREE.Sprite;
|
|
4594
3767
|
// A sprite's geometry is three's one shared quad: left alone.
|
|
4595
|
-
if (!(drawn as THREE.Sprite).isSprite) drawn.geometry
|
|
4596
|
-
const material = drawn.material as
|
|
4597
|
-
material
|
|
4598
|
-
material
|
|
3768
|
+
if (!(drawn as THREE.Sprite).isSprite) drawn.geometry.dispose();
|
|
3769
|
+
const material = drawn.material as THREE.Material & { map?: THREE.Texture | null };
|
|
3770
|
+
material.map?.dispose();
|
|
3771
|
+
material.dispose();
|
|
4599
3772
|
}
|
|
4600
3773
|
this._vcBalls.length = 0;
|
|
4601
3774
|
this._vcStalks.length = 0;
|
|
@@ -4631,7 +3804,7 @@ export class EditorViewport {
|
|
|
4631
3804
|
// The look's navigation colours, else its axis colours, else the editor's own.
|
|
4632
3805
|
const lookAxes = this._gizmoLook.navigation ?? this._gizmoLook.axes;
|
|
4633
3806
|
const color =
|
|
4634
|
-
lookAxes === null ?
|
|
3807
|
+
lookAxes === null ? this._gizmoAxisColor(sourceAxis) : new THREE.Color(lookAxes[sourceAxis]!);
|
|
4635
3808
|
const letter = AXIS_LETTER[sourceAxis]!;
|
|
4636
3809
|
const axis = axes[i]!.clone().multiplyScalar(positive);
|
|
4637
3810
|
|
|
@@ -4701,11 +3874,11 @@ export class EditorViewport {
|
|
|
4701
3874
|
for (let i = 0; i < 3; i++) {
|
|
4702
3875
|
const [sourceAxis, positive] = this._stageAxisFrame[i]!;
|
|
4703
3876
|
const color =
|
|
4704
|
-
lookAxes === null ?
|
|
3877
|
+
lookAxes === null ? this._gizmoAxisColor(sourceAxis) : new THREE.Color(lookAxes[sourceAxis]!);
|
|
4705
3878
|
const letter = AXIS_LETTER[sourceAxis]!;
|
|
4706
3879
|
const axis = axes[i]!.clone().multiplyScalar(positive);
|
|
4707
3880
|
if (form === 'triad') {
|
|
4708
|
-
// Unreal's triad is about 40 px, opaque and undimmed by facing
|
|
3881
|
+
// Unreal's triad is about 40 px, opaque and undimmed by facing; its
|
|
4709
3882
|
// size is the look's (`navigationSize`, a multiple of this 24 px).
|
|
4710
3883
|
const scale = this._gizmoLook.navigationSize ?? 1;
|
|
4711
3884
|
const length = (24 * scale) / perUnit;
|
|
@@ -4752,14 +3925,7 @@ export class EditorViewport {
|
|
|
4752
3925
|
}
|
|
4753
3926
|
}
|
|
4754
3927
|
|
|
4755
|
-
/**
|
|
4756
|
-
* The gizmo's depth cue, per frame: a ball pointing AWAY from the viewer is
|
|
4757
|
-
* drawn behind, dimmer and SMALLER, exactly as Blender's is. Both laws are
|
|
4758
|
-
* measured off the reference's own six balls and stated where they are
|
|
4759
|
-
* applied below. Sprites carry no depth of their own here
|
|
4760
|
-
* (`depthTest: false`, so the stage never occludes the gizmo), so the draw
|
|
4761
|
-
* ORDER is set here too.
|
|
4762
|
-
*/
|
|
3928
|
+
/** Order navigation sprites by depth and dim and shrink balls that point away from the viewer. */
|
|
4763
3929
|
private _syncOrientationGizmoDepth(): void {
|
|
4764
3930
|
const perUnit = COMPASS_BOX_PX / 3;
|
|
4765
3931
|
const view = this._vcDir.copy(this._vcCamera.position).normalize();
|
|
@@ -4767,9 +3933,7 @@ export class EditorViewport {
|
|
|
4767
3933
|
// a ball's colour is its axis colour mixed with the viewport's background by its depth
|
|
4768
3934
|
// (`fading_color`: `(depth + 1) · 0.25 + 0.5`), a negative ball a 25% tint of it ringed in
|
|
4769
3935
|
// that colour, and a view looking straight down an axis (`axis_align`) hides that axis's far
|
|
4770
|
-
// ball, fills its near negative one (ringed halfway to white) and letters it `-Y`.
|
|
4771
|
-
// against both reference frames: the default view's +X fill (245,54,81) is this mix at depth
|
|
4772
|
-
// 0.82, the front view's (204,55,78) at depth 0.
|
|
3936
|
+
// ball, fills its near negative one (ringed halfway to white) and letters it `-Y`. Use the view-depth mix for the visible axis colours.
|
|
4773
3937
|
if (this._gizmoLook.navigationForm === 'godot') {
|
|
4774
3938
|
this._syncGodotNavigation(view);
|
|
4775
3939
|
return;
|
|
@@ -4812,15 +3976,12 @@ export class EditorViewport {
|
|
|
4812
3976
|
ringMaterial.color.copy(fading);
|
|
4813
3977
|
ringMaterial.opacity = 1;
|
|
4814
3978
|
}
|
|
4815
|
-
//
|
|
4816
|
-
// draws it black at 0.9 over a small glyph, and this is what that reads as).
|
|
3979
|
+
// Letter ink uses the axis fill darkened by 0.356.
|
|
4817
3980
|
letterMaterial.color.copy(mix(black, fillMaterial.color, COMPASS_LETTER_INK, scratch));
|
|
4818
3981
|
fill.visible = !alignedBack;
|
|
4819
3982
|
ring.visible = !alignedBack;
|
|
4820
3983
|
letter.visible = (positive || axis === aligned) && !alignedBack;
|
|
4821
|
-
//
|
|
4822
|
-
// for free, and Blender's six balls measure a straight line in `facing` (the fit is in the
|
|
4823
|
-
// box's docblock).
|
|
3984
|
+
// Scale each ball linearly with facing; orthographic projection supplies no depth scaling.
|
|
4824
3985
|
const scale = (COMPASS_BALL_BACK_PX + COMPASS_BALL_DEPTH_GAIN_PX * facing) / perUnit;
|
|
4825
3986
|
const order = 10 + Math.round(facing * 100) * 3;
|
|
4826
3987
|
for (const [part, rank] of [[fill, 0], [ring, 1], [letter, 2]] as const) {
|
|
@@ -4893,12 +4054,8 @@ export class EditorViewport {
|
|
|
4893
4054
|
}
|
|
4894
4055
|
|
|
4895
4056
|
/**
|
|
4896
|
-
*
|
|
4897
|
-
*
|
|
4898
|
-
* different reasons: the single click snaps the camera with it, and the
|
|
4899
|
-
* double click (drill-in) must simply not treat the cube as geometry —
|
|
4900
|
-
* without this guard, double-clicking the cube drilled into whatever the
|
|
4901
|
-
* pointer found BEHIND it.
|
|
4057
|
+
* Test the pointer against the orientation cube’s screen-space box without changing state. Single
|
|
4058
|
+
* clicks use it for axis navigation; double-click selection excludes it from scene geometry.
|
|
4902
4059
|
*/
|
|
4903
4060
|
private _isOverViewCube(clientX: number, clientY: number): boolean {
|
|
4904
4061
|
// An indicator is only drawn, and a triad has nothing to click: clicks pass to the stage.
|
|
@@ -4911,15 +4068,10 @@ export class EditorViewport {
|
|
|
4911
4068
|
return x >= vcLeft && x <= vcLeft + this._vcSize && y >= vcTop && y <= vcTop + this._vcSize;
|
|
4912
4069
|
}
|
|
4913
4070
|
|
|
4914
|
-
/**
|
|
4915
|
-
* WHERE THE NAVIGATION GIZMO'S BOX SITS, top-left in CSS px from the canvas's top-left: the
|
|
4916
|
-
* look's corner (`navigationCorner`) — Blender's top right, set by its measured margins, or
|
|
4917
|
-
* Unreal's bottom left, one small margin in from both edges — inset by the canvas's bleed.
|
|
4918
|
-
*/
|
|
4071
|
+
/** Place the gizmo box in the look’s navigation corner, inset by canvas bleed. */
|
|
4919
4072
|
private _vcOrigin(width: number, height: number, inset: number): { left: number; top: number } {
|
|
4920
4073
|
if (this._gizmoLook.navigationCorner === 'bottom-left') {
|
|
4921
|
-
//
|
|
4922
|
-
// 4 px margin put the triad's lower half under it).
|
|
4074
|
+
// Leave room above the stage’s two-line camera readout.
|
|
4923
4075
|
const left = 4;
|
|
4924
4076
|
const bottom = 48;
|
|
4925
4077
|
return { left: left + inset, top: height - this._vcSize - bottom - inset };
|
|
@@ -5005,8 +4157,8 @@ export class EditorViewport {
|
|
|
5005
4157
|
});
|
|
5006
4158
|
};
|
|
5007
4159
|
// Primary (if it's a real entity, not pivot dummy)
|
|
5008
|
-
const obj = this._activeGizmo.object
|
|
5009
|
-
if (obj
|
|
4160
|
+
const obj = this._activeGizmo.object!;
|
|
4161
|
+
if (obj !== this._pivotDummy) {
|
|
5010
4162
|
applyLive(this._attachedAuthoringId ?? this._authoringIdForObject(obj), obj);
|
|
5011
4163
|
}
|
|
5012
4164
|
// Other selected objects (active-element / individual-origins)
|
|
@@ -5195,9 +4347,6 @@ export class EditorViewport {
|
|
|
5195
4347
|
targets: {
|
|
5196
4348
|
id: string;
|
|
5197
4349
|
obj: THREE.Object3D;
|
|
5198
|
-
startPos: THREE.Vector3;
|
|
5199
|
-
startQuat: THREE.Quaternion;
|
|
5200
|
-
startScale: THREE.Vector3;
|
|
5201
4350
|
}[],
|
|
5202
4351
|
): void {
|
|
5203
4352
|
// Collect all dragged entity IDs to exclude from raycast
|
|
@@ -5214,11 +4363,6 @@ export class EditorViewport {
|
|
|
5214
4363
|
// Also include the grid as a snap target
|
|
5215
4364
|
sceneTargets.push(this.grid);
|
|
5216
4365
|
|
|
5217
|
-
if (sceneTargets.length === 0) {
|
|
5218
|
-
this._surfaceSnapIndicator.visible = false;
|
|
5219
|
-
return;
|
|
5220
|
-
}
|
|
5221
|
-
|
|
5222
4366
|
// Raycast downward from the gizmo object position
|
|
5223
4367
|
const origin = gizmoObj.position.clone();
|
|
5224
4368
|
origin.y += 50; // Start high above
|
|
@@ -5269,7 +4413,7 @@ export class EditorViewport {
|
|
|
5269
4413
|
obj.traverse((child) => {
|
|
5270
4414
|
if (isInEditorOwnedSubtree(child)) return;
|
|
5271
4415
|
const mesh = child as THREE.Mesh;
|
|
5272
|
-
if (!mesh.isMesh
|
|
4416
|
+
if (!mesh.isMesh) return;
|
|
5273
4417
|
const posAttr = mesh.geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
5274
4418
|
if (!posAttr) return;
|
|
5275
4419
|
const count = Math.min(posAttr.count, MAX_PER_MESH);
|
|
@@ -5289,7 +4433,7 @@ export class EditorViewport {
|
|
|
5289
4433
|
*/
|
|
5290
4434
|
private _applyVertexSnap(
|
|
5291
4435
|
gizmoObj: THREE.Object3D,
|
|
5292
|
-
targets: { obj: THREE.Object3D
|
|
4436
|
+
targets: { obj: THREE.Object3D }[],
|
|
5293
4437
|
): void {
|
|
5294
4438
|
const entityPos = gizmoObj.position;
|
|
5295
4439
|
const projected = entityPos.clone().project(this._screenCamera);
|
|
@@ -5323,7 +4467,7 @@ export class EditorViewport {
|
|
|
5323
4467
|
}
|
|
5324
4468
|
this._vertexSnapIndicator.position.copy(bestVertex);
|
|
5325
4469
|
this._vertexSnapIndicator.visible = true;
|
|
5326
|
-
//
|
|
4470
|
+
// Scale the indicator with its distance from the drawing camera.
|
|
5327
4471
|
const dist = bestVertex.distanceTo(this._screenCamera.position);
|
|
5328
4472
|
this._vertexSnapIndicator.scale.setScalar(dist * 0.008);
|
|
5329
4473
|
} else {
|
|
@@ -5339,7 +4483,7 @@ export class EditorViewport {
|
|
|
5339
4483
|
this._otherDragObjects = [];
|
|
5340
4484
|
this._allDragObjects = [];
|
|
5341
4485
|
this._preservedChildObjects = [];
|
|
5342
|
-
// Persist the final transform
|
|
4486
|
+
// Persist the final transform and mark dirty. A mid-drag autosave may
|
|
5343
4487
|
// have already fired and cleared dirty; this re-schedules the final write.
|
|
5344
4488
|
const authoring = this._authoring();
|
|
5345
4489
|
for (const id of editIds) endAuthoringTransformEdit(authoring, id);
|
|
@@ -5354,61 +4498,7 @@ export class EditorViewport {
|
|
|
5354
4498
|
return false;
|
|
5355
4499
|
}
|
|
5356
4500
|
|
|
5357
|
-
/**
|
|
5358
|
-
/**
|
|
5359
|
-
* THE GIZMO IS A CONSTANT NUMBER OF PIXELS WHEN THE LOOK STATES ONE (owner,
|
|
5360
|
-
* 2026-09-21: *"is gizmo perhaps the wrong size?"*).
|
|
5361
|
-
*
|
|
5362
|
-
* Our gizmo is three's `TransformControls`, whose handles scale as
|
|
5363
|
-
* `factor · size / 4` (`TransformControls.js:1539-1551` at the pinned
|
|
5364
|
-
* 0.180.0). Work the projection through that and the distance cancels:
|
|
5365
|
-
*
|
|
5366
|
-
* perspective — `factor = d · min(1.9·tan(fov/2)/zoom, 7)`, and one world
|
|
5367
|
-
* unit at distance `d` spans `H·zoom / (2·d·tan(fov/2))` px, so one
|
|
5368
|
-
* gizmo-LOCAL unit spans `1.9·size·H/8 = 0.2375 · size · H` px. Constant
|
|
5369
|
-
* in `d` and in `zoom`, and a FRACTION OF THE VIEWPORT: at `size` 1 an
|
|
5370
|
-
* 800 px viewport gave 190 px per local unit and a 1400 px one gave 332.
|
|
5371
|
-
* orthographic — `factor = (top − bottom)/zoom` and one world unit spans
|
|
5372
|
-
* `H·zoom / (top − bottom)` px, so one local unit spans `size · H / 4`.
|
|
5373
|
-
*
|
|
5374
|
-
* Blender's rule is the same SHAPE with its own unit: one gizmo unit is
|
|
5375
|
-
* `U.gizmo_size` CSS px, flat (`wm_gizmo.cc:450-474`, and
|
|
5376
|
-
* `StageContribution.gizmoSize` for the whole derivation). So the
|
|
5377
|
-
* look states Blender's number and this converts it — and the conversion is
|
|
5378
|
-
* the one thing neither side can state, because THREE'S UNIT IS HALF
|
|
5379
|
-
* BLENDER'S. Measured off the two libraries' own handle geometry, all three
|
|
5380
|
-
* families agreeing within 5%:
|
|
5381
|
-
*
|
|
5382
|
-
* translate arm, origin → arrow tip
|
|
5383
|
-
* three 0.6 (stem `lineGeometry2` 0→0.5, `TransformControls.js:1207`;
|
|
5384
|
-
* cone 0.1 long placed at 0.5, `:1198-1199`, `:1235`)
|
|
5385
|
-
* Blender 1.25 (stem ends at `gizmo_line_range`'s `end` 1.0,
|
|
5386
|
-
* `transform_gizmo_3d.cc:1173-1203`; head 0.25,
|
|
5387
|
-
* `arrow3d_gizmo.cc:191`) ratio 2.08
|
|
5388
|
-
* rotate ring radius
|
|
5389
|
-
* three 0.5 (`CircleGeometry(0.5, …)`, `TransformControls.js:1315-1323`)
|
|
5390
|
-
* Blender 1.0 (`imm_draw_circle_wire_3d(…, 1.0f, …)`,
|
|
5391
|
-
* `dial3d_gizmo.cc:162`) ratio 2.00
|
|
5392
|
-
* scale handle, origin → outer face
|
|
5393
|
-
* three 0.58 (0.08 box lifted to 0.54, `:1235`-family)
|
|
5394
|
-
* Blender 1.10 (line end 1.0 + box `size` 0.05 ×2,
|
|
5395
|
-
* `arrow3d_gizmo.cc:175-177`) ratio 1.90
|
|
5396
|
-
*
|
|
5397
|
-
* so `pixels per three unit = 2 · gizmoSize`, which lands the rotate ring on
|
|
5398
|
-
* Blender's 75 px radius exactly and the move arrow at 90 px against
|
|
5399
|
-
* Blender's 93.75. The brief this was built from proposed
|
|
5400
|
-
* `size = px / (0.2375·H)` — that reads Blender's number as px per THREE
|
|
5401
|
-
* unit, which would draw every handle at half Blender's, and the three
|
|
5402
|
-
* ratios above are why it is not what landed.
|
|
5403
|
-
*
|
|
5404
|
-
* A look that states no size keeps `size` 1 — three's own default and what
|
|
5405
|
-
* every non-Blender look drew before this existed.
|
|
5406
|
-
*
|
|
5407
|
-
* CALLED ON EVERY RESIZE (the px↔local conversion has `H` in it), on a
|
|
5408
|
-
* projection change (the two branches differ) and on a theme change. It is
|
|
5409
|
-
* NOT called per frame: `getComputedStyle` is the expensive half and the
|
|
5410
|
-
* theme subscription is what invalidates it.
|
|
5411
|
-
*/
|
|
4501
|
+
/** Resolve look-defined transform-handle size into TransformControls.size using camera projection and viewport height. Recompute on look changes rather than reading computed style every frame. */
|
|
5412
4502
|
private _applyGizmoSize(): void {
|
|
5413
4503
|
// The theme subscription fires SYNCHRONOUSLY on install, which is earlier
|
|
5414
4504
|
// in the constructor than the controls — the same ordering the grid's own
|
|
@@ -5428,39 +4518,11 @@ export class EditorViewport {
|
|
|
5428
4518
|
for (const controls of this._allGizmos()) controls.size = size;
|
|
5429
4519
|
}
|
|
5430
4520
|
|
|
5431
|
-
/**
|
|
5432
|
-
|
|
5433
|
-
*
|
|
5434
|
-
* three's translate gizmo draws each axis TWICE — a stem with an arrowhead
|
|
5435
|
-
* at `+0.5` and a second bare arrowhead at `-0.5`
|
|
5436
|
-
* (`TransformControls.js`'s `gizmoTranslate`, two `arrowGeometry` entries
|
|
5437
|
-
* per axis) — so the handle set reads as six directions. No editor we draw
|
|
5438
|
-
* against does that: Blender's move gizmo is three arms out of the origin
|
|
5439
|
-
* plus the plane squares and the white view circle
|
|
5440
|
-
* (`transform_gizmo_3d.cc`'s `gizmo_line_range`, and the reference frame
|
|
5441
|
-
* `b6/blender-reference/gizmo-move.png`), and measured on the Model
|
|
5442
|
-
* document at walk 5 parity row 2 the stray heads read as three extra
|
|
5443
|
-
* "axes" pointing back through the object.
|
|
5444
|
-
*
|
|
5445
|
-
* The PLANE squares, the centre octahedron and every rotate ring are
|
|
5446
|
-
* untouched; this drops only the negative-side handles and the pick volumes
|
|
5447
|
-
* behind them, so nothing that used to be draggable stops being draggable —
|
|
5448
|
-
* a negative handle and its positive twin drive the same axis.
|
|
5449
|
-
*
|
|
5450
|
-
* WHICH CHILD IS WHICH is read off the GEOMETRY, because three's
|
|
5451
|
-
* `setupGizmo` bakes each handle's placement into a cloned geometry and
|
|
5452
|
-
* resets the object transform to the identity every frame — the same reason
|
|
5453
|
-
* {@link _patchCombinedScaleGizmo} measures bounding boxes rather than
|
|
5454
|
-
* positions. A stem spans 0 → 0.5 and centres at +0.25, so only the bare
|
|
5455
|
-
* `-0.5` head has a centre on the negative side of its own axis.
|
|
5456
|
-
*/
|
|
5457
|
-
private _dropNegativeAxisHandles(helper: THREE.Object3D, modes: readonly string[]): void {
|
|
4521
|
+
/** Keep only the positive translate and scale heads; identify the negative heads by their geometry centres rather than helper names. */
|
|
4522
|
+
private _dropNegativeAxisHandles(helper: THREE.Object3D, modes: readonly ('translate' | 'scale')[]): void {
|
|
5458
4523
|
const gizmoNode = helper.children.find(
|
|
5459
4524
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5460
|
-
) as
|
|
5461
|
-
| { gizmo: Record<string, THREE.Object3D>; picker: Record<string, THREE.Object3D> }
|
|
5462
|
-
| undefined;
|
|
5463
|
-
if (!gizmoNode) return;
|
|
4525
|
+
) as TransformControlsGizmo;
|
|
5464
4526
|
const axisOf: Record<string, THREE.Vector3> = {
|
|
5465
4527
|
X: new THREE.Vector3(1, 0, 0),
|
|
5466
4528
|
Y: new THREE.Vector3(0, 1, 0),
|
|
@@ -5468,23 +4530,19 @@ export class EditorViewport {
|
|
|
5468
4530
|
};
|
|
5469
4531
|
for (const mode of modes)
|
|
5470
4532
|
for (const group of [gizmoNode.gizmo[mode], gizmoNode.picker[mode]]) {
|
|
5471
|
-
if (!group) continue;
|
|
5472
4533
|
for (const child of [...group.children]) {
|
|
5473
4534
|
const axis = axisOf[child.name];
|
|
5474
4535
|
if (axis === undefined) continue;
|
|
5475
4536
|
const mesh = child as THREE.Mesh;
|
|
5476
|
-
if (!mesh.geometry) continue;
|
|
5477
4537
|
mesh.geometry.computeBoundingBox();
|
|
5478
|
-
const bounds = mesh.geometry.boundingBox
|
|
5479
|
-
if (!bounds) continue;
|
|
4538
|
+
const bounds = mesh.geometry.boundingBox!;
|
|
5480
4539
|
if (bounds.getCenter(new THREE.Vector3()).dot(axis) < 0) group.remove(child);
|
|
5481
4540
|
}
|
|
5482
4541
|
}
|
|
5483
4542
|
}
|
|
5484
4543
|
|
|
5485
4544
|
/**
|
|
5486
|
-
* THE COLOUR EACH HANDLE CARRIES, in the frame the
|
|
5487
|
-
* presenting.
|
|
4545
|
+
* THE COLOUR EACH HANDLE CARRIES, in the world frame the view presentation supplies.
|
|
5488
4546
|
*
|
|
5489
4547
|
* A handle is NAMED for three's axis and COLOURED for the source axis that
|
|
5490
4548
|
* three axis carries ({@link StageAxisFrame}) — under three's own frame the
|
|
@@ -5507,15 +4565,8 @@ export class EditorViewport {
|
|
|
5507
4565
|
};
|
|
5508
4566
|
}
|
|
5509
4567
|
|
|
5510
|
-
/**
|
|
5511
|
-
|
|
5512
|
-
*
|
|
5513
|
-
* Called once the gizmos exist and again on every theme change, and it is
|
|
5514
|
-
* idempotent by construction: the mirror below is applied as the DIFFERENCE
|
|
5515
|
-
* between the frame the geometry is already in ({@link _appliedAxisSigns})
|
|
5516
|
-
* and the one the look now asks for, and a mirror is its own inverse.
|
|
5517
|
-
*/
|
|
5518
|
-
private _readLookStage(_canvas: HTMLElement): void {
|
|
4568
|
+
/** Apply the current stage axis frame and rebuild the orientation gizmo after a look change. */
|
|
4569
|
+
private _readLookStage(): void {
|
|
5519
4570
|
this._applyGizmoAxisFrame();
|
|
5520
4571
|
this._buildOrientationGizmo();
|
|
5521
4572
|
}
|
|
@@ -5523,7 +4574,7 @@ export class EditorViewport {
|
|
|
5523
4574
|
/**
|
|
5524
4575
|
* THE STAGE'S FUNCTION from its view's presentation (ARCHITECTURE.md rule 7): which axis of
|
|
5525
4576
|
* the presented world is up — what the gizmos name and orient their axes by — and what a box
|
|
5526
|
-
* drag selects.
|
|
4577
|
+
* drag selects. The look does not determine the world's coordinate system.
|
|
5527
4578
|
*/
|
|
5528
4579
|
setStageFunction(
|
|
5529
4580
|
world: { readonly upAxis: 'y' | 'z'; readonly handedness: 'right' | 'left' },
|
|
@@ -5561,8 +4612,8 @@ export class EditorViewport {
|
|
|
5561
4612
|
* colour.
|
|
5562
4613
|
*
|
|
5563
4614
|
* three authors its translate arms, its scale stalks and its plane squares
|
|
5564
|
-
* on the
|
|
5565
|
-
*
|
|
4615
|
+
* on the positive side of each axis. Negative-side heads are hidden so
|
|
4616
|
+
* the set reads as three arms rather than six. Under
|
|
5566
4617
|
* a Z-up world one of those three arms is pointing the wrong WAY: the
|
|
5567
4618
|
* source's +Y is three's −Z, and Blender draws an arm along +Y and nothing
|
|
5568
4619
|
* along −Y. So the handles of a flipped axis are MIRRORED onto the other
|
|
@@ -5598,9 +4649,8 @@ export class EditorViewport {
|
|
|
5598
4649
|
// its geometry is not mirrored — doing either would apply the permutation
|
|
5599
4650
|
// a second time.
|
|
5600
4651
|
//
|
|
5601
|
-
//
|
|
5602
|
-
//
|
|
5603
|
-
// drag where it used to be moved nothing at all.
|
|
4652
|
+
// With the permutation applied to all three families, the scale handle
|
|
4653
|
+
// that points up would be mirrored to point DOWN.
|
|
5604
4654
|
const identity: Record<string, THREE.Color> = {
|
|
5605
4655
|
X: this._gizmoAxisColor(0),
|
|
5606
4656
|
Y: this._gizmoAxisColor(1),
|
|
@@ -5609,48 +4659,43 @@ export class EditorViewport {
|
|
|
5609
4659
|
YZ: this._gizmoAxisColor(0),
|
|
5610
4660
|
XZ: this._gizmoAxisColor(1),
|
|
5611
4661
|
};
|
|
5612
|
-
//
|
|
5613
|
-
// ONE `materialLib` per `TransformControlsGizmo` and hands the same
|
|
5614
|
-
// `matGreen` to the translate arm, the rotate ring and the scale stalk of
|
|
5615
|
-
// that axis (`TransformControls.js`'s `materialLib`, and the public
|
|
5616
|
-
// `setColors` that writes it). So a family that needs its OWN ink must
|
|
5617
|
-
// take its own material first; without that the last paint wins for every
|
|
5618
|
-
// family at once (measured 2026-09-21: painting scale identity turned the
|
|
5619
|
-
// translate arms back to three's colours in the same call).
|
|
4662
|
+
// Clone shared handle materials when a gizmo family needs separate colours, so changing scale colours cannot recolour translate or rotate.
|
|
5620
4663
|
const paint = (
|
|
5621
|
-
group: THREE.Object3D
|
|
4664
|
+
group: THREE.Object3D,
|
|
5622
4665
|
map: Record<string, THREE.Color>,
|
|
5623
4666
|
flip: boolean,
|
|
5624
4667
|
ownMaterial: boolean,
|
|
5625
4668
|
handles: boolean,
|
|
5626
4669
|
) => {
|
|
5627
|
-
if (!group) return;
|
|
5628
4670
|
group.traverse((child) => {
|
|
5629
|
-
const mesh = child as THREE.
|
|
4671
|
+
const mesh = child as THREE.Object3D & {
|
|
4672
|
+
geometry?: THREE.BufferGeometry;
|
|
4673
|
+
material?: (THREE.MeshBasicMaterial | THREE.LineBasicMaterial) & {
|
|
4674
|
+
_color?: THREE.Color | undefined;
|
|
4675
|
+
_opacity?: number | undefined;
|
|
4676
|
+
};
|
|
4677
|
+
};
|
|
5630
4678
|
if (flip && mesh.geometry) mesh.geometry.scale(delta[0]!, delta[1]!, delta[2]!);
|
|
5631
4679
|
if (
|
|
5632
4680
|
ownMaterial &&
|
|
5633
4681
|
mesh.material &&
|
|
5634
|
-
!Array.isArray(mesh.material) &&
|
|
5635
4682
|
!mesh.userData['volterOwnMaterial']
|
|
5636
4683
|
) {
|
|
5637
|
-
mesh.material =
|
|
4684
|
+
mesh.material = mesh.material.clone();
|
|
5638
4685
|
mesh.userData['volterOwnMaterial'] = true;
|
|
5639
4686
|
}
|
|
5640
|
-
const material = mesh.material
|
|
5641
|
-
|
|
5642
|
-
| undefined;
|
|
5643
|
-
if (!material || Array.isArray(material)) return;
|
|
4687
|
+
const material = mesh.material;
|
|
4688
|
+
if (!material) return;
|
|
5644
4689
|
if (flip) material.side = THREE.DoubleSide;
|
|
5645
4690
|
const ink = map[child.name];
|
|
5646
|
-
if (ink
|
|
4691
|
+
if (ink) {
|
|
5647
4692
|
// THE LOOK'S RESTING OPACITY over the handle's own (three draws its plane squares
|
|
5648
4693
|
// translucent); three caches it like the colour, so the cache is dropped too.
|
|
5649
4694
|
// Handles only: the drag's axis lines (the `helper` family) keep three's own.
|
|
5650
4695
|
if (handles) {
|
|
5651
4696
|
const base = (material.userData['volterBaseOpacity'] ??= material.opacity) as number;
|
|
5652
4697
|
material.opacity = base * (this._gizmoLook.opacity ?? 1);
|
|
5653
|
-
|
|
4698
|
+
material._opacity = undefined;
|
|
5654
4699
|
}
|
|
5655
4700
|
material.color.copy(ink);
|
|
5656
4701
|
// THREE CACHES A HANDLE'S RESTING COLOUR ON ITS FIRST UPDATE
|
|
@@ -5668,22 +4713,15 @@ export class EditorViewport {
|
|
|
5668
4713
|
.getHelper()
|
|
5669
4714
|
.children.find(
|
|
5670
4715
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5671
|
-
) as
|
|
5672
|
-
| {
|
|
5673
|
-
gizmo: Record<string, THREE.Object3D>;
|
|
5674
|
-
picker: Record<string, THREE.Object3D>;
|
|
5675
|
-
helper: Record<string, THREE.Object3D>;
|
|
5676
|
-
}
|
|
5677
|
-
| undefined;
|
|
5678
|
-
if (!node) continue;
|
|
4716
|
+
) as TransformControlsGizmo;
|
|
5679
4717
|
for (const family of ['gizmo', 'picker', 'helper'] as const) {
|
|
5680
4718
|
// ONLY THE TRANSLATE ARMS ARE MIRRORED. A rotate RING is symmetric
|
|
5681
4719
|
// about every axis it spans, so the permutation is visible in its
|
|
5682
4720
|
// colour alone; the scale family is in the object's own frame (above).
|
|
5683
4721
|
const handles = family === 'gizmo';
|
|
5684
|
-
paint(node[family]
|
|
5685
|
-
paint(node[family]
|
|
5686
|
-
paint(node[family]
|
|
4722
|
+
paint(node[family].translate, permuted, mirrored, false, handles);
|
|
4723
|
+
paint(node[family].rotate, permuted, false, false, handles);
|
|
4724
|
+
paint(node[family].scale, identity, false, true, handles);
|
|
5687
4725
|
}
|
|
5688
4726
|
}
|
|
5689
4727
|
if (mirrored) this._appliedAxisSigns = want as readonly (1 | -1)[];
|
|
@@ -5713,17 +4751,7 @@ export class EditorViewport {
|
|
|
5713
4751
|
.getHelper()
|
|
5714
4752
|
.children.find(
|
|
5715
4753
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5716
|
-
) as
|
|
5717
|
-
| (THREE.Object3D & {
|
|
5718
|
-
gizmo: Record<string, THREE.Object3D>;
|
|
5719
|
-
picker: Record<string, THREE.Object3D>;
|
|
5720
|
-
axis: string | null;
|
|
5721
|
-
mode: string;
|
|
5722
|
-
enabled: boolean;
|
|
5723
|
-
dragging: boolean;
|
|
5724
|
-
})
|
|
5725
|
-
| undefined;
|
|
5726
|
-
if (!node) return;
|
|
4754
|
+
) as TransformControlsGizmo & Pick<TransformControls, 'axis' | 'mode' | 'enabled' | 'dragging'>;
|
|
5727
4755
|
const threeUpdate = node.updateMatrixWorld.bind(node);
|
|
5728
4756
|
node.updateMatrixWorld = (force?: boolean) => {
|
|
5729
4757
|
threeUpdate(force);
|
|
@@ -5741,7 +4769,7 @@ export class EditorViewport {
|
|
|
5741
4769
|
: null;
|
|
5742
4770
|
if (hidden !== null) {
|
|
5743
4771
|
for (const family of [node.gizmo, node.picker]) {
|
|
5744
|
-
for (const handle of family[node.mode]
|
|
4772
|
+
for (const handle of family[node.mode].children) {
|
|
5745
4773
|
if (handle.name === hidden) handle.visible = false;
|
|
5746
4774
|
}
|
|
5747
4775
|
}
|
|
@@ -5757,12 +4785,10 @@ export class EditorViewport {
|
|
|
5757
4785
|
)
|
|
5758
4786
|
return;
|
|
5759
4787
|
const fixed = node.dragging ? (look.drag ?? look.hover) : look.hover;
|
|
5760
|
-
for (const handle of node.gizmo[node.mode]
|
|
4788
|
+
for (const handle of node.gizmo[node.mode].children) {
|
|
5761
4789
|
if (handle.name !== axis && !axis.split('').some((letter) => handle.name === letter)) continue;
|
|
5762
|
-
const material = (handle as THREE.Mesh).material as
|
|
5763
|
-
|
|
5764
|
-
| undefined;
|
|
5765
|
-
if (!material || Array.isArray(material) || !material.color) continue;
|
|
4790
|
+
const material = (handle as THREE.Mesh | THREE.Line).material as
|
|
4791
|
+
(THREE.MeshBasicMaterial | THREE.LineBasicMaterial) & { _color?: THREE.Color };
|
|
5766
4792
|
if (fixed !== null) {
|
|
5767
4793
|
material.color.setHex(fixed);
|
|
5768
4794
|
continue;
|
|
@@ -5808,19 +4834,15 @@ export class EditorViewport {
|
|
|
5808
4834
|
.getHelper()
|
|
5809
4835
|
.children.find(
|
|
5810
4836
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5811
|
-
) as
|
|
5812
|
-
| { gizmo: Record<string, THREE.Object3D>; picker: Record<string, THREE.Object3D> }
|
|
5813
|
-
| undefined;
|
|
5814
|
-
if (!node) return;
|
|
4837
|
+
) as TransformControlsGizmo;
|
|
5815
4838
|
const axisIndex: Record<string, number> = { X: 0, Y: 1, Z: 2 };
|
|
5816
4839
|
const along = (index: number, factor: number): [number, number, number] =>
|
|
5817
4840
|
[0, 1, 2].map((i) => (i === index ? factor : 1)) as [number, number, number];
|
|
5818
|
-
for (const handle of node.gizmo
|
|
4841
|
+
for (const handle of node.gizmo.translate.children) {
|
|
5819
4842
|
const index = axisIndex[handle.name];
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
const
|
|
5823
|
-
const isHead = cylinder.parameters?.radiusTop === 0;
|
|
4843
|
+
if (index === undefined) continue;
|
|
4844
|
+
const geometry = (handle as THREE.Mesh<THREE.CylinderGeometry>).geometry;
|
|
4845
|
+
const isHead = geometry.parameters.radiusTop === 0;
|
|
5824
4846
|
if (!isHead) {
|
|
5825
4847
|
geometry.scale(...along(index, stretch));
|
|
5826
4848
|
continue;
|
|
@@ -5838,10 +4860,10 @@ export class EditorViewport {
|
|
|
5838
4860
|
offset[index] = base * stretch;
|
|
5839
4861
|
geometry.translate(...offset);
|
|
5840
4862
|
}
|
|
5841
|
-
for (const handle of node.picker
|
|
4863
|
+
for (const handle of node.picker.translate.children) {
|
|
5842
4864
|
const index = axisIndex[handle.name];
|
|
5843
|
-
|
|
5844
|
-
|
|
4865
|
+
if (index === undefined) continue;
|
|
4866
|
+
const geometry = (handle as THREE.Mesh<THREE.CylinderGeometry>).geometry;
|
|
5845
4867
|
geometry.scale(...along(index, tipScale));
|
|
5846
4868
|
}
|
|
5847
4869
|
this._appliedArrow = want;
|
|
@@ -5866,14 +4888,13 @@ export class EditorViewport {
|
|
|
5866
4888
|
.getHelper()
|
|
5867
4889
|
.children.find(
|
|
5868
4890
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5869
|
-
) as
|
|
5870
|
-
for (const handle of node
|
|
5871
|
-
const geometry = (handle as THREE.Mesh).geometry as THREE.BufferGeometry | undefined;
|
|
4891
|
+
) as TransformControlsGizmo;
|
|
4892
|
+
for (const handle of node.gizmo.rotate.children) {
|
|
5872
4893
|
// The axis rings only: the trackball circle and the view ring stay hairlines (Godot's
|
|
5873
4894
|
// grey circle is one).
|
|
5874
4895
|
if (!['X', 'Y', 'Z'].includes(handle.name)) continue;
|
|
5875
|
-
|
|
5876
|
-
const radius =
|
|
4896
|
+
const geometry = (handle as THREE.Mesh<THREE.TorusGeometry>).geometry;
|
|
4897
|
+
const radius = geometry.parameters.radius;
|
|
5877
4898
|
geometry.computeBoundingBox();
|
|
5878
4899
|
geometry.boundingBox!.getSize(size);
|
|
5879
4900
|
const normal = size.x <= size.y && size.x <= size.z ? 0 : size.y <= size.z ? 1 : 2;
|
|
@@ -5894,8 +4915,6 @@ export class EditorViewport {
|
|
|
5894
4915
|
}
|
|
5895
4916
|
|
|
5896
4917
|
private _patchGizmo(gizmoHelper: THREE.Object3D): void {
|
|
5897
|
-
const colorMap = this._axisColorMap();
|
|
5898
|
-
|
|
5899
4918
|
// Plane geometry offsets to bring inner corners to object center
|
|
5900
4919
|
const planeOffsets: Record<string, [number, number, number]> = {
|
|
5901
4920
|
XY: [-0.075, -0.075, 0],
|
|
@@ -5904,44 +4923,20 @@ export class EditorViewport {
|
|
|
5904
4923
|
};
|
|
5905
4924
|
|
|
5906
4925
|
gizmoHelper.traverse((child) => {
|
|
5907
|
-
const mesh = child as THREE.Mesh;
|
|
5908
|
-
|
|
5909
|
-
// Recolor handle materials
|
|
5910
|
-
const newColor = colorMap[child.name];
|
|
5911
|
-
if (newColor && mesh.material) {
|
|
5912
|
-
const mat = mesh.material as THREE.MeshBasicMaterial | THREE.LineBasicMaterial;
|
|
5913
|
-
if (mat.color) mat.color.copy(newColor);
|
|
5914
|
-
}
|
|
5915
|
-
|
|
5916
4926
|
// Shift plane handle geometry
|
|
5917
4927
|
const offset = planeOffsets[child.name];
|
|
5918
|
-
if (offset
|
|
5919
|
-
|
|
4928
|
+
if (offset) {
|
|
4929
|
+
(child as THREE.Mesh).geometry.translate(offset[0], offset[1], offset[2]);
|
|
5920
4930
|
}
|
|
5921
4931
|
});
|
|
5922
4932
|
}
|
|
5923
4933
|
|
|
5924
|
-
/**
|
|
5925
|
-
* Combined-mode surgery on the aux SCALE gizmo. The stock scale handles
|
|
5926
|
-
* share their axis span with the translate arrows (cubes at ±0.5, picker
|
|
5927
|
-
* cones 0–0.6 — identical to translate's pickers), so with three
|
|
5928
|
-
* synchronized gizmos the translate picker always wins the pointer and
|
|
5929
|
-
* scale is undraggable (measured: dragging the X cube moved position).
|
|
5930
|
-
* Slide the axis cubes and their pickers outward past the arrow tips, and
|
|
5931
|
-
* drop the plane/uniform handles and axis lines whose footprint translate
|
|
5932
|
-
* already owns. Placement is baked into geometry by three's setupGizmo, so
|
|
5933
|
-
* geometry.translate is frame-safe where object transforms are not
|
|
5934
|
-
* (TransformControlsGizmo.updateMatrixWorld resets handle transforms every
|
|
5935
|
-
* frame). Single-mode scale (R) uses the primary gizmo and is untouched.
|
|
5936
|
-
*/
|
|
4934
|
+
/** Shift auxiliary scale cubes and picker geometry clear of the combined tool's translate controls. */
|
|
5937
4935
|
private _patchCombinedScaleGizmo(helper: THREE.Object3D): void {
|
|
5938
4936
|
const AXIS_OFFSET = 0.55;
|
|
5939
4937
|
const gizmoNode = helper.children.find(
|
|
5940
4938
|
(child) => (child as { isTransformControlsGizmo?: boolean }).isTransformControlsGizmo,
|
|
5941
|
-
) as
|
|
5942
|
-
| { gizmo: Record<string, THREE.Object3D>; picker: Record<string, THREE.Object3D> }
|
|
5943
|
-
| undefined;
|
|
5944
|
-
if (!gizmoNode) return;
|
|
4939
|
+
) as TransformControlsGizmo;
|
|
5945
4940
|
const axisOf: Record<string, THREE.Vector3> = {
|
|
5946
4941
|
X: new THREE.Vector3(1, 0, 0),
|
|
5947
4942
|
Y: new THREE.Vector3(0, 1, 0),
|
|
@@ -5953,24 +4948,22 @@ export class EditorViewport {
|
|
|
5953
4948
|
const CUBE_CENTER = 0.54 + AXIS_OFFSET;
|
|
5954
4949
|
const PICKER_SIZE = 0.3;
|
|
5955
4950
|
for (const [group, isPicker] of [
|
|
5956
|
-
[gizmoNode.gizmo
|
|
5957
|
-
[gizmoNode.picker
|
|
4951
|
+
[gizmoNode.gizmo.scale, false],
|
|
4952
|
+
[gizmoNode.picker.scale, true],
|
|
5958
4953
|
] as const) {
|
|
5959
|
-
if (!group) continue;
|
|
5960
4954
|
for (const child of [...group.children]) {
|
|
5961
|
-
const mesh = child as THREE.Mesh
|
|
4955
|
+
const mesh = child as THREE.Mesh<THREE.BoxGeometry | THREE.CylinderGeometry>;
|
|
5962
4956
|
const axis = axisOf[child.name];
|
|
5963
4957
|
const isAxisLine =
|
|
5964
4958
|
axis !== undefined &&
|
|
5965
|
-
mesh.geometry
|
|
4959
|
+
mesh.geometry.type === 'CylinderGeometry' &&
|
|
5966
4960
|
(mesh.geometry as THREE.CylinderGeometry).parameters.radiusTop < 0.05;
|
|
5967
4961
|
if (axis === undefined || isAxisLine) {
|
|
5968
4962
|
group.remove(child);
|
|
5969
4963
|
continue;
|
|
5970
4964
|
}
|
|
5971
4965
|
mesh.geometry.computeBoundingBox();
|
|
5972
|
-
const bounds = mesh.geometry.boundingBox
|
|
5973
|
-
if (!bounds) continue;
|
|
4966
|
+
const bounds = mesh.geometry.boundingBox!;
|
|
5974
4967
|
const sign = Math.sign(bounds.getCenter(new THREE.Vector3()).dot(axis)) || 1;
|
|
5975
4968
|
if (isPicker) {
|
|
5976
4969
|
const at = axis.clone().multiplyScalar(sign * CUBE_CENTER);
|
|
@@ -6005,8 +4998,6 @@ export class EditorViewport {
|
|
|
6005
4998
|
private static _FLY_KEYS = new Set(['w', 'a', 's', 'd', 'q', 'e']);
|
|
6006
4999
|
|
|
6007
5000
|
private _enterFlyMode(): void {
|
|
6008
|
-
if (this._flyActive) return;
|
|
6009
|
-
if (this._cameraViewMode === 'view') return;
|
|
6010
5001
|
this._flyActive = true;
|
|
6011
5002
|
this._snapAnimating = false; // cancel any view snap
|
|
6012
5003
|
this.orbitControls.enabled = false;
|
|
@@ -6017,10 +5008,9 @@ export class EditorViewport {
|
|
|
6017
5008
|
}
|
|
6018
5009
|
|
|
6019
5010
|
private _exitFlyMode(): void {
|
|
6020
|
-
if (!this._flyActive) return;
|
|
6021
5011
|
this._flyActive = false;
|
|
6022
5012
|
this._flyKeys.clear();
|
|
6023
|
-
this.orbitControls.enabled =
|
|
5013
|
+
this.orbitControls.enabled = true;
|
|
6024
5014
|
document.exitPointerLock();
|
|
6025
5015
|
// Sync orbit target to a point in front of the camera
|
|
6026
5016
|
const camera = this.renderCamera;
|
|
@@ -6078,10 +5068,6 @@ export class EditorViewport {
|
|
|
6078
5068
|
|
|
6079
5069
|
private _onFlyKeyUp = (e: KeyboardEvent): void => {
|
|
6080
5070
|
const key = e.key.toLowerCase();
|
|
6081
|
-
if (key === 'shift') {
|
|
6082
|
-
this._flyKeys.delete('shift');
|
|
6083
|
-
return;
|
|
6084
|
-
}
|
|
6085
5071
|
this._flyKeys.delete(key);
|
|
6086
5072
|
};
|
|
6087
5073
|
|
|
@@ -6120,11 +5106,8 @@ export class EditorViewport {
|
|
|
6120
5106
|
const distance = this.camera.position.distanceTo(this.orbitControls.target);
|
|
6121
5107
|
const worldPerPixel =
|
|
6122
5108
|
(2 * Math.tan(((this.camera.fov / 2) * Math.PI) / 180) * distance) / height;
|
|
6123
|
-
//
|
|
6124
|
-
//
|
|
6125
|
-
// of the viewport per click and flung a human tester off the map ("went
|
|
6126
|
-
// to the bottom, can't get out" — runhuman pass 17). Clamp each event to
|
|
6127
|
-
// a modest step; trackpad deltas sit far below the clamp and keep 1:1.
|
|
5109
|
+
// Clamp mouse-wheel pan events to a modest step. Fine trackpad deltas stay below the clamp and
|
|
5110
|
+
// preserve their scale.
|
|
6128
5111
|
const unit = e.deltaMode === 1 ? 16 : 1;
|
|
6129
5112
|
const clampStep = (value: number): number =>
|
|
6130
5113
|
Math.sign(value) * Math.min(Math.abs(value * unit), 30);
|
|
@@ -6156,8 +5139,9 @@ export class EditorViewport {
|
|
|
6156
5139
|
if (e.button === 0 && !e.altKey) {
|
|
6157
5140
|
if (this._selectConstraintControlAt(e)) return;
|
|
6158
5141
|
const mesh = this._raycastSpatialHandle(e);
|
|
6159
|
-
|
|
6160
|
-
|
|
5142
|
+
if (!mesh) return;
|
|
5143
|
+
const binding = spatialHandleBinding(mesh);
|
|
5144
|
+
if (binding.handle.writable) {
|
|
6161
5145
|
e.stopPropagation();
|
|
6162
5146
|
this._activeSpatialHandle = binding;
|
|
6163
5147
|
this._spatialHandleDragPoint.copy(mesh.position);
|
|
@@ -6203,21 +5187,14 @@ export class EditorViewport {
|
|
|
6203
5187
|
|
|
6204
5188
|
if (!this._marqueeDiv) {
|
|
6205
5189
|
this._marqueeDiv = document.createElement('div');
|
|
6206
|
-
//
|
|
6207
|
-
// otherwise-numeric z-index scale); now the canonical `zIndex.sticky`
|
|
6208
|
-
// token. Sticky chrome stays above the editor root while dropdowns
|
|
6209
|
-
// retain their distinct higher tier.
|
|
5190
|
+
// Use the sticky chrome tier above the editor root and below dropdowns.
|
|
6210
5191
|
Object.assign(this._marqueeDiv.style, {
|
|
6211
5192
|
position: 'fixed',
|
|
6212
5193
|
pointerEvents: 'none',
|
|
6213
5194
|
zIndex: zIndex.sticky,
|
|
6214
5195
|
});
|
|
6215
|
-
//
|
|
6216
|
-
//
|
|
6217
|
-
// body-parented div resolves both colors to nothing and the rubber
|
|
6218
|
-
// band is INVISIBLE — three human passes marqueed blind, reading the
|
|
6219
|
-
// selection count jump 4/0 and calling the tool broken (runhuman
|
|
6220
|
-
// 27–29). `position: fixed` keeps the client-coordinate math.
|
|
5196
|
+
// Mount inside the theme scope so --volter-* colors resolve. Fixed positioning keeps the
|
|
5197
|
+
// client-coordinate math.
|
|
6221
5198
|
(document.getElementById('editor-chrome-root') ?? document.body).appendChild(
|
|
6222
5199
|
this._marqueeDiv,
|
|
6223
5200
|
);
|
|
@@ -6228,12 +5205,12 @@ export class EditorViewport {
|
|
|
6228
5205
|
Object.assign(this._marqueeDiv.style, marqueeStyle(nativeViewportMarqueeDash(this._canvas)));
|
|
6229
5206
|
}
|
|
6230
5207
|
|
|
6231
|
-
if (this._marqueeActive
|
|
5208
|
+
if (this._marqueeActive) {
|
|
6232
5209
|
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
6233
5210
|
const top = Math.min(this._pointerDownPos.y, e.clientY);
|
|
6234
5211
|
const w = Math.abs(e.clientX - this._pointerDownPos.x);
|
|
6235
5212
|
const h = Math.abs(e.clientY - this._pointerDownPos.y);
|
|
6236
|
-
Object.assign(this._marqueeDiv
|
|
5213
|
+
Object.assign(this._marqueeDiv!.style, {
|
|
6237
5214
|
left: `${left}px`,
|
|
6238
5215
|
top: `${top}px`,
|
|
6239
5216
|
width: `${w}px`,
|
|
@@ -6243,6 +5220,14 @@ export class EditorViewport {
|
|
|
6243
5220
|
}
|
|
6244
5221
|
};
|
|
6245
5222
|
|
|
5223
|
+
/** A drag lost its release: a spatial handle goes back uncommitted, a constraint-control
|
|
5224
|
+
* session closes, and the pointer session with them. */
|
|
5225
|
+
private _onInteractionDragLost = (): void => {
|
|
5226
|
+
if (this._activeSpatialHandle) this._finishSpatialHandleDrag(false);
|
|
5227
|
+
this._constraintControlPointerSession = false;
|
|
5228
|
+
this._pointerSessionActive = false;
|
|
5229
|
+
};
|
|
5230
|
+
|
|
6246
5231
|
private _onPointerUp = (e: PointerEvent): void => {
|
|
6247
5232
|
// Only complete gestures that STARTED on the canvas (`_onPointerDown`) —
|
|
6248
5233
|
// this listener lives on the interaction element (see the binding comment
|
|
@@ -6260,18 +5245,12 @@ export class EditorViewport {
|
|
|
6260
5245
|
return;
|
|
6261
5246
|
}
|
|
6262
5247
|
|
|
6263
|
-
//
|
|
6264
|
-
//
|
|
6265
|
-
// objects), never the composite's other (react/pixi) layered children. A
|
|
6266
|
-
// layered/rect-based marquee across every visible layer is out of scope
|
|
6267
|
-
// for this task (D12) — single-click picking is layered (below), marquee
|
|
6268
|
-
// is not.
|
|
5248
|
+
// Marquee scans this viewport's native object map. Other composite roots are outside
|
|
5249
|
+
// its screen-projection selection; single-click picking below can cross those roots.
|
|
6269
5250
|
if (this._marqueeActive) {
|
|
6270
5251
|
this._marqueeActive = false;
|
|
6271
5252
|
this.orbitControls.enabled = true;
|
|
6272
|
-
|
|
6273
|
-
this._marqueeDiv.style.display = 'none';
|
|
6274
|
-
}
|
|
5253
|
+
this._marqueeDiv!.style.display = 'none';
|
|
6275
5254
|
|
|
6276
5255
|
// Find all entities whose screen projection falls within the marquee
|
|
6277
5256
|
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
@@ -6283,33 +5262,12 @@ export class EditorViewport {
|
|
|
6283
5262
|
const corner = new THREE.Vector3();
|
|
6284
5263
|
const bounds = new THREE.Box3();
|
|
6285
5264
|
for (const [id, obj] of this._objectMap) {
|
|
6286
|
-
// Skip
|
|
6287
|
-
// behavior for free (THREE.Raycaster's own `intersectObject` already
|
|
6288
|
-
// stops descending into an `obj.visible === false` subtree, so a
|
|
6289
|
-
// single click can never land on one). Marquee did its own screen-
|
|
6290
|
-
// space rect test with no such check, so a world hidden via the
|
|
6291
|
-
// group-row eye (`world-hidden-viewport.ts` — forces `obj.visible =
|
|
6292
|
-
// false` for the whole world while hidden) stayed marquee-selectable
|
|
6293
|
-
// even though it was invisible and unclickable. Also correctly skips
|
|
6294
|
-
// an entity hidden via its OWN eye, for the same reason.
|
|
5265
|
+
// Skip objects whose own visibility is off.
|
|
6295
5266
|
if (!obj.visible) continue;
|
|
6296
|
-
//
|
|
6297
|
-
// origin test, so a rectangle around scene content kept catching the
|
|
6298
|
-
// template's authored camera — and camera selection pops the live
|
|
6299
|
-
// preview overlay, which two consecutive human passes read as "it
|
|
6300
|
-
// instantly plays" and which then sat over the viewport blocking
|
|
6301
|
-
// their next drag (runhuman passes 27/28; play state was measured
|
|
6302
|
-
// stopped throughout). Click and hierarchy selection still reach it.
|
|
5267
|
+
// Exclude cameras from marquee selection; click and hierarchy selection still reach them.
|
|
6303
5268
|
if ((obj as THREE.Camera).isCamera === true) continue;
|
|
6304
|
-
//
|
|
6305
|
-
//
|
|
6306
|
-
// Environment group — all at scene center) landed inside any central
|
|
6307
|
-
// rectangle, so circling three boxes selected seven things and a
|
|
6308
|
-
// human tester gave up on marquee ("I still select more than what is
|
|
6309
|
-
// needed" — runhuman pass 25). A rectangle dragged AROUND something
|
|
6310
|
-
// selects what fits inside it; a fixture larger than the rectangle
|
|
6311
|
-
// cannot. Entities with no renderable bounds (lights, cameras,
|
|
6312
|
-
// empty groups) keep the origin test — circling a light selects it.
|
|
5269
|
+
// Test projected geometry bounds against the rectangle using the presentation’s selection rule.
|
|
5270
|
+
// Objects with no renderable bounds use their projected origin.
|
|
6313
5271
|
bounds.setFromObject(obj);
|
|
6314
5272
|
if (bounds.isEmpty()) {
|
|
6315
5273
|
const screen = this._projectToScreen(obj);
|
|
@@ -6318,21 +5276,7 @@ export class EditorViewport {
|
|
|
6318
5276
|
}
|
|
6319
5277
|
continue;
|
|
6320
5278
|
}
|
|
6321
|
-
//
|
|
6322
|
-
// editor's own answer is CONTAINMENT, and Blender's Select Box is
|
|
6323
|
-
// TOUCH — its object-mode box select reads the object-id buffer under
|
|
6324
|
-
// the rectangle, so any drawn part inside it selects the object. Both
|
|
6325
|
-
// are real answers and the field's own note carries why neither can be
|
|
6326
|
-
// the other's default. Measured 2026-09-21 on the Model document: a
|
|
6327
|
-
// rectangle holding the whole cube and cutting the torus selected the
|
|
6328
|
-
// cube alone, where Blender takes both.
|
|
6329
|
-
//
|
|
6330
|
-
// TOUCH is the projected bounding box INTERSECTING the rectangle,
|
|
6331
|
-
// which is a superset of Blender's drawn-pixel test — an honest
|
|
6332
|
-
// approximation, and the closest one available without a picking
|
|
6333
|
-
// buffer. It is stated rather than hidden: a long thin diagonal object
|
|
6334
|
-
// whose box crosses the rectangle while none of its pixels do is the
|
|
6335
|
-
// case where the two differ.
|
|
5279
|
+
// Use the presentation’s boxSelect rule: containment or overlap. Geometry bounds approximate Blender’s pixel-touch selection and can include a thin diagonal that draws outside the rectangle.
|
|
6336
5280
|
const touch = this._stageBoxSelect === 'touch';
|
|
6337
5281
|
const rect = this._canvas.getBoundingClientRect();
|
|
6338
5282
|
// Through the camera on screen: an orthographic document view or a camera view draws with
|
|
@@ -6369,8 +5313,7 @@ export class EditorViewport {
|
|
|
6369
5313
|
}
|
|
6370
5314
|
|
|
6371
5315
|
if (e.shiftKey) {
|
|
6372
|
-
//
|
|
6373
|
-
// extension is Phase-F; first-party store op for now).
|
|
5316
|
+
// Extend the store selection for the additive modifier.
|
|
6374
5317
|
for (const id of hitIds) this._store.shell.addToSelection(id);
|
|
6375
5318
|
} else {
|
|
6376
5319
|
setAuthoringSelection(this._authoring(), hitIds);
|
|
@@ -6389,63 +5332,26 @@ export class EditorViewport {
|
|
|
6389
5332
|
// Don't interfere with gizmo clicks
|
|
6390
5333
|
if (this._anyGizmoDragging()) return;
|
|
6391
5334
|
|
|
6392
|
-
// Check orientation gizmo
|
|
6393
|
-
// without a visible three surface, so it can't be clicked either)
|
|
5335
|
+
// Check the orientation gizmo first; it is clickable only on a visible three surface.
|
|
6394
5336
|
if (this._threejsToolContextActive && this._handleViewCubeClick(e)) return;
|
|
6395
5337
|
|
|
6396
|
-
//
|
|
6397
|
-
//
|
|
6398
|
-
// falling through to the three raycast (now `VolterSceneAuthoringAdapter
|
|
6399
|
-
// .pickable`) — see `pickTopmost`'s doc comment. Note: an INTERACTIVE
|
|
6400
|
-
// layer (session `interactive` toggle, B1) is mutually exclusive with this
|
|
6401
|
-
// walk running at all, by construction — its `pointer-events:auto` DOM
|
|
6402
|
-
// intercepts the event before it ever reaches this canvas listener, so
|
|
6403
|
-
// there is no special-case here for it.
|
|
5338
|
+
// Try the active adapter’s pickable layers from topmost to bottommost. Interactive DOM layers
|
|
5339
|
+
// intercept their own pointer events before they reach this canvas.
|
|
6404
5340
|
const deepSelection = e.shiftKey && (e.metaKey || e.ctrlKey);
|
|
6405
|
-
//
|
|
6406
|
-
//
|
|
6407
|
-
// against the scope that WAS open, so `pickAcrossScopeExit` lets the stack
|
|
6408
|
-
// follow the hit and re-resolves once when it moved — without that, a
|
|
6409
|
-
// three-deep stack answered one level too shallow and took a second click
|
|
6410
|
-
// to land (`authoring/selection-scope.ts`).
|
|
5341
|
+
// A click outside the open scope leaves it; empty space closes every level. pickAcrossScopeExit
|
|
5342
|
+
// updates the scope stack and resolves the hit again when the stack changes.
|
|
6411
5343
|
const entityId = pickAcrossScopeExit(this._scopeAuthoring(), () =>
|
|
6412
5344
|
this._pickAt(e.clientX, e.clientY, deepSelection ? 'deep' : 'normal'),
|
|
6413
5345
|
);
|
|
6414
5346
|
|
|
6415
|
-
if (deepSelection) {
|
|
6416
|
-
setAuthoringSelection(this._authoring(), entityId ? [entityId] : []);
|
|
6417
|
-
} else if (e.shiftKey && entityId) {
|
|
6418
|
-
// TOGGLE, not add. On a canvas this modifier is the ONE gesture for
|
|
6419
|
-
// "and also this / no, not that one" — Figma, Blender and Unity all
|
|
6420
|
-
// remove an already-selected object on a second shift-click, and adding
|
|
6421
|
-
// only meant a mis-click was unrecoverable: "if it's like 20 items and
|
|
6422
|
-
// you mess up one, you're gonna have to start all over" (runhuman pass
|
|
6423
|
-
// 94, their top finding). The Hierarchy keeps the LIST convention
|
|
6424
|
-
// instead — shift extends a RANGE there — because a row list has an
|
|
6425
|
-
// order for a range to mean something and a viewport does not.
|
|
6426
|
-
this._store.shell.toggleSelection(entityId);
|
|
6427
|
-
} else if ((e.metaKey || e.ctrlKey) && entityId) {
|
|
5347
|
+
if (!deepSelection && entityId && (e.shiftKey || e.metaKey || e.ctrlKey)) {
|
|
6428
5348
|
this._store.shell.toggleSelection(entityId);
|
|
6429
5349
|
} else {
|
|
6430
5350
|
setAuthoringSelection(this._authoring(), entityId ? [entityId] : []);
|
|
6431
5351
|
}
|
|
6432
5352
|
};
|
|
6433
5353
|
|
|
6434
|
-
/**
|
|
6435
|
-
* The adapter a selection SCOPE is keyed on.
|
|
6436
|
-
*
|
|
6437
|
-
* `authoring/selection-scope.ts` keys its stack by adapter IDENTITY, and the
|
|
6438
|
-
* Hierarchy — its breadcrumb, and its own double-click scope entry — resolves
|
|
6439
|
-
* the PANEL adapter (`authoring/panel-authoring.ts`), which for a Scene
|
|
6440
|
-
* document is the composite's Three child, not the composite itself.
|
|
6441
|
-
* A scope opened against a different object of the same tree is invisible
|
|
6442
|
-
* there (measured: a viewport drill-in selected the deeper member correctly
|
|
6443
|
-
* and the breadcrumb never appeared). One resolution, so the viewport gesture
|
|
6444
|
-
* and the panel are talking about the same scope.
|
|
6445
|
-
*
|
|
6446
|
-
* A host that INJECTED its own adapter (the Object3D document's own viewport)
|
|
6447
|
-
* is not driving the shared panels at all and keeps its own.
|
|
6448
|
-
*/
|
|
5354
|
+
/** Key selection scopes to the same panel adapter used by the Hierarchy. An injected document adapter keeps its own scope. */
|
|
6449
5355
|
private _scopeAuthoring(): AuthoringAdapter {
|
|
6450
5356
|
return this._standaloneAuthoring
|
|
6451
5357
|
? this._authoring()
|
|
@@ -6461,25 +5367,10 @@ export class EditorViewport {
|
|
|
6461
5367
|
}
|
|
6462
5368
|
|
|
6463
5369
|
/**
|
|
6464
|
-
* Double-click
|
|
6465
|
-
*
|
|
6466
|
-
*
|
|
6467
|
-
*
|
|
6468
|
-
* as one instance no matter where you click it. That is Figma's instance
|
|
6469
|
-
* semantics and it is correct; what was missing was the way in. Double-click
|
|
6470
|
-
* opens that instance's scope (`enterSelectionScope`) and selects the next
|
|
6471
|
-
* member down the same owner chain — repeat to keep descending, Escape
|
|
6472
|
-
* (`editor-hotkeys.ts`) walks back out, and a click outside the open scope
|
|
6473
|
-
* leaves it (`_onPointerUp`).
|
|
6474
|
-
*
|
|
6475
|
-
* The deeper member is not computed here: `layered-pick.ts` already hands
|
|
6476
|
-
* `currentSelectionScopeId()` to the adapter's own resolve, so re-running the
|
|
6477
|
-
* ORDINARY pick with the scope open is what answers. No second chain rule,
|
|
6478
|
-
* nothing per-surface, no new state.
|
|
6479
|
-
*
|
|
6480
|
-
* The two single-clicks that precede this both select that same outermost
|
|
6481
|
-
* boundary, so they cannot conflict with the drill — they are the step it
|
|
6482
|
-
* starts from.
|
|
5370
|
+
* Double-click opens the selected component’s scope and selects the next member down its owner
|
|
5371
|
+
* chain. Escape walks out and an outside click leaves the scope. The ordinary layered pick
|
|
5372
|
+
* resolves through the adapter with currentSelectionScopeId, including the two single-clicks
|
|
5373
|
+
* preceding this gesture.
|
|
6483
5374
|
*/
|
|
6484
5375
|
private _onDoubleClick = (e: MouseEvent): void => {
|
|
6485
5376
|
if (e.button !== 0) return;
|
|
@@ -6498,181 +5389,4 @@ export class EditorViewport {
|
|
|
6498
5389
|
this._pickAt(e.clientX, e.clientY, 'normal'),
|
|
6499
5390
|
);
|
|
6500
5391
|
};
|
|
6501
|
-
|
|
6502
|
-
// --- Asset drag-and-drop onto viewport ---
|
|
6503
|
-
|
|
6504
|
-
/**
|
|
6505
|
-
* Screen → world point for the path-probe tool (W1a): prefer a hit on the
|
|
6506
|
-
* baked navmesh overlay (guarantees a point on the mesh), fall back to the
|
|
6507
|
-
* ground plane so probing still lands a marker pre-bake.
|
|
6508
|
-
*/
|
|
6509
|
-
/**
|
|
6510
|
-
* Raycast from screen coordinates to the DECLARED ground plane. Returns world
|
|
6511
|
-
* position or null.
|
|
6512
|
-
*/
|
|
6513
|
-
private _raycastGroundPlane(e: { clientX: number; clientY: number }): THREE.Vector3 | null {
|
|
6514
|
-
const rect = this._canvas.getBoundingClientRect();
|
|
6515
|
-
const mouse = new THREE.Vector2(
|
|
6516
|
-
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
6517
|
-
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
|
6518
|
-
);
|
|
6519
|
-
this.raycaster.setFromCamera(mouse, this._screenCamera);
|
|
6520
|
-
const basis = presentationRegionBasis('three');
|
|
6521
|
-
this._groundPlane.set(
|
|
6522
|
-
basis.up === 'z' ? UP_Z : UP_Y,
|
|
6523
|
-
// `Plane.constant` is the SIGNED distance from the origin along -normal,
|
|
6524
|
-
// so a ground at `h` is `-h` here.
|
|
6525
|
-
-basis.groundHeight,
|
|
6526
|
-
);
|
|
6527
|
-
const target = new THREE.Vector3();
|
|
6528
|
-
const hit = this.raycaster.ray.intersectPlane(this._groundPlane, target);
|
|
6529
|
-
return hit;
|
|
6530
|
-
}
|
|
6531
|
-
|
|
6532
|
-
private _showDropOverlay(): void {
|
|
6533
|
-
if (this._dropOverlay) return;
|
|
6534
|
-
const overlay = document.createElement('div');
|
|
6535
|
-
// V-12 — the stray string `'5'` (below every named tier in the z-index
|
|
6536
|
-
// scale) becomes the numeric `zIndex.base`, the lowest defined tier,
|
|
6537
|
-
// preserving its relative order (still below the marquee's `zIndex.
|
|
6538
|
-
// sticky` and every dropdown/menu). The semantic accent tokens reconcile this
|
|
6539
|
-
// block's former one-off blue onto the canonical palette accent tint per
|
|
6540
|
-
// the audit's own §3 absorption list.
|
|
6541
|
-
Object.assign(overlay.style, {
|
|
6542
|
-
position: 'fixed',
|
|
6543
|
-
inset: '0',
|
|
6544
|
-
pointerEvents: 'none',
|
|
6545
|
-
border: `2px dashed ${themeVars.accent.default}`,
|
|
6546
|
-
background: themeVars.accent.muted,
|
|
6547
|
-
zIndex: zIndex.base,
|
|
6548
|
-
});
|
|
6549
|
-
// Same class as the marquee band: `--volter-*` resolves only under
|
|
6550
|
-
// `#editor-chrome-root`, so a body-parented overlay draws with NO border
|
|
6551
|
-
// or fill — the drag-to-viewport highlight has been invisible.
|
|
6552
|
-
(document.getElementById('editor-chrome-root') ?? document.body).appendChild(overlay);
|
|
6553
|
-
this._dropOverlay = overlay;
|
|
6554
|
-
}
|
|
6555
|
-
|
|
6556
|
-
private _hideDropOverlay(): void {
|
|
6557
|
-
if (this._dropOverlay) {
|
|
6558
|
-
this._dropOverlay.remove();
|
|
6559
|
-
this._dropOverlay = null;
|
|
6560
|
-
}
|
|
6561
|
-
}
|
|
6562
|
-
|
|
6563
|
-
private _onDragOver = (e: DragEvent): void => {
|
|
6564
|
-
if (!e.dataTransfer?.types.includes('application/x-editor-asset')) return;
|
|
6565
|
-
e.preventDefault();
|
|
6566
|
-
e.dataTransfer.dropEffect = 'copy';
|
|
6567
|
-
this._showDropOverlay();
|
|
6568
|
-
};
|
|
6569
|
-
|
|
6570
|
-
private _onDragLeave = (_e: DragEvent): void => {
|
|
6571
|
-
this._hideDropOverlay();
|
|
6572
|
-
};
|
|
6573
|
-
|
|
6574
|
-
private _onDrop = (e: DragEvent): void => {
|
|
6575
|
-
this._hideDropOverlay();
|
|
6576
|
-
const raw = e.dataTransfer?.getData('application/x-editor-asset');
|
|
6577
|
-
if (!raw) return;
|
|
6578
|
-
e.preventDefault();
|
|
6579
|
-
|
|
6580
|
-
let data: {
|
|
6581
|
-
path: string;
|
|
6582
|
-
kind: string;
|
|
6583
|
-
name: string;
|
|
6584
|
-
component?: AssetDropContext['item'];
|
|
6585
|
-
online?: OnlineAssetDrop;
|
|
6586
|
-
};
|
|
6587
|
-
try {
|
|
6588
|
-
data = JSON.parse(raw);
|
|
6589
|
-
} catch {
|
|
6590
|
-
return;
|
|
6591
|
-
}
|
|
6592
|
-
|
|
6593
|
-
// Find drop position via ground-plane raycast, fallback to the ground's own
|
|
6594
|
-
// origin. The two in-plane components are rounded to centimetres; the UP
|
|
6595
|
-
// component is pinned to the DECLARED ground height rather than to a
|
|
6596
|
-
// hardcoded 0 — which axis that is comes from the region's basis, not from
|
|
6597
|
-
// an assumption that every game is Y-up.
|
|
6598
|
-
const basis = presentationRegionBasis('three');
|
|
6599
|
-
const worldPos = this._raycastGroundPlane(e);
|
|
6600
|
-
const cm = (value: number): number => Math.round(value * 100) / 100;
|
|
6601
|
-
const pos: [number, number, number] =
|
|
6602
|
-
basis.up === 'z'
|
|
6603
|
-
? [cm(worldPos?.x ?? 0), cm(worldPos?.y ?? 0), basis.groundHeight]
|
|
6604
|
-
: [cm(worldPos?.x ?? 0), basis.groundHeight, cm(worldPos?.z ?? 0)];
|
|
6605
|
-
|
|
6606
|
-
// A viewport drop is an AUTHORING op, so it goes through the active
|
|
6607
|
-
// adapter's `assetDrop` provider — not straight into the first-party
|
|
6608
|
-
// document. An adapter that declares no `assetDrop` (for example a LIVE
|
|
6609
|
-
// adopted play scene) accepts nothing: dropping into it used to mutate the
|
|
6610
|
-
// retired descriptor document of a world that was not even on screen.
|
|
6611
|
-
// Absent ⇒ nothing happens, which is the honest degrade.
|
|
6612
|
-
const authoring = this._authoring();
|
|
6613
|
-
const assetDrop = authoring.assetDrop;
|
|
6614
|
-
const item: AssetDropContext['item'] = data.component ?? {
|
|
6615
|
-
kind: 'file',
|
|
6616
|
-
name: data.name,
|
|
6617
|
-
};
|
|
6618
|
-
// A refused drop SAYS so: the silent degrade read as "drag and drop is
|
|
6619
|
-
// not working" to a human who tried it both ways (runhuman pass 45).
|
|
6620
|
-
if (!assetDrop) {
|
|
6621
|
-
editorConsole.warn(
|
|
6622
|
-
`Dropped “${data.name}” was not placed: this document does not accept dropped assets.`,
|
|
6623
|
-
'editor',
|
|
6624
|
-
);
|
|
6625
|
-
return;
|
|
6626
|
-
}
|
|
6627
|
-
|
|
6628
|
-
// Online asset: the SHELL's resolver turns it into a project-local path
|
|
6629
|
-
// (cache, file list, download, history — see `OnlineAssetResolver`), then
|
|
6630
|
-
// it takes the same adapter drop path a local asset takes.
|
|
6631
|
-
//
|
|
6632
|
-
// The shell owns no document, so there is nothing to place a progress
|
|
6633
|
-
// placeholder INTO: the resolve settles silently and a failure is reported
|
|
6634
|
-
// to the console.
|
|
6635
|
-
if (data.online) {
|
|
6636
|
-
const online = data.online;
|
|
6637
|
-
const resolveOnline = this._onlineAssetResolver;
|
|
6638
|
-
// Same rule as a missing `assetDrop` above: a refused drop SAYS so,
|
|
6639
|
-
// naming the missing collaborator rather than swallowing the gesture.
|
|
6640
|
-
if (!resolveOnline) {
|
|
6641
|
-
editorConsole.warn(
|
|
6642
|
-
`Dropped “${data.name}” was not placed: this viewport has no online-asset resolver installed.`,
|
|
6643
|
-
'editor',
|
|
6644
|
-
);
|
|
6645
|
-
return;
|
|
6646
|
-
}
|
|
6647
|
-
// The download takes seconds and used to be silent: "it does not
|
|
6648
|
-
// seem to be adding… oh, that took some time, but it did happen"
|
|
6649
|
-
// (runhuman pass 136). Say it where the hand is — a cached asset
|
|
6650
|
-
// answers at once and the two hints simply follow each other.
|
|
6651
|
-
showTransientHint(`Downloading ${online.name} from the library…`);
|
|
6652
|
-
resolveOnline(online)
|
|
6653
|
-
.then((path) => {
|
|
6654
|
-
// `null` = the resolver already reported why on the console.
|
|
6655
|
-
if (path) {
|
|
6656
|
-
showTransientHint(`${online.name} downloaded — placing it.`);
|
|
6657
|
-
void dropAuthoringAsset(authoring, '', path, { position: pos });
|
|
6658
|
-
} else {
|
|
6659
|
-
showTransientHint(`Could not download ${online.name} — see the Console.`);
|
|
6660
|
-
}
|
|
6661
|
-
})
|
|
6662
|
-
// Browser mode (no Node dev server) throws instead of resolving —
|
|
6663
|
-
// the one failure nobody else reported, and no unhandled rejection.
|
|
6664
|
-
.catch(() => {
|
|
6665
|
-
showTransientHint(`Could not download ${online.name} — see the Console.`);
|
|
6666
|
-
editorConsole.error(`Could not download ${online.name}.`, 'asset');
|
|
6667
|
-
});
|
|
6668
|
-
return;
|
|
6669
|
-
}
|
|
6670
|
-
|
|
6671
|
-
// Model / image / audio / component: the adapter owns what an asset path
|
|
6672
|
-
// MEANS in its world, and its `drop` plans the same answer `accepts` would
|
|
6673
|
-
// — but when it refuses it SAYS WHY (transient hint + console), which a
|
|
6674
|
-
// boolean pre-check never could. The viewport contributes only the world
|
|
6675
|
-
// point under the cursor.
|
|
6676
|
-
void dropAuthoringAsset(authoring, '', data.path, { position: pos, item });
|
|
6677
|
-
};
|
|
6678
5392
|
}
|