@volter/editor-threejs 0.5.210 → 0.5.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +476 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/kit/viewport-surface-status.tsx +44 -0
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The lazy boundary for a stage's overlays. The host names them; only a stage
|
|
3
3
|
* that actually has a shell above it (and therefore something for them to
|
|
4
|
-
* read) evaluates the module. See `stage-overlay-set.tsx` for what they are
|
|
5
|
-
* and `world-root-stage.ts`'s header for why the condition gates the LOAD.
|
|
4
|
+
* read) evaluates the module. See `stage-overlay-set.tsx` for what they are.
|
|
6
5
|
*/
|
|
7
6
|
import { lazy, Suspense } from 'react';
|
|
8
7
|
import type { StageOverlaySetProps } from './stage-overlay-set';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { themeVars } from '@volter/sdk/widgets';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { useEditorStats } from '@volter/sdk/
|
|
4
|
-
import { latestTabCensus } from '@volter/sdk/
|
|
3
|
+
import { useEditorStats } from '@volter/sdk/editor-runtime';
|
|
4
|
+
import { latestTabCensus } from '@volter/sdk/tab-census';
|
|
5
5
|
|
|
6
6
|
export function StatsOverlay() {
|
|
7
7
|
const stats = useEditorStats();
|
|
@@ -19,26 +19,14 @@ export function StatsOverlay() {
|
|
|
19
19
|
}, []);
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
//
|
|
23
|
-
// value (`rgba(0,0,0,0.7)`, flat black, no border) directly stacked
|
|
24
|
-
// above-right of ViewportOverlay's cluster and CameraInfo's pill in the
|
|
25
|
-
// same corner. Given the same border/radius treatment as its siblings so
|
|
26
|
-
// the whole corner reads as one family.
|
|
27
|
-
// P6-U6 (owner taste decision 3: HUD overlays → real islands): the paint
|
|
28
|
-
// moved to `.volter-hud-overlay` so islands chrome can promote it to the
|
|
29
|
-
// shared glass-island material (compact tier); bars chrome paints the
|
|
30
|
-
// exact pre-U6 overlay-whisper recipe from the same class.
|
|
22
|
+
// Use the shared HUD overlay treatment for this statistics readout.
|
|
31
23
|
<div
|
|
32
|
-
className="volter-hud-overlay volter-chrome-island
|
|
24
|
+
className="volter-hud-overlay volter-chrome-island"
|
|
33
25
|
data-island-scale="compact"
|
|
34
26
|
data-hud-border="strong"
|
|
35
27
|
style={{
|
|
36
28
|
position: 'absolute',
|
|
37
|
-
//
|
|
38
|
-
// The var carries the glass breathing band (54px islands / 102px the
|
|
39
|
-
// floating header-island clearance, scoped in theme.css); the bare
|
|
40
|
-
// fallback is the pre-glass 48px so treatment-less chrome ("bars":
|
|
41
|
-
// graphite/classic/lite/reduced) stays pixel-identical to main.
|
|
29
|
+
// Place the second HUD tier below the viewport toolbar using the look’s clearance variable.
|
|
42
30
|
top: 'var(--volter-viewport-overlay-hud-top, 48px)',
|
|
43
31
|
right: 8,
|
|
44
32
|
padding: '6px 10px',
|
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* axis gizmo at the top-right — zoom, pan, frame, projection). Drawn by the
|
|
6
|
-
* 3D document host (`StageHost`) over its stage for every
|
|
7
|
-
* document with chrome; nothing here holds state of its own. Hidden while the
|
|
8
|
-
* host has no viewport yet.
|
|
9
|
-
*
|
|
10
|
-
* WHICH OBJECT A BUTTON DRIVES IS THE QUESTION TO ASK BEFORE ADDING ONE.
|
|
11
|
-
* `StageHost` is this component's only caller and every stage it hosts is
|
|
12
|
-
* painted by `Object3DDocumentSession.renderViewport` — through
|
|
13
|
-
* `session.camera()`, not through `viewport.renderCamera`. So a verb whose
|
|
14
|
-
* effect is the CAMERA'S POSE reaches the picture through the viewport (the
|
|
15
|
-
* session's cameras are derived from that pose every frame), while a verb
|
|
16
|
-
* that changes WHICH camera paints must go to the session. The projection
|
|
17
|
-
* button got that wrong and was inert everywhere it was drawn; see its own
|
|
18
|
-
* comment below for the measurement, and do not route a new control to
|
|
19
|
-
* `viewport.*` without checking which side of that line it falls on.
|
|
2
|
+
* Shared viewport furniture for chromed 3D stages: view text, axis navigation, zoom, pan, framing
|
|
3
|
+
* and projection controls. StageHost owns the state. Camera-pose controls drive the viewport;
|
|
4
|
+
* projection controls drive the document session when present and the viewport otherwise.
|
|
20
5
|
*/
|
|
21
6
|
import type { ToolCameraView, ToolViewportStatistic } from '../../object3d-contributions';
|
|
22
7
|
import {
|
|
@@ -31,7 +16,7 @@ import { Fragment, type PointerEvent as ReactPointerEvent, useCallback, useEffec
|
|
|
31
16
|
import * as THREE from 'three';
|
|
32
17
|
import { axisViewName } from '../asset-workflow/model-inspection';
|
|
33
18
|
import type { Object3DDocumentSession } from '../authoring/object3d-document-session';
|
|
34
|
-
import type { ShellStore } from '@volter/sdk/
|
|
19
|
+
import type { ShellStore } from '@volter/sdk/shell-store';
|
|
35
20
|
import {
|
|
36
21
|
COMPASS_CENTER_RIGHT_PX,
|
|
37
22
|
COMPASS_CLUSTER_TOP_PX,
|
|
@@ -44,19 +29,17 @@ import {
|
|
|
44
29
|
subscribeViewportPresentation,
|
|
45
30
|
viewPresentation,
|
|
46
31
|
viewportPresentationVersion,
|
|
47
|
-
} from '@volter/sdk/
|
|
32
|
+
} from '@volter/sdk/viewport-presentation';
|
|
48
33
|
import { ViewportViewMenu } from './ViewportViewMenu';
|
|
49
34
|
import {
|
|
50
35
|
lookDeclaresViewportColors,
|
|
51
36
|
lookPaintsLightViewport,
|
|
52
37
|
subscribeNativeSelectionTheme,
|
|
53
38
|
useViewportChrome,
|
|
54
|
-
} from '@volter/sdk/
|
|
55
|
-
import type { ThreeViewportProjection } from '
|
|
39
|
+
} from '@volter/sdk/native-selection-style';
|
|
40
|
+
import type { ThreeViewportProjection } from '../three-viewport-presentation';
|
|
56
41
|
|
|
57
|
-
/**
|
|
58
|
-
* compass's own box (`editor-viewport.ts`, measured against Blender's
|
|
59
|
-
* navigation gizmo), never restated here. */
|
|
42
|
+
/** Position the navigation cluster relative to the compass’s shared box. */
|
|
60
43
|
const CLUSTER_TOP = COMPASS_CLUSTER_TOP_PX;
|
|
61
44
|
|
|
62
45
|
/** Module-level for `useSyncExternalStore`'s stable identity; the theme root
|
|
@@ -71,27 +54,7 @@ function subscribeThemeViewportGroup(onChange: () => void): () => void {
|
|
|
71
54
|
const NO_SESSION_SUBSCRIBE = (): (() => void) => () => {};
|
|
72
55
|
const NO_SESSION_SNAPSHOT = (): number => 0;
|
|
73
56
|
|
|
74
|
-
/**
|
|
75
|
-
* THE STATISTICS BLOCK'S GEOMETRY, measured on `sculpting.png` — the ONE
|
|
76
|
-
* reference frame that photographs the overlay's counts (every other frame
|
|
77
|
-
* has the Statistics overlay off; `modeling-front-ortho.png`'s third line is
|
|
78
|
-
* the ortho grid's unit, not a count). Device px at native 2x, CSS halved:
|
|
79
|
-
*
|
|
80
|
-
* `User Perspective` ink y 181..201, x 138..306 (baseline ~196.5)
|
|
81
|
-
* `(1) Cube | Cube` ink y 211..237, x 139..300 (baseline ~231.5)
|
|
82
|
-
* `Vertices` `8` ink y 273..288, label x 136..216, value x 252..262
|
|
83
|
-
* `Faces` `6` ink y 308..323, label x 138..194, value x 252..261
|
|
84
|
-
*
|
|
85
|
-
* Three facts that become the two constants below. The line step INSIDE a
|
|
86
|
-
* block is 35 device (17.5 CSS) — the same step the two identity lines use.
|
|
87
|
-
* The step from the subject line to `Vertices` is 56.5 device, so the stats
|
|
88
|
-
* block stands one extra 21.5 device (10.75 CSS) off the lines above it;
|
|
89
|
-
* `space-5` is 10, the nearest rung, and the scale has nothing at 11.
|
|
90
|
-
* And the VALUE is a COLUMN, not a space: both rows start their number at
|
|
91
|
-
* exactly x 252 device while their labels end at 216 and 194, so the label
|
|
92
|
-
* cell is a fixed 115 device / 57.5 CSS measured from the block's own left
|
|
93
|
-
* edge (x 137) — 58 at the rounding the rest of this file uses.
|
|
94
|
-
*/
|
|
57
|
+
/** Statistics use the identity text’s line step, a space-5 gap and a fixed 58 CSS-pixel value column. */
|
|
95
58
|
const STATISTICS_BLOCK_OFFSET = 5 as const;
|
|
96
59
|
/** See {@link STATISTICS_BLOCK_OFFSET}: Blender's value column, CSS px from
|
|
97
60
|
* the overlay block's left edge. */
|
|
@@ -154,7 +117,7 @@ export function ViewportFurniture({
|
|
|
154
117
|
subject: documentSubject,
|
|
155
118
|
gridScale,
|
|
156
119
|
}: ViewportFurnitureProps) {
|
|
157
|
-
useSyncExternalStore(store.subscribe, store.getShellSnapshot
|
|
120
|
+
useSyncExternalStore(store.subscribe, store.getShellSnapshot);
|
|
158
121
|
// THE LINES FOLLOW THE CAMERA: the view text names the axis the view looks down and the grid
|
|
159
122
|
// line names the step at the current zoom, and neither is a store or session change.
|
|
160
123
|
useSyncExternalStore(
|
|
@@ -168,37 +131,13 @@ export function ViewportFurniture({
|
|
|
168
131
|
),
|
|
169
132
|
() => (viewport ? cameraSignature(viewport, session) : ''),
|
|
170
133
|
);
|
|
171
|
-
//
|
|
172
|
-
// with. A document session carries its own (`session.camera()` returns its
|
|
173
|
-
// orthographic camera off it), and the viewport's flag says nothing about
|
|
174
|
-
// it — so the text read "User Perspective" over an orthographic stage the
|
|
175
|
-
// moment the document toolbar's own projection menu was used.
|
|
134
|
+
// Read projection from the document session’s drawing camera so the view text matches the stage.
|
|
176
135
|
useSyncExternalStore(
|
|
177
136
|
session?.subscribe ?? NO_SESSION_SUBSCRIBE,
|
|
178
137
|
session?.getSnapshot ?? NO_SESSION_SNAPSHOT,
|
|
179
138
|
session?.getSnapshot ?? NO_SESSION_SNAPSHOT,
|
|
180
139
|
);
|
|
181
|
-
//
|
|
182
|
-
// place Blender goes to pure white: measured on `modeling-edit-none.png` at
|
|
183
|
-
// native 2x, both overlay lines plateau at 255 (240 px at 255 against 296 at
|
|
184
|
-
// its AA neighbour 253) where its panel text inks 229. Ours drew
|
|
185
|
-
// `content.primary` — one step back, over the busiest backdrop in the
|
|
186
|
-
// window.
|
|
187
|
-
//
|
|
188
|
-
// `content.onAccent` IS THE MEMBER THAT HOLDS 255, and its NAME is
|
|
189
|
-
// misleading for this role: Blender spends that ink mostly NOT on an accent.
|
|
190
|
-
// DO NOT RENAME IT — it is a pre-v3 core key in all three palette lists and
|
|
191
|
-
// every shipped palette document names it, so a rename breaks every skin on
|
|
192
|
-
// disk to make a docblock read better. This comment is the correction.
|
|
193
|
-
//
|
|
194
|
-
// AND IT IS GATED, because the member's real role is "ink that must read
|
|
195
|
-
// over a fill the PALETTE chose". Measured: Classic's `content.onAccent` is
|
|
196
|
-
// #101820 — near-black, correct against its light accent chip and ruinous
|
|
197
|
-
// over a mid-grey viewport. The gate is the predicate that already decides
|
|
198
|
-
// every other viewport paint question (`lookDeclaresViewportColors`, the
|
|
199
|
-
// same one the grid's distance fade reads): a look that declares what the
|
|
200
|
-
// viewport is painted has answered for both values, and a look that leaves
|
|
201
|
-
// the backdrop to the editor keeps the ink it had. Classic is bit-identical.
|
|
140
|
+
// Use onAccent viewport ink only when the look declares viewport colours; otherwise retain the palette’s ordinary text colour.
|
|
202
141
|
const lookPaintsViewport = useSyncExternalStore(
|
|
203
142
|
subscribeThemeViewportGroup,
|
|
204
143
|
lookDeclaresViewportColors,
|
|
@@ -215,10 +154,7 @@ export function ViewportFurniture({
|
|
|
215
154
|
useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion, viewportPresentationVersion);
|
|
216
155
|
const navigationControls = viewPresentation(documentId).overlays.navigationControls;
|
|
217
156
|
if (!viewport) return null;
|
|
218
|
-
//
|
|
219
|
-
// "Front Orthographic" on numpad 1 (`modeling-front-ortho.png`), "User
|
|
220
|
-
// Perspective" the moment the view is orbited off that axis. Derived from
|
|
221
|
-
// the live camera, so it reverts on the first drag the way Blender's does.
|
|
157
|
+
// Derive the view's direction and projection labels from its live camera.
|
|
222
158
|
const axis =
|
|
223
159
|
axisViewName(
|
|
224
160
|
viewport.camera.position.clone().sub(viewport.orbitControls.target),
|
|
@@ -231,39 +167,7 @@ export function ViewportFurniture({
|
|
|
231
167
|
const viewText = through
|
|
232
168
|
? `Camera ${through.projection === 'perspective' ? 'Perspective' : 'Orthographic'}`
|
|
233
169
|
: `${axis} ${drawn === 'perspective' ? 'Perspective' : 'Orthographic'}`;
|
|
234
|
-
//
|
|
235
|
-
// object>` — THREE parts, and which part is which was settled by CONTRAST
|
|
236
|
-
// across the frames, never from one of them. `modeling-object-none.png`,
|
|
237
|
-
// `modeling-object-selected.png` and `layout.png` all read
|
|
238
|
-
// `(1) Collection | Cube`; `sculpting.png` reads `(1) Cube | Cube` only
|
|
239
|
-
// because THAT file's collection is itself named Cube. So the leading part
|
|
240
|
-
// is a CONTAINER and the trailing part is the ACTIVE OBJECT — neither is
|
|
241
|
-
// the mesh datablock, which Blender never puts on this line at all.
|
|
242
|
-
//
|
|
243
|
-
// We have one of Blender's three. There is no frame (no timeline) and no
|
|
244
|
-
// collection; the DOCUMENT is the only container an object on this stage
|
|
245
|
-
// belongs to, so it takes the leading slot — a world-root stage names a
|
|
246
|
-
// scene there and the selection names an object inside it.
|
|
247
|
-
//
|
|
248
|
-
// AND THE CONTAINER SLOT IS DROPPED WHEN IT IS NOT A CONTAINER. A Model
|
|
249
|
-
// document names its own subject, so both slots resolve to ONE string from
|
|
250
|
-
// ONE
|
|
251
|
-
// source and the overlay rendered `cube | cube` — one entity printed
|
|
252
|
-
// twice, which is not Blender's pair. Measured live on a cold models build
|
|
253
|
-
// before this change. Blender would happily print two identical words
|
|
254
|
-
// there (sculpting.png does); ours would be printing one thing twice, so
|
|
255
|
-
// it says that one thing once.
|
|
256
|
-
//
|
|
257
|
-
// THE TRAILING PART IS THE ACTIVE OBJECT, not the oldest selected one.
|
|
258
|
-
// `store.selectedEntityId` is the store's own active id — `[...selection]
|
|
259
|
-
// .at(-1)`, what the transform gizmo binds to and what the Outliner marks
|
|
260
|
-
// as its active row. This read `[...selectedEntityIds][0]`, the OLDEST id,
|
|
261
|
-
// which is the same defect `inspector-selection.ts`'s docblock records
|
|
262
|
-
// fixing for the inspector on 2026-09-18 (click A, ctrl-click B, and the
|
|
263
|
-
// gizmo moved to B while the inspector kept showing A). Back-applied here,
|
|
264
|
-
// the third reader of that selection. A Model document holds one entity,
|
|
265
|
-
// so this half is not visible in its frame; it shows on any stage where
|
|
266
|
-
// two objects can be selected at once.
|
|
170
|
+
// Show the document container and active object, omitting a duplicate container when the document already names the subject. Use selectedEntityId for the active object.
|
|
267
171
|
const activeId = store.selectedEntityId;
|
|
268
172
|
const activeName = (activeId === null ? null : objectName(activeId)) || null;
|
|
269
173
|
const subject =
|
|
@@ -298,8 +202,8 @@ export function ViewportFurniture({
|
|
|
298
202
|
const move = (moveEvent: PointerEvent): void => {
|
|
299
203
|
const lenNew = 5 + moveEvent.clientY - regionTop;
|
|
300
204
|
const factor = Math.max(0.01, 2 * (lenNew / lenOld - 1) + 1);
|
|
301
|
-
if (frameZoom0 !== null && session
|
|
302
|
-
session
|
|
205
|
+
if (frameZoom0 !== null && session!.cameraView()) {
|
|
206
|
+
session!.setCameraViewZoom(frameZoom0 / factor);
|
|
303
207
|
} else if (ortho) {
|
|
304
208
|
ortho.zoom = zoom0 / factor;
|
|
305
209
|
ortho.updateProjectionMatrix();
|
|
@@ -431,8 +335,7 @@ export function ViewportFurniture({
|
|
|
431
335
|
</div>
|
|
432
336
|
) : null}
|
|
433
337
|
{chrome.viewName === 'gizmo' ? (
|
|
434
|
-
//
|
|
435
|
-
// its name, and a click toggles the projection, as Unity's does.
|
|
338
|
+
// The projection label below the scene gizmo also toggles the projection.
|
|
436
339
|
<button
|
|
437
340
|
type="button"
|
|
438
341
|
data-testid="viewport-view-name"
|
|
@@ -465,13 +368,8 @@ export function ViewportFurniture({
|
|
|
465
368
|
{stageViewName(viewport, drawn, 'short')}
|
|
466
369
|
</button>
|
|
467
370
|
) : null}
|
|
468
|
-
{/*
|
|
469
|
-
|
|
470
|
-
gap, pitch and capsule width are Blender's (`modeling-edit-none.png`: glyph boxes 16
|
|
471
|
-
CSS, pitch 30, a 28 CSS capsule). Framing is not here: its home is the document
|
|
472
|
-
header's view control and the Home and numpad-period keys, as Blender's is its View
|
|
473
|
-
menu. The Camera button is not drawn yet: looking through a scene camera is not a
|
|
474
|
-
view this stage has. */}
|
|
371
|
+
{/* Navigation offers zoom and pan drags, a supplied camera view, and a projection
|
|
372
|
+
toggle. Framing belongs to the document header and Home/numpad-period keys. */}
|
|
475
373
|
{navigationControls && store.showGizmos ? (
|
|
476
374
|
<div
|
|
477
375
|
data-testid="viewport-navigation"
|
|
@@ -482,16 +380,7 @@ export function ViewportFurniture({
|
|
|
482
380
|
style={{
|
|
483
381
|
position: 'absolute',
|
|
484
382
|
top: CLUSTER_TOP,
|
|
485
|
-
// 12
|
|
486
|
-
// read, and this panel's rect runs ~3 CSS past the last pixel the
|
|
487
|
-
// viewport paints (the dock's sash). At 8 the gap measured 5.0 CSS
|
|
488
|
-
// against Blender's 9.0 (`modeling-edit-none.png`: the capsule's
|
|
489
|
-
// last column is device 2817 and the area's dark boundary starts at
|
|
490
|
-
// 2836); at 12 it is 9.0.
|
|
491
|
-
// UNGATED, like every other number this cluster carries: its 28 and
|
|
492
|
-
// 16 are Blender's for every skin already (`theme.css`'s
|
|
493
|
-
// `.volter-viewport-navigation` block, no palette selector), and the
|
|
494
|
-
// estate's `css-style-identity-selector` gate forbids one anyway.
|
|
383
|
+
// Offset the cluster 12 CSS pixels from the panel boundary to retain the visible gap beyond the sash.
|
|
495
384
|
right: 'var(--volter-space-6)',
|
|
496
385
|
display: 'flex',
|
|
497
386
|
flexDirection: 'column',
|
|
@@ -514,23 +403,7 @@ export function ViewportFurniture({
|
|
|
514
403
|
<EditorIcon size="2xl" icon={editorIcons.viewport.pan} />
|
|
515
404
|
</IconButton>
|
|
516
405
|
</Tooltip>
|
|
517
|
-
{/*
|
|
518
|
-
VIEWPORT'S. It drove `viewport.setProjection` and was INERT
|
|
519
|
-
wherever this cluster is drawn — `StageHost` is its only caller,
|
|
520
|
-
every stage it hosts is painted by
|
|
521
|
-
`Object3DDocumentSession.renderViewport` through `session.camera()`,
|
|
522
|
-
and that camera reads the SESSION's own `projection` (the comment
|
|
523
|
-
on the view text above already said so). Measured 2026-09-19 in a
|
|
524
|
-
cold models scaffold: the click flipped `aria-pressed` and left the
|
|
525
|
-
frame hash byte-identical, while the document header's Camera view
|
|
526
|
-
menu — `session.setProjection`, the same intent one row up —
|
|
527
|
-
changed both the picture and the view text. It was worse than
|
|
528
|
-
inert: `dolly` below branches on `viewport.renderCamera`, so while
|
|
529
|
-
this button was "pressed" Zoom in and Zoom out went dead too, and
|
|
530
|
-
flipping back nudged the camera as `_syncPerspectiveFromOrthographic`
|
|
531
|
-
copied the phantom pose home. Transcribed from the sibling door
|
|
532
|
-
(`Object3DDocumentToolbar`), with the viewport kept as the answer
|
|
533
|
-
for a stage that has no session. */}
|
|
406
|
+
{/* Toggle the document session's camera view when a camera is available. */}
|
|
534
407
|
{session?.hasCameraView() ? (
|
|
535
408
|
<Tooltip text={through ? 'Leave the camera view' : 'Look through the camera'}>
|
|
536
409
|
<IconButton size="comfortable" aria-label="Toggle the camera view" onClick={() => session.toggleCameraView()}>
|
|
@@ -541,12 +414,12 @@ export function ViewportFurniture({
|
|
|
541
414
|
) : null}
|
|
542
415
|
{/* In a camera view, the lock that makes navigating it move the camera
|
|
543
416
|
(`View3D.lock_camera`): `VIEW_LOCKED` while it holds, `VIEW_UNLOCKED` otherwise. */}
|
|
544
|
-
{through && session
|
|
545
|
-
<Tooltip text={session
|
|
546
|
-
<IconButton size="comfortable" aria-label="Lock the camera to the view" onClick={() => session
|
|
417
|
+
{through && session!.cameraViewLocked() !== null ? (
|
|
418
|
+
<Tooltip text={session!.cameraViewLocked() ? 'Unlock the camera from the view' : 'Lock the camera to the view'}>
|
|
419
|
+
<IconButton size="comfortable" aria-label="Lock the camera to the view" onClick={() => session!.toggleCameraViewLock()}>
|
|
547
420
|
<EditorIcon
|
|
548
421
|
size="2xl"
|
|
549
|
-
icon={session
|
|
422
|
+
icon={session!.cameraViewLocked() ? editorIcons.viewport.cameraLocked : editorIcons.viewport.cameraUnlocked}
|
|
550
423
|
/>
|
|
551
424
|
</IconButton>
|
|
552
425
|
</Tooltip>
|
|
@@ -575,8 +448,8 @@ export function ViewportFurniture({
|
|
|
575
448
|
)}
|
|
576
449
|
</div>
|
|
577
450
|
) : null}
|
|
578
|
-
{through
|
|
579
|
-
<CameraFrame view={through} canvas={session
|
|
451
|
+
{through ? (
|
|
452
|
+
<CameraFrame view={through} canvas={session!.renderer.domElement} locked={session!.cameraViewLocked() === true} />
|
|
580
453
|
) : null}
|
|
581
454
|
</>
|
|
582
455
|
);
|
|
@@ -2,44 +2,43 @@ import { faBorderAll, faLightbulb } from '@fortawesome/free-solid-svg-icons';
|
|
|
2
2
|
import { EditorIcon, FloatingToolbar, IconButton, Tooltip } from '@volter/sdk/widgets';
|
|
3
3
|
import type { AuthoringAdapter } from '@volter/project/adapter';
|
|
4
4
|
import { memo, useEffect, useId, useMemo, useReducer, useState, useSyncExternalStore } from 'react';
|
|
5
|
-
import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/sdk/
|
|
5
|
+
import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/sdk/authoring/active-adapter';
|
|
6
6
|
import {
|
|
7
7
|
object3DDocumentSession,
|
|
8
8
|
object3DDocumentSessionsVersion,
|
|
9
9
|
subscribeObject3DDocumentSessions,
|
|
10
10
|
} from '../authoring/object3d-document-session-registry';
|
|
11
|
-
import { resolvePanelAuthoring } from '@volter/sdk/
|
|
11
|
+
import { resolvePanelAuthoring } from '@volter/sdk/authoring/panel-authoring';
|
|
12
12
|
import type { EditorShellStore } from '../editor-shell-store';
|
|
13
|
-
import { activeLightCount } from '@volter/sdk/
|
|
14
|
-
import { CORE_WORKSPACE_UTILITIES } from '@volter/sdk/
|
|
13
|
+
import { activeLightCount } from '@volter/sdk/light-explorer-model';
|
|
14
|
+
import { CORE_WORKSPACE_UTILITIES } from '@volter/sdk/workspace-core-utilities';
|
|
15
15
|
import {
|
|
16
16
|
subscribeWorkspaceDocuments,
|
|
17
17
|
workspaceDocumentRegistryVersion,
|
|
18
|
-
} from '@volter/sdk/
|
|
19
|
-
import { showWorkspaceUtility } from '@volter/sdk/
|
|
20
|
-
import { ViewportOverlaysMenu } from '
|
|
18
|
+
} from '@volter/sdk/workspace-document-registry';
|
|
19
|
+
import { showWorkspaceUtility } from '@volter/sdk/workspace-host-commands';
|
|
20
|
+
import { ViewportOverlaysMenu } from './ViewportOverlaysMenu';
|
|
21
21
|
import {
|
|
22
22
|
setViewGridVisible,
|
|
23
23
|
subscribeViewportPresentation,
|
|
24
24
|
viewGridVisible,
|
|
25
25
|
viewportPresentationVersion,
|
|
26
|
-
} from '@volter/sdk/
|
|
26
|
+
} from '@volter/sdk/viewport-presentation';
|
|
27
27
|
import { ViewportShadingMenu } from './ViewportShadingMenu';
|
|
28
28
|
import type { ViewportShadingMode } from '@volter/editor-threejs/render/viewport-shading';
|
|
29
29
|
import { ViewportViewMenu } from './ViewportViewMenu';
|
|
30
|
-
import { useViewportChrome, useViewportWords } from '@volter/sdk/
|
|
30
|
+
import { useViewportChrome, useViewportWords } from '@volter/sdk/native-selection-style';
|
|
31
31
|
import { createPortal } from 'react-dom';
|
|
32
32
|
import { stageViewName } from './stage-view-name';
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Three-specific viewport display controls, mounted over ONE STAGE and driving
|
|
36
|
-
* THAT stage
|
|
36
|
+
* THAT stage: `store` is the stage's own
|
|
37
37
|
* store, so a prefab's grid button toggles the prefab's grid rather than the
|
|
38
|
-
* world's.
|
|
39
|
-
* expose one source-following instrument.
|
|
38
|
+
* world's.
|
|
40
39
|
*
|
|
41
40
|
* GRID is the stage's VIEW's switch (`overlays.grid.visible` in
|
|
42
|
-
*
|
|
41
|
+
* `@volter/sdk/viewport-presentation`, keyed by `documentId`): the one flag this
|
|
43
42
|
* button, the document's header, the `toggle.grid` action, `set-grid` and
|
|
44
43
|
* the editor's `status` command's `showGrid` all read and write.
|
|
45
44
|
*/
|
|
@@ -118,10 +117,7 @@ export function ViewportOverlay({
|
|
|
118
117
|
<HelpersButton store={store} word={words.helpers} />
|
|
119
118
|
<ViewportShadingMenu
|
|
120
119
|
mode={sessionShading(session) ?? store.shadingMode}
|
|
121
|
-
//
|
|
122
|
-
// menu shows: the session's `setMode` is what `set-shading-mode` drives, and writing the
|
|
123
|
-
// store alone relabelled the menu over an unchanged picture (measured under Unreal's look:
|
|
124
|
-
// "Wireframe" over the shaded box).
|
|
120
|
+
// Set the document session’s mode as well as the menu store so the rendered picture follows the choice.
|
|
125
121
|
onChange={(mode) => (session ? session.setMode(mode) : store.setShadingMode(mode))}
|
|
126
122
|
words={words.shading}
|
|
127
123
|
/>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { faCaretDown, faCheck, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
EditorIcon,
|
|
5
|
+
IconButton,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuItem,
|
|
8
|
+
MenuSeparator,
|
|
9
|
+
SplitButtonGroup,
|
|
10
|
+
Tooltip,
|
|
11
|
+
} from '@volter/sdk/widgets';
|
|
12
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
13
|
+
|
|
14
|
+
export interface ViewportOverlayChoice {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly label: string;
|
|
17
|
+
readonly enabled: boolean;
|
|
18
|
+
readonly disabled?: boolean;
|
|
19
|
+
readonly onToggle: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* BLENDER'S OVERLAY MARK, transcribed from `modeling-edit-none.png` at its
|
|
24
|
+
* native 2x (the 3D View header's Show Overlays toggle, ink x 2424..2455,
|
|
25
|
+
* y 64..91): a filled disc up and to the right, a stroked ring down and to
|
|
26
|
+
* the left, and a GAP between them where the ring passes in front — the gap
|
|
27
|
+
* is what keeps the ring readable against the disc, and without it the two
|
|
28
|
+
* merge into one blob at 14 px. The disc-minus-gap is traced as one boundary
|
|
29
|
+
* (the disc's far arc, then the gap circle's arc back) rather than knocked
|
|
30
|
+
* out, because a knockout drawn over a single fill leaks wherever it reaches
|
|
31
|
+
* past that fill's edge.
|
|
32
|
+
*
|
|
33
|
+
* Drawn here rather than through `EditorIcon`: the icon-set seam is keyed by
|
|
34
|
+
* Font Awesome name, and no name in the set carries two overlapping circles —
|
|
35
|
+
* hanging this mark on `clone` or `layer-group` would move those marks too.
|
|
36
|
+
*/
|
|
37
|
+
export function ViewportOverlaysGlyph() {
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
viewBox="0 0 16 16"
|
|
41
|
+
className="volter-viewport-overlays-glyph"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
focusable="false"
|
|
44
|
+
>
|
|
45
|
+
<path
|
|
46
|
+
d="M5.24 4.51 A5.3 5.3 0 1 1 11.89 11.16 A5.85 5.85 0 0 0 5.24 4.51 Z"
|
|
47
|
+
fill="currentColor"
|
|
48
|
+
/>
|
|
49
|
+
<circle cx="6.1" cy="10.3" r="4.4" fill="none" stroke="currentColor" strokeWidth="1.1" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ViewportOverlaysMenu({
|
|
55
|
+
label = 'Overlays',
|
|
56
|
+
choices,
|
|
57
|
+
master,
|
|
58
|
+
glyph,
|
|
59
|
+
word,
|
|
60
|
+
}: {
|
|
61
|
+
readonly label?: string;
|
|
62
|
+
/** The menu's name in the target's own words (Unreal's `Show`, Unity's `Gizmos`), drawn as
|
|
63
|
+
* a word that opens the menu instead of the eye and its caret; its master switch stays the
|
|
64
|
+
* menu's first item. */
|
|
65
|
+
readonly word?: string;
|
|
66
|
+
readonly choices: readonly ViewportOverlayChoice[];
|
|
67
|
+
readonly master?: { readonly enabled: boolean; readonly onToggle: () => void };
|
|
68
|
+
/** Replaces the eye — a caller whose overlays have Blender's own mark. */
|
|
69
|
+
readonly glyph?: ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
const [open, setOpen] = useState(false);
|
|
72
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
73
|
+
const anyEnabled = choices.some((choice) => choice.enabled);
|
|
74
|
+
const active = master?.enabled ?? anyEnabled;
|
|
75
|
+
const mark = glyph ?? <EditorIcon icon={active ? faEye : faEyeSlash} size="md" />;
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open) return;
|
|
79
|
+
const handler = (event: MouseEvent) => {
|
|
80
|
+
if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false);
|
|
81
|
+
};
|
|
82
|
+
document.addEventListener('mousedown', handler);
|
|
83
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
84
|
+
}, [open]);
|
|
85
|
+
|
|
86
|
+
const menu = open ? (
|
|
87
|
+
<Menu className="volter-helpers-menu" onDismiss={() => setOpen(false)}>
|
|
88
|
+
{master ? (
|
|
89
|
+
<>
|
|
90
|
+
<MenuItem
|
|
91
|
+
role="menuitemcheckbox"
|
|
92
|
+
aria-checked={master.enabled}
|
|
93
|
+
onSelect={master.onToggle}
|
|
94
|
+
>
|
|
95
|
+
<span className="volter-menu-check">
|
|
96
|
+
{master.enabled && <EditorIcon icon={faCheck} size="xs" />}
|
|
97
|
+
</span>
|
|
98
|
+
Show {label}
|
|
99
|
+
</MenuItem>
|
|
100
|
+
<MenuSeparator />
|
|
101
|
+
</>
|
|
102
|
+
) : null}
|
|
103
|
+
{choices.map((choice) => (
|
|
104
|
+
<MenuItem
|
|
105
|
+
key={choice.id}
|
|
106
|
+
role="menuitemcheckbox"
|
|
107
|
+
aria-checked={choice.enabled}
|
|
108
|
+
disabled={choice.disabled}
|
|
109
|
+
onSelect={choice.onToggle}
|
|
110
|
+
>
|
|
111
|
+
<span className="volter-menu-check">
|
|
112
|
+
{choice.enabled && <EditorIcon icon={faCheck} size="xs" />}
|
|
113
|
+
</span>
|
|
114
|
+
{choice.label}
|
|
115
|
+
</MenuItem>
|
|
116
|
+
))}
|
|
117
|
+
</Menu>
|
|
118
|
+
) : null;
|
|
119
|
+
|
|
120
|
+
if (word !== undefined) {
|
|
121
|
+
return (
|
|
122
|
+
<div ref={ref} className="volter-viewport-popover-anchor">
|
|
123
|
+
<Button
|
|
124
|
+
variant="ghost"
|
|
125
|
+
size="comfortable"
|
|
126
|
+
aria-label={`Choose ${label.toLowerCase()}`}
|
|
127
|
+
aria-expanded={open}
|
|
128
|
+
onClick={() => setOpen(!open)}
|
|
129
|
+
>
|
|
130
|
+
{word}
|
|
131
|
+
<EditorIcon icon={faCaretDown} size="xs" />
|
|
132
|
+
</Button>
|
|
133
|
+
{menu}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div ref={ref} className="volter-viewport-popover-anchor">
|
|
140
|
+
{master ? (
|
|
141
|
+
<SplitButtonGroup>
|
|
142
|
+
<Tooltip text={`${label}: ${active ? 'On' : 'Off'}`}>
|
|
143
|
+
<IconButton
|
|
144
|
+
aria-label={`Toggle ${label.toLowerCase()}`}
|
|
145
|
+
aria-pressed={active}
|
|
146
|
+
size="comfortable"
|
|
147
|
+
onClick={master.onToggle}
|
|
148
|
+
>
|
|
149
|
+
{mark}
|
|
150
|
+
</IconButton>
|
|
151
|
+
</Tooltip>
|
|
152
|
+
<IconButton
|
|
153
|
+
aria-label={`Choose ${label.toLowerCase()}`}
|
|
154
|
+
aria-expanded={open}
|
|
155
|
+
size="comfortable"
|
|
156
|
+
onClick={() => setOpen(!open)}
|
|
157
|
+
>
|
|
158
|
+
<EditorIcon icon={faCaretDown} size="xs" />
|
|
159
|
+
</IconButton>
|
|
160
|
+
</SplitButtonGroup>
|
|
161
|
+
) : (
|
|
162
|
+
<Tooltip text={`${label}: ${active ? 'On' : 'Off'}`}>
|
|
163
|
+
<IconButton
|
|
164
|
+
aria-label={`Choose ${label.toLowerCase()}`}
|
|
165
|
+
aria-expanded={open}
|
|
166
|
+
aria-pressed={active}
|
|
167
|
+
size="comfortable"
|
|
168
|
+
onClick={() => setOpen(!open)}
|
|
169
|
+
>
|
|
170
|
+
{mark}
|
|
171
|
+
</IconButton>
|
|
172
|
+
</Tooltip>
|
|
173
|
+
)}
|
|
174
|
+
{menu}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -88,40 +88,7 @@ export function perModeShadingSegments(
|
|
|
88
88
|
];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
* The three marks, transcribed from `modeling-edit-none.png` at its native 2x
|
|
93
|
-
* — the 3D View header's shading group, cells at x 2628 / 2666 / 2704, y
|
|
94
|
-
* 60..95, each a 36x36 device cell carrying 28..32 device px of ink. On the
|
|
95
|
-
* 16-unit box here (the icon set's own grid, so these sit at the same optical
|
|
96
|
-
* size as every `EditorIcon` beside them):
|
|
97
|
-
*
|
|
98
|
-
* - WIREFRAME is a wire globe: a ring, two latitude chords and ONE meridian,
|
|
99
|
-
* and the frame settles the spacing — measured off the circle's own centre
|
|
100
|
-
* (device rows 64..91, so centre row 77.5, radius 14) the chords sit at
|
|
101
|
-
* ±5 device px and the meridian 5 device px LEFT of centre, i.e. all three
|
|
102
|
-
* at 0.357 of the radius. The off-centre meridian is what makes it read as
|
|
103
|
-
* a globe seen from three-quarters rather than a crosshair.
|
|
104
|
-
* - SPHERE is a plain filled disc. Blender's carries a near-white body
|
|
105
|
-
* inside a lighter rim; at 14 CSS px the rim is below this instrument's
|
|
106
|
-
* floor and a flat disc is the honest reduction.
|
|
107
|
-
* - PREVIEW is the ring with its upper-left lobe filled: the fill's right
|
|
108
|
-
* edge is a near-vertical chord just left of centre (device offset 13 of
|
|
109
|
-
* 28, held from row 68 down) and its lower edge a cut from the ring at
|
|
110
|
-
* row 78.5 down-right to row 82 — measured, because the first cut put
|
|
111
|
-
* that edge two device rows high and the mark read as a pie chart rather
|
|
112
|
-
* than a lit sphere. Blender hatches the remaining lower-right quadrant;
|
|
113
|
-
* three hatch strokes cannot survive 14 px and are deliberately not
|
|
114
|
-
* drawn, so this mark is Blender's silhouette without its texture.
|
|
115
|
-
*
|
|
116
|
-
* These are drawn HERE rather than fetched through `EditorIcon` because the
|
|
117
|
-
* icon-set seam is keyed by Font Awesome NAME, and the three names that would
|
|
118
|
-
* carry these marks are already spent on other jobs (`globe` is the
|
|
119
|
-
* transform-space toggle, `circle` is the Outliner's generic object, and
|
|
120
|
-
* `circle-half-stroke` is the LUT document's before/after). Re-drawing a
|
|
121
|
-
* shared name would move those marks too. The precedent is the sibling
|
|
122
|
-
* segmented group in this same header — `@volter/editor-blender`'s select-mode cells draw
|
|
123
|
-
* their own glyphs inline for the same reason.
|
|
124
|
-
*/
|
|
91
|
+
/** Local wireframe, Solid and Material Preview glyphs share the header’s 16-unit icon grid without replacing icon-set names used elsewhere. */
|
|
125
92
|
function ViewportShadingGlyph({
|
|
126
93
|
glyph,
|
|
127
94
|
}: {
|
|
@@ -279,14 +246,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
279
246
|
return (
|
|
280
247
|
<div style={{ position: 'relative' }}>
|
|
281
248
|
<Tooltip text={`Viewport shading: ${active.label}`}>
|
|
282
|
-
{/*
|
|
283
|
-
`mode === 'solid' ? 'ghost' : 'secondary'`, so the trigger's well
|
|
284
|
-
appeared and disappeared as you used it — and at rest (`solid`,
|
|
285
|
-
labelled "Material") it was a transparent slab where Blender paints
|
|
286
|
-
a bordered well: measured in `modeling-object-none.png`, `Object
|
|
287
|
-
Mode v` is #272727 inside a 1px #3c3c3c border, 20px tall, on a
|
|
288
|
-
#343434 header. `secondary` is this estate's select-like trigger
|
|
289
|
-
already (FontPicker's `.volter-field-trigger`), so no new variant. */}
|
|
249
|
+
{/* Keep one select-like trigger variant across modes. */}
|
|
290
250
|
<Button
|
|
291
251
|
ref={ref}
|
|
292
252
|
type="button"
|