@volter/editor-threejs 0.5.211 → 0.5.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +6 -11
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +192 -439
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +6 -17
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +5 -3
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +486 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
|
@@ -1,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,23 @@ 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
|
+
* What the floor's overlays (its grid and its axes) are drawn with: a surface in their own plane, a level's ground at
|
|
482
|
+
* 0, shows them on it, as Blender's grid passes its depth test on a tie (`DRW_STATE_DEPTH_LESS_EQUAL`,
|
|
483
|
+
* overlay_grid.hh). three's depth test passes a tie too; the offset keeps it where the two surfaces' depths round
|
|
484
|
+
* apart, which would split it pixel by pixel.
|
|
485
|
+
*/
|
|
486
|
+
const ON_THE_FLOOR = { polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 } as const;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The floor grid as a shader over one plane (`extent` metres square, y = 0).
|
|
490
|
+
* Lines are `fwidth`-antialiased so they keep one on-screen width at every
|
|
491
|
+
* distance; the 1 m level fades out where its cells fall under a few pixels
|
|
492
|
+
* (Blender's adaptive grid never lets lines merge), the 10 m level stays; the
|
|
493
|
+
* whole floor fades to its edge. `uColor` is the palette's grid colour
|
|
494
|
+
* (`_applyViewportLook`). Depth-tested, never written: content standing on
|
|
495
|
+
* the floor occludes it, and it occludes nothing.
|
|
496
|
+
*/
|
|
701
497
|
function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial> {
|
|
702
498
|
const geometry = new THREE.PlaneGeometry(extent, extent);
|
|
703
499
|
geometry.rotateX(-Math.PI / 2);
|
|
@@ -715,7 +511,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
715
511
|
uFadeEnd: { value: extent / 2 },
|
|
716
512
|
// The grazing fade's amount, 0..1 — set by `_applyViewportLook` from
|
|
717
513
|
// the one predicate that already decides whether a look paints the
|
|
718
|
-
// viewport at all.
|
|
514
|
+
// viewport at all. {@link GRAZING_FADE_GLSL} computes its angle profile.
|
|
719
515
|
uGrazingFade: { value: 0 },
|
|
720
516
|
// The plane the grid lies in: the floor, or an axis-aligned orthographic view's own plane
|
|
721
517
|
// (`EditorViewport.alignGridToView`). A unit axis.
|
|
@@ -756,12 +552,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
756
552
|
uniform float uReach;
|
|
757
553
|
varying vec3 vWorld;
|
|
758
554
|
${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.
|
|
555
|
+
// Device-pixel line coverage with a full-strength core and a one-pixel antialiased edge.
|
|
765
556
|
float gridLine(vec2 coord, float width) {
|
|
766
557
|
vec2 d = max(fwidth(coord), vec2(1e-6));
|
|
767
558
|
vec2 dist = abs(fract(coord - 0.5) - 0.5) / d;
|
|
@@ -791,12 +582,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
791
582
|
float pixel = max(fwidth(world).x, fwidth(world).y);
|
|
792
583
|
float bottomAlpha = uMinorFade * (1.0 - smoothstep(uUnit * 0.015625, uUnit * 0.25, pixel));
|
|
793
584
|
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.)
|
|
585
|
+
// Normalize the theme grid alpha for a single aligned pass. Emphasis uses full opacity.
|
|
800
586
|
float levelAlpha = (top > 0.5 || middle > 0.5 ? 1.0 : bottom * bottomAlpha) * mix(0.533, 1.0, emphasis);
|
|
801
587
|
float edge = 1.0 - smoothstep(uFadeStart * uReach, uFadeEnd * uReach, length(world - uCenter));
|
|
802
588
|
float alignedAlpha = levelAlpha * edge * uOpacity;
|
|
@@ -807,37 +593,17 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
807
593
|
}
|
|
808
594
|
vec2 pixelsPerMetre = 1.0 / max(fwidth(p), vec2(1e-6));
|
|
809
595
|
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.
|
|
596
|
+
// Fade minor lines as their cells approach pixel size.
|
|
814
597
|
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.
|
|
598
|
+
// Major lines use their own width and color.
|
|
821
599
|
float major = gridLine(p / uMajorEvery, uMajorWidth);
|
|
822
600
|
float line = max(minor, major);
|
|
823
601
|
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.
|
|
602
|
+
// Apply the floor’s grazing-angle attenuation independently of its distance fade.
|
|
828
603
|
fade *= volterGrazingFade(vWorld, cameraPosition, uPlaneNormal, uGrazingFade);
|
|
829
604
|
float alpha = line * fade * uOpacity;
|
|
830
605
|
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.
|
|
606
|
+
// Mix colors by the major line’s share of coverage, including antialiased skirts.
|
|
841
607
|
float majorShare = clamp(major / max(line, 1e-6), 0.0, 1.0);
|
|
842
608
|
gl_FragColor = vec4(mix(uColor, uMajorColor, majorShare), alpha);
|
|
843
609
|
#include <colorspace_fragment>
|
|
@@ -846,6 +612,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
846
612
|
transparent: true,
|
|
847
613
|
depthWrite: false,
|
|
848
614
|
side: THREE.DoubleSide,
|
|
615
|
+
...ON_THE_FLOOR,
|
|
849
616
|
});
|
|
850
617
|
const mesh = new THREE.Mesh(geometry, material);
|
|
851
618
|
mesh.name = 'editor-floor-grid';
|
|
@@ -890,6 +657,8 @@ export class EditorViewport {
|
|
|
890
657
|
readonly camera: THREE.PerspectiveCamera;
|
|
891
658
|
readonly orthographicCamera: THREE.OrthographicCamera;
|
|
892
659
|
readonly orbitControls: OrbitControls;
|
|
660
|
+
/** Fit clip planes only for the editor-owned camera; a live project camera retains its own projection. */
|
|
661
|
+
readonly projectionOwner = { owned: false };
|
|
893
662
|
readonly transformControls: TransformControls;
|
|
894
663
|
/** The rotate/scale halves of the 'combined' gizmo (Unity's Transform
|
|
895
664
|
* tool). Constructed always, shown only in combined mode — three's
|
|
@@ -906,16 +675,14 @@ export class EditorViewport {
|
|
|
906
675
|
})();
|
|
907
676
|
/** The floor: a shader grid (`createFloorGrid`), not line geometry. */
|
|
908
677
|
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. */
|
|
678
|
+
/** Keyed `bounds:<id>`, `selection:<id>` or `wire:<id>:<index>` for diagnostic boxes,
|
|
679
|
+
* selection brackets and mesh wires. A rebuild disposes every stored helper. */
|
|
913
680
|
private _boxHelpers = new Map<string, BoundsHelper>();
|
|
914
|
-
/** The view's selection marks (
|
|
681
|
+
/** The view's selection marks (`@volter/sdk/viewport-presentation` overlays.selection): the native
|
|
915
682
|
* outline is the stage's own switch; `box` brackets every selected object, not only those
|
|
916
683
|
* without geometry (Godot); `wire` draws the selected meshes' wireframes (Unity). */
|
|
917
|
-
private _selectionMarks = {
|
|
918
|
-
private _unsubscribeSelectionTheme: () => void
|
|
684
|
+
private _selectionMarks = { wire: false, box: false };
|
|
685
|
+
private _unsubscribeSelectionTheme: () => void;
|
|
919
686
|
private _constraintHelpers = new Map<string, ConstraintHelper>();
|
|
920
687
|
private _constraintControlRaycaster = (() => {
|
|
921
688
|
const raycaster = new THREE.Raycaster();
|
|
@@ -929,7 +696,7 @@ export class EditorViewport {
|
|
|
929
696
|
private _cameraHelpers = new Map<string, { source: THREE.Camera; helper: THREE.CameraHelper }>();
|
|
930
697
|
private _lightHelpers = new Map<
|
|
931
698
|
string,
|
|
932
|
-
{ source: THREE.Light; helper: THREE.Object3D & { update
|
|
699
|
+
{ source: THREE.Light; helper: THREE.Object3D & { update(): void; dispose(): void } }
|
|
933
700
|
>();
|
|
934
701
|
private _audioHelpers = new Map<string, { source: THREE.Object3D; helper: THREE.Group }>();
|
|
935
702
|
private _spatialHandleRoots: THREE.Object3D[] = [];
|
|
@@ -944,8 +711,8 @@ export class EditorViewport {
|
|
|
944
711
|
private _spatialHandleDragPlane = new THREE.Plane();
|
|
945
712
|
private _spatialHandleDragOffset = new THREE.Vector3();
|
|
946
713
|
private _spatialHandleDragPoint = new THREE.Vector3();
|
|
947
|
-
private _orbitEnabledBeforeHandleDrag
|
|
948
|
-
private _transformEnabledBeforeHandleDrag
|
|
714
|
+
private _orbitEnabledBeforeHandleDrag!: boolean;
|
|
715
|
+
private _transformEnabledBeforeHandleDrag!: boolean;
|
|
949
716
|
private _gizmoHelper: THREE.Object3D;
|
|
950
717
|
private _editorAmbient: THREE.AmbientLight;
|
|
951
718
|
private _editorDirLight: THREE.DirectionalLight;
|
|
@@ -953,53 +720,50 @@ export class EditorViewport {
|
|
|
953
720
|
private readonly _lightRig: boolean;
|
|
954
721
|
|
|
955
722
|
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}. */
|
|
723
|
+
/** Cached world AABB of non-editor content; empty until the first _contentBounds refresh. */
|
|
959
724
|
private readonly _contentBoundsBox = new THREE.Box3();
|
|
960
725
|
/** `performance.now()` of the last content-bounds measurement; 0 invalidates
|
|
961
726
|
* it (a scene swap), so the next reader re-measures immediately. */
|
|
962
727
|
private _contentBoundsAt = 0;
|
|
963
|
-
private _renderer: THREE.WebGLRenderer | undefined;
|
|
728
|
+
private readonly _renderer: THREE.WebGLRenderer | undefined;
|
|
964
729
|
private _sparkRenderer: SparkRenderer | null = null;
|
|
965
730
|
private _sparkRendererLoading = false;
|
|
966
731
|
private _sparkRendererFailed = false;
|
|
967
732
|
private _lastSparkDiscoveryAt = Number.NEGATIVE_INFINITY;
|
|
968
733
|
private _disposed = false;
|
|
969
734
|
/** Editor infrastructure objects that must move with the active scene. */
|
|
970
|
-
private _editorObjects: THREE.Object3D[]
|
|
735
|
+
private _editorObjects: THREE.Object3D[];
|
|
971
736
|
/** The view presentation this stage is lit by, when bound (`bindPresentation`). */
|
|
972
737
|
private _presentation: { readonly rig: StagePresentationRig; readonly stop: () => void } | null = null;
|
|
973
738
|
/** The world's axis lines: WHICH show is the view's (`overlays.axes`, {@link setAxisLines}),
|
|
974
739
|
* their colours and width the look's (`color.viewport.axisX/Y/Z`, `axisLineWidth`). */
|
|
975
|
-
private _axisLines: LineSegments2 | null
|
|
740
|
+
private _axisLines: LineSegments2 | null;
|
|
976
741
|
/** The world's vertical axis line, a child of {@link _axisLines} (see `_rebuildAxisLines`). */
|
|
977
742
|
private _verticalAxisLine: LineSegments2 | null = null;
|
|
978
743
|
/** The look's axis-line width in device pixels (null: the editor's own), which an
|
|
979
744
|
* axis-aligned view sets aside for one pixel. */
|
|
980
|
-
private _axisLineWidth
|
|
745
|
+
private _axisLineWidth!: number | null;
|
|
981
746
|
/** A device-pixel width as `LineMaterial` takes it: CSS pixels, three keeping its viewport in
|
|
982
747
|
* CSS units. The editor's own is 2 CSS. */
|
|
983
748
|
private _axisLineCss(device: number | null): number {
|
|
984
749
|
return device === null ? 2 : device / (this._renderer?.getPixelRatio() ?? 1);
|
|
985
750
|
}
|
|
986
|
-
private _axesWanted
|
|
751
|
+
private _axesWanted!: boolean;
|
|
987
752
|
private _stageAxes: ViewportOverlays['axes'] = 'floor';
|
|
988
753
|
/** The look's axis colours by WORLD axis (X, Y, Z), each `null` for the gizmo's own. */
|
|
989
|
-
private _lookAxisHexes
|
|
754
|
+
private _lookAxisHexes!: readonly [number | null, number | null, number | null];
|
|
990
755
|
/** 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. */
|
|
756
|
+
private _lookGridHex!: number;
|
|
757
|
+
/** The palette's background hex, used to recognize the viewport's own background
|
|
758
|
+
* and preserve a host-supplied background. */
|
|
995
759
|
private _lookBackgroundHex: number | null = null;
|
|
996
760
|
private _lookToneMapping: THREE.ToneMapping | null = null;
|
|
997
|
-
private _lookExposure
|
|
761
|
+
private _lookExposure!: number;
|
|
998
762
|
/** The active look's `stage.gizmoSize`, px per Blender gizmo unit
|
|
999
763
|
* — null under every look that names none. Cached off the theme token
|
|
1000
764
|
* rather than read per frame; {@link _applyGizmoSize} is what turns it into
|
|
1001
765
|
* three's `size`. */
|
|
1002
|
-
private _lookGizmoSizePx
|
|
766
|
+
private _lookGizmoSizePx!: number | null;
|
|
1003
767
|
/** The stage's world up axis (its presentation's `world.upAxis`, `setStageFunction`), as
|
|
1004
768
|
* the table every gizmo label and colour reads (see {@link StageAxisFrame}). Defaults to
|
|
1005
769
|
* three's own frame. */
|
|
@@ -1010,23 +774,8 @@ export class EditorViewport {
|
|
|
1010
774
|
* makes a delta enough. */
|
|
1011
775
|
private _appliedAxisSigns: readonly (1 | -1)[] = [1, 1, 1];
|
|
1012
776
|
/** 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
|
-
};
|
|
777
|
+
* editor's own axis colours, three's yellow highlight and each handle's native opacity. */
|
|
778
|
+
private _gizmoLook!: NativeGizmoLook;
|
|
1030
779
|
/** The stage's box-select rule (its presentation's `interaction.boxSelect`) — `'touch'` is
|
|
1031
780
|
* Blender's Select Box, `'contain'` (and null) the editor's own. Read by the marquee at the
|
|
1032
781
|
* moment it resolves, never cached into the rectangle. */
|
|
@@ -1048,24 +797,19 @@ export class EditorViewport {
|
|
|
1048
797
|
/** Camera input belongs to the whole viewport surface, including authoring
|
|
1049
798
|
* overlays rendered above the WebGL canvas. */
|
|
1050
799
|
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;
|
|
800
|
+
/** Relay double-clicks from the element OrbitControls captures. */
|
|
801
|
+
private readonly _onInteractionDblClick: (e: MouseEvent) => void;
|
|
1057
802
|
private readonly _authoring: () => AuthoringAdapter;
|
|
1058
803
|
private readonly _pick: ((clientX: number, clientY: number) => string | null) | undefined;
|
|
1059
804
|
private readonly _drawCamera: (() => THREE.Camera) | undefined;
|
|
1060
805
|
/** The camera on screen: the stage's drawing camera where it states one, else the viewport's.
|
|
1061
806
|
* Every screen test asks it (picks, the box select, vertex snap, handles, the ground plane);
|
|
1062
|
-
* fly mode
|
|
807
|
+
* fly mode moves the viewport's camera; grid alignment takes its caller's camera. */
|
|
1063
808
|
private get _screenCamera(): THREE.Camera {
|
|
1064
809
|
return this._drawCamera?.() ?? this.renderCamera;
|
|
1065
810
|
}
|
|
1066
811
|
private readonly _standaloneAuthoring: boolean;
|
|
1067
812
|
private readonly _publishPickContext: boolean;
|
|
1068
|
-
private readonly _onlineAssetResolver: OnlineAssetResolver | undefined;
|
|
1069
813
|
private readonly _onProjectionChange: ((projection: ThreeViewportProjection) => void) | undefined;
|
|
1070
814
|
private _pointerDownPos = new THREE.Vector2();
|
|
1071
815
|
private _skipNextSync = false;
|
|
@@ -1075,14 +819,14 @@ export class EditorViewport {
|
|
|
1075
819
|
/** True between a canvas pointerdown and its pointerup — see `_onPointerUp`. */
|
|
1076
820
|
private _pointerSessionActive = false;
|
|
1077
821
|
private _marqueeDiv: HTMLDivElement | null = null;
|
|
1078
|
-
private _pointerDownButton
|
|
822
|
+
private _pointerDownButton!: number;
|
|
1079
823
|
private _altDragOrbit = false;
|
|
1080
824
|
|
|
1081
825
|
// -- Orientation gizmo --
|
|
1082
826
|
private _vcScene = new THREE.Scene();
|
|
1083
827
|
private _vcCamera: THREE.OrthographicCamera;
|
|
1084
828
|
private _vcClickTargets: THREE.Mesh[] = [];
|
|
1085
|
-
/** The gizmo's six balls and
|
|
829
|
+
/** The gizmo's six balls and six stalks, kept for the per-frame depth
|
|
1086
830
|
* cue (`_syncOrientationGizmoDepth`). */
|
|
1087
831
|
private _vcBalls: Array<{
|
|
1088
832
|
readonly fill: THREE.Sprite;
|
|
@@ -1100,7 +844,7 @@ export class EditorViewport {
|
|
|
1100
844
|
readonly positive: boolean;
|
|
1101
845
|
readonly color: THREE.Color;
|
|
1102
846
|
}> = [];
|
|
1103
|
-
/**
|
|
847
|
+
/** Cone heads and triad lines, depth-sorted without dimming. */
|
|
1104
848
|
private _vcSolids: Array<{ readonly mesh: THREE.Mesh; readonly direction: THREE.Vector3 }> = [];
|
|
1105
849
|
private _vcSize = COMPASS_BOX_PX;
|
|
1106
850
|
/** The view's navigation gizmo (`overlays.navigation`): clicked, only drawn, or neither. */
|
|
@@ -1112,54 +856,24 @@ export class EditorViewport {
|
|
|
1112
856
|
private _vcMarginRight = COMPASS_MARGIN_RIGHT_PX;
|
|
1113
857
|
private _vcMarginTop = COMPASS_MARGIN_TOP_PX;
|
|
1114
858
|
/** See {@link EditorViewportOptions.chromeInsetPx}. */
|
|
1115
|
-
private _chromeInsetPx
|
|
859
|
+
private readonly _chromeInsetPx: number;
|
|
1116
860
|
/** Scratch dir vector reused each frame by renderViewCube (avoids per-frame alloc). */
|
|
1117
861
|
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
862
|
private _projection: ThreeViewportProjection = 'perspective';
|
|
1124
|
-
private
|
|
1125
|
-
private _viewportAspect = 1;
|
|
863
|
+
private _viewportAspect: number;
|
|
1126
864
|
/** The view's field of view (`ViewportCamera.fov`), held on its side as the region reshapes. */
|
|
1127
865
|
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). */
|
|
866
|
+
private _orthographicHeight!: number;
|
|
867
|
+
/** Inputs last reconciled by syncFromStore's helper membership and visibility work. */
|
|
1135
868
|
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
|
-
*/
|
|
869
|
+
private _lastGizmoSig!: string;
|
|
870
|
+
private _lastGizmoEpoch!: number;
|
|
871
|
+
/** Selection ownership and surface visibility cached for helper, handle, gizmo and input consumers. */
|
|
1145
872
|
private _threejsToolContextActive = false;
|
|
1146
873
|
/** A three stage is painted here (selection or not) — the floor's and the compass's gate. */
|
|
1147
874
|
private _threeSurfaceShowing = false;
|
|
1148
875
|
private _vcRaycaster = new THREE.Raycaster();
|
|
1149
876
|
|
|
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
877
|
// -- Multi-drag: track primary object's transform at drag start --
|
|
1164
878
|
private _dragStartPos = new THREE.Vector3();
|
|
1165
879
|
private _dragStartQuat = new THREE.Quaternion();
|
|
@@ -1218,30 +932,28 @@ export class EditorViewport {
|
|
|
1218
932
|
* debug mesh, … — scene-root objects toggled by the Helpers menu. */
|
|
1219
933
|
private _helpers = new Map<string, THREE.Object3D>();
|
|
1220
934
|
|
|
1221
|
-
//
|
|
1222
|
-
|
|
1223
|
-
// -- Snap-to-view animation --
|
|
935
|
+
// Snap-to-view animation.
|
|
1224
936
|
private _snapAnimating = false;
|
|
1225
|
-
private _snapStartTime
|
|
937
|
+
private _snapStartTime!: number;
|
|
1226
938
|
private _snapDuration = 400;
|
|
1227
939
|
private _snapQ1 = new THREE.Quaternion();
|
|
1228
940
|
private _snapQ2 = new THREE.Quaternion();
|
|
1229
|
-
private _snapDist
|
|
941
|
+
private _snapDist!: number;
|
|
1230
942
|
|
|
1231
943
|
// -- Fly camera (right-click + WASD) --
|
|
1232
944
|
private _flyActive = false;
|
|
1233
945
|
private _snapHold = false;
|
|
1234
946
|
private _flyKeys = new Set<string>();
|
|
1235
947
|
private _flySpeed = 5;
|
|
1236
|
-
private _flyEuler = new THREE.Euler(
|
|
948
|
+
private _flyEuler = new THREE.Euler();
|
|
1237
949
|
private _rightMouseDown = false;
|
|
1238
950
|
|
|
1239
951
|
constructor(
|
|
1240
952
|
canvas: HTMLCanvasElement,
|
|
1241
953
|
scene: THREE.Scene,
|
|
1242
954
|
store: EditorShellStore,
|
|
1243
|
-
interactionElement: HTMLElement
|
|
1244
|
-
options: EditorViewportOptions
|
|
955
|
+
interactionElement: HTMLElement,
|
|
956
|
+
options: EditorViewportOptions,
|
|
1245
957
|
) {
|
|
1246
958
|
// The integration's registrations (the authoring policy this viewport asks, its verbs, the
|
|
1247
959
|
// view-state persistence) exist before any viewport does; idempotent.
|
|
@@ -1258,7 +970,6 @@ export class EditorViewport {
|
|
|
1258
970
|
this._standaloneAuthoring = options.authoring !== undefined;
|
|
1259
971
|
this._publishPickContext = options.publishPickContext ?? true;
|
|
1260
972
|
this._chromeInsetPx = options.chromeInsetPx ?? 0;
|
|
1261
|
-
this._onlineAssetResolver = options.onlineAssetResolver;
|
|
1262
973
|
this._onProjectionChange = options.onProjectionChange;
|
|
1263
974
|
|
|
1264
975
|
// Camera — enableAll so editor sees both game content (layer 0) and editor objects (layer 31)
|
|
@@ -1283,63 +994,36 @@ export class EditorViewport {
|
|
|
1283
994
|
this.orthographicCamera.layers.enableAll();
|
|
1284
995
|
this._syncOrthographicFromPerspective();
|
|
1285
996
|
|
|
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.
|
|
997
|
+
// Draw anti-aliased screen-width floor lines, adaptively fading fine levels while retaining the major grid and radial edge fade.
|
|
1295
998
|
this.grid = createFloorGrid(400);
|
|
1296
999
|
this.grid.layers.set(EDITOR_LAYER);
|
|
1297
1000
|
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.
|
|
1001
|
+
// Draw world axes as screen-space lines sized by the viewport look and projection.
|
|
1301
1002
|
const axisGeometry = new LineSegmentsGeometry();
|
|
1302
|
-
axisGeometry.setPositions(axisSegmentPositions(0));
|
|
1303
|
-
axisGeometry.setColors(new Array(AXIS_SEGMENTS_PER_AXIS * 2 * 3).fill(1));
|
|
1304
1003
|
this._axisLines = new LineSegments2(
|
|
1305
1004
|
axisGeometry,
|
|
1306
1005
|
new LineMaterial({
|
|
1307
1006
|
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.
|
|
1007
|
+
// Keep axis colours at full strength while blending their draw order over the floor.
|
|
1314
1008
|
transparent: true,
|
|
1315
1009
|
opacity: 1,
|
|
1316
1010
|
depthWrite: false,
|
|
1317
1011
|
toneMapped: false,
|
|
1012
|
+
...ON_THE_FLOOR,
|
|
1318
1013
|
}),
|
|
1319
1014
|
);
|
|
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.
|
|
1015
|
+
// Share the floor's grazing fade. LineMaterial interpolates the segment endpoints with
|
|
1016
|
+
// clip w intact, keeping the world-position varying perspective-correct along each axis.
|
|
1327
1017
|
applyAxisGrazingFade(
|
|
1328
1018
|
this._axisLines.material as LineMaterial,
|
|
1329
1019
|
this.grid.material.uniforms['uGrazingFade'] as { value: number },
|
|
1330
1020
|
this.grid.material.uniforms['uPlaneNormal'] as { value: THREE.Vector3 },
|
|
1331
1021
|
);
|
|
1332
|
-
this._axisLines.visible = false;
|
|
1333
1022
|
this._axisLines.layers.set(EDITOR_LAYER);
|
|
1334
1023
|
scene.add(this._axisLines);
|
|
1335
1024
|
|
|
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')".
|
|
1025
|
+
// Subscribe after constructing the grid and axis lines: subscribeNativeSelectionTheme
|
|
1026
|
+
// invokes its listener synchronously, and that listener applies their viewport look.
|
|
1343
1027
|
this._unsubscribeSelectionTheme = subscribeNativeSelectionTheme(canvas, () => {
|
|
1344
1028
|
invalidateStages();
|
|
1345
1029
|
// The selection marks carry their look whole (colour, box form and frame, wire colour and
|
|
@@ -1361,14 +1045,13 @@ export class EditorViewport {
|
|
|
1361
1045
|
this._lookGizmoSizePx = nativeViewportGizmoSize(canvas);
|
|
1362
1046
|
this._applyGizmoSize();
|
|
1363
1047
|
this._applyGizmoArrows();
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
this._readLookStage(canvas);
|
|
1048
|
+
// Refresh the gizmos for the current look and presentation frame.
|
|
1049
|
+
// The initial callback precedes transform-control construction.
|
|
1050
|
+
this._readLookStage();
|
|
1368
1051
|
});
|
|
1369
1052
|
|
|
1370
1053
|
// Editor lighting (on EDITOR_LAYER so game camera doesn't see them)
|
|
1371
|
-
this._lightRig = options
|
|
1054
|
+
this._lightRig = options.lightRig !== false;
|
|
1372
1055
|
this._editorAmbient = new THREE.AmbientLight(0xffffff, 0.5);
|
|
1373
1056
|
this._editorAmbient.layers.set(EDITOR_LAYER);
|
|
1374
1057
|
scene.add(this._editorAmbient);
|
|
@@ -1376,7 +1059,7 @@ export class EditorViewport {
|
|
|
1376
1059
|
this._editorDirLight.position.set(10, 20, 10);
|
|
1377
1060
|
this._editorDirLight.layers.set(EDITOR_LAYER);
|
|
1378
1061
|
scene.add(this._editorDirLight);
|
|
1379
|
-
// `
|
|
1062
|
+
// `setScene` is where the rig's visibility is decided for every scene
|
|
1380
1063
|
// AFTER this one; the first scene arrives here, so it is decided here too.
|
|
1381
1064
|
this._editorAmbient.visible = this._lightRig;
|
|
1382
1065
|
this._editorDirLight.visible = this._lightRig;
|
|
@@ -1386,15 +1069,7 @@ export class EditorViewport {
|
|
|
1386
1069
|
this.batchedRenderer.layers.set(EDITOR_LAYER);
|
|
1387
1070
|
scene.add(this.batchedRenderer);
|
|
1388
1071
|
|
|
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.
|
|
1072
|
+
// Register trackpad navigation before OrbitControls so it owns the wheel: horizontal or Shift wheel pans in screen space.
|
|
1398
1073
|
interactionElement.addEventListener('wheel', this._onTrackpadWheel, { passive: false });
|
|
1399
1074
|
|
|
1400
1075
|
// OrbitControls: the editor's own mouse is right-drag to orbit, middle-drag to pan and the
|
|
@@ -1403,24 +1078,21 @@ export class EditorViewport {
|
|
|
1403
1078
|
// (its mouse look) the browser refuses capture with an uncaught InvalidStateError on each
|
|
1404
1079
|
// click. Capture is meaningless under a lock (every move already comes to the locked
|
|
1405
1080
|
// element), so it is skipped then.
|
|
1406
|
-
|
|
1081
|
+
// Look up capture on the element's prototype at each call so editor.document.click can
|
|
1082
|
+
// suppress capture while dispatching a synthetic pointer, which has no browser pointer id.
|
|
1083
|
+
const realm = (interactionElement.ownerDocument.defaultView ?? window) as Window & typeof globalThis;
|
|
1407
1084
|
interactionElement.setPointerCapture = (pointerId: number) => {
|
|
1408
1085
|
if (interactionElement.ownerDocument.pointerLockElement) return;
|
|
1409
|
-
|
|
1086
|
+
realm.Element.prototype.setPointerCapture.call(interactionElement, pointerId);
|
|
1410
1087
|
};
|
|
1411
1088
|
this.orbitControls = new OrbitControls(this.camera, interactionElement);
|
|
1412
1089
|
this.orbitControls.enableDamping = true;
|
|
1413
1090
|
this.orbitControls.dampingFactor = 0.1;
|
|
1414
1091
|
this.applyKeymapNavigation();
|
|
1415
|
-
this._unsubscribeKeymap =
|
|
1092
|
+
this._unsubscribeKeymap = editorHost().keyboard.subscribe(() => this.applyKeymapNavigation());
|
|
1416
1093
|
this._installTurntable();
|
|
1417
1094
|
|
|
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).
|
|
1095
|
+
// Register translate, scale and rotate controls in pointer-priority order; combined mode shows all three.
|
|
1424
1096
|
this.transformControls = new TransformControls(this.camera, canvas);
|
|
1425
1097
|
this._auxScaleControls = new TransformControls(this.camera, canvas);
|
|
1426
1098
|
this._auxScaleControls.setMode('scale');
|
|
@@ -1428,12 +1100,7 @@ export class EditorViewport {
|
|
|
1428
1100
|
this._auxRotateControls.setMode('rotate');
|
|
1429
1101
|
this._activeGizmo = this.transformControls;
|
|
1430
1102
|
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.
|
|
1103
|
+
// Refresh the hovered drag plane before pointerDown reads it, including a hover first established by a touch press.
|
|
1437
1104
|
const pressed = gizmoControls.pointerDown.bind(gizmoControls);
|
|
1438
1105
|
gizmoControls.pointerDown = (pointer) => {
|
|
1439
1106
|
gizmoControls.getHelper().updateMatrixWorld(true);
|
|
@@ -1443,8 +1110,7 @@ export class EditorViewport {
|
|
|
1443
1110
|
gizmoControls.getRaycaster().layers.enableAll();
|
|
1444
1111
|
gizmoControls.addEventListener('dragging-changed', (event) => {
|
|
1445
1112
|
const dragging = (event as unknown as { value: boolean }).value;
|
|
1446
|
-
//
|
|
1447
|
-
// (live-gesture-lock.ts) — a swap mid-drag snapped moved objects back.
|
|
1113
|
+
// Hold the design-session remount while a transform drag owns the live objects.
|
|
1448
1114
|
if (dragging) beginLiveGesture();
|
|
1449
1115
|
else endLiveGesture();
|
|
1450
1116
|
this.orbitControls.enabled = !dragging;
|
|
@@ -1454,7 +1120,7 @@ export class EditorViewport {
|
|
|
1454
1120
|
for (const other of this._allGizmos()) {
|
|
1455
1121
|
if (other !== gizmoControls) other.enabled = false;
|
|
1456
1122
|
}
|
|
1457
|
-
const gizmoObj = gizmoControls.object
|
|
1123
|
+
const gizmoObj = gizmoControls.object!;
|
|
1458
1124
|
const channel =
|
|
1459
1125
|
gizmoControls.mode === 'translate'
|
|
1460
1126
|
? 'position'
|
|
@@ -1464,63 +1130,61 @@ export class EditorViewport {
|
|
|
1464
1130
|
const writable = (id: string): boolean =>
|
|
1465
1131
|
this._authoring().transforms?.editability?.(id, channel).writable ?? true;
|
|
1466
1132
|
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
|
-
}
|
|
1133
|
+
this._attachedAuthoringId ?? this._authoringIdForObject(gizmoObj) ?? '';
|
|
1134
|
+
this._dragStartPos.copy(gizmoObj.position);
|
|
1135
|
+
this._dragStartQuat.copy(gizmoObj.quaternion);
|
|
1136
|
+
this._dragStartScale.copy(gizmoObj.scale);
|
|
1137
|
+
this._otherDragObjects = [];
|
|
1138
|
+
this._allDragObjects = [];
|
|
1139
|
+
this._pivotPos.copy(gizmoObj.position);
|
|
1140
|
+
const primaryId =
|
|
1141
|
+
gizmoObj === this._pivotDummy ? null : this._authoringIdForObject(gizmoObj);
|
|
1142
|
+
if (!primaryId) {
|
|
1143
|
+
// Without a semantic primary ID, derive drag subjects from the writable selection.
|
|
1144
|
+
for (const id of this._store.shell.selectedEntityIds) {
|
|
1145
|
+
if (!writable(id)) continue;
|
|
1146
|
+
const obj = this._objectForAuthoringId(id);
|
|
1147
|
+
if (!obj) continue;
|
|
1148
|
+
this._allDragObjects.push({
|
|
1149
|
+
id,
|
|
1150
|
+
obj,
|
|
1151
|
+
startPos: obj.position.clone(),
|
|
1152
|
+
startQuat: obj.quaternion.clone(),
|
|
1153
|
+
startScale: obj.scale.clone(),
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
for (const id of this._store.shell.selectedEntityIds) {
|
|
1158
|
+
if (id === primaryDragId) continue;
|
|
1159
|
+
if (!writable(id)) continue;
|
|
1160
|
+
const obj = this._objectForAuthoringId(id);
|
|
1161
|
+
if (!obj) continue;
|
|
1162
|
+
this._otherDragObjects.push({
|
|
1163
|
+
id,
|
|
1164
|
+
obj,
|
|
1165
|
+
startPos: obj.position.clone(),
|
|
1166
|
+
startQuat: obj.quaternion.clone(),
|
|
1167
|
+
startScale: obj.scale.clone(),
|
|
1168
|
+
});
|
|
1505
1169
|
}
|
|
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
1170
|
}
|
|
1171
|
+
const editIds =
|
|
1172
|
+
gizmoObj === this._pivotDummy
|
|
1173
|
+
? this._allDragObjects.map(({ id }) => id)
|
|
1174
|
+
: [primaryDragId, ...this._otherDragObjects.map(({ id }) => id)];
|
|
1175
|
+
this._preservedChildObjects = this._preparePreservedChildren(editIds.filter(Boolean));
|
|
1176
|
+
this._activeTransformEditIds = [
|
|
1177
|
+
...new Set([
|
|
1178
|
+
...editIds.filter(Boolean),
|
|
1179
|
+
...this._preservedChildObjects.map(({ id }) => id),
|
|
1180
|
+
]),
|
|
1181
|
+
];
|
|
1182
|
+
// Begin every semantic source edit before TransformControls mutates
|
|
1183
|
+
// any native object. Source-backed adapters need the pre-drag pose of
|
|
1184
|
+
// secondary selections too; physics-backed adapters freeze each body.
|
|
1185
|
+
const authoring = this._authoring();
|
|
1186
|
+
for (const id of this._activeTransformEditIds)
|
|
1187
|
+
beginAuthoringTransformEdit(authoring, id);
|
|
1524
1188
|
} else {
|
|
1525
1189
|
// Hide snap indicators when drag ends
|
|
1526
1190
|
this._surfaceSnapIndicator.visible = false;
|
|
@@ -1530,8 +1194,7 @@ export class EditorViewport {
|
|
|
1530
1194
|
}
|
|
1531
1195
|
});
|
|
1532
1196
|
gizmoControls.addEventListener('objectChange', () => {
|
|
1533
|
-
const gizmoObj = gizmoControls.object
|
|
1534
|
-
if (!gizmoObj) return;
|
|
1197
|
+
const gizmoObj = gizmoControls.object!;
|
|
1535
1198
|
const mode = gizmoControls.mode;
|
|
1536
1199
|
// A CENTRE-anchored gizmo takes the individual-origins math on purpose:
|
|
1537
1200
|
// its anchor is where the handles are DRAWN, not a pivot the edit is
|
|
@@ -1599,9 +1262,8 @@ export class EditorViewport {
|
|
|
1599
1262
|
}
|
|
1600
1263
|
this._restorePreservedChildren();
|
|
1601
1264
|
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
// LineSegments) does not, hence the guard.
|
|
1265
|
+
// Bounds helpers and selection brackets update mid-drag; mesh wires and
|
|
1266
|
+
// diagnostic wire cubes are LineSegments without an update method.
|
|
1605
1267
|
for (const helper of this._boxHelpers.values()) {
|
|
1606
1268
|
if ('update' in helper) helper.update();
|
|
1607
1269
|
}
|
|
@@ -1650,9 +1312,7 @@ export class EditorViewport {
|
|
|
1650
1312
|
this._vertexSnapIndicator.layers.set(EDITOR_LAYER);
|
|
1651
1313
|
scene.add(this._vertexSnapIndicator);
|
|
1652
1314
|
|
|
1653
|
-
//
|
|
1654
|
-
this._gizmoLook = nativeGizmoLook(canvas);
|
|
1655
|
-
this._rebuildAxisLines();
|
|
1315
|
+
// Install handle highlights and align the gizmo geometry.
|
|
1656
1316
|
for (const controls of this._allGizmos()) this._installGizmoHighlight(controls);
|
|
1657
1317
|
this._patchGizmo(this._gizmoHelper);
|
|
1658
1318
|
this._patchGizmo(this._auxRotateControls.getHelper());
|
|
@@ -1661,22 +1321,14 @@ export class EditorViewport {
|
|
|
1661
1321
|
this._dropNegativeAxisHandles(this._auxScaleControls.getHelper(), ['scale']);
|
|
1662
1322
|
this._patchCombinedScaleGizmo(this._auxScaleControls.getHelper());
|
|
1663
1323
|
this._applyGizmoArrows();
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
// synchronous call found none (see {@link _applyGizmoSize}).
|
|
1667
|
-
this._lookGizmoSizePx = nativeViewportGizmoSize(canvas);
|
|
1324
|
+
// Apply the look’s gizmo size once the controls exist; the theme subscription may run before
|
|
1325
|
+
// their construction.
|
|
1668
1326
|
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.
|
|
1327
|
+
// Apply the stage's axis frame after constructing and patching its gizmo geometry.
|
|
1675
1328
|
this._applyGizmoAxisFrame();
|
|
1676
1329
|
|
|
1677
|
-
//
|
|
1330
|
+
// The theme subscription builds the orientation gizmo; this camera draws it.
|
|
1678
1331
|
this._vcCamera = new THREE.OrthographicCamera(-1.5, 1.5, 1.5, -1.5, 0.1, 10);
|
|
1679
|
-
this._initOrientationGizmo();
|
|
1680
1332
|
|
|
1681
1333
|
// -- Fly camera input (right-click + WASD) --
|
|
1682
1334
|
window.addEventListener('keydown', this._onFlyKeyDown);
|
|
@@ -1688,58 +1340,22 @@ export class EditorViewport {
|
|
|
1688
1340
|
|
|
1689
1341
|
// Alt+Left-drag = orbit (Unity-style), via capture phase so it fires before OrbitControls
|
|
1690
1342
|
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.
|
|
1343
|
+
// Listen on window so an Alt+drag released outside the canvas restores the controls.
|
|
1344
|
+
// pointercancel handles interrupted drags; dispose removes both listeners.
|
|
1697
1345
|
window.addEventListener('pointerup', this._onAltOrbitRelease, { capture: true });
|
|
1698
1346
|
window.addEventListener('pointercancel', this._onAltOrbitRelease, { capture: true });
|
|
1347
|
+
// A handle or constraint-control drag whose release never arrives (the pointer cancelled, the
|
|
1348
|
+
// window lost focus mid-drag) ends here: its flags would otherwise keep `interacting()` true
|
|
1349
|
+
// and the stage redrawing every frame (`StageHost`).
|
|
1350
|
+
window.addEventListener('pointercancel', this._onInteractionDragLost, { capture: true });
|
|
1351
|
+
window.addEventListener('blur', this._onInteractionDragLost);
|
|
1699
1352
|
window.addEventListener('keydown', this._onSnapHoldKey);
|
|
1700
1353
|
window.addEventListener('keyup', this._onSnapHoldKey);
|
|
1701
1354
|
|
|
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.
|
|
1355
|
+
// Bind pointer selection to the interaction element that OrbitControls captures, and handle pointerup there so real drags release their session flags.
|
|
1736
1356
|
canvas.addEventListener('pointerdown', this._onPointerDown);
|
|
1737
1357
|
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);
|
|
1358
|
+
interactionElement.addEventListener('pointerup', this._onPointerUp);
|
|
1743
1359
|
|
|
1744
1360
|
// Double-click = DRILL IN (see `_onDoubleClick`). Bound on the interaction
|
|
1745
1361
|
// element for the same reason `pointerup` is: OrbitControls captures the
|
|
@@ -1751,15 +1367,10 @@ export class EditorViewport {
|
|
|
1751
1367
|
};
|
|
1752
1368
|
interactionElement.addEventListener('dblclick', this._onInteractionDblClick);
|
|
1753
1369
|
|
|
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
1370
|
// Collect editor infrastructure objects so they can be moved between scenes
|
|
1760
1371
|
this._editorObjects = [
|
|
1761
1372
|
this.grid,
|
|
1762
|
-
|
|
1373
|
+
this._axisLines,
|
|
1763
1374
|
this._editorAmbient,
|
|
1764
1375
|
this._editorDirLight,
|
|
1765
1376
|
this.batchedRenderer,
|
|
@@ -1771,11 +1382,9 @@ export class EditorViewport {
|
|
|
1771
1382
|
this._vertexSnapIndicator,
|
|
1772
1383
|
];
|
|
1773
1384
|
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
|
|
1777
|
-
this._installPickContext(this.camera);
|
|
1778
|
-
}
|
|
1385
|
+
// Publish this viewport's camera, canvas and editor controls for adapter raycasting.
|
|
1386
|
+
// Cleared in dispose().
|
|
1387
|
+
this._installPickContext(this.camera);
|
|
1779
1388
|
}
|
|
1780
1389
|
|
|
1781
1390
|
/** Swap the scene the viewport renders. Moves editor infrastructure objects to the new scene. */
|
|
@@ -1813,14 +1422,8 @@ export class EditorViewport {
|
|
|
1813
1422
|
}
|
|
1814
1423
|
this._scene = newScene;
|
|
1815
1424
|
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;
|
|
1425
|
+
// Use the editor rig only when enabled and no view presentation supplies the lighting.
|
|
1426
|
+
const rigOn = this._lightRig && this._presentation === null;
|
|
1824
1427
|
this._editorAmbient.visible = rigOn;
|
|
1825
1428
|
this._editorDirLight.visible = rigOn;
|
|
1826
1429
|
}
|
|
@@ -1829,11 +1432,9 @@ export class EditorViewport {
|
|
|
1829
1432
|
return this._scene;
|
|
1830
1433
|
}
|
|
1831
1434
|
|
|
1832
|
-
/** Camera that
|
|
1833
|
-
* free editor camera; view-through/pilot temporarily returns the authored
|
|
1834
|
-
* native camera without changing its ownership. */
|
|
1435
|
+
/** Camera that paints the Scene document: the free editor camera. */
|
|
1835
1436
|
get renderCamera(): THREE.Camera {
|
|
1836
|
-
return this.
|
|
1437
|
+
return this.freeCamera;
|
|
1837
1438
|
}
|
|
1838
1439
|
|
|
1839
1440
|
get freeCamera(): THREE.PerspectiveCamera | THREE.OrthographicCamera {
|
|
@@ -1859,10 +1460,6 @@ export class EditorViewport {
|
|
|
1859
1460
|
}
|
|
1860
1461
|
|
|
1861
1462
|
setProjection(projection: ThreeViewportProjection): void {
|
|
1862
|
-
if (this._cameraView) {
|
|
1863
|
-
this._pendingProjection = projection;
|
|
1864
|
-
return;
|
|
1865
|
-
}
|
|
1866
1463
|
if (this._projection === projection) return;
|
|
1867
1464
|
if (projection === 'orthographic') this._syncOrthographicFromPerspective();
|
|
1868
1465
|
else this._syncPerspectiveFromOrthographic();
|
|
@@ -1936,7 +1533,7 @@ export class EditorViewport {
|
|
|
1936
1533
|
* the right button then does nothing here, as in Blender, where it is the context menu's.
|
|
1937
1534
|
*/
|
|
1938
1535
|
private applyKeymapNavigation(): void {
|
|
1939
|
-
const { orbit, turntable } =
|
|
1536
|
+
const { orbit, turntable } = editorHost().keyboard.navigation();
|
|
1940
1537
|
// Only a turntable keeps a roll: under any other orbit the view comes back level (a Top
|
|
1941
1538
|
// view's screen up, which is no roll, stands).
|
|
1942
1539
|
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 +1544,7 @@ export class EditorViewport {
|
|
|
1947
1544
|
RIGHT: orbit === 'middle' ? (-1 as THREE.MOUSE) : THREE.MOUSE.ROTATE,
|
|
1948
1545
|
};
|
|
1949
1546
|
}
|
|
1950
|
-
private _unsubscribeKeymap: () => void
|
|
1547
|
+
private _unsubscribeKeymap: () => void;
|
|
1951
1548
|
|
|
1952
1549
|
/**
|
|
1953
1550
|
* A KEYMAP'S TURNTABLE (`KeymapNavigation.turntable`) in place of three's spherical orbit, on
|
|
@@ -1977,17 +1574,17 @@ export class EditorViewport {
|
|
|
1977
1574
|
controls._handleMouseDownDolly = (event) => {
|
|
1978
1575
|
dollyDown(event);
|
|
1979
1576
|
dolly = null;
|
|
1980
|
-
if (
|
|
1577
|
+
if (editorHost().keyboard.navigation().zoom?.drag !== 'dolly') return;
|
|
1981
1578
|
const top = this._canvas.getBoundingClientRect().top;
|
|
1982
1579
|
const camera = this.orbitControls.object;
|
|
1983
1580
|
dolly = {
|
|
1984
1581
|
len0: Math.max(5 + event.clientY - top, 1),
|
|
1985
1582
|
offset: camera.position.clone().sub(this.orbitControls.target),
|
|
1986
|
-
zoom0: (camera as THREE.OrthographicCamera).zoom
|
|
1583
|
+
zoom0: (camera as THREE.PerspectiveCamera | THREE.OrthographicCamera).zoom,
|
|
1987
1584
|
};
|
|
1988
1585
|
};
|
|
1989
1586
|
controls._handleMouseMoveDolly = (event) => {
|
|
1990
|
-
if (
|
|
1587
|
+
if (editorHost().keyboard.navigation().zoom?.drag !== 'dolly' || !dolly) {
|
|
1991
1588
|
dollyMove(event);
|
|
1992
1589
|
return;
|
|
1993
1590
|
}
|
|
@@ -2017,13 +1614,13 @@ export class EditorViewport {
|
|
|
2017
1614
|
down(event);
|
|
2018
1615
|
drag = null;
|
|
2019
1616
|
for (const listener of [...this._rotateStartListeners]) listener();
|
|
2020
|
-
if (!
|
|
1617
|
+
if (!editorHost().keyboard.navigation().turntable) return;
|
|
2021
1618
|
const camera = this.orbitControls.object;
|
|
2022
1619
|
const viewUp = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
|
|
2023
1620
|
drag = { x: event.clientX, y: event.clientY, reverse: viewUp.y < 0 ? -1 : 1 };
|
|
2024
1621
|
};
|
|
2025
1622
|
controls._handleMouseMoveRotate = (event) => {
|
|
2026
|
-
const turntable =
|
|
1623
|
+
const turntable = editorHost().keyboard.navigation().turntable;
|
|
2027
1624
|
if (!turntable || !drag) {
|
|
2028
1625
|
move(event);
|
|
2029
1626
|
return;
|
|
@@ -2040,7 +1637,7 @@ export class EditorViewport {
|
|
|
2040
1637
|
// Auto Perspective on a stage that draws with the viewport's own projection (a document's
|
|
2041
1638
|
// session keeps its own and answers for it).
|
|
2042
1639
|
() => {
|
|
2043
|
-
if (!
|
|
1640
|
+
if (!editorHost().keyboard.navigation().autoPerspective || this._projection !== 'orthographic') return;
|
|
2044
1641
|
const offset = this.camera.position.clone().sub(this.orbitControls.target);
|
|
2045
1642
|
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(this.camera.quaternion);
|
|
2046
1643
|
if (axisViewName(offset, up) !== null) this.setProjection('perspective');
|
|
@@ -2054,7 +1651,7 @@ export class EditorViewport {
|
|
|
2054
1651
|
*/
|
|
2055
1652
|
private readonly _axisViewListeners: (() => boolean)[] = [
|
|
2056
1653
|
() => {
|
|
2057
|
-
if (
|
|
1654
|
+
if (editorHost().keyboard.navigation().autoPerspective) this.setProjection('orthographic');
|
|
2058
1655
|
return true;
|
|
2059
1656
|
},
|
|
2060
1657
|
];
|
|
@@ -2155,7 +1752,7 @@ export class EditorViewport {
|
|
|
2155
1752
|
* orbit's pivot, as a whole gesture (a camera lock writes it).
|
|
2156
1753
|
*/
|
|
2157
1754
|
zoomStep(direction: 1 | -1): void {
|
|
2158
|
-
const step =
|
|
1755
|
+
const step = editorHost().keyboard.navigation().zoom?.step ?? 1.2;
|
|
2159
1756
|
const factor = direction > 0 ? 1 / step : step;
|
|
2160
1757
|
const camera = this.orbitControls.object;
|
|
2161
1758
|
this.orbitControls.dispatchEvent({ type: 'start' });
|
|
@@ -2202,54 +1799,6 @@ export class EditorViewport {
|
|
|
2202
1799
|
this.orbitControls.update();
|
|
2203
1800
|
}
|
|
2204
1801
|
|
|
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
1802
|
get objectMap(): Map<string, THREE.Object3D> {
|
|
2254
1803
|
return this._objectMap;
|
|
2255
1804
|
}
|
|
@@ -2286,7 +1835,7 @@ export class EditorViewport {
|
|
|
2286
1835
|
}
|
|
2287
1836
|
|
|
2288
1837
|
private _authoringIdForObject(object: THREE.Object3D): string | null {
|
|
2289
|
-
return this._authoring().hierarchy.
|
|
1838
|
+
return this._authoring().hierarchy.idForNativeObject?.(object) ?? entityIdOf(object) ?? null;
|
|
2290
1839
|
}
|
|
2291
1840
|
|
|
2292
1841
|
/**
|
|
@@ -2315,22 +1864,20 @@ export class EditorViewport {
|
|
|
2315
1864
|
color: new THREE.Color(nativeSelectionColors(this._canvas).visible),
|
|
2316
1865
|
toneMapped: false,
|
|
2317
1866
|
});
|
|
2318
|
-
const wire = new THREE.LineSegments(edges, mat) as
|
|
1867
|
+
const wire = new THREE.LineSegments(edges, mat) as THREE.LineSegments & TaggedHelper;
|
|
2319
1868
|
obj.getWorldPosition(wire.position);
|
|
2320
|
-
//
|
|
2321
|
-
wire.isBoxHelper = true;
|
|
1869
|
+
// Retain the source so the wire cube follows its world position each frame.
|
|
2322
1870
|
wire._entityObj = obj;
|
|
2323
1871
|
return wire;
|
|
2324
1872
|
}
|
|
2325
1873
|
|
|
2326
1874
|
/**
|
|
2327
|
-
* Rebuild
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
* painted by the native postprocessing silhouette instead.
|
|
1875
|
+
* Rebuild diagnostic bounds and the presentation's selection wires/brackets.
|
|
1876
|
+
* Non-renderable selections use brackets unless their document draws its own overlay;
|
|
1877
|
+
* geometric selections can use wires, boxes and the native silhouette.
|
|
2331
1878
|
*/
|
|
2332
1879
|
private _syncBoxHelpers(): void {
|
|
2333
|
-
//
|
|
1880
|
+
// Dispose the previous geometry and materials before rebuilding.
|
|
2334
1881
|
for (const helper of this._boxHelpers.values()) {
|
|
2335
1882
|
this._scene.remove(helper);
|
|
2336
1883
|
helper.geometry.dispose();
|
|
@@ -2362,7 +1909,7 @@ export class EditorViewport {
|
|
|
2362
1909
|
let index = 0;
|
|
2363
1910
|
obj.traverse((child) => {
|
|
2364
1911
|
const mesh = child as THREE.Mesh;
|
|
2365
|
-
if (!mesh.isMesh ||
|
|
1912
|
+
if (!mesh.isMesh || isInEditorOwnedSubtree(mesh)) return;
|
|
2366
1913
|
const look = nativeViewportWire(this._canvas);
|
|
2367
1914
|
const wire = new THREE.LineSegments(
|
|
2368
1915
|
new THREE.WireframeGeometry(mesh.geometry),
|
|
@@ -2399,7 +1946,7 @@ export class EditorViewport {
|
|
|
2399
1946
|
if (this._standaloneAuthoring && (obj as THREE.Bone).isBone) continue;
|
|
2400
1947
|
// Constraint owners, targets and poles already have a semantic native
|
|
2401
1948
|
// viewport presentation (lines plus a target/pole handle). Drawing the
|
|
2402
|
-
//
|
|
1949
|
+
// empty-transform corner brackets over that presentation creates
|
|
2403
1950
|
// two competing selection languages and obscures the draggable handle.
|
|
2404
1951
|
if ([...this._constraintHelpers.values()].some((helper) => helper.presents(obj))) continue;
|
|
2405
1952
|
// Nor over an object whose document draws its overlay itself, selection colour included
|
|
@@ -2420,12 +1967,12 @@ export class EditorViewport {
|
|
|
2420
1967
|
* The source component stays an empty, ordinary transform node; these
|
|
2421
1968
|
* helpers live on EDITOR_LAYER and therefore never enter Game view.
|
|
2422
1969
|
*/
|
|
2423
|
-
private _syncReflectionProbeHelpers(
|
|
2424
|
-
if (
|
|
1970
|
+
private _syncReflectionProbeHelpers(reconcileMembership: boolean): void {
|
|
1971
|
+
if (reconcileMembership) {
|
|
2425
1972
|
const current = new Map<string, THREE.Object3D>();
|
|
2426
1973
|
const admitted = new Set<THREE.Object3D>();
|
|
2427
1974
|
for (const [id, object] of this._objectMap) {
|
|
2428
|
-
if (
|
|
1975
|
+
if (admitted.has(object) || !reflectionProbeOf(object)) continue;
|
|
2429
1976
|
admitted.add(object);
|
|
2430
1977
|
current.set(id, object);
|
|
2431
1978
|
}
|
|
@@ -2435,28 +1982,6 @@ export class EditorViewport {
|
|
|
2435
1982
|
this._reflectionProbeHelpers.delete(id);
|
|
2436
1983
|
}
|
|
2437
1984
|
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
1985
|
}
|
|
2461
1986
|
for (const [id, helper] of this._reflectionProbeHelpers) {
|
|
2462
1987
|
helper.setSelected(this._store.shell.selectedEntityIds.has(id));
|
|
@@ -2475,21 +2000,13 @@ export class EditorViewport {
|
|
|
2475
2000
|
this._scene.add(helper);
|
|
2476
2001
|
}
|
|
2477
2002
|
|
|
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) {
|
|
2003
|
+
/** Draw editor-owned trigger-volume guides on EDITOR_LAYER without adding geometry to the project tree. */
|
|
2004
|
+
private _syncTriggerVolumeHelpers(reconcileMembership: boolean): void {
|
|
2005
|
+
if (reconcileMembership) {
|
|
2489
2006
|
const current = new Map<string, THREE.Object3D>();
|
|
2490
2007
|
const admitted = new Set<THREE.Object3D>();
|
|
2491
2008
|
for (const [id, object] of this._objectMap) {
|
|
2492
|
-
if (
|
|
2009
|
+
if (admitted.has(object) || !triggerVolumeOf(object)) continue;
|
|
2493
2010
|
admitted.add(object);
|
|
2494
2011
|
current.set(id, object);
|
|
2495
2012
|
}
|
|
@@ -2498,27 +2015,6 @@ export class EditorViewport {
|
|
|
2498
2015
|
this._disposeTriggerVolumeHelper(id, helper);
|
|
2499
2016
|
}
|
|
2500
2017
|
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
2018
|
}
|
|
2523
2019
|
const visible =
|
|
2524
2020
|
this._threeSurfaceShowing &&
|
|
@@ -2544,12 +2040,12 @@ export class EditorViewport {
|
|
|
2544
2040
|
}
|
|
2545
2041
|
|
|
2546
2042
|
/** Keep one editor-owned projection beside every source-owned constraint owner. */
|
|
2547
|
-
private _syncConstraintHelpers(
|
|
2548
|
-
if (
|
|
2043
|
+
private _syncConstraintHelpers(reconcileMembership: boolean): void {
|
|
2044
|
+
if (reconcileMembership) {
|
|
2549
2045
|
const current = new Map<string, THREE.Object3D>();
|
|
2550
2046
|
const admitted = new Set<THREE.Object3D>();
|
|
2551
2047
|
for (const [id, object] of this._objectMap) {
|
|
2552
|
-
if (
|
|
2048
|
+
if (admitted.has(object) || constraintsOf(object).length === 0) continue;
|
|
2553
2049
|
admitted.add(object);
|
|
2554
2050
|
current.set(id, object);
|
|
2555
2051
|
}
|
|
@@ -2559,28 +2055,6 @@ export class EditorViewport {
|
|
|
2559
2055
|
this._constraintHelpers.delete(id);
|
|
2560
2056
|
}
|
|
2561
2057
|
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
2058
|
}
|
|
2585
2059
|
|
|
2586
2060
|
const selectedObjects = new Set<THREE.Object3D>();
|
|
@@ -2698,45 +2172,19 @@ export class EditorViewport {
|
|
|
2698
2172
|
}
|
|
2699
2173
|
|
|
2700
2174
|
/** 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.
|
|
2175
|
+
private _syncCameraHelpers(): void {
|
|
2176
|
+
const current = new Map<string, THREE.Camera>();
|
|
2177
|
+
for (const [id, object] of this._objectMap) {
|
|
2178
|
+
const camera = this._nativeCamera(object);
|
|
2179
|
+
if (camera) current.set(id, camera);
|
|
2180
|
+
}
|
|
2181
|
+
for (const [id, entry] of this._cameraHelpers) {
|
|
2182
|
+
if (current.get(id) === entry.source) continue;
|
|
2183
|
+
this._disposeCameraHelper(id, entry);
|
|
2184
|
+
}
|
|
2185
|
+
for (const [id, camera] of current) this._ensureCameraHelper(id, camera);
|
|
2186
|
+
// Keep camera icons visible with the stage’s persistent furniture so unselected cameras can be
|
|
2187
|
+
// found and selected.
|
|
2740
2188
|
const visible =
|
|
2741
2189
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.cameras;
|
|
2742
2190
|
for (const entry of this._cameraHelpers.values()) {
|
|
@@ -2746,55 +2194,28 @@ export class EditorViewport {
|
|
|
2746
2194
|
}
|
|
2747
2195
|
|
|
2748
2196
|
/**
|
|
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.
|
|
2197
|
+
* Reconcile one editor-owned three.js helper per native light when Helpers → Lights is enabled,
|
|
2198
|
+
* using the same overlay lifecycle as camera helpers. Helpers remain outside the authored tree
|
|
2199
|
+
* and hidden in Game view.
|
|
2759
2200
|
*/
|
|
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
|
-
}
|
|
2201
|
+
private _syncLightHelpers(): void {
|
|
2202
|
+
const current = new Map<string, THREE.Light>();
|
|
2203
|
+
for (const [id, object] of this._objectMap) {
|
|
2204
|
+
const light = this._nativeLight(object);
|
|
2205
|
+
if (light) current.set(id, light);
|
|
2206
|
+
}
|
|
2207
|
+
for (const [id, entry] of this._lightHelpers) {
|
|
2208
|
+
if (current.get(id) === entry.source) continue;
|
|
2209
|
+
this._disposeLightHelper(id, entry);
|
|
2790
2210
|
}
|
|
2211
|
+
for (const [id, light] of current) this._ensureLightHelper(id, light);
|
|
2791
2212
|
// Persistent for the same reason as the camera frustum above: a light you
|
|
2792
2213
|
// have not selected is exactly the light you are looking for.
|
|
2793
2214
|
const visible =
|
|
2794
2215
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.lights;
|
|
2795
2216
|
for (const entry of this._lightHelpers.values()) {
|
|
2796
2217
|
entry.helper.visible = visible;
|
|
2797
|
-
entry.helper.update
|
|
2218
|
+
entry.helper.update();
|
|
2798
2219
|
}
|
|
2799
2220
|
}
|
|
2800
2221
|
|
|
@@ -2807,20 +2228,18 @@ export class EditorViewport {
|
|
|
2807
2228
|
|
|
2808
2229
|
private _disposeLightHelper(
|
|
2809
2230
|
id: string,
|
|
2810
|
-
entry: {
|
|
2231
|
+
entry: { helper: THREE.Object3D & { dispose(): void } },
|
|
2811
2232
|
): void {
|
|
2812
2233
|
entry.helper.removeFromParent();
|
|
2813
|
-
entry.helper.dispose
|
|
2234
|
+
entry.helper.dispose();
|
|
2814
2235
|
this._lightHelpers.delete(id);
|
|
2815
2236
|
}
|
|
2816
2237
|
|
|
2817
2238
|
private _ensureLightHelper(id: string, light: THREE.Light): void {
|
|
2818
2239
|
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.
|
|
2240
|
+
// Draw helpers for supported light classes, including hemisphere lights; ambient lights have no helper.
|
|
2822
2241
|
const size = 0.5;
|
|
2823
|
-
let helper: (THREE.Object3D & { update
|
|
2242
|
+
let helper: (THREE.Object3D & { update(): void; dispose(): void }) | null = null;
|
|
2824
2243
|
const candidate = light as THREE.Light & {
|
|
2825
2244
|
isDirectionalLight?: boolean;
|
|
2826
2245
|
isPointLight?: boolean;
|
|
@@ -2844,26 +2263,13 @@ export class EditorViewport {
|
|
|
2844
2263
|
helper.name = `Light helper: ${light.name || id}`;
|
|
2845
2264
|
helper.userData['editorOnly'] = true;
|
|
2846
2265
|
helper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
2847
|
-
helper.layers.set(EDITOR_LAYER);
|
|
2848
2266
|
this._lightHelpers.set(id, { source: light, helper });
|
|
2849
2267
|
this._scene.add(helper);
|
|
2850
2268
|
}
|
|
2851
2269
|
|
|
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) {
|
|
2270
|
+
/** Locate native audio listeners/sources with editor-only markers. Adapter spatial handles own distance/direction edits. */
|
|
2271
|
+
private _syncAudioHelpers(reconcileMembership: boolean): void {
|
|
2272
|
+
if (reconcileMembership) {
|
|
2867
2273
|
const current = new Map<string, THREE.Object3D>();
|
|
2868
2274
|
for (const [id, object] of this._objectMap) {
|
|
2869
2275
|
const audio = this._nativeAudio(object);
|
|
@@ -2874,16 +2280,6 @@ export class EditorViewport {
|
|
|
2874
2280
|
this._disposeAudioHelper(id, entry);
|
|
2875
2281
|
}
|
|
2876
2282
|
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
2283
|
}
|
|
2888
2284
|
const visible =
|
|
2889
2285
|
this._threeSurfaceShowing && this._store.shell.showHelpers && this._store.shell.helperVisibility.audio;
|
|
@@ -2912,22 +2308,20 @@ export class EditorViewport {
|
|
|
2912
2308
|
helper.name = `Audio helper: ${source.name || id}`;
|
|
2913
2309
|
helper.userData['editorOnly'] = true;
|
|
2914
2310
|
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.
|
|
2311
|
+
// Parent the marker to its source so it follows the source's transform.
|
|
2918
2312
|
source.add(helper);
|
|
2919
2313
|
this._audioHelpers.set(id, { source, helper });
|
|
2920
2314
|
}
|
|
2921
2315
|
|
|
2922
2316
|
private _disposeAudioHelper(
|
|
2923
2317
|
id: string,
|
|
2924
|
-
entry: {
|
|
2318
|
+
entry: { helper: THREE.Group },
|
|
2925
2319
|
): void {
|
|
2926
2320
|
entry.helper.traverse((child) => {
|
|
2927
2321
|
const mesh = child as THREE.Mesh;
|
|
2928
|
-
mesh.geometry?.dispose
|
|
2322
|
+
mesh.geometry?.dispose();
|
|
2929
2323
|
const material = mesh.material as THREE.Material | undefined;
|
|
2930
|
-
material?.dispose
|
|
2324
|
+
material?.dispose();
|
|
2931
2325
|
});
|
|
2932
2326
|
entry.helper.removeFromParent();
|
|
2933
2327
|
this._audioHelpers.delete(id);
|
|
@@ -2935,7 +2329,7 @@ export class EditorViewport {
|
|
|
2935
2329
|
|
|
2936
2330
|
private _disposeCameraHelper(
|
|
2937
2331
|
id: string,
|
|
2938
|
-
entry: {
|
|
2332
|
+
entry: { helper: THREE.CameraHelper },
|
|
2939
2333
|
): void {
|
|
2940
2334
|
entry.helper.removeFromParent();
|
|
2941
2335
|
entry.helper.dispose();
|
|
@@ -2944,9 +2338,7 @@ export class EditorViewport {
|
|
|
2944
2338
|
|
|
2945
2339
|
private _ensureCameraHelper(id: string, camera: THREE.Camera): void {
|
|
2946
2340
|
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.
|
|
2341
|
+
// 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
2342
|
let helper: THREE.CameraHelper;
|
|
2951
2343
|
try {
|
|
2952
2344
|
helper = new THREE.CameraHelper(camera);
|
|
@@ -2968,12 +2360,7 @@ export class EditorViewport {
|
|
|
2968
2360
|
this._refusedCameraOverlays.add(id);
|
|
2969
2361
|
editorConsole.warn(
|
|
2970
2362
|
`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.',
|
|
2363
|
+
`${error instanceof Error ? error.message : String(error)}. Framing, selection and inspection remain available.`,
|
|
2977
2364
|
'authoring',
|
|
2978
2365
|
);
|
|
2979
2366
|
}
|
|
@@ -3040,13 +2427,13 @@ export class EditorViewport {
|
|
|
3040
2427
|
}
|
|
3041
2428
|
|
|
3042
2429
|
private _finishSpatialHandleDrag(commit: boolean): void {
|
|
3043
|
-
const active = this._activeSpatialHandle
|
|
2430
|
+
const active = this._activeSpatialHandle!;
|
|
3044
2431
|
this._activeSpatialHandle = null;
|
|
3045
2432
|
this.orbitControls.enabled = this._orbitEnabledBeforeHandleDrag;
|
|
3046
2433
|
this.transformControls.enabled = this._transformEnabledBeforeHandleDrag;
|
|
3047
2434
|
this.syncFromStore();
|
|
3048
2435
|
this._canvas.style.cursor = '';
|
|
3049
|
-
if (commit
|
|
2436
|
+
if (commit) {
|
|
3050
2437
|
void spatialHandlesForAdapter(this._authoring())?.commit(active.nodeId, active.handle.id, [
|
|
3051
2438
|
this._spatialHandleDragPoint.x,
|
|
3052
2439
|
this._spatialHandleDragPoint.y,
|
|
@@ -3084,41 +2471,34 @@ export class EditorViewport {
|
|
|
3084
2471
|
}
|
|
3085
2472
|
setSpatialHandleHovered(hovered, true);
|
|
3086
2473
|
const binding = spatialHandleBinding(hovered);
|
|
3087
|
-
this._canvas.style.cursor = binding
|
|
2474
|
+
this._canvas.style.cursor = binding.handle.writable ? 'grab' : 'not-allowed';
|
|
3088
2475
|
}
|
|
3089
2476
|
|
|
3090
2477
|
private _updateEditorHandleInteraction(e: PointerEvent): boolean {
|
|
3091
2478
|
return this._updateSpatialHandleDrag(e) || this._updateConstraintControlHover(e);
|
|
3092
2479
|
}
|
|
3093
2480
|
|
|
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
|
-
];
|
|
2481
|
+
private _allGizmos(): TransformControls[] {
|
|
2482
|
+
return [this.transformControls, this._auxRotateControls, this._auxScaleControls];
|
|
3108
2483
|
}
|
|
3109
2484
|
|
|
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
|
-
};
|
|
2485
|
+
/** A drag is under way: a transform gizmo, a spatial handle or a constraint control. Every
|
|
2486
|
+
* frame of it is a change of the picture (`StageHost`), whatever the drag signals. */
|
|
2487
|
+
interacting(): boolean {
|
|
2488
|
+
return this._anyGizmoDragging() || this._activeSpatialHandle !== null || this._constraintControlPointerSession;
|
|
3118
2489
|
}
|
|
3119
2490
|
|
|
3120
|
-
|
|
3121
|
-
|
|
2491
|
+
/**
|
|
2492
|
+
* WHAT INPUT CHANGES IN THE STAGE'S OWN FURNITURE: the hovered constraint control and spatial
|
|
2493
|
+
* handle, and each transform gizmo's shown state, highlighted axis and drag. A stage counts a
|
|
2494
|
+
* pointer move as a change of its picture only when this (or the camera) moves, so hovering
|
|
2495
|
+
* over a still view does not start its accumulation over (`StageHost`).
|
|
2496
|
+
*/
|
|
2497
|
+
interactionState(): string {
|
|
2498
|
+
const gizmos = this._allGizmos()
|
|
2499
|
+
.map((controls) => `${controls.getHelper().visible ? 1 : 0}${controls.enabled ? 1 : 0}${controls.axis ?? '-'}${controls.dragging ? 1 : 0}`)
|
|
2500
|
+
.join(',');
|
|
2501
|
+
return `${this._hoveredConstraintControl?.mesh.uuid ?? '-'}|${this._hoveredSpatialHandle?.uuid ?? '-'}|${gizmos}`;
|
|
3122
2502
|
}
|
|
3123
2503
|
|
|
3124
2504
|
private _anyGizmoDragging(): boolean {
|
|
@@ -3137,14 +2517,9 @@ export class EditorViewport {
|
|
|
3137
2517
|
|
|
3138
2518
|
/** Sync mode/space/snap from store. */
|
|
3139
2519
|
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
2520
|
const mode = this._store.shell.transformMode;
|
|
3144
2521
|
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.
|
|
2522
|
+
// Select uses translate mode with the controls detached, so it draws no transform gizmo.
|
|
3148
2523
|
const primaryMode = combined || mode === 'select' ? 'translate' : mode;
|
|
3149
2524
|
if (this.transformControls.mode !== primaryMode) {
|
|
3150
2525
|
this.transformControls.setMode(primaryMode);
|
|
@@ -3152,18 +2527,14 @@ export class EditorViewport {
|
|
|
3152
2527
|
this._applySnapToGizmos();
|
|
3153
2528
|
for (const controls of this._allGizmos()) controls.setSpace(this._store.shell.transformSpace);
|
|
3154
2529
|
|
|
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.
|
|
2530
|
+
// Enable transform tools only for the visible three.js stage that owns this selection.
|
|
2531
|
+
// Camera previews and persistent furniture have their own visibility gates.
|
|
3161
2532
|
const policy = viewportAuthoringPolicy();
|
|
3162
2533
|
const authoring = this._authoring();
|
|
3163
2534
|
const toolOwner = policy.toolOwner(authoring, this._store.shell.selectedEntityIds);
|
|
3164
2535
|
this._threejsToolContextActive =
|
|
3165
2536
|
(this._standaloneAuthoring || policy.toolOwnerPainted(this._store.shell, toolOwner)) &&
|
|
3166
|
-
toolOwner?.kind === '
|
|
2537
|
+
toolOwner?.kind === 'stage';
|
|
3167
2538
|
|
|
3168
2539
|
// Grid visibility — the user's toggle, AND'd with a three stage showing at
|
|
3169
2540
|
// all (selection or not: Blender's floor is persistent; the gizmos above
|
|
@@ -3174,30 +2545,12 @@ export class EditorViewport {
|
|
|
3174
2545
|
this.grid.visible = this._presentationGrid && threeSurface;
|
|
3175
2546
|
if (this._axisLines) this._axisLines.visible = this.grid.visible && this._axesWanted;
|
|
3176
2547
|
|
|
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.
|
|
2548
|
+
// Reconcile helper membership, icon visibility and camera/light helpers when
|
|
2549
|
+
// the object map, gizmo epoch, helper settings or stage visibility changes.
|
|
3182
2550
|
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
2551
|
const hiddenRootId = this._standaloneAuthoring ? null : policy.threeViewportRootId(this._store.shell);
|
|
3193
2552
|
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).
|
|
2553
|
+
// Reapply hidden state on each notification, including in-place object replacements.
|
|
3201
2554
|
if (hidden) policy.applyRootHiddenVisibility(this._objectMap, hiddenRootId);
|
|
3202
2555
|
// Scene-LEVEL environment (background/skybox/fog/IBL). Nothing here has to
|
|
3203
2556
|
// RESTORE it on the show edge — an adapter-backed world paints its own
|
|
@@ -3205,33 +2558,17 @@ export class EditorViewport {
|
|
|
3205
2558
|
// same rationale as the per-notify `applyRootHiddenVisibility` above.
|
|
3206
2559
|
if (hidden) policy.suppressRootEnvironment(this._scene);
|
|
3207
2560
|
const gizmoEpoch = this._store.gizmoEpoch;
|
|
3208
|
-
const gizmoSig = `${this._store.shell.showHelpers}|${
|
|
2561
|
+
const gizmoSig = `${this._store.shell.showHelpers}|${JSON.stringify(vis)}|${threeSurface}`;
|
|
3209
2562
|
const mapChanged = this._objectMap !== this._lastGizmoObjectMap;
|
|
3210
2563
|
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;
|
|
2564
|
+
const reconcileMembership =
|
|
2565
|
+
mapChanged || settingsChanged || gizmoEpoch !== this._lastGizmoEpoch;
|
|
3219
2566
|
this._lastGizmoObjectMap = this._objectMap;
|
|
3220
2567
|
this._lastGizmoSig = gizmoSig;
|
|
3221
2568
|
this._lastGizmoEpoch = gizmoEpoch;
|
|
3222
2569
|
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
2570
|
// Helper + icon visibility — per-type filtering
|
|
3231
|
-
const helperRoots =
|
|
3232
|
-
changedIds === null
|
|
3233
|
-
? topmostPublishedObjects(this._objectMap)
|
|
3234
|
-
: topmostChangedObjects(this._objectMap, changedIds);
|
|
2571
|
+
const helperRoots = topmostPublishedObjects(this._objectMap);
|
|
3235
2572
|
for (const obj of helperRoots) {
|
|
3236
2573
|
obj.traverse((child) => {
|
|
3237
2574
|
if (getUserData(child, 'editorIcon')) {
|
|
@@ -3261,16 +2598,16 @@ export class EditorViewport {
|
|
|
3261
2598
|
helper.visible =
|
|
3262
2599
|
this._store.shell.showHelpers && (!(kind in vis) || (vis as Record<string, boolean>)[kind]!);
|
|
3263
2600
|
}
|
|
3264
|
-
this._syncCameraHelpers(
|
|
3265
|
-
this._syncLightHelpers(
|
|
2601
|
+
this._syncCameraHelpers();
|
|
2602
|
+
this._syncLightHelpers();
|
|
3266
2603
|
}
|
|
3267
2604
|
|
|
3268
|
-
this._syncAudioHelpers(
|
|
3269
|
-
this._syncReflectionProbeHelpers(
|
|
3270
|
-
this._syncTriggerVolumeHelpers(
|
|
3271
|
-
this._syncConstraintHelpers(
|
|
2605
|
+
this._syncAudioHelpers(reconcileMembership);
|
|
2606
|
+
this._syncReflectionProbeHelpers(reconcileMembership);
|
|
2607
|
+
this._syncTriggerVolumeHelpers(reconcileMembership);
|
|
2608
|
+
this._syncConstraintHelpers(reconcileMembership);
|
|
3272
2609
|
|
|
3273
|
-
// Attach gizmo based on pivot mode.
|
|
2610
|
+
// Attach gizmo based on pivot mode. No gizmo without a visible
|
|
3274
2611
|
// three surface — this whole block re-derives per notify, so detaching
|
|
3275
2612
|
// here is self-healing (the un-hide notify re-attaches to the same
|
|
3276
2613
|
// selection with no extra bookkeeping).
|
|
@@ -3284,8 +2621,7 @@ export class EditorViewport {
|
|
|
3284
2621
|
// Combined mode filters/attaches on POSITION (its median-filter channel)
|
|
3285
2622
|
// and attaches when ANY channel is writable — a scale-locked prefab
|
|
3286
2623
|
// instance still gets move+rotate handles instead of no gizmo at all
|
|
3287
|
-
// (
|
|
3288
|
-
// an empty viewport with no visible refusal).
|
|
2624
|
+
// (an empty viewport with no visible refusal).
|
|
3289
2625
|
const activeTransformChannel =
|
|
3290
2626
|
mode === 'translate' || mode === 'combined'
|
|
3291
2627
|
? 'position'
|
|
@@ -3315,12 +2651,8 @@ export class EditorViewport {
|
|
|
3315
2651
|
}
|
|
3316
2652
|
const obj = this._objectForAuthoringId(id);
|
|
3317
2653
|
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).
|
|
2654
|
+
// Compute the scene-root pivot from world matrices. Local position fields do not
|
|
2655
|
+
// describe nested nodes or objects driven directly through their matrices.
|
|
3324
2656
|
median.add(obj.getWorldPosition(_medianTmp));
|
|
3325
2657
|
count++;
|
|
3326
2658
|
}
|
|
@@ -3355,8 +2687,8 @@ export class EditorViewport {
|
|
|
3355
2687
|
// `matrixWorld` (three's `getWorld*` decompose it), never from the
|
|
3356
2688
|
// vector fields: a node whose driver owns its matrix keeps
|
|
3357
2689
|
// `.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`).
|
|
2690
|
+
// would put the gizmo where the object was at spawn while its bounds
|
|
2691
|
+
// draw where it is (`authoring/live-object-transform.ts`).
|
|
3360
2692
|
const pivotLocal = new THREE.Vector3(entityPivot[0], entityPivot[1], entityPivot[2]);
|
|
3361
2693
|
pivotLocal.applyQuaternion(obj.getWorldQuaternion(_pivotQuat));
|
|
3362
2694
|
this._pivotDummy.position.copy(obj.getWorldPosition(_pivotPos)).add(pivotLocal);
|
|
@@ -3381,13 +2713,6 @@ export class EditorViewport {
|
|
|
3381
2713
|
this._syncCombinedGizmoHalves();
|
|
3382
2714
|
}
|
|
3383
2715
|
|
|
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
2716
|
/** Snap values reach the gizmos from the store's toggle OR a held
|
|
3392
2717
|
* Ctrl/⌘ (the Unity/Blender hold-to-snap convention) — three's controls
|
|
3393
2718
|
* read the snap on every pointer move, so a mid-drag hold takes effect. */
|
|
@@ -3407,7 +2732,7 @@ export class EditorViewport {
|
|
|
3407
2732
|
private _modifiedZoom = false;
|
|
3408
2733
|
|
|
3409
2734
|
private readonly _onAltOrbitStart = (e: PointerEvent): void => {
|
|
3410
|
-
if (e.button === 1 && (e.ctrlKey || e.metaKey) &&
|
|
2735
|
+
if (e.button === 1 && (e.ctrlKey || e.metaKey) && editorHost().keyboard.navigation().orbit === 'middle') {
|
|
3411
2736
|
this._modifiedZoom = true;
|
|
3412
2737
|
this.orbitControls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY;
|
|
3413
2738
|
}
|
|
@@ -3417,9 +2742,9 @@ export class EditorViewport {
|
|
|
3417
2742
|
}
|
|
3418
2743
|
};
|
|
3419
2744
|
|
|
3420
|
-
private readonly _onAltOrbitRelease = (event
|
|
2745
|
+
private readonly _onAltOrbitRelease = (event: PointerEvent): void => {
|
|
3421
2746
|
// The zoom is the middle button's gesture, and ends with that button's release.
|
|
3422
|
-
if (this._modifiedZoom && (
|
|
2747
|
+
if (this._modifiedZoom && (event.type === 'pointercancel' || event.button === 1)) {
|
|
3423
2748
|
this._modifiedZoom = false;
|
|
3424
2749
|
this.applyKeymapNavigation();
|
|
3425
2750
|
}
|
|
@@ -3435,14 +2760,15 @@ export class EditorViewport {
|
|
|
3435
2760
|
if (hold === this._snapHold) return;
|
|
3436
2761
|
this._snapHold = hold;
|
|
3437
2762
|
this._applySnapToGizmos();
|
|
3438
|
-
//
|
|
3439
|
-
// snapping (human pass 10 said exactly that) — name it while it happens.
|
|
2763
|
+
// Show a hint when the modifier temporarily enables snapping during a drag.
|
|
3440
2764
|
if (hold && this._anyGizmoDragging() && !this._store.shell.snapEnabled) {
|
|
3441
2765
|
const { translate, rotate, scale } = this._store.shell.snapValues;
|
|
3442
2766
|
showTransientHint(`Snapping while held: ${translate} units · ${rotate}° · ×${scale}`);
|
|
3443
2767
|
}
|
|
3444
2768
|
};
|
|
3445
2769
|
|
|
2770
|
+
/** In combined mode, show each transform half only when its channel is
|
|
2771
|
+
* writable for the entity attached by the current sync pass. */
|
|
3446
2772
|
private _syncCombinedGizmoHalves(): void {
|
|
3447
2773
|
if (this._anyGizmoDragging()) return;
|
|
3448
2774
|
const combined = this._store.shell.transformMode === 'combined';
|
|
@@ -3463,17 +2789,9 @@ export class EditorViewport {
|
|
|
3463
2789
|
const scaleHelper = this._auxScaleControls.getHelper();
|
|
3464
2790
|
rotateHelper.visible = rotateOn;
|
|
3465
2791
|
scaleHelper.visible = scaleOn;
|
|
3466
|
-
for (const helper of [rotateHelper, scaleHelper]) {
|
|
3467
|
-
helper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
3468
|
-
}
|
|
3469
2792
|
this.transformControls.enabled = true;
|
|
3470
2793
|
}
|
|
3471
2794
|
|
|
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
2795
|
/** Move all editor gizmos from the editor scene into a target scene (e.g. the live game scene). */
|
|
3478
2796
|
moveGizmosTo(target: THREE.Scene): void {
|
|
3479
2797
|
const objs = this._gizmoObjects();
|
|
@@ -3505,22 +2823,14 @@ export class EditorViewport {
|
|
|
3505
2823
|
}
|
|
3506
2824
|
|
|
3507
2825
|
/**
|
|
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.
|
|
2826
|
+
* Cache the active scene’s content AABB for CONTENT_BOUNDS_MAX_AGE_MS as its live graph changes.
|
|
2827
|
+
* Per-frame clip planes use the cached extent; focusOnScene forces a fresh measurement.
|
|
3518
2828
|
*/
|
|
3519
|
-
private _contentBounds(
|
|
2829
|
+
private _contentBounds(): THREE.Box3 {
|
|
3520
2830
|
const now = performance.now();
|
|
3521
2831
|
const stale =
|
|
3522
2832
|
this._contentBoundsAt === 0 || now - this._contentBoundsAt > CONTENT_BOUNDS_MAX_AGE_MS;
|
|
3523
|
-
if (
|
|
2833
|
+
if (stale) {
|
|
3524
2834
|
this._contentBoundsBox.makeEmpty();
|
|
3525
2835
|
for (const child of this._scene.children) {
|
|
3526
2836
|
if (isInEditorOwnedSubtree(child)) continue;
|
|
@@ -3551,15 +2861,9 @@ export class EditorViewport {
|
|
|
3551
2861
|
return boxes;
|
|
3552
2862
|
}
|
|
3553
2863
|
|
|
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
|
-
*/
|
|
2864
|
+
/** Grow clip planes to reach cached content bounds. Update the projection only when the required planes change. */
|
|
3562
2865
|
private _updateClipPlanes(): void {
|
|
2866
|
+
if (this.projectionOwner.owned) return;
|
|
3563
2867
|
const box = this._contentBounds();
|
|
3564
2868
|
let distance = 0;
|
|
3565
2869
|
let radius = 0;
|
|
@@ -3586,14 +2890,9 @@ export class EditorViewport {
|
|
|
3586
2890
|
* Returns false — camera untouched — when that view cannot be used: the
|
|
3587
2891
|
* world is behind it, on it, or PRESSED AGAINST IT (a camera placed inside
|
|
3588
2892
|
* 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).
|
|
2893
|
+
* useless first look — `seededViewShowsWorld` is the measurement).
|
|
3591
2894
|
*
|
|
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.
|
|
2895
|
+
* A caller can retry as asynchronous scene content and its camera mount.
|
|
3597
2896
|
*/
|
|
3598
2897
|
seedFromGameCamera(gameCamera: THREE.Camera): boolean {
|
|
3599
2898
|
const box = frameableContentBounds(this._measureContentBounds());
|
|
@@ -3647,8 +2946,8 @@ export class EditorViewport {
|
|
|
3647
2946
|
box.getSize(new THREE.Vector3()).x / this._viewportAspect,
|
|
3648
2947
|
) * 1.15;
|
|
3649
2948
|
this._syncOrthographicFromPerspective(true);
|
|
3650
|
-
// Before any draw: the framing distance for a world-sized scene
|
|
3651
|
-
//
|
|
2949
|
+
// Before any draw: the framing distance for a world-sized scene can land
|
|
2950
|
+
// past a stale far plane.
|
|
3652
2951
|
this._updateClipPlanes();
|
|
3653
2952
|
return true;
|
|
3654
2953
|
}
|
|
@@ -3752,7 +3051,7 @@ export class EditorViewport {
|
|
|
3752
3051
|
|
|
3753
3052
|
/**
|
|
3754
3053
|
* Show (or clear with null) the editor-only helper of `kind` —
|
|
3755
|
-
*
|
|
3054
|
+
* The object joins the editor
|
|
3756
3055
|
* scene as a scene-root helper (`editorHelper`, `editorHelperType = kind`)
|
|
3757
3056
|
* and follows the Helpers menu's toggle for its kind.
|
|
3758
3057
|
*/
|
|
@@ -3885,32 +3184,20 @@ export class EditorViewport {
|
|
|
3885
3184
|
// every draw. Nothing here should shadow that with a second source.
|
|
3886
3185
|
}
|
|
3887
3186
|
|
|
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
|
-
*/
|
|
3187
|
+
/** Resolve the current presentation source without applying the rig's own lights. */
|
|
3894
3188
|
resolvePresentation(): void {
|
|
3895
|
-
this._presentation?.rig.resolveSource({ light:
|
|
3189
|
+
this._presentation?.rig.resolveSource({ light: false });
|
|
3896
3190
|
}
|
|
3897
3191
|
|
|
3898
|
-
update(dt
|
|
3192
|
+
update(dt: number): void {
|
|
3899
3193
|
this._ensureSparkRenderer();
|
|
3900
3194
|
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.
|
|
3195
|
+
// Detach stale gizmo objects after a remount, then resolve the current selection again.
|
|
3906
3196
|
const attachedObject = this.transformControls.object;
|
|
3907
3197
|
if (attachedObject && attachedObject !== this._pivotDummy && !attachedObject.parent) {
|
|
3908
3198
|
this._detachGizmos();
|
|
3909
3199
|
this.syncFromStore();
|
|
3910
3200
|
}
|
|
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
3201
|
// Fly camera movement (before orbit so it takes priority)
|
|
3915
3202
|
this._updateFlyCamera(dt);
|
|
3916
3203
|
|
|
@@ -3973,21 +3260,9 @@ export class EditorViewport {
|
|
|
3973
3260
|
}
|
|
3974
3261
|
|
|
3975
3262
|
/**
|
|
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.
|
|
3263
|
+
* Apply pinned LOD levels each frame so asynchronous loads and rebuilt instances inherit the view
|
|
3264
|
+
* state. Auto removes the pin and restores autoUpdate; WebGLRenderer then calls
|
|
3265
|
+
* LOD.update(camera) during rendering.
|
|
3991
3266
|
*/
|
|
3992
3267
|
private _enforceLodForcedLevels(): void {
|
|
3993
3268
|
const projectedObjects = new Set(this._objectMap.values());
|
|
@@ -4011,22 +3286,19 @@ export class EditorViewport {
|
|
|
4011
3286
|
*/
|
|
4012
3287
|
renderWithInfrastructure(draw: () => void): void {
|
|
4013
3288
|
const spark = this._sparkRenderer;
|
|
4014
|
-
const restoreCameraView = this._cameraView ? this._suspendPreviewInfrastructure() : null;
|
|
4015
3289
|
if (spark) this._scene.add(spark);
|
|
4016
3290
|
try {
|
|
4017
3291
|
draw();
|
|
4018
3292
|
} finally {
|
|
4019
3293
|
spark?.removeFromParent();
|
|
4020
|
-
restoreCameraView?.();
|
|
4021
3294
|
}
|
|
4022
3295
|
}
|
|
4023
3296
|
|
|
4024
|
-
/** Render the standard orientation gizmo in the
|
|
3297
|
+
/** Render the standard orientation gizmo in the corner selected by the look. */
|
|
4025
3298
|
renderViewCube(renderer: THREE.WebGLRenderer): void {
|
|
4026
3299
|
if (!this._threeSurfaceShowing) return; // no three stage, no compass (Blender's is persistent on one)
|
|
4027
3300
|
if (this._navigation === 'hidden') return;
|
|
4028
3301
|
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
3302
|
const w = renderer.domElement.clientWidth;
|
|
4031
3303
|
const h = renderer.domElement.clientHeight;
|
|
4032
3304
|
const size = this._vcSize;
|
|
@@ -4068,47 +3340,7 @@ export class EditorViewport {
|
|
|
4068
3340
|
}
|
|
4069
3341
|
}
|
|
4070
3342
|
|
|
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). */
|
|
3343
|
+
/** True while the fly-camera session is active. */
|
|
4112
3344
|
get isFlying(): boolean {
|
|
4113
3345
|
return this._flyActive;
|
|
4114
3346
|
}
|
|
@@ -4126,6 +3358,8 @@ export class EditorViewport {
|
|
|
4126
3358
|
this._canvas.removeEventListener('pointerdown', this._onAltOrbitStart, { capture: true });
|
|
4127
3359
|
window.removeEventListener('pointerup', this._onAltOrbitRelease, { capture: true });
|
|
4128
3360
|
window.removeEventListener('pointercancel', this._onAltOrbitRelease, { capture: true });
|
|
3361
|
+
window.removeEventListener('pointercancel', this._onInteractionDragLost, { capture: true });
|
|
3362
|
+
window.removeEventListener('blur', this._onInteractionDragLost);
|
|
4129
3363
|
window.removeEventListener('keydown', this._onSnapHoldKey);
|
|
4130
3364
|
window.removeEventListener('keyup', this._onSnapHoldKey);
|
|
4131
3365
|
this._interactionElement.removeEventListener('pointerup', this._onFlyPointerUp);
|
|
@@ -4134,15 +3368,8 @@ export class EditorViewport {
|
|
|
4134
3368
|
this._interactionElement.removeEventListener('wheel', this._onTrackpadWheel);
|
|
4135
3369
|
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
4136
3370
|
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);
|
|
3371
|
+
this._interactionElement.removeEventListener('pointerup', this._onPointerUp);
|
|
3372
|
+
this._interactionElement.removeEventListener('dblclick', this._onInteractionDblClick);
|
|
4146
3373
|
// Dispose controls now: deferred source cleanup must never disconnect
|
|
4147
3374
|
// the reused element after the replacement controls have connected.
|
|
4148
3375
|
this.orbitControls.dispose();
|
|
@@ -4150,9 +3377,9 @@ export class EditorViewport {
|
|
|
4150
3377
|
}
|
|
4151
3378
|
|
|
4152
3379
|
/**
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
3380
|
+
* Apply the palette's viewport group (`EditorTheme.color.viewport`): cache the grid
|
|
3381
|
+
* ink for `_applyGridLines`, paint axis-line colours and widths (the view owns visibility),
|
|
3382
|
+
* and a flat background when
|
|
4156
3383
|
* named — replacing only a background this viewport painted or none at all,
|
|
4157
3384
|
* never a host's own (a document's declared colour, a world radiance).
|
|
4158
3385
|
*/
|
|
@@ -4164,9 +3391,7 @@ export class EditorViewport {
|
|
|
4164
3391
|
// stage's colours or keeps none of them: Classic Graphite declares no
|
|
4165
3392
|
// group and keeps its floor without the grazing fade. Which axis lines
|
|
4166
3393
|
// show is the view's (`overlays.axes`), whatever the look.
|
|
4167
|
-
|
|
4168
|
-
if (grazing) grazing.value = look.background !== null ? 1 : 0;
|
|
4169
|
-
this._paintLookGrid();
|
|
3394
|
+
this.grid.material.uniforms['uGrazingFade']!.value = look.background !== null ? 1 : 0;
|
|
4170
3395
|
this._lookAxisHexes = [look.axisX, look.axisY, look.axisZ];
|
|
4171
3396
|
if (this._axisLines) {
|
|
4172
3397
|
(this._axisLines.material as LineMaterial).linewidth = this._axisLineCss(look.axisLineWidth);
|
|
@@ -4190,22 +3415,7 @@ export class EditorViewport {
|
|
|
4190
3415
|
}
|
|
4191
3416
|
|
|
4192
3417
|
/**
|
|
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
|
|
3418
|
+
* LIGHT AND DRESS THIS STAGE BY A VIEW'S PRESENTATION (`@volter/sdk/viewport-presentation`): its studio
|
|
4209
3419
|
* or preview lighting through a `StagePresentationRig` in this scene (in place of the
|
|
4210
3420
|
* viewport's own ambient and directional), its selection marks and its grid's major step.
|
|
4211
3421
|
* The stage's render pipeline keeps its tone mapping. The scene's own lighting is what an
|
|
@@ -4240,7 +3450,7 @@ export class EditorViewport {
|
|
|
4240
3450
|
rig.dispose();
|
|
4241
3451
|
this._editorObjects = this._editorObjects.filter((object) => !roots.includes(object));
|
|
4242
3452
|
if (this._presentation === binding) this._presentation = null;
|
|
4243
|
-
const rigOn = this._lightRig
|
|
3453
|
+
const rigOn = this._lightRig;
|
|
4244
3454
|
this._editorAmbient.visible = rigOn;
|
|
4245
3455
|
this._editorDirLight.visible = rigOn;
|
|
4246
3456
|
},
|
|
@@ -4252,11 +3462,11 @@ export class EditorViewport {
|
|
|
4252
3462
|
return binding.stop;
|
|
4253
3463
|
}
|
|
4254
3464
|
|
|
4255
|
-
/** The view's
|
|
4256
|
-
setSelectionMarks(marks: { readonly
|
|
3465
|
+
/** The view's wire and box marks; a change rebuilds them for the current selection. */
|
|
3466
|
+
setSelectionMarks(marks: { readonly wire: boolean; readonly box: boolean }): void {
|
|
4257
3467
|
const current = this._selectionMarks;
|
|
4258
|
-
if (current.
|
|
4259
|
-
this._selectionMarks = {
|
|
3468
|
+
if (current.wire === marks.wire && current.box === marks.box) return;
|
|
3469
|
+
this._selectionMarks = { wire: marks.wire, box: marks.box };
|
|
4260
3470
|
this._syncBoxHelpers();
|
|
4261
3471
|
}
|
|
4262
3472
|
|
|
@@ -4338,23 +3548,21 @@ export class EditorViewport {
|
|
|
4338
3548
|
|
|
4339
3549
|
/** Minor cells per major line (`overlays.grid.majorEvery`; Blender 10, Godot 8). */
|
|
4340
3550
|
setGridMajorEvery(every: number): void {
|
|
4341
|
-
|
|
4342
|
-
if (uniform && Number.isFinite(every) && every >= 1) uniform.value = every;
|
|
3551
|
+
if (Number.isFinite(every) && every >= 1) this.grid.material.uniforms['uMajorEvery']!.value = every;
|
|
4343
3552
|
}
|
|
4344
3553
|
|
|
4345
3554
|
/** The look's floor lines: widths in device pixels and the major level's contrast. */
|
|
4346
|
-
private _gridMajorContrast
|
|
3555
|
+
private _gridMajorContrast!: number;
|
|
4347
3556
|
private _applyGridLines(lines: ReturnType<typeof nativeViewportGrid>): void {
|
|
4348
3557
|
const uniforms = this.grid.material.uniforms;
|
|
4349
|
-
|
|
4350
|
-
|
|
3558
|
+
uniforms['uLineWidth']!.value = lines.lineWidth;
|
|
3559
|
+
uniforms['uMajorWidth']!.value = lines.majorWidth;
|
|
4351
3560
|
this._gridMajorContrast = lines.majorContrast;
|
|
4352
3561
|
this._paintLookGrid();
|
|
4353
3562
|
}
|
|
4354
3563
|
|
|
4355
3564
|
private _paintLookGrid(): void {
|
|
4356
3565
|
const hex = this._lookGridHex;
|
|
4357
|
-
if (hex === null) return;
|
|
4358
3566
|
const backgroundHex = this._lookBackgroundHex ?? 0x3f3f3f;
|
|
4359
3567
|
const channel = (shift: number) => {
|
|
4360
3568
|
const line = (hex >> shift) & 0xff;
|
|
@@ -4364,18 +3572,11 @@ export class EditorViewport {
|
|
|
4364
3572
|
const majorHex = (channel(16) << 16) | (channel(8) << 8) | channel(0);
|
|
4365
3573
|
const minor = toneMappedSourceColor(hex, this._renderer);
|
|
4366
3574
|
const major = toneMappedSourceColor(majorHex, this._renderer);
|
|
4367
|
-
(this.grid.material.uniforms['uColor']
|
|
4368
|
-
(this.grid.material.uniforms['uMajorColor']
|
|
3575
|
+
(this.grid.material.uniforms['uColor']!.value as THREE.Color).copy(minor);
|
|
3576
|
+
(this.grid.material.uniforms['uMajorColor']!.value as THREE.Color).copy(major);
|
|
4369
3577
|
}
|
|
4370
3578
|
|
|
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
|
-
*/
|
|
3579
|
+
/** 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
3580
|
private _rebuildAxisLines(): void {
|
|
4380
3581
|
const lines = this._axisLines;
|
|
4381
3582
|
if (!lines) return;
|
|
@@ -4389,16 +3590,14 @@ export class EditorViewport {
|
|
|
4389
3590
|
this._axesWanted = shown.length > 0;
|
|
4390
3591
|
lines.visible = this.grid.visible && this._axesWanted;
|
|
4391
3592
|
if (!this._axesWanted) return;
|
|
4392
|
-
//
|
|
4393
|
-
// its first draw (`_maxInstanceCount`), so growing one from two lines to three drew only
|
|
4394
|
-
// the first two.
|
|
3593
|
+
// Replace geometry on rebuild so its cached instance count matches the current axes.
|
|
4395
3594
|
const build = (threeAxes: readonly number[]): LineSegmentsGeometry => {
|
|
4396
3595
|
const positions: number[] = [];
|
|
4397
3596
|
const colors: number[] = [];
|
|
4398
3597
|
for (const threeAxis of threeAxes) {
|
|
4399
3598
|
const world = frame[threeAxis]![0];
|
|
4400
|
-
const hex = this._lookAxisHexes[world]
|
|
4401
|
-
const color = hex === null
|
|
3599
|
+
const hex = this._lookAxisHexes[world]!;
|
|
3600
|
+
const color = hex === null ? this._gizmoAxisColor(world) : new THREE.Color(hex);
|
|
4402
3601
|
positions.push(...axisSegmentPositions(threeAxis));
|
|
4403
3602
|
for (let i = 0; i < AXIS_SEGMENTS_PER_AXIS * 2; i++) colors.push(color.r, color.g, color.b);
|
|
4404
3603
|
}
|
|
@@ -4494,14 +3693,12 @@ export class EditorViewport {
|
|
|
4494
3693
|
(this._axisLines.material as THREE.Material).dispose();
|
|
4495
3694
|
this._axisLines = null;
|
|
4496
3695
|
}
|
|
4497
|
-
this.clearCameraView();
|
|
4498
3696
|
if (this._sparkRenderer) {
|
|
4499
3697
|
this._scene.remove(this._sparkRenderer);
|
|
4500
3698
|
disposeSparkRendererWhenIdle(this._sparkRenderer);
|
|
4501
3699
|
this._sparkRenderer = null;
|
|
4502
3700
|
}
|
|
4503
3701
|
this.disconnectInput();
|
|
4504
|
-
this._hideDropOverlay();
|
|
4505
3702
|
for (const helper of this._helpers.values()) this._scene.remove(helper);
|
|
4506
3703
|
this._helpers.clear();
|
|
4507
3704
|
for (const helper of this._boxHelpers.values()) {
|
|
@@ -4526,8 +3723,9 @@ export class EditorViewport {
|
|
|
4526
3723
|
// --- Private ---
|
|
4527
3724
|
|
|
4528
3725
|
private _ensureSparkRenderer(): void {
|
|
3726
|
+
const renderer = this._renderer;
|
|
4529
3727
|
if (
|
|
4530
|
-
!
|
|
3728
|
+
!renderer ||
|
|
4531
3729
|
this._sparkRenderer ||
|
|
4532
3730
|
this._sparkRendererLoading ||
|
|
4533
3731
|
this._sparkRendererFailed
|
|
@@ -4540,8 +3738,8 @@ export class EditorViewport {
|
|
|
4540
3738
|
this._sparkRendererLoading = true;
|
|
4541
3739
|
void import('@sparkjsdev/spark')
|
|
4542
3740
|
.then(({ SparkRenderer }) => {
|
|
4543
|
-
if (this._disposed
|
|
4544
|
-
const spark = new SparkRenderer({ renderer
|
|
3741
|
+
if (this._disposed) return;
|
|
3742
|
+
const spark = new SparkRenderer({ renderer, enableLod: false });
|
|
4545
3743
|
spark.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
4546
3744
|
this._sparkRenderer = spark;
|
|
4547
3745
|
invalidateStages();
|
|
@@ -4557,21 +3755,7 @@ export class EditorViewport {
|
|
|
4557
3755
|
}
|
|
4558
3756
|
|
|
4559
3757
|
/**
|
|
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.
|
|
3758
|
+
* BUILD (or REBUILD) THE NAVIGATION GIZMO in the presented world's axis frame.
|
|
4575
3759
|
*
|
|
4576
3760
|
* Blender's gizmo draws a stalk and a FILLED, LETTERED ball on each axis's
|
|
4577
3761
|
* POSITIVE side and a hollow one opposite, and the axis it calls Z is the
|
|
@@ -4586,16 +3770,15 @@ export class EditorViewport {
|
|
|
4586
3770
|
* a fact about the scene and not about what the axis is called.
|
|
4587
3771
|
*/
|
|
4588
3772
|
private _buildOrientationGizmo(): void {
|
|
4589
|
-
if (this._vcScene === undefined) return;
|
|
4590
3773
|
// Rebuilt on every look change, so what it held is released, textures included.
|
|
4591
3774
|
for (const held of [...this._vcScene.children]) {
|
|
4592
3775
|
this._vcScene.remove(held);
|
|
4593
3776
|
const drawn = held as THREE.Mesh | THREE.Sprite;
|
|
4594
3777
|
// 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
|
|
3778
|
+
if (!(drawn as THREE.Sprite).isSprite) drawn.geometry.dispose();
|
|
3779
|
+
const material = drawn.material as THREE.Material & { map?: THREE.Texture | null };
|
|
3780
|
+
material.map?.dispose();
|
|
3781
|
+
material.dispose();
|
|
4599
3782
|
}
|
|
4600
3783
|
this._vcBalls.length = 0;
|
|
4601
3784
|
this._vcStalks.length = 0;
|
|
@@ -4631,7 +3814,7 @@ export class EditorViewport {
|
|
|
4631
3814
|
// The look's navigation colours, else its axis colours, else the editor's own.
|
|
4632
3815
|
const lookAxes = this._gizmoLook.navigation ?? this._gizmoLook.axes;
|
|
4633
3816
|
const color =
|
|
4634
|
-
lookAxes === null ?
|
|
3817
|
+
lookAxes === null ? this._gizmoAxisColor(sourceAxis) : new THREE.Color(lookAxes[sourceAxis]!);
|
|
4635
3818
|
const letter = AXIS_LETTER[sourceAxis]!;
|
|
4636
3819
|
const axis = axes[i]!.clone().multiplyScalar(positive);
|
|
4637
3820
|
|
|
@@ -4701,11 +3884,11 @@ export class EditorViewport {
|
|
|
4701
3884
|
for (let i = 0; i < 3; i++) {
|
|
4702
3885
|
const [sourceAxis, positive] = this._stageAxisFrame[i]!;
|
|
4703
3886
|
const color =
|
|
4704
|
-
lookAxes === null ?
|
|
3887
|
+
lookAxes === null ? this._gizmoAxisColor(sourceAxis) : new THREE.Color(lookAxes[sourceAxis]!);
|
|
4705
3888
|
const letter = AXIS_LETTER[sourceAxis]!;
|
|
4706
3889
|
const axis = axes[i]!.clone().multiplyScalar(positive);
|
|
4707
3890
|
if (form === 'triad') {
|
|
4708
|
-
// Unreal's triad is about 40 px, opaque and undimmed by facing
|
|
3891
|
+
// Unreal's triad is about 40 px, opaque and undimmed by facing; its
|
|
4709
3892
|
// size is the look's (`navigationSize`, a multiple of this 24 px).
|
|
4710
3893
|
const scale = this._gizmoLook.navigationSize ?? 1;
|
|
4711
3894
|
const length = (24 * scale) / perUnit;
|
|
@@ -4752,14 +3935,7 @@ export class EditorViewport {
|
|
|
4752
3935
|
}
|
|
4753
3936
|
}
|
|
4754
3937
|
|
|
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
|
-
*/
|
|
3938
|
+
/** Order navigation sprites by depth and dim and shrink balls that point away from the viewer. */
|
|
4763
3939
|
private _syncOrientationGizmoDepth(): void {
|
|
4764
3940
|
const perUnit = COMPASS_BOX_PX / 3;
|
|
4765
3941
|
const view = this._vcDir.copy(this._vcCamera.position).normalize();
|
|
@@ -4767,9 +3943,7 @@ export class EditorViewport {
|
|
|
4767
3943
|
// a ball's colour is its axis colour mixed with the viewport's background by its depth
|
|
4768
3944
|
// (`fading_color`: `(depth + 1) · 0.25 + 0.5`), a negative ball a 25% tint of it ringed in
|
|
4769
3945
|
// 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.
|
|
3946
|
+
// 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
3947
|
if (this._gizmoLook.navigationForm === 'godot') {
|
|
4774
3948
|
this._syncGodotNavigation(view);
|
|
4775
3949
|
return;
|
|
@@ -4812,15 +3986,12 @@ export class EditorViewport {
|
|
|
4812
3986
|
ringMaterial.color.copy(fading);
|
|
4813
3987
|
ringMaterial.opacity = 1;
|
|
4814
3988
|
}
|
|
4815
|
-
//
|
|
4816
|
-
// draws it black at 0.9 over a small glyph, and this is what that reads as).
|
|
3989
|
+
// Letter ink uses the axis fill darkened by 0.356.
|
|
4817
3990
|
letterMaterial.color.copy(mix(black, fillMaterial.color, COMPASS_LETTER_INK, scratch));
|
|
4818
3991
|
fill.visible = !alignedBack;
|
|
4819
3992
|
ring.visible = !alignedBack;
|
|
4820
3993
|
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).
|
|
3994
|
+
// Scale each ball linearly with facing; orthographic projection supplies no depth scaling.
|
|
4824
3995
|
const scale = (COMPASS_BALL_BACK_PX + COMPASS_BALL_DEPTH_GAIN_PX * facing) / perUnit;
|
|
4825
3996
|
const order = 10 + Math.round(facing * 100) * 3;
|
|
4826
3997
|
for (const [part, rank] of [[fill, 0], [ring, 1], [letter, 2]] as const) {
|
|
@@ -4893,12 +4064,8 @@ export class EditorViewport {
|
|
|
4893
4064
|
}
|
|
4894
4065
|
|
|
4895
4066
|
/**
|
|
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.
|
|
4067
|
+
* Test the pointer against the orientation cube’s screen-space box without changing state. Single
|
|
4068
|
+
* clicks use it for axis navigation; double-click selection excludes it from scene geometry.
|
|
4902
4069
|
*/
|
|
4903
4070
|
private _isOverViewCube(clientX: number, clientY: number): boolean {
|
|
4904
4071
|
// An indicator is only drawn, and a triad has nothing to click: clicks pass to the stage.
|
|
@@ -4911,15 +4078,10 @@ export class EditorViewport {
|
|
|
4911
4078
|
return x >= vcLeft && x <= vcLeft + this._vcSize && y >= vcTop && y <= vcTop + this._vcSize;
|
|
4912
4079
|
}
|
|
4913
4080
|
|
|
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
|
-
*/
|
|
4081
|
+
/** Place the gizmo box in the look’s navigation corner, inset by canvas bleed. */
|
|
4919
4082
|
private _vcOrigin(width: number, height: number, inset: number): { left: number; top: number } {
|
|
4920
4083
|
if (this._gizmoLook.navigationCorner === 'bottom-left') {
|
|
4921
|
-
//
|
|
4922
|
-
// 4 px margin put the triad's lower half under it).
|
|
4084
|
+
// Leave room above the stage’s two-line camera readout.
|
|
4923
4085
|
const left = 4;
|
|
4924
4086
|
const bottom = 48;
|
|
4925
4087
|
return { left: left + inset, top: height - this._vcSize - bottom - inset };
|
|
@@ -5005,8 +4167,8 @@ export class EditorViewport {
|
|
|
5005
4167
|
});
|
|
5006
4168
|
};
|
|
5007
4169
|
// Primary (if it's a real entity, not pivot dummy)
|
|
5008
|
-
const obj = this._activeGizmo.object
|
|
5009
|
-
if (obj
|
|
4170
|
+
const obj = this._activeGizmo.object!;
|
|
4171
|
+
if (obj !== this._pivotDummy) {
|
|
5010
4172
|
applyLive(this._attachedAuthoringId ?? this._authoringIdForObject(obj), obj);
|
|
5011
4173
|
}
|
|
5012
4174
|
// Other selected objects (active-element / individual-origins)
|
|
@@ -5195,9 +4357,6 @@ export class EditorViewport {
|
|
|
5195
4357
|
targets: {
|
|
5196
4358
|
id: string;
|
|
5197
4359
|
obj: THREE.Object3D;
|
|
5198
|
-
startPos: THREE.Vector3;
|
|
5199
|
-
startQuat: THREE.Quaternion;
|
|
5200
|
-
startScale: THREE.Vector3;
|
|
5201
4360
|
}[],
|
|
5202
4361
|
): void {
|
|
5203
4362
|
// Collect all dragged entity IDs to exclude from raycast
|
|
@@ -5214,11 +4373,6 @@ export class EditorViewport {
|
|
|
5214
4373
|
// Also include the grid as a snap target
|
|
5215
4374
|
sceneTargets.push(this.grid);
|
|
5216
4375
|
|
|
5217
|
-
if (sceneTargets.length === 0) {
|
|
5218
|
-
this._surfaceSnapIndicator.visible = false;
|
|
5219
|
-
return;
|
|
5220
|
-
}
|
|
5221
|
-
|
|
5222
4376
|
// Raycast downward from the gizmo object position
|
|
5223
4377
|
const origin = gizmoObj.position.clone();
|
|
5224
4378
|
origin.y += 50; // Start high above
|
|
@@ -5269,7 +4423,7 @@ export class EditorViewport {
|
|
|
5269
4423
|
obj.traverse((child) => {
|
|
5270
4424
|
if (isInEditorOwnedSubtree(child)) return;
|
|
5271
4425
|
const mesh = child as THREE.Mesh;
|
|
5272
|
-
if (!mesh.isMesh
|
|
4426
|
+
if (!mesh.isMesh) return;
|
|
5273
4427
|
const posAttr = mesh.geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
5274
4428
|
if (!posAttr) return;
|
|
5275
4429
|
const count = Math.min(posAttr.count, MAX_PER_MESH);
|
|
@@ -5289,7 +4443,7 @@ export class EditorViewport {
|
|
|
5289
4443
|
*/
|
|
5290
4444
|
private _applyVertexSnap(
|
|
5291
4445
|
gizmoObj: THREE.Object3D,
|
|
5292
|
-
targets: { obj: THREE.Object3D
|
|
4446
|
+
targets: { obj: THREE.Object3D }[],
|
|
5293
4447
|
): void {
|
|
5294
4448
|
const entityPos = gizmoObj.position;
|
|
5295
4449
|
const projected = entityPos.clone().project(this._screenCamera);
|
|
@@ -5323,7 +4477,7 @@ export class EditorViewport {
|
|
|
5323
4477
|
}
|
|
5324
4478
|
this._vertexSnapIndicator.position.copy(bestVertex);
|
|
5325
4479
|
this._vertexSnapIndicator.visible = true;
|
|
5326
|
-
//
|
|
4480
|
+
// Scale the indicator with its distance from the drawing camera.
|
|
5327
4481
|
const dist = bestVertex.distanceTo(this._screenCamera.position);
|
|
5328
4482
|
this._vertexSnapIndicator.scale.setScalar(dist * 0.008);
|
|
5329
4483
|
} else {
|
|
@@ -5339,7 +4493,7 @@ export class EditorViewport {
|
|
|
5339
4493
|
this._otherDragObjects = [];
|
|
5340
4494
|
this._allDragObjects = [];
|
|
5341
4495
|
this._preservedChildObjects = [];
|
|
5342
|
-
// Persist the final transform
|
|
4496
|
+
// Persist the final transform and mark dirty. A mid-drag autosave may
|
|
5343
4497
|
// have already fired and cleared dirty; this re-schedules the final write.
|
|
5344
4498
|
const authoring = this._authoring();
|
|
5345
4499
|
for (const id of editIds) endAuthoringTransformEdit(authoring, id);
|
|
@@ -5354,61 +4508,7 @@ export class EditorViewport {
|
|
|
5354
4508
|
return false;
|
|
5355
4509
|
}
|
|
5356
4510
|
|
|
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
|
-
*/
|
|
4511
|
+
/** 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
4512
|
private _applyGizmoSize(): void {
|
|
5413
4513
|
// The theme subscription fires SYNCHRONOUSLY on install, which is earlier
|
|
5414
4514
|
// in the constructor than the controls — the same ordering the grid's own
|
|
@@ -5428,39 +4528,11 @@ export class EditorViewport {
|
|
|
5428
4528
|
for (const controls of this._allGizmos()) controls.size = size;
|
|
5429
4529
|
}
|
|
5430
4530
|
|
|
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 {
|
|
4531
|
+
/** Keep only the positive translate and scale heads; identify the negative heads by their geometry centres rather than helper names. */
|
|
4532
|
+
private _dropNegativeAxisHandles(helper: THREE.Object3D, modes: readonly ('translate' | 'scale')[]): void {
|
|
5458
4533
|
const gizmoNode = helper.children.find(
|
|
5459
4534
|
(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;
|
|
4535
|
+
) as TransformControlsGizmo;
|
|
5464
4536
|
const axisOf: Record<string, THREE.Vector3> = {
|
|
5465
4537
|
X: new THREE.Vector3(1, 0, 0),
|
|
5466
4538
|
Y: new THREE.Vector3(0, 1, 0),
|
|
@@ -5468,23 +4540,19 @@ export class EditorViewport {
|
|
|
5468
4540
|
};
|
|
5469
4541
|
for (const mode of modes)
|
|
5470
4542
|
for (const group of [gizmoNode.gizmo[mode], gizmoNode.picker[mode]]) {
|
|
5471
|
-
if (!group) continue;
|
|
5472
4543
|
for (const child of [...group.children]) {
|
|
5473
4544
|
const axis = axisOf[child.name];
|
|
5474
4545
|
if (axis === undefined) continue;
|
|
5475
4546
|
const mesh = child as THREE.Mesh;
|
|
5476
|
-
if (!mesh.geometry) continue;
|
|
5477
4547
|
mesh.geometry.computeBoundingBox();
|
|
5478
|
-
const bounds = mesh.geometry.boundingBox
|
|
5479
|
-
if (!bounds) continue;
|
|
4548
|
+
const bounds = mesh.geometry.boundingBox!;
|
|
5480
4549
|
if (bounds.getCenter(new THREE.Vector3()).dot(axis) < 0) group.remove(child);
|
|
5481
4550
|
}
|
|
5482
4551
|
}
|
|
5483
4552
|
}
|
|
5484
4553
|
|
|
5485
4554
|
/**
|
|
5486
|
-
* THE COLOUR EACH HANDLE CARRIES, in the frame the
|
|
5487
|
-
* presenting.
|
|
4555
|
+
* THE COLOUR EACH HANDLE CARRIES, in the world frame the view presentation supplies.
|
|
5488
4556
|
*
|
|
5489
4557
|
* A handle is NAMED for three's axis and COLOURED for the source axis that
|
|
5490
4558
|
* three axis carries ({@link StageAxisFrame}) — under three's own frame the
|
|
@@ -5507,15 +4575,8 @@ export class EditorViewport {
|
|
|
5507
4575
|
};
|
|
5508
4576
|
}
|
|
5509
4577
|
|
|
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 {
|
|
4578
|
+
/** Apply the current stage axis frame and rebuild the orientation gizmo after a look change. */
|
|
4579
|
+
private _readLookStage(): void {
|
|
5519
4580
|
this._applyGizmoAxisFrame();
|
|
5520
4581
|
this._buildOrientationGizmo();
|
|
5521
4582
|
}
|
|
@@ -5523,7 +4584,7 @@ export class EditorViewport {
|
|
|
5523
4584
|
/**
|
|
5524
4585
|
* THE STAGE'S FUNCTION from its view's presentation (ARCHITECTURE.md rule 7): which axis of
|
|
5525
4586
|
* the presented world is up — what the gizmos name and orient their axes by — and what a box
|
|
5526
|
-
* drag selects.
|
|
4587
|
+
* drag selects. The look does not determine the world's coordinate system.
|
|
5527
4588
|
*/
|
|
5528
4589
|
setStageFunction(
|
|
5529
4590
|
world: { readonly upAxis: 'y' | 'z'; readonly handedness: 'right' | 'left' },
|
|
@@ -5561,8 +4622,8 @@ export class EditorViewport {
|
|
|
5561
4622
|
* colour.
|
|
5562
4623
|
*
|
|
5563
4624
|
* three authors its translate arms, its scale stalks and its plane squares
|
|
5564
|
-
* on the
|
|
5565
|
-
*
|
|
4625
|
+
* on the positive side of each axis. Negative-side heads are hidden so
|
|
4626
|
+
* the set reads as three arms rather than six. Under
|
|
5566
4627
|
* a Z-up world one of those three arms is pointing the wrong WAY: the
|
|
5567
4628
|
* source's +Y is three's −Z, and Blender draws an arm along +Y and nothing
|
|
5568
4629
|
* along −Y. So the handles of a flipped axis are MIRRORED onto the other
|
|
@@ -5598,9 +4659,8 @@ export class EditorViewport {
|
|
|
5598
4659
|
// its geometry is not mirrored — doing either would apply the permutation
|
|
5599
4660
|
// a second time.
|
|
5600
4661
|
//
|
|
5601
|
-
//
|
|
5602
|
-
//
|
|
5603
|
-
// drag where it used to be moved nothing at all.
|
|
4662
|
+
// With the permutation applied to all three families, the scale handle
|
|
4663
|
+
// that points up would be mirrored to point DOWN.
|
|
5604
4664
|
const identity: Record<string, THREE.Color> = {
|
|
5605
4665
|
X: this._gizmoAxisColor(0),
|
|
5606
4666
|
Y: this._gizmoAxisColor(1),
|
|
@@ -5609,48 +4669,43 @@ export class EditorViewport {
|
|
|
5609
4669
|
YZ: this._gizmoAxisColor(0),
|
|
5610
4670
|
XZ: this._gizmoAxisColor(1),
|
|
5611
4671
|
};
|
|
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).
|
|
4672
|
+
// Clone shared handle materials when a gizmo family needs separate colours, so changing scale colours cannot recolour translate or rotate.
|
|
5620
4673
|
const paint = (
|
|
5621
|
-
group: THREE.Object3D
|
|
4674
|
+
group: THREE.Object3D,
|
|
5622
4675
|
map: Record<string, THREE.Color>,
|
|
5623
4676
|
flip: boolean,
|
|
5624
4677
|
ownMaterial: boolean,
|
|
5625
4678
|
handles: boolean,
|
|
5626
4679
|
) => {
|
|
5627
|
-
if (!group) return;
|
|
5628
4680
|
group.traverse((child) => {
|
|
5629
|
-
const mesh = child as THREE.
|
|
4681
|
+
const mesh = child as THREE.Object3D & {
|
|
4682
|
+
geometry?: THREE.BufferGeometry;
|
|
4683
|
+
material?: (THREE.MeshBasicMaterial | THREE.LineBasicMaterial) & {
|
|
4684
|
+
_color?: THREE.Color | undefined;
|
|
4685
|
+
_opacity?: number | undefined;
|
|
4686
|
+
};
|
|
4687
|
+
};
|
|
5630
4688
|
if (flip && mesh.geometry) mesh.geometry.scale(delta[0]!, delta[1]!, delta[2]!);
|
|
5631
4689
|
if (
|
|
5632
4690
|
ownMaterial &&
|
|
5633
4691
|
mesh.material &&
|
|
5634
|
-
!Array.isArray(mesh.material) &&
|
|
5635
4692
|
!mesh.userData['volterOwnMaterial']
|
|
5636
4693
|
) {
|
|
5637
|
-
mesh.material =
|
|
4694
|
+
mesh.material = mesh.material.clone();
|
|
5638
4695
|
mesh.userData['volterOwnMaterial'] = true;
|
|
5639
4696
|
}
|
|
5640
|
-
const material = mesh.material
|
|
5641
|
-
|
|
5642
|
-
| undefined;
|
|
5643
|
-
if (!material || Array.isArray(material)) return;
|
|
4697
|
+
const material = mesh.material;
|
|
4698
|
+
if (!material) return;
|
|
5644
4699
|
if (flip) material.side = THREE.DoubleSide;
|
|
5645
4700
|
const ink = map[child.name];
|
|
5646
|
-
if (ink
|
|
4701
|
+
if (ink) {
|
|
5647
4702
|
// THE LOOK'S RESTING OPACITY over the handle's own (three draws its plane squares
|
|
5648
4703
|
// translucent); three caches it like the colour, so the cache is dropped too.
|
|
5649
4704
|
// Handles only: the drag's axis lines (the `helper` family) keep three's own.
|
|
5650
4705
|
if (handles) {
|
|
5651
4706
|
const base = (material.userData['volterBaseOpacity'] ??= material.opacity) as number;
|
|
5652
4707
|
material.opacity = base * (this._gizmoLook.opacity ?? 1);
|
|
5653
|
-
|
|
4708
|
+
material._opacity = undefined;
|
|
5654
4709
|
}
|
|
5655
4710
|
material.color.copy(ink);
|
|
5656
4711
|
// THREE CACHES A HANDLE'S RESTING COLOUR ON ITS FIRST UPDATE
|
|
@@ -5668,22 +4723,15 @@ export class EditorViewport {
|
|
|
5668
4723
|
.getHelper()
|
|
5669
4724
|
.children.find(
|
|
5670
4725
|
(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;
|
|
4726
|
+
) as TransformControlsGizmo;
|
|
5679
4727
|
for (const family of ['gizmo', 'picker', 'helper'] as const) {
|
|
5680
4728
|
// ONLY THE TRANSLATE ARMS ARE MIRRORED. A rotate RING is symmetric
|
|
5681
4729
|
// about every axis it spans, so the permutation is visible in its
|
|
5682
4730
|
// colour alone; the scale family is in the object's own frame (above).
|
|
5683
4731
|
const handles = family === 'gizmo';
|
|
5684
|
-
paint(node[family]
|
|
5685
|
-
paint(node[family]
|
|
5686
|
-
paint(node[family]
|
|
4732
|
+
paint(node[family].translate, permuted, mirrored, false, handles);
|
|
4733
|
+
paint(node[family].rotate, permuted, false, false, handles);
|
|
4734
|
+
paint(node[family].scale, identity, false, true, handles);
|
|
5687
4735
|
}
|
|
5688
4736
|
}
|
|
5689
4737
|
if (mirrored) this._appliedAxisSigns = want as readonly (1 | -1)[];
|
|
@@ -5713,17 +4761,7 @@ export class EditorViewport {
|
|
|
5713
4761
|
.getHelper()
|
|
5714
4762
|
.children.find(
|
|
5715
4763
|
(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;
|
|
4764
|
+
) as TransformControlsGizmo & Pick<TransformControls, 'axis' | 'mode' | 'enabled' | 'dragging'>;
|
|
5727
4765
|
const threeUpdate = node.updateMatrixWorld.bind(node);
|
|
5728
4766
|
node.updateMatrixWorld = (force?: boolean) => {
|
|
5729
4767
|
threeUpdate(force);
|
|
@@ -5741,7 +4779,7 @@ export class EditorViewport {
|
|
|
5741
4779
|
: null;
|
|
5742
4780
|
if (hidden !== null) {
|
|
5743
4781
|
for (const family of [node.gizmo, node.picker]) {
|
|
5744
|
-
for (const handle of family[node.mode]
|
|
4782
|
+
for (const handle of family[node.mode].children) {
|
|
5745
4783
|
if (handle.name === hidden) handle.visible = false;
|
|
5746
4784
|
}
|
|
5747
4785
|
}
|
|
@@ -5757,12 +4795,10 @@ export class EditorViewport {
|
|
|
5757
4795
|
)
|
|
5758
4796
|
return;
|
|
5759
4797
|
const fixed = node.dragging ? (look.drag ?? look.hover) : look.hover;
|
|
5760
|
-
for (const handle of node.gizmo[node.mode]
|
|
4798
|
+
for (const handle of node.gizmo[node.mode].children) {
|
|
5761
4799
|
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;
|
|
4800
|
+
const material = (handle as THREE.Mesh | THREE.Line).material as
|
|
4801
|
+
(THREE.MeshBasicMaterial | THREE.LineBasicMaterial) & { _color?: THREE.Color };
|
|
5766
4802
|
if (fixed !== null) {
|
|
5767
4803
|
material.color.setHex(fixed);
|
|
5768
4804
|
continue;
|
|
@@ -5808,19 +4844,15 @@ export class EditorViewport {
|
|
|
5808
4844
|
.getHelper()
|
|
5809
4845
|
.children.find(
|
|
5810
4846
|
(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;
|
|
4847
|
+
) as TransformControlsGizmo;
|
|
5815
4848
|
const axisIndex: Record<string, number> = { X: 0, Y: 1, Z: 2 };
|
|
5816
4849
|
const along = (index: number, factor: number): [number, number, number] =>
|
|
5817
4850
|
[0, 1, 2].map((i) => (i === index ? factor : 1)) as [number, number, number];
|
|
5818
|
-
for (const handle of node.gizmo
|
|
4851
|
+
for (const handle of node.gizmo.translate.children) {
|
|
5819
4852
|
const index = axisIndex[handle.name];
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
const
|
|
5823
|
-
const isHead = cylinder.parameters?.radiusTop === 0;
|
|
4853
|
+
if (index === undefined) continue;
|
|
4854
|
+
const geometry = (handle as THREE.Mesh<THREE.CylinderGeometry>).geometry;
|
|
4855
|
+
const isHead = geometry.parameters.radiusTop === 0;
|
|
5824
4856
|
if (!isHead) {
|
|
5825
4857
|
geometry.scale(...along(index, stretch));
|
|
5826
4858
|
continue;
|
|
@@ -5838,10 +4870,10 @@ export class EditorViewport {
|
|
|
5838
4870
|
offset[index] = base * stretch;
|
|
5839
4871
|
geometry.translate(...offset);
|
|
5840
4872
|
}
|
|
5841
|
-
for (const handle of node.picker
|
|
4873
|
+
for (const handle of node.picker.translate.children) {
|
|
5842
4874
|
const index = axisIndex[handle.name];
|
|
5843
|
-
|
|
5844
|
-
|
|
4875
|
+
if (index === undefined) continue;
|
|
4876
|
+
const geometry = (handle as THREE.Mesh<THREE.CylinderGeometry>).geometry;
|
|
5845
4877
|
geometry.scale(...along(index, tipScale));
|
|
5846
4878
|
}
|
|
5847
4879
|
this._appliedArrow = want;
|
|
@@ -5866,14 +4898,13 @@ export class EditorViewport {
|
|
|
5866
4898
|
.getHelper()
|
|
5867
4899
|
.children.find(
|
|
5868
4900
|
(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;
|
|
4901
|
+
) as TransformControlsGizmo;
|
|
4902
|
+
for (const handle of node.gizmo.rotate.children) {
|
|
5872
4903
|
// The axis rings only: the trackball circle and the view ring stay hairlines (Godot's
|
|
5873
4904
|
// grey circle is one).
|
|
5874
4905
|
if (!['X', 'Y', 'Z'].includes(handle.name)) continue;
|
|
5875
|
-
|
|
5876
|
-
const radius =
|
|
4906
|
+
const geometry = (handle as THREE.Mesh<THREE.TorusGeometry>).geometry;
|
|
4907
|
+
const radius = geometry.parameters.radius;
|
|
5877
4908
|
geometry.computeBoundingBox();
|
|
5878
4909
|
geometry.boundingBox!.getSize(size);
|
|
5879
4910
|
const normal = size.x <= size.y && size.x <= size.z ? 0 : size.y <= size.z ? 1 : 2;
|
|
@@ -5894,8 +4925,6 @@ export class EditorViewport {
|
|
|
5894
4925
|
}
|
|
5895
4926
|
|
|
5896
4927
|
private _patchGizmo(gizmoHelper: THREE.Object3D): void {
|
|
5897
|
-
const colorMap = this._axisColorMap();
|
|
5898
|
-
|
|
5899
4928
|
// Plane geometry offsets to bring inner corners to object center
|
|
5900
4929
|
const planeOffsets: Record<string, [number, number, number]> = {
|
|
5901
4930
|
XY: [-0.075, -0.075, 0],
|
|
@@ -5904,44 +4933,20 @@ export class EditorViewport {
|
|
|
5904
4933
|
};
|
|
5905
4934
|
|
|
5906
4935
|
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
4936
|
// Shift plane handle geometry
|
|
5917
4937
|
const offset = planeOffsets[child.name];
|
|
5918
|
-
if (offset
|
|
5919
|
-
|
|
4938
|
+
if (offset) {
|
|
4939
|
+
(child as THREE.Mesh).geometry.translate(offset[0], offset[1], offset[2]);
|
|
5920
4940
|
}
|
|
5921
4941
|
});
|
|
5922
4942
|
}
|
|
5923
4943
|
|
|
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
|
-
*/
|
|
4944
|
+
/** Shift auxiliary scale cubes and picker geometry clear of the combined tool's translate controls. */
|
|
5937
4945
|
private _patchCombinedScaleGizmo(helper: THREE.Object3D): void {
|
|
5938
4946
|
const AXIS_OFFSET = 0.55;
|
|
5939
4947
|
const gizmoNode = helper.children.find(
|
|
5940
4948
|
(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;
|
|
4949
|
+
) as TransformControlsGizmo;
|
|
5945
4950
|
const axisOf: Record<string, THREE.Vector3> = {
|
|
5946
4951
|
X: new THREE.Vector3(1, 0, 0),
|
|
5947
4952
|
Y: new THREE.Vector3(0, 1, 0),
|
|
@@ -5953,24 +4958,22 @@ export class EditorViewport {
|
|
|
5953
4958
|
const CUBE_CENTER = 0.54 + AXIS_OFFSET;
|
|
5954
4959
|
const PICKER_SIZE = 0.3;
|
|
5955
4960
|
for (const [group, isPicker] of [
|
|
5956
|
-
[gizmoNode.gizmo
|
|
5957
|
-
[gizmoNode.picker
|
|
4961
|
+
[gizmoNode.gizmo.scale, false],
|
|
4962
|
+
[gizmoNode.picker.scale, true],
|
|
5958
4963
|
] as const) {
|
|
5959
|
-
if (!group) continue;
|
|
5960
4964
|
for (const child of [...group.children]) {
|
|
5961
|
-
const mesh = child as THREE.Mesh
|
|
4965
|
+
const mesh = child as THREE.Mesh<THREE.BoxGeometry | THREE.CylinderGeometry>;
|
|
5962
4966
|
const axis = axisOf[child.name];
|
|
5963
4967
|
const isAxisLine =
|
|
5964
4968
|
axis !== undefined &&
|
|
5965
|
-
mesh.geometry
|
|
4969
|
+
mesh.geometry.type === 'CylinderGeometry' &&
|
|
5966
4970
|
(mesh.geometry as THREE.CylinderGeometry).parameters.radiusTop < 0.05;
|
|
5967
4971
|
if (axis === undefined || isAxisLine) {
|
|
5968
4972
|
group.remove(child);
|
|
5969
4973
|
continue;
|
|
5970
4974
|
}
|
|
5971
4975
|
mesh.geometry.computeBoundingBox();
|
|
5972
|
-
const bounds = mesh.geometry.boundingBox
|
|
5973
|
-
if (!bounds) continue;
|
|
4976
|
+
const bounds = mesh.geometry.boundingBox!;
|
|
5974
4977
|
const sign = Math.sign(bounds.getCenter(new THREE.Vector3()).dot(axis)) || 1;
|
|
5975
4978
|
if (isPicker) {
|
|
5976
4979
|
const at = axis.clone().multiplyScalar(sign * CUBE_CENTER);
|
|
@@ -6005,8 +5008,6 @@ export class EditorViewport {
|
|
|
6005
5008
|
private static _FLY_KEYS = new Set(['w', 'a', 's', 'd', 'q', 'e']);
|
|
6006
5009
|
|
|
6007
5010
|
private _enterFlyMode(): void {
|
|
6008
|
-
if (this._flyActive) return;
|
|
6009
|
-
if (this._cameraViewMode === 'view') return;
|
|
6010
5011
|
this._flyActive = true;
|
|
6011
5012
|
this._snapAnimating = false; // cancel any view snap
|
|
6012
5013
|
this.orbitControls.enabled = false;
|
|
@@ -6017,10 +5018,9 @@ export class EditorViewport {
|
|
|
6017
5018
|
}
|
|
6018
5019
|
|
|
6019
5020
|
private _exitFlyMode(): void {
|
|
6020
|
-
if (!this._flyActive) return;
|
|
6021
5021
|
this._flyActive = false;
|
|
6022
5022
|
this._flyKeys.clear();
|
|
6023
|
-
this.orbitControls.enabled =
|
|
5023
|
+
this.orbitControls.enabled = true;
|
|
6024
5024
|
document.exitPointerLock();
|
|
6025
5025
|
// Sync orbit target to a point in front of the camera
|
|
6026
5026
|
const camera = this.renderCamera;
|
|
@@ -6078,10 +5078,6 @@ export class EditorViewport {
|
|
|
6078
5078
|
|
|
6079
5079
|
private _onFlyKeyUp = (e: KeyboardEvent): void => {
|
|
6080
5080
|
const key = e.key.toLowerCase();
|
|
6081
|
-
if (key === 'shift') {
|
|
6082
|
-
this._flyKeys.delete('shift');
|
|
6083
|
-
return;
|
|
6084
|
-
}
|
|
6085
5081
|
this._flyKeys.delete(key);
|
|
6086
5082
|
};
|
|
6087
5083
|
|
|
@@ -6120,11 +5116,8 @@ export class EditorViewport {
|
|
|
6120
5116
|
const distance = this.camera.position.distanceTo(this.orbitControls.target);
|
|
6121
5117
|
const worldPerPixel =
|
|
6122
5118
|
(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.
|
|
5119
|
+
// Clamp mouse-wheel pan events to a modest step. Fine trackpad deltas stay below the clamp and
|
|
5120
|
+
// preserve their scale.
|
|
6128
5121
|
const unit = e.deltaMode === 1 ? 16 : 1;
|
|
6129
5122
|
const clampStep = (value: number): number =>
|
|
6130
5123
|
Math.sign(value) * Math.min(Math.abs(value * unit), 30);
|
|
@@ -6156,8 +5149,9 @@ export class EditorViewport {
|
|
|
6156
5149
|
if (e.button === 0 && !e.altKey) {
|
|
6157
5150
|
if (this._selectConstraintControlAt(e)) return;
|
|
6158
5151
|
const mesh = this._raycastSpatialHandle(e);
|
|
6159
|
-
|
|
6160
|
-
|
|
5152
|
+
if (!mesh) return;
|
|
5153
|
+
const binding = spatialHandleBinding(mesh);
|
|
5154
|
+
if (binding.handle.writable) {
|
|
6161
5155
|
e.stopPropagation();
|
|
6162
5156
|
this._activeSpatialHandle = binding;
|
|
6163
5157
|
this._spatialHandleDragPoint.copy(mesh.position);
|
|
@@ -6203,21 +5197,14 @@ export class EditorViewport {
|
|
|
6203
5197
|
|
|
6204
5198
|
if (!this._marqueeDiv) {
|
|
6205
5199
|
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.
|
|
5200
|
+
// Use the sticky chrome tier above the editor root and below dropdowns.
|
|
6210
5201
|
Object.assign(this._marqueeDiv.style, {
|
|
6211
5202
|
position: 'fixed',
|
|
6212
5203
|
pointerEvents: 'none',
|
|
6213
5204
|
zIndex: zIndex.sticky,
|
|
6214
5205
|
});
|
|
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.
|
|
5206
|
+
// Mount inside the theme scope so --volter-* colors resolve. Fixed positioning keeps the
|
|
5207
|
+
// client-coordinate math.
|
|
6221
5208
|
(document.getElementById('editor-chrome-root') ?? document.body).appendChild(
|
|
6222
5209
|
this._marqueeDiv,
|
|
6223
5210
|
);
|
|
@@ -6228,12 +5215,12 @@ export class EditorViewport {
|
|
|
6228
5215
|
Object.assign(this._marqueeDiv.style, marqueeStyle(nativeViewportMarqueeDash(this._canvas)));
|
|
6229
5216
|
}
|
|
6230
5217
|
|
|
6231
|
-
if (this._marqueeActive
|
|
5218
|
+
if (this._marqueeActive) {
|
|
6232
5219
|
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
6233
5220
|
const top = Math.min(this._pointerDownPos.y, e.clientY);
|
|
6234
5221
|
const w = Math.abs(e.clientX - this._pointerDownPos.x);
|
|
6235
5222
|
const h = Math.abs(e.clientY - this._pointerDownPos.y);
|
|
6236
|
-
Object.assign(this._marqueeDiv
|
|
5223
|
+
Object.assign(this._marqueeDiv!.style, {
|
|
6237
5224
|
left: `${left}px`,
|
|
6238
5225
|
top: `${top}px`,
|
|
6239
5226
|
width: `${w}px`,
|
|
@@ -6243,6 +5230,14 @@ export class EditorViewport {
|
|
|
6243
5230
|
}
|
|
6244
5231
|
};
|
|
6245
5232
|
|
|
5233
|
+
/** A drag lost its release: a spatial handle goes back uncommitted, a constraint-control
|
|
5234
|
+
* session closes, and the pointer session with them. */
|
|
5235
|
+
private _onInteractionDragLost = (): void => {
|
|
5236
|
+
if (this._activeSpatialHandle) this._finishSpatialHandleDrag(false);
|
|
5237
|
+
this._constraintControlPointerSession = false;
|
|
5238
|
+
this._pointerSessionActive = false;
|
|
5239
|
+
};
|
|
5240
|
+
|
|
6246
5241
|
private _onPointerUp = (e: PointerEvent): void => {
|
|
6247
5242
|
// Only complete gestures that STARTED on the canvas (`_onPointerDown`) —
|
|
6248
5243
|
// this listener lives on the interaction element (see the binding comment
|
|
@@ -6260,18 +5255,12 @@ export class EditorViewport {
|
|
|
6260
5255
|
return;
|
|
6261
5256
|
}
|
|
6262
5257
|
|
|
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.
|
|
5258
|
+
// Marquee scans this viewport's native object map. Other composite roots are outside
|
|
5259
|
+
// its screen-projection selection; single-click picking below can cross those roots.
|
|
6269
5260
|
if (this._marqueeActive) {
|
|
6270
5261
|
this._marqueeActive = false;
|
|
6271
5262
|
this.orbitControls.enabled = true;
|
|
6272
|
-
|
|
6273
|
-
this._marqueeDiv.style.display = 'none';
|
|
6274
|
-
}
|
|
5263
|
+
this._marqueeDiv!.style.display = 'none';
|
|
6275
5264
|
|
|
6276
5265
|
// Find all entities whose screen projection falls within the marquee
|
|
6277
5266
|
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
@@ -6283,33 +5272,12 @@ export class EditorViewport {
|
|
|
6283
5272
|
const corner = new THREE.Vector3();
|
|
6284
5273
|
const bounds = new THREE.Box3();
|
|
6285
5274
|
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.
|
|
5275
|
+
// Skip objects whose own visibility is off.
|
|
6295
5276
|
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.
|
|
5277
|
+
// Exclude cameras from marquee selection; click and hierarchy selection still reach them.
|
|
6303
5278
|
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.
|
|
5279
|
+
// Test projected geometry bounds against the rectangle using the presentation’s selection rule.
|
|
5280
|
+
// Objects with no renderable bounds use their projected origin.
|
|
6313
5281
|
bounds.setFromObject(obj);
|
|
6314
5282
|
if (bounds.isEmpty()) {
|
|
6315
5283
|
const screen = this._projectToScreen(obj);
|
|
@@ -6318,21 +5286,7 @@ export class EditorViewport {
|
|
|
6318
5286
|
}
|
|
6319
5287
|
continue;
|
|
6320
5288
|
}
|
|
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.
|
|
5289
|
+
// 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
5290
|
const touch = this._stageBoxSelect === 'touch';
|
|
6337
5291
|
const rect = this._canvas.getBoundingClientRect();
|
|
6338
5292
|
// Through the camera on screen: an orthographic document view or a camera view draws with
|
|
@@ -6369,8 +5323,7 @@ export class EditorViewport {
|
|
|
6369
5323
|
}
|
|
6370
5324
|
|
|
6371
5325
|
if (e.shiftKey) {
|
|
6372
|
-
//
|
|
6373
|
-
// extension is Phase-F; first-party store op for now).
|
|
5326
|
+
// Extend the store selection for the additive modifier.
|
|
6374
5327
|
for (const id of hitIds) this._store.shell.addToSelection(id);
|
|
6375
5328
|
} else {
|
|
6376
5329
|
setAuthoringSelection(this._authoring(), hitIds);
|
|
@@ -6389,63 +5342,26 @@ export class EditorViewport {
|
|
|
6389
5342
|
// Don't interfere with gizmo clicks
|
|
6390
5343
|
if (this._anyGizmoDragging()) return;
|
|
6391
5344
|
|
|
6392
|
-
// Check orientation gizmo
|
|
6393
|
-
// without a visible three surface, so it can't be clicked either)
|
|
5345
|
+
// Check the orientation gizmo first; it is clickable only on a visible three surface.
|
|
6394
5346
|
if (this._threejsToolContextActive && this._handleViewCubeClick(e)) return;
|
|
6395
5347
|
|
|
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.
|
|
5348
|
+
// Try the active adapter’s pickable layers from topmost to bottommost. Interactive DOM layers
|
|
5349
|
+
// intercept their own pointer events before they reach this canvas.
|
|
6404
5350
|
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`).
|
|
5351
|
+
// A click outside the open scope leaves it; empty space closes every level. pickAcrossScopeExit
|
|
5352
|
+
// updates the scope stack and resolves the hit again when the stack changes.
|
|
6411
5353
|
const entityId = pickAcrossScopeExit(this._scopeAuthoring(), () =>
|
|
6412
5354
|
this._pickAt(e.clientX, e.clientY, deepSelection ? 'deep' : 'normal'),
|
|
6413
5355
|
);
|
|
6414
5356
|
|
|
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) {
|
|
5357
|
+
if (!deepSelection && entityId && (e.shiftKey || e.metaKey || e.ctrlKey)) {
|
|
6428
5358
|
this._store.shell.toggleSelection(entityId);
|
|
6429
5359
|
} else {
|
|
6430
5360
|
setAuthoringSelection(this._authoring(), entityId ? [entityId] : []);
|
|
6431
5361
|
}
|
|
6432
5362
|
};
|
|
6433
5363
|
|
|
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
|
-
*/
|
|
5364
|
+
/** Key selection scopes to the same panel adapter used by the Hierarchy. An injected document adapter keeps its own scope. */
|
|
6449
5365
|
private _scopeAuthoring(): AuthoringAdapter {
|
|
6450
5366
|
return this._standaloneAuthoring
|
|
6451
5367
|
? this._authoring()
|
|
@@ -6461,25 +5377,10 @@ export class EditorViewport {
|
|
|
6461
5377
|
}
|
|
6462
5378
|
|
|
6463
5379
|
/**
|
|
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.
|
|
5380
|
+
* Double-click opens the selected component’s scope and selects the next member down its owner
|
|
5381
|
+
* chain. Escape walks out and an outside click leaves the scope. The ordinary layered pick
|
|
5382
|
+
* resolves through the adapter with currentSelectionScopeId, including the two single-clicks
|
|
5383
|
+
* preceding this gesture.
|
|
6483
5384
|
*/
|
|
6484
5385
|
private _onDoubleClick = (e: MouseEvent): void => {
|
|
6485
5386
|
if (e.button !== 0) return;
|
|
@@ -6498,181 +5399,4 @@ export class EditorViewport {
|
|
|
6498
5399
|
this._pickAt(e.clientX, e.clientY, 'normal'),
|
|
6499
5400
|
);
|
|
6500
5401
|
};
|
|
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
5402
|
}
|