@volter/editor-threejs 0.5.67 → 0.5.69
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 +3 -3
- package/contributions/animation-mixers.service.ts +1 -1
- package/contributions/animation-timeline.utility.tsx +1 -1
- package/dist-node/serving.mjs +10 -10
- package/package.json +4 -4
- package/serving/animation-live-module.ts +6 -6
- package/serving/animation-stamp.ts +5 -5
- package/serving/index.ts +1 -1
- package/src/adapter/hierarchy-marks.ts +5 -5
- package/src/adapter/ingest/scene-capture.ts +7 -7
- package/src/adapter/ingest/structural-ids.ts +1 -1
- package/src/adapter/ingest/visible-capture-window.ts +3 -3
- package/src/asset-loaders.ts +1 -1
- package/src/ecs/object-marks.ts +4 -4
- package/src/ecs/user-data.ts +3 -3
- package/src/kit/asset-compare.ts +2 -2
- package/src/kit/asset-preview-command.ts +2 -2
- package/src/kit/asset-preview.ts +1 -1
- package/src/kit/authoring/component-instance-root.ts +2 -2
- package/src/kit/authoring/object3d-document-session.ts +1 -1
- package/src/kit/authoring/three-projection-core.ts +1 -1
- package/src/kit/authoring/viewport-pick-context.ts +1 -1
- package/src/kit/authoring/viewport-raycast.ts +1 -1
- package/src/kit/components/CameraInfo.tsx +1 -1
- package/src/kit/components/InspectorObjectPreview.tsx +1 -1
- package/src/kit/components/Object3DDocumentToolbar.tsx +10 -10
- package/src/kit/components/StageHost.tsx +14 -14
- package/src/kit/components/StatsOverlay.tsx +4 -4
- package/src/kit/components/ViewportFurniture.tsx +22 -22
- package/src/kit/components/ViewportOverlay.tsx +4 -4
- package/src/kit/components/ViewportShadingMenu.tsx +9 -9
- package/src/kit/components/ViewportViewMenu.tsx +1 -1
- package/src/kit/components/stage-presence-markers.ts +2 -2
- package/src/kit/components/standard-viewport-dressing.ts +18 -18
- package/src/kit/editor-shell-store.ts +1 -1
- package/src/kit/editor-viewport.ts +17 -17
- package/src/kit/live-module-source.ts +1 -1
- package/src/kit/projection/three.ts +3 -3
- package/src/kit/three-viewport/selection-outline.ts +9 -9
- package/src/kit/three-viewport/skeleton-helper.ts +1 -1
- package/src/kit/three-viewport/studio-environment.ts +1 -1
- package/src/kit/viewport-hotkeys.ts +2 -2
- package/src/viewport/content-bounds.ts +1 -1
|
@@ -1327,17 +1327,17 @@ export function Object3DDocumentViewport({
|
|
|
1327
1327
|
}
|
|
1328
1328
|
// AN OBJECT DRAWN BY A HELPER is picked through it: a document that draws an object's
|
|
1329
1329
|
// overlay itself (a Blender camera's wire, a light's icon) marks each part with the
|
|
1330
|
-
// object it stands for (`userData.
|
|
1330
|
+
// object it stands for (`userData.volterPicksAs`), and the nearer hit wins, as Blender's
|
|
1331
1331
|
// pick over its whole drawing does.
|
|
1332
|
-
// Only helpers that say they can be picked (`userData.
|
|
1332
|
+
// Only helpers that say they can be picked (`userData.volterPickable`) are asked, and a
|
|
1333
1333
|
// part is hit only where it is shown (three's raycast reads layers, not `visible`).
|
|
1334
|
-
const pickable = viewport.visibleHelpers().filter((helper) => helper.userData['
|
|
1334
|
+
const pickable = viewport.visibleHelpers().filter((helper) => helper.userData['volterPickable'] === true);
|
|
1335
1335
|
for (const hit of raycaster.intersectObjects(pickable, true)) {
|
|
1336
1336
|
let proxy: THREE.Object3D | null = null;
|
|
1337
1337
|
let shown = true;
|
|
1338
1338
|
for (let object: THREE.Object3D | null = hit.object; object; object = object.parent) {
|
|
1339
1339
|
if (!object.visible) shown = false;
|
|
1340
|
-
proxy ??= (object.userData['
|
|
1340
|
+
proxy ??= (object.userData['volterPicksAs'] as THREE.Object3D | undefined) ?? null;
|
|
1341
1341
|
}
|
|
1342
1342
|
if (!shown) continue;
|
|
1343
1343
|
const id = proxy ? idOf(proxy) : null;
|
|
@@ -1402,7 +1402,7 @@ export function Object3DDocumentViewport({
|
|
|
1402
1402
|
// Under a group of the stage's own, which the view's presentation shows only while it
|
|
1403
1403
|
// lights by the document's studio; the document keeps its say over what is inside.
|
|
1404
1404
|
const viewLockedHolder = new THREE.Group();
|
|
1405
|
-
viewLockedHolder.name = '
|
|
1405
|
+
viewLockedHolder.name = 'volter:document-studio';
|
|
1406
1406
|
viewLockedHolder.add(dressingViewLocked);
|
|
1407
1407
|
camera.add(viewLockedHolder);
|
|
1408
1408
|
host.documentStudio = viewLockedHolder;
|
|
@@ -2373,8 +2373,8 @@ export function Object3DDocumentViewport({
|
|
|
2373
2373
|
const viewport = (
|
|
2374
2374
|
<div
|
|
2375
2375
|
data-testid="tool-object3d-authoring"
|
|
2376
|
-
data-
|
|
2377
|
-
className={studioStage ? '
|
|
2376
|
+
data-volter-chromeless={chromeless || undefined}
|
|
2377
|
+
className={studioStage ? 'volter-object3d-studio-stage' : undefined}
|
|
2378
2378
|
style={{
|
|
2379
2379
|
position: chromeless || hasShell ? 'relative' : 'absolute',
|
|
2380
2380
|
// Inside the Asset Editor shell the stage fills its own box exactly;
|
|
@@ -2446,20 +2446,20 @@ export function Object3DDocumentViewport({
|
|
|
2446
2446
|
top: 0,
|
|
2447
2447
|
display: 'flex',
|
|
2448
2448
|
justifyContent: 'center',
|
|
2449
|
-
padding: 'var(--
|
|
2449
|
+
padding: 'var(--volter-space-3)',
|
|
2450
2450
|
pointerEvents: 'none',
|
|
2451
2451
|
}}
|
|
2452
2452
|
>
|
|
2453
2453
|
<div
|
|
2454
|
-
className="
|
|
2454
|
+
className="volter-chrome-island"
|
|
2455
2455
|
style={{
|
|
2456
2456
|
display: 'flex',
|
|
2457
2457
|
alignItems: 'center',
|
|
2458
|
-
gap: 'var(--
|
|
2458
|
+
gap: 'var(--volter-space-3)',
|
|
2459
2459
|
maxWidth: '80%',
|
|
2460
|
-
padding: 'var(--
|
|
2461
|
-
borderRadius: 'var(--
|
|
2462
|
-
border: `var(--
|
|
2460
|
+
padding: 'var(--volter-space-2) var(--volter-space-3)',
|
|
2461
|
+
borderRadius: 'var(--volter-radius-md)',
|
|
2462
|
+
border: `var(--volter-stroke-resting) solid ${themeVars.semantic.danger}`,
|
|
2463
2463
|
background: themeVars.surface.raised,
|
|
2464
2464
|
color: themeVars.semantic.danger,
|
|
2465
2465
|
pointerEvents: 'auto',
|
|
@@ -2530,7 +2530,7 @@ export function Object3DDocumentViewport({
|
|
|
2530
2530
|
selection={readSelection}
|
|
2531
2531
|
fill
|
|
2532
2532
|
>
|
|
2533
|
-
<div className="
|
|
2533
|
+
<div className="volter-object3d-asset-workspace">
|
|
2534
2534
|
{viewport}
|
|
2535
2535
|
{/* Where the deleted preview strip was. It draws nothing until this
|
|
2536
2536
|
stage's transport has a subject, so a document with nothing to show
|
|
@@ -25,11 +25,11 @@ export function StatsOverlay() {
|
|
|
25
25
|
// same corner. Given the same border/radius treatment as its siblings so
|
|
26
26
|
// the whole corner reads as one family.
|
|
27
27
|
// P6-U6 (owner taste decision 3: HUD overlays → real islands): the paint
|
|
28
|
-
// moved to `.
|
|
28
|
+
// moved to `.volter-hud-overlay` so islands chrome can promote it to the
|
|
29
29
|
// shared glass-island material (compact tier); bars chrome paints the
|
|
30
30
|
// exact pre-U6 overlay-whisper recipe from the same class.
|
|
31
31
|
<div
|
|
32
|
-
className="
|
|
32
|
+
className="volter-hud-overlay volter-chrome-island volter-glass-island"
|
|
33
33
|
data-island-scale="compact"
|
|
34
34
|
data-hud-border="strong"
|
|
35
35
|
style={{
|
|
@@ -39,11 +39,11 @@ export function StatsOverlay() {
|
|
|
39
39
|
// floating header-island clearance, scoped in theme.css); the bare
|
|
40
40
|
// fallback is the pre-glass 48px so treatment-less chrome ("bars":
|
|
41
41
|
// graphite/classic/lite/reduced) stays pixel-identical to main.
|
|
42
|
-
top: 'var(--
|
|
42
|
+
top: 'var(--volter-viewport-overlay-hud-top, 48px)',
|
|
43
43
|
right: 8,
|
|
44
44
|
padding: '6px 10px',
|
|
45
45
|
pointerEvents: 'auto',
|
|
46
|
-
fontSize: 'var(--
|
|
46
|
+
fontSize: 'var(--volter-font-base)',
|
|
47
47
|
color: themeVars.content.primary,
|
|
48
48
|
fontFamily: themeVars.typography.mono,
|
|
49
49
|
lineHeight: '16px',
|
|
@@ -369,17 +369,17 @@ export function ViewportFurniture({
|
|
|
369
369
|
aria-hidden="true"
|
|
370
370
|
style={{
|
|
371
371
|
position: 'absolute',
|
|
372
|
-
top: 'var(--
|
|
372
|
+
top: 'var(--volter-space-3)',
|
|
373
373
|
// Clear of the tool rail at the stage's left edge (Blender's text
|
|
374
374
|
// starts past its toolbar): the rail is one control wide plus its
|
|
375
375
|
// own inset.
|
|
376
|
-
left: 'calc(var(--
|
|
376
|
+
left: 'calc(var(--volter-space-4) + var(--volter-control-comfortable-height) * 2 + var(--volter-space-4))',
|
|
377
377
|
display: 'flex',
|
|
378
378
|
flexDirection: 'column',
|
|
379
|
-
gap: 'var(--
|
|
380
|
-
fontSize: 'var(--
|
|
379
|
+
gap: 'var(--volter-space-1)',
|
|
380
|
+
fontSize: 'var(--volter-font-sm)',
|
|
381
381
|
color: overlayInk,
|
|
382
|
-
textShadow: 'var(--
|
|
382
|
+
textShadow: 'var(--volter-content-text-shadow, none)',
|
|
383
383
|
pointerEvents: 'none',
|
|
384
384
|
userSelect: 'none',
|
|
385
385
|
}}
|
|
@@ -396,7 +396,7 @@ export function ViewportFurniture({
|
|
|
396
396
|
// The column's own rows keep the block's line step; the block
|
|
397
397
|
// itself stands off the lines above it. See
|
|
398
398
|
// `STATISTICS_BLOCK_OFFSET`.
|
|
399
|
-
rowGap: 'var(--
|
|
399
|
+
rowGap: 'var(--volter-space-1)',
|
|
400
400
|
marginTop: spaceVar[STATISTICS_BLOCK_OFFSET],
|
|
401
401
|
}}
|
|
402
402
|
>
|
|
@@ -412,15 +412,15 @@ export function ViewportFurniture({
|
|
|
412
412
|
) : null}
|
|
413
413
|
{chrome.viewName === 'menu' ? (
|
|
414
414
|
<div
|
|
415
|
-
className="
|
|
415
|
+
className="volter-viewport-view-pill"
|
|
416
416
|
style={{
|
|
417
417
|
position: 'absolute',
|
|
418
|
-
top: 'var(--
|
|
418
|
+
top: 'var(--volter-viewport-overlay-top, var(--volter-space-4))',
|
|
419
419
|
// Past the shelf rail when it draws anything, as the view text is; at the edge when
|
|
420
|
-
// it is empty (`data-
|
|
420
|
+
// it is empty (`data-volter-stage-rail`, `workspace-surfaces.css`).
|
|
421
421
|
left:
|
|
422
|
-
'var(--
|
|
423
|
-
zIndex: 'calc(var(--
|
|
422
|
+
'var(--volter-stage-name-left, calc(var(--volter-space-4) + var(--volter-control-comfortable-height) * 2 + var(--volter-space-4)))',
|
|
423
|
+
zIndex: 'calc(var(--volter-z-dropdown, 1000) - 1)',
|
|
424
424
|
pointerEvents: 'auto',
|
|
425
425
|
}}
|
|
426
426
|
>
|
|
@@ -433,7 +433,7 @@ export function ViewportFurniture({
|
|
|
433
433
|
<button
|
|
434
434
|
type="button"
|
|
435
435
|
data-testid="viewport-view-name"
|
|
436
|
-
className="
|
|
436
|
+
className="volter-viewport-gizmo-label"
|
|
437
437
|
aria-label={drawn === 'perspective' ? 'Switch to orthographic' : 'Switch to perspective'}
|
|
438
438
|
onClick={() => {
|
|
439
439
|
const next = drawn === 'perspective' ? 'orthographic' : 'perspective';
|
|
@@ -447,13 +447,13 @@ export function ViewportFurniture({
|
|
|
447
447
|
transform: 'translateX(50%)',
|
|
448
448
|
display: 'inline-flex',
|
|
449
449
|
alignItems: 'center',
|
|
450
|
-
gap: 'var(--
|
|
450
|
+
gap: 'var(--volter-space-1)',
|
|
451
451
|
padding: 0,
|
|
452
452
|
border: 0,
|
|
453
453
|
background: 'none',
|
|
454
|
-
fontSize: 'var(--
|
|
454
|
+
fontSize: 'var(--volter-font-sm)',
|
|
455
455
|
color: overlayInk,
|
|
456
|
-
textShadow: 'var(--
|
|
456
|
+
textShadow: 'var(--volter-content-text-shadow, none)',
|
|
457
457
|
cursor: 'pointer',
|
|
458
458
|
pointerEvents: 'auto',
|
|
459
459
|
}}
|
|
@@ -475,7 +475,7 @@ export function ViewportFurniture({
|
|
|
475
475
|
role="toolbar"
|
|
476
476
|
aria-label="Navigation"
|
|
477
477
|
aria-orientation="vertical"
|
|
478
|
-
className="
|
|
478
|
+
className="volter-chrome-island volter-viewport-navigation"
|
|
479
479
|
style={{
|
|
480
480
|
position: 'absolute',
|
|
481
481
|
top: CLUSTER_TOP,
|
|
@@ -487,17 +487,17 @@ export function ViewportFurniture({
|
|
|
487
487
|
// 2836); at 12 it is 9.0.
|
|
488
488
|
// UNGATED, like every other number this cluster carries: its 28 and
|
|
489
489
|
// 16 are Blender's for every skin already (`theme.css`'s
|
|
490
|
-
// `.
|
|
490
|
+
// `.volter-viewport-navigation` block, no palette selector), and the
|
|
491
491
|
// estate's `css-style-identity-selector` gate forbids one anyway.
|
|
492
|
-
right: 'var(--
|
|
492
|
+
right: 'var(--volter-space-6)',
|
|
493
493
|
display: 'flex',
|
|
494
494
|
flexDirection: 'column',
|
|
495
|
-
gap: 'var(--
|
|
495
|
+
gap: 'var(--volter-space-1)',
|
|
496
496
|
// Blender's capsule is exactly as wide as its buttons: it insets
|
|
497
497
|
// only along the column.
|
|
498
|
-
padding: 'var(--
|
|
499
|
-
borderRadius: 'var(--
|
|
500
|
-
background: 'var(--
|
|
498
|
+
padding: 'var(--volter-space-1) 0',
|
|
499
|
+
borderRadius: 'var(--volter-radius-full)',
|
|
500
|
+
background: 'var(--volter-island-surface, var(--volter-surface-overlay))',
|
|
501
501
|
pointerEvents: 'auto',
|
|
502
502
|
}}
|
|
503
503
|
>
|
|
@@ -41,7 +41,7 @@ import { stageViewName } from './stage-view-name';
|
|
|
41
41
|
* GRID is the stage's VIEW's switch (`overlays.grid.visible` in
|
|
42
42
|
* `kit/viewport-presentation`, keyed by `documentId`): the one flag this
|
|
43
43
|
* button, the document's header, the `toggle.grid` action, `set-grid` and
|
|
44
|
-
* `
|
|
44
|
+
* the editor's `status` command's `showGrid` all read and write.
|
|
45
45
|
*/
|
|
46
46
|
export function ViewportOverlay({
|
|
47
47
|
store,
|
|
@@ -71,8 +71,8 @@ export function ViewportOverlay({
|
|
|
71
71
|
const [anchor, setAnchor] = useState<HTMLSpanElement | null>(null);
|
|
72
72
|
const findSlot = (): HTMLElement | null =>
|
|
73
73
|
anchor
|
|
74
|
-
?.closest('.
|
|
75
|
-
?.querySelector<HTMLElement>(':scope > .
|
|
74
|
+
?.closest('.volter-dock-document-content')
|
|
75
|
+
?.querySelector<HTMLElement>(':scope > .volter-stage-bar [data-stage-bar-slot="display"]') ?? null;
|
|
76
76
|
// ONE OVERLAY PER SLOT: a document hosting two stages has one bar, so the first overlay to
|
|
77
77
|
// claim its slot draws there and any other keeps its own place.
|
|
78
78
|
const owner = useId();
|
|
@@ -96,7 +96,7 @@ export function ViewportOverlay({
|
|
|
96
96
|
const toolbar = (
|
|
97
97
|
<FloatingToolbar
|
|
98
98
|
label="Viewport display"
|
|
99
|
-
className="
|
|
99
|
+
className="volter-viewport-toolbar volter-viewport-toolbar-right volter-stage-display"
|
|
100
100
|
>
|
|
101
101
|
{chrome.viewName === 'bar' && session ? (
|
|
102
102
|
<ViewportViewMenu
|
|
@@ -130,7 +130,7 @@ function ViewportShadingGlyph({
|
|
|
130
130
|
return (
|
|
131
131
|
<svg
|
|
132
132
|
viewBox="0 0 16 16"
|
|
133
|
-
className="
|
|
133
|
+
className="volter-viewport-shading-glyph"
|
|
134
134
|
aria-hidden="true"
|
|
135
135
|
focusable="false"
|
|
136
136
|
>
|
|
@@ -148,7 +148,7 @@ function ViewportShadingGlyph({
|
|
|
148
148
|
) : null}
|
|
149
149
|
{glyph === 'rendered' ? (
|
|
150
150
|
// Blender's Rendered mark is a lit ball: the filled disc with a highlight up and left.
|
|
151
|
-
<circle cx="5.6" cy="5.6" r="2.3" fill="var(--
|
|
151
|
+
<circle cx="5.6" cy="5.6" r="2.3" fill="var(--volter-surface-overlay, #ffffff)" fillOpacity="0.7" />
|
|
152
152
|
) : null}
|
|
153
153
|
{glyph === 'preview' ? (
|
|
154
154
|
<path
|
|
@@ -185,7 +185,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
185
185
|
const menu =
|
|
186
186
|
open && !disabled ? (
|
|
187
187
|
<AnchoredMenu
|
|
188
|
-
id="
|
|
188
|
+
id="volter-viewport-shading-menu"
|
|
189
189
|
anchorRef={ref}
|
|
190
190
|
aria-label="Viewport shading modes"
|
|
191
191
|
onDismiss={() => setOpen(false)}
|
|
@@ -224,7 +224,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
224
224
|
{children ? (
|
|
225
225
|
<>
|
|
226
226
|
<MenuSeparator />
|
|
227
|
-
<div className="
|
|
227
|
+
<div className="volter-viewport-shading-options">{children}</div>
|
|
228
228
|
</>
|
|
229
229
|
) : null}
|
|
230
230
|
</AnchoredMenu>
|
|
@@ -237,8 +237,8 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
237
237
|
// corners and a shared border — so the five read as one widget with one
|
|
238
238
|
// cell lit, and `aria-pressed` is the only thing that paints the lit one.
|
|
239
239
|
return (
|
|
240
|
-
<div className="
|
|
241
|
-
<span className="
|
|
240
|
+
<div className="volter-viewport-popover-anchor">
|
|
241
|
+
<span className="volter-viewport-shading-segments" role="group" aria-label="Viewport shading">
|
|
242
242
|
{segments.map((segment) => (
|
|
243
243
|
<Tooltip key={segment.mode} text={`Viewport shading: ${segment.label}`}>
|
|
244
244
|
<IconButton
|
|
@@ -263,7 +263,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
263
263
|
aria-label="Shading options"
|
|
264
264
|
aria-haspopup="menu"
|
|
265
265
|
aria-expanded={open}
|
|
266
|
-
aria-controls={open ? '
|
|
266
|
+
aria-controls={open ? 'volter-viewport-shading-menu' : undefined}
|
|
267
267
|
disabled={disabled}
|
|
268
268
|
onClick={() => setOpen((value) => !value)}
|
|
269
269
|
>
|
|
@@ -286,7 +286,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
286
286
|
a bordered well: measured in `modeling-object-none.png`, `Object
|
|
287
287
|
Mode v` is #272727 inside a 1px #3c3c3c border, 20px tall, on a
|
|
288
288
|
#343434 header. `secondary` is this estate's select-like trigger
|
|
289
|
-
already (FontPicker's `.
|
|
289
|
+
already (FontPicker's `.volter-field-trigger`), so no new variant. */}
|
|
290
290
|
<Button
|
|
291
291
|
ref={ref}
|
|
292
292
|
type="button"
|
|
@@ -295,7 +295,7 @@ export function ViewportDisplayModeMenu<T extends string>({
|
|
|
295
295
|
aria-label={`Viewport shading: ${active.label}`}
|
|
296
296
|
aria-haspopup="menu"
|
|
297
297
|
aria-expanded={open}
|
|
298
|
-
aria-controls={open ? '
|
|
298
|
+
aria-controls={open ? 'volter-viewport-shading-menu' : undefined}
|
|
299
299
|
disabled={disabled}
|
|
300
300
|
onClick={() => setOpen((value) => !value)}
|
|
301
301
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 5, minWidth: 66 }}
|
|
@@ -86,7 +86,7 @@ export function ViewportViewMenu({
|
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
|
-
<div ref={anchor} className="
|
|
89
|
+
<div ref={anchor} className="volter-viewport-popover-anchor">
|
|
90
90
|
{label === undefined ? (
|
|
91
91
|
<Tooltip text={projection === 'perspective' ? 'Perspective' : 'Orthographic'}>
|
|
92
92
|
<IconButton
|
|
@@ -57,7 +57,7 @@ interface RemoteRayMarker {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Remote-camera labels paint into a DETACHED canvas, and a 2D context cannot
|
|
60
|
-
* resolve `var(--
|
|
60
|
+
* resolve `var(--volter-…)` — so the theme's value has to be read off the live
|
|
61
61
|
* theme root before it reaches `fillStyle`. Before a theme is installed (unit
|
|
62
62
|
* environments), fall back to the default theme's own value rather than a raw
|
|
63
63
|
* literal, so the label never depends on a color the theme contract doesn't own.
|
|
@@ -65,7 +65,7 @@ interface RemoteRayMarker {
|
|
|
65
65
|
function contentOnAccentColor(): string {
|
|
66
66
|
const root = document.querySelector(`.${EDITOR_THEME_CLASS}`);
|
|
67
67
|
const resolved = root
|
|
68
|
-
? getComputedStyle(root).getPropertyValue('--
|
|
68
|
+
? getComputedStyle(root).getPropertyValue('--volter-content-on-accent').trim()
|
|
69
69
|
: '';
|
|
70
70
|
return resolved || graphiteDarkEditorTheme.color.content.onAccent;
|
|
71
71
|
}
|
|
@@ -78,7 +78,7 @@ export interface BackdropStops {
|
|
|
78
78
|
*/
|
|
79
79
|
export function paletteBackdropStops(): BackdropStops {
|
|
80
80
|
const root =
|
|
81
|
-
typeof document === 'undefined' ? null : document.querySelector('[data-
|
|
81
|
+
typeof document === 'undefined' ? null : document.querySelector('[data-volter-palette]');
|
|
82
82
|
if (!root) return { bottom: GRADIENT_BOTTOM.clone(), top: GRADIENT_TOP.clone() };
|
|
83
83
|
const style = getComputedStyle(root);
|
|
84
84
|
const canvas = document.createElement('canvas');
|
|
@@ -91,15 +91,15 @@ export function paletteBackdropStops(): BackdropStops {
|
|
|
91
91
|
// their CSS colors over the shell; Three.Color has no alpha channel.
|
|
92
92
|
context.fillStyle = fallback.getStyle();
|
|
93
93
|
context.fillRect(0, 0, 1, 1);
|
|
94
|
-
context.fillStyle = style.getPropertyValue('--
|
|
94
|
+
context.fillStyle = style.getPropertyValue('--volter-surface-shell').trim();
|
|
95
95
|
context.fillRect(0, 0, 1, 1);
|
|
96
96
|
context.fillStyle = value;
|
|
97
97
|
context.fillRect(0, 0, 1, 1);
|
|
98
98
|
const [r, g, b] = context.getImageData(0, 0, 1, 1).data;
|
|
99
99
|
return new THREE.Color().setRGB(r! / 255, g! / 255, b! / 255, THREE.SRGBColorSpace);
|
|
100
100
|
};
|
|
101
|
-
const panel = read('--
|
|
102
|
-
const raised = read('--
|
|
101
|
+
const panel = read('--volter-surface-panel', GRADIENT_BOTTOM);
|
|
102
|
+
const raised = read('--volter-surface-raised', GRADIENT_TOP);
|
|
103
103
|
// A background texture is not tone-mapped, so what is authored here is
|
|
104
104
|
// what the screen shows. The small lift keeps the ground a step above the
|
|
105
105
|
// panel it sits beside (Blender's viewport is lighter than its editors).
|
|
@@ -112,14 +112,14 @@ export function paletteBackdropStops(): BackdropStops {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Fires when the palette the backdrop follows changes. The theme installer
|
|
115
|
-
* stamps its root with `data-
|
|
115
|
+
* stamps its root with `data-volter-palette` / `data-volter-material` on every
|
|
116
116
|
* switch, so the DOM is the contract here — a document stage never imports
|
|
117
117
|
* the shell's preference store (its closure is pinned,
|
|
118
118
|
* `scripts/validate-editor-closure.mjs`). Returns the unsubscribe.
|
|
119
119
|
*/
|
|
120
120
|
export function watchPaletteBackdrop(onChange: () => void): () => void {
|
|
121
121
|
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {};
|
|
122
|
-
const root = document.querySelector('[data-
|
|
122
|
+
const root = document.querySelector('[data-volter-palette]');
|
|
123
123
|
if (!root) return () => {};
|
|
124
124
|
const observer = new MutationObserver(() => {
|
|
125
125
|
onChange();
|
|
@@ -127,7 +127,7 @@ export function watchPaletteBackdrop(onChange: () => void): () => void {
|
|
|
127
127
|
});
|
|
128
128
|
observer.observe(root, {
|
|
129
129
|
attributes: true,
|
|
130
|
-
attributeFilter: ['data-
|
|
130
|
+
attributeFilter: ['data-volter-palette', 'data-volter-material'],
|
|
131
131
|
});
|
|
132
132
|
return () => observer.disconnect();
|
|
133
133
|
}
|
|
@@ -248,7 +248,7 @@ export function applyGridDistanceFade(material: THREE.Material, gridSize: number
|
|
|
248
248
|
);
|
|
249
249
|
};
|
|
250
250
|
// Distinct fade bands must not share one cached program.
|
|
251
|
-
material.customProgramCacheKey = () => `
|
|
251
|
+
material.customProgramCacheKey = () => `volter-grid-fade:${fadeStart}:${fadeEnd}`;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export interface StandardViewportDressingOptions {
|
|
@@ -302,7 +302,7 @@ export function applyStandardViewportDressing(
|
|
|
302
302
|
// document stage is lit by its view's presentation instead (`StagePresentationRig`, and
|
|
303
303
|
// `StageHost` turns this key off); a stage without a presentation still gets this one.
|
|
304
304
|
const key = new THREE.DirectionalLight(0xfff3dd, 1.9);
|
|
305
|
-
key.name = '
|
|
305
|
+
key.name = 'volter:standard-dressing-key';
|
|
306
306
|
key.position.set(6, 10, -4);
|
|
307
307
|
key.castShadow = true;
|
|
308
308
|
// `userData.editorHelper` keeps it out of hierarchy walks and picks;
|
|
@@ -336,7 +336,7 @@ export function applyStandardViewportDressing(
|
|
|
336
336
|
gridMaterial.opacity = 0.35;
|
|
337
337
|
gridMaterial.depthWrite = false;
|
|
338
338
|
applyGridDistanceFade(gridMaterial, size);
|
|
339
|
-
helper.name = '
|
|
339
|
+
helper.name = 'volter:standard-dressing-grid';
|
|
340
340
|
// Just below the ground line so content standing exactly on y=0 never
|
|
341
341
|
// z-fights the grid lines; centred on the content's own footprint.
|
|
342
342
|
helper.position.set(centre[0], -0.02, centre[1]);
|
|
@@ -402,7 +402,7 @@ const LIGHT_DISTANCE = 12;
|
|
|
402
402
|
* else on the page fills.
|
|
403
403
|
*/
|
|
404
404
|
const GODOT_FILMIC = /* glsl */ `
|
|
405
|
-
vec3
|
|
405
|
+
vec3 volterHable( vec3 x ) {
|
|
406
406
|
const float A = 0.22 * 4.0;
|
|
407
407
|
const float B = 0.30 * 2.0;
|
|
408
408
|
const float C = 0.10;
|
|
@@ -413,7 +413,7 @@ vec3 vgaiHable( vec3 x ) {
|
|
|
413
413
|
}
|
|
414
414
|
vec3 CustomToneMapping( vec3 color ) {
|
|
415
415
|
color *= toneMappingExposure;
|
|
416
|
-
return clamp(
|
|
416
|
+
return clamp( volterHable( max( vec3( 0.0 ), color ) ) / volterHable( vec3( 1.0 ) ), 0.0, 1.0 );
|
|
417
417
|
}`;
|
|
418
418
|
{
|
|
419
419
|
const chunk = THREE.ShaderChunk.tonemapping_pars_fragment;
|
|
@@ -548,7 +548,7 @@ function fadingFloorMaterial(): THREE.MeshStandardMaterial {
|
|
|
548
548
|
`#include <dithering_fragment>\ngl_FragColor.a *= 1.0 - smoothstep(${(half * 0.3).toFixed(1)}, ${(half * 0.95).toFixed(1)}, length(vFloorPlane));`,
|
|
549
549
|
);
|
|
550
550
|
};
|
|
551
|
-
material.customProgramCacheKey = () => '
|
|
551
|
+
material.customProgramCacheKey = () => 'volter-fading-floor';
|
|
552
552
|
return material;
|
|
553
553
|
}
|
|
554
554
|
|
|
@@ -595,19 +595,19 @@ export class StagePresentationRig {
|
|
|
595
595
|
|
|
596
596
|
/** `onReady`: an environment image arrived after the apply that asked for it; draw again. */
|
|
597
597
|
constructor(scene: THREE.Scene, private readonly onReady?: () => void) {
|
|
598
|
-
this.group.name = '
|
|
598
|
+
this.group.name = 'volter:stage-presentation-rig';
|
|
599
599
|
// Kept out of hierarchy walks and picks, like every editor helper.
|
|
600
600
|
this.group.userData['editorHelper'] = true;
|
|
601
601
|
this.group.add(this.ambient);
|
|
602
602
|
scene.add(this.group);
|
|
603
|
-
this.previewGroup.name = '
|
|
603
|
+
this.previewGroup.name = 'volter:stage-preview-rig';
|
|
604
604
|
this.previewGroup.userData['editorHelper'] = true;
|
|
605
|
-
this.sun.name = '
|
|
605
|
+
this.sun.name = 'volter:preview-sun';
|
|
606
606
|
this.sun.userData['editorHelper'] = true;
|
|
607
607
|
this.previewGroup.add(this.sun, this.sun.target);
|
|
608
608
|
this.previewGroup.visible = false;
|
|
609
609
|
scene.add(this.previewGroup);
|
|
610
|
-
this.floor.name = '
|
|
610
|
+
this.floor.name = 'volter:preview-floor';
|
|
611
611
|
this.floor.userData['editorHelper'] = true;
|
|
612
612
|
this.floor.receiveShadow = true;
|
|
613
613
|
// Transparent now (its fade), so it is sorted with the grid: drawn first, under it.
|
|
@@ -1013,7 +1013,7 @@ export class StagePresentationRig {
|
|
|
1013
1013
|
this.ambient.intensity = preset.ambient.intensity;
|
|
1014
1014
|
for (const spec of preset.lights) {
|
|
1015
1015
|
const light = new THREE.DirectionalLight(spec.color, spec.intensity);
|
|
1016
|
-
light.name = `
|
|
1016
|
+
light.name = `volter:studio-light:${preset.id}`;
|
|
1017
1017
|
light.castShadow = spec.castShadow === true;
|
|
1018
1018
|
light.userData['editorHelper'] = true;
|
|
1019
1019
|
this.group.add(light);
|
|
@@ -612,7 +612,7 @@ export class EditorShellStore {
|
|
|
612
612
|
renderer.setRenderTarget(sceneTarget);
|
|
613
613
|
// The same view policy the on-screen draw applies (`scene-view-fog.ts`).
|
|
614
614
|
// This capture is the ONLY look anyone gets at the Edit viewport — the
|
|
615
|
-
// `
|
|
615
|
+
// the editor's `screenshot` command evidence and every project thumbnail — so a capture
|
|
616
616
|
// that renders the game's fog while the viewport does not would make the
|
|
617
617
|
// one door onto the image disagree with the image.
|
|
618
618
|
withSceneFogNeutralized(this._scene, () =>
|
|
@@ -611,7 +611,7 @@ export const COMPASS_CLUSTER_TOP_PX =
|
|
|
611
611
|
* at the same pixels.
|
|
612
612
|
*/
|
|
613
613
|
export const GRAZING_FADE_GLSL = /* glsl */ `
|
|
614
|
-
float
|
|
614
|
+
float volterGrazingFade(vec3 worldPosition, vec3 eye, vec3 planeNormal, float amount) {
|
|
615
615
|
vec3 toEye = eye - worldPosition;
|
|
616
616
|
float toEyeLength = max(length(toEye), 1e-6);
|
|
617
617
|
float graze = 1.0 - abs(dot(toEye / toEyeLength, planeNormal));
|
|
@@ -686,10 +686,10 @@ function applyAxisGrazingFade(
|
|
|
686
686
|
)
|
|
687
687
|
.replace(
|
|
688
688
|
'gl_FragColor = vec4( diffuseColor.rgb, alpha );',
|
|
689
|
-
'gl_FragColor = vec4( diffuseColor.rgb, alpha *
|
|
689
|
+
'gl_FragColor = vec4( diffuseColor.rgb, alpha * volterGrazingFade( vAxisWorld, cameraPosition, uPlaneNormal, uGrazingFade ) );',
|
|
690
690
|
);
|
|
691
691
|
};
|
|
692
|
-
material.customProgramCacheKey = () => '
|
|
692
|
+
material.customProgramCacheKey = () => 'volter-axis-grazing-fade';
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
const _gridView = new THREE.Vector3();
|
|
@@ -824,7 +824,7 @@ function createFloorGrid(extent: number): THREE.Mesh<THREE.PlaneGeometry, THREE.
|
|
|
824
824
|
// Blender's own grazing-angle profile, transcribed from its frames
|
|
825
825
|
// (see GRAZING_FADE_GLSL above). Without it this lattice reads the
|
|
826
826
|
// same ink at the front of the frame and at the horizon.
|
|
827
|
-
fade *=
|
|
827
|
+
fade *= volterGrazingFade(vWorld, cameraPosition, uPlaneNormal, uGrazingFade);
|
|
828
828
|
float alpha = line * fade * uOpacity;
|
|
829
829
|
if (alpha <= 0.002) discard;
|
|
830
830
|
// WHICH level a fragment belongs to is the major's SHARE of the
|
|
@@ -1731,7 +1731,7 @@ export class EditorViewport {
|
|
|
1731
1731
|
this._vertexSnapIndicator,
|
|
1732
1732
|
];
|
|
1733
1733
|
|
|
1734
|
-
// D12 (B4) — publish the camera/canvas seam `
|
|
1734
|
+
// D12 (B4) — publish the camera/canvas seam `VolterSceneAuthoringAdapter.pickable`
|
|
1735
1735
|
// needs (it has no other way to reach them). Cleared in `dispose()`.
|
|
1736
1736
|
if (this._publishPickContext) {
|
|
1737
1737
|
this._installPickContext(this.camera);
|
|
@@ -2363,8 +2363,8 @@ export class EditorViewport {
|
|
|
2363
2363
|
// two competing selection languages and obscures the draggable handle.
|
|
2364
2364
|
if ([...this._constraintHelpers.values()].some((helper) => helper.presents(obj))) continue;
|
|
2365
2365
|
// Nor over an object whose document draws its overlay itself, selection colour included
|
|
2366
|
-
// (`userData.
|
|
2367
|
-
if (obj.userData['
|
|
2366
|
+
// (`userData.volterOwnOverlay`: a Blender camera, light or empty).
|
|
2367
|
+
if (obj.userData['volterOwnOverlay']) continue;
|
|
2368
2368
|
add(
|
|
2369
2369
|
`selection:${id}`,
|
|
2370
2370
|
new SelectionBrackets(obj, {
|
|
@@ -2759,9 +2759,9 @@ export class EditorViewport {
|
|
|
2759
2759
|
}
|
|
2760
2760
|
|
|
2761
2761
|
/** A light the editor draws a helper for: not one whose document draws its own overlay
|
|
2762
|
-
* (`userData.
|
|
2762
|
+
* (`userData.volterOwnOverlay`, the way `volterOwnMaterial` keeps a material its owner's). */
|
|
2763
2763
|
private _nativeLight(object: THREE.Object3D): THREE.Light | null {
|
|
2764
|
-
if (object.userData['
|
|
2764
|
+
if (object.userData['volterOwnOverlay']) return null;
|
|
2765
2765
|
return (object as THREE.Light).isLight ? (object as THREE.Light) : null;
|
|
2766
2766
|
}
|
|
2767
2767
|
|
|
@@ -2939,7 +2939,7 @@ export class EditorViewport {
|
|
|
2939
2939
|
}
|
|
2940
2940
|
|
|
2941
2941
|
private _nativeCamera(object: THREE.Object3D): THREE.Camera | null {
|
|
2942
|
-
if (object.userData['
|
|
2942
|
+
if (object.userData['volterOwnOverlay']) return null;
|
|
2943
2943
|
if ((object as THREE.Camera).isCamera) return object as THREE.Camera;
|
|
2944
2944
|
const owned = getUserData(object, '_camera');
|
|
2945
2945
|
return owned?.isCamera ? owned : null;
|
|
@@ -3733,7 +3733,7 @@ export class EditorViewport {
|
|
|
3733
3733
|
}
|
|
3734
3734
|
|
|
3735
3735
|
/** The helper groups on screen now (`setHelper`), for a pick that lets a helper stand for the
|
|
3736
|
-
* object it draws (`userData.
|
|
3736
|
+
* object it draws (`userData.volterPicksAs`). */
|
|
3737
3737
|
visibleHelpers(): THREE.Object3D[] {
|
|
3738
3738
|
return [...this._helpers.values()].filter((helper) => helper.visible && helper.parent !== null);
|
|
3739
3739
|
}
|
|
@@ -5577,10 +5577,10 @@ export class EditorViewport {
|
|
|
5577
5577
|
ownMaterial &&
|
|
5578
5578
|
mesh.material &&
|
|
5579
5579
|
!Array.isArray(mesh.material) &&
|
|
5580
|
-
!mesh.userData['
|
|
5580
|
+
!mesh.userData['volterOwnMaterial']
|
|
5581
5581
|
) {
|
|
5582
5582
|
mesh.material = (mesh.material as THREE.Material).clone();
|
|
5583
|
-
mesh.userData['
|
|
5583
|
+
mesh.userData['volterOwnMaterial'] = true;
|
|
5584
5584
|
}
|
|
5585
5585
|
const material = mesh.material as
|
|
5586
5586
|
| (THREE.MeshBasicMaterial & { _color?: THREE.Color | undefined })
|
|
@@ -5593,7 +5593,7 @@ export class EditorViewport {
|
|
|
5593
5593
|
// translucent); three caches it like the colour, so the cache is dropped too.
|
|
5594
5594
|
// Handles only: the drag's axis lines (the `helper` family) keep three's own.
|
|
5595
5595
|
if (handles) {
|
|
5596
|
-
const base = (material.userData['
|
|
5596
|
+
const base = (material.userData['volterBaseOpacity'] ??= material.opacity) as number;
|
|
5597
5597
|
material.opacity = base * (this._gizmoLook.opacity ?? 1);
|
|
5598
5598
|
(material as { _opacity?: number | undefined })._opacity = undefined;
|
|
5599
5599
|
}
|
|
@@ -6159,7 +6159,7 @@ export class EditorViewport {
|
|
|
6159
6159
|
pointerEvents: 'none',
|
|
6160
6160
|
zIndex: zIndex.sticky,
|
|
6161
6161
|
});
|
|
6162
|
-
// INSIDE the theme scope, never document.body: the `--
|
|
6162
|
+
// INSIDE the theme scope, never document.body: the `--volter-*` tokens
|
|
6163
6163
|
// live on `#editor-chrome-root` (deliberately not `:root`), so a
|
|
6164
6164
|
// body-parented div resolves both colors to nothing and the rubber
|
|
6165
6165
|
// band is INVISIBLE — three human passes marqueed blind, reading the
|
|
@@ -6338,7 +6338,7 @@ export class EditorViewport {
|
|
|
6338
6338
|
|
|
6339
6339
|
// D12 (B4) — topmost-first layered pick: the composite's non-threejs
|
|
6340
6340
|
// children (react/pixi design-time layers) are tried topmost-first before
|
|
6341
|
-
// falling through to the three raycast (now `
|
|
6341
|
+
// falling through to the three raycast (now `VolterSceneAuthoringAdapter
|
|
6342
6342
|
// .pickable`) — see `pickTopmost`'s doc comment. Note: an INTERACTIVE
|
|
6343
6343
|
// layer (session `interactive` toggle, B1) is mutually exclusive with this
|
|
6344
6344
|
// walk running at all, by construction — its `pointer-events:auto` DOM
|
|
@@ -6489,7 +6489,7 @@ export class EditorViewport {
|
|
|
6489
6489
|
background: themeVars.accent.muted,
|
|
6490
6490
|
zIndex: zIndex.base,
|
|
6491
6491
|
});
|
|
6492
|
-
// Same class as the marquee band: `--
|
|
6492
|
+
// Same class as the marquee band: `--volter-*` resolves only under
|
|
6493
6493
|
// `#editor-chrome-root`, so a body-parented overlay draws with NO border
|
|
6494
6494
|
// or fill — the drag-to-viewport highlight has been invisible.
|
|
6495
6495
|
(document.getElementById('editor-chrome-root') ?? document.body).appendChild(overlay);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`catalog/project-source/src/tools/module-source.ts`), and deliberately the
|
|
5
5
|
* SAME contract: a project TypeScript module whose export builds a native
|
|
6
6
|
* `THREE.Object3D`. `project.bake.module` / `project.bake.preview` /
|
|
7
|
-
* `
|
|
7
|
+
* the editor's `screenshot <module>` command already define that contract; the live modeling
|
|
8
8
|
* document opens the same thing, so a module that bakes opens, and a module
|
|
9
9
|
* that opens bakes (docs/BLENDER-PARITY.md §The model file).
|
|
10
10
|
*
|