@volter/editor-blender 0.1.9 → 0.1.11

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.
@@ -26,8 +26,7 @@
26
26
  * WHAT IS DRAWN IS WHAT ACTS. A row Blender draws that this unit does not act
27
27
  * on is simply not here — never a row that opens a dialog we do not have, and
28
28
  * never one that no-ops. Where a row exists in Blender and is REFUSED here it
29
- * is drawn disabled with the refusal as its reason: Set Origin ▸ Origin to 3D
30
- * Cursor (there is no 3D cursor yet) and the four Add rows that want a FILE
29
+ * is drawn disabled with the refusal as its reason: the four Add rows that want a FILE
31
30
  * (Image ▸ Reference…/Background…/Mesh Plane…, Volume ▸ Import OpenVDB… —
32
31
  * `BlenderCreatableKind.refusal` carries each sentence). Add is the whole of
33
32
  * `VIEW3D_MT_add` now, so "not here" means Blender's menu-search row and
@@ -95,13 +94,9 @@ const ORIGIN_ROWS: readonly {
95
94
  // `type` enum's own UI names.
96
95
  { label: 'Geometry to Origin', type: 'GEOMETRY_ORIGIN' },
97
96
  { label: 'Origin to Geometry', type: 'ORIGIN_GEOMETRY' },
98
- {
99
- label: 'Origin to 3D Cursor',
100
- type: 'ORIGIN_CURSOR',
101
- refusal:
102
- 'There is no 3D cursor in this editor yet, so there is no point to move the origin to. ' +
103
- 'Blender places it with Shift+Right-click; that gesture is not built here.',
104
- },
97
+ // The cursor is `Scene.cursor`, placed with Shift+Right-click on the model
98
+ // (`blender-runtime.document.tsx`).
99
+ { label: 'Origin to 3D Cursor', type: 'ORIGIN_CURSOR' },
105
100
  ];
106
101
 
107
102
  /**
@@ -147,6 +147,13 @@ const OUTLINER_ICON_NAMES = [
147
147
  // under names of OURS for the same reason the render pair has them.
148
148
  ['outliner-exclude-on', 'checkbox_hlt'],
149
149
  ['outliner-exclude-off', 'checkbox_dehlt'],
150
+ // The navigation cluster's camera button (`view3d_gizmo_navigate.cc`): `VIEW_CAMERA_UNSELECTED`
151
+ // outside the camera view and `VIEW_CAMERA` in it, under names of OURS for the same reason.
152
+ ['viewport-camera', 'view_camera_unselected'],
153
+ ['viewport-camera-view', 'view_camera'],
154
+ // And, in a camera view, the lock to it: `VIEW_LOCKED` / `VIEW_UNLOCKED`.
155
+ ['viewport-camera-locked', 'view_locked'],
156
+ ['viewport-camera-unlocked', 'view_unlocked'],
150
157
  ];
151
158
 
152
159
  const TRACED = new URL('./blender.icons.traced.json', import.meta.url);
@@ -693,11 +700,30 @@ g('magnifying-glass', ring(7.1, 7.1, 3.7, 1.3) + cap(9.9, 9.9, 13.2, 13.2, 1.4))
693
700
  // and the round cap floated off as a separate lozenge at 16px — measured in
694
701
  // round 1's frame, where the navigation cluster showed a disc and a diamond.
695
702
  const zoomLens = (sign) => dot(7.1, 7.1, 4.25) + sign + cap(9.3, 9.3, 13.4, 13.4, 2);
696
- g(
697
- 'magnifying-glass-plus',
698
- zoomLens(holeBar(7.1, 5.2, 7.1, 9, 1.1) + holeBar(5.2, 7.1, 9, 7.1, 1.1)),
699
- );
700
- g('magnifying-glass-minus', zoomLens(holeBar(5.2, 7.1, 9, 7.1, 1.1)));
703
+ // THE PLUS IS ONE HOLE, a cross outline: two crossing hole bars wind the crossing twice, and
704
+ // under nonzero fill that square paints lens-coloured again (the frame showed a pale centre).
705
+ // Its arms are Blender's on `gizmo-select-box.png`'s cluster: 14 device px across a 26 px
706
+ // lens, 2.2 px thick.
707
+ const plusHole = (cx, cy, a, t) =>
708
+ poly(
709
+ [
710
+ [cx - t, cy - a],
711
+ [cx + t, cy - a],
712
+ [cx + t, cy - t],
713
+ [cx + a, cy - t],
714
+ [cx + a, cy + t],
715
+ [cx + t, cy + t],
716
+ [cx + t, cy + a],
717
+ [cx - t, cy + a],
718
+ [cx - t, cy + t],
719
+ [cx - a, cy + t],
720
+ [cx - a, cy - t],
721
+ [cx - t, cy - t],
722
+ ],
723
+ true,
724
+ );
725
+ g('magnifying-glass-plus', zoomLens(plusHole(7.1, 7.1, 2.3, 0.36)));
726
+ g('magnifying-glass-minus', zoomLens(holeBar(4.8, 7.1, 9.4, 7.1, 0.72)));
701
727
  g('ellipsis', dot(3.6, 8, 1.15) + dot(8, 8, 1.15) + dot(12.4, 8, 1.15));
702
728
  g('grip-lines', cap(3, 6.3, 13, 6.3, 1.2) + cap(3, 9.7, 13, 9.7, 1.2));
703
729
  g('grip-lines-vertical', cap(6.3, 3, 6.3, 13, 1.2) + cap(9.7, 3, 9.7, 13, 1.2));
@@ -1011,6 +1037,19 @@ const perspectiveGrid = () => {
1011
1037
  );
1012
1038
  };
1013
1039
  g('viewport-projection', perspectiveGrid());
1040
+ /*
1041
+ * VIEW_ORTHO, the same toggle in an orthographic view: a flat square grid of three by three
1042
+ * cells, as wide as the perspective trapezoid (32 device px on `gizmo-select-box.png`'s
1043
+ * navigation capsule at 2x) and centred with it, its rules the trapezoid's weight.
1044
+ */
1045
+ const orthographicGrid = () => {
1046
+ const lo = 2.61;
1047
+ const hi = 13.41;
1048
+ const w = 0.74;
1049
+ const at = [0, 1, 2, 3].map((i) => lo + ((hi - lo) * i) / 3);
1050
+ return at.map((v) => cap(lo, v, hi, v, w) + cap(v, lo, v, hi, w)).join('');
1051
+ };
1052
+ g('viewport-orthographic', orthographicGrid());
1014
1053
  g(
1015
1054
  'list',
1016
1055
  dot(3.2, 4.4, 1) +
@@ -1487,6 +1526,35 @@ g(
1487
1526
  [9.3, 4],
1488
1527
  ]),
1489
1528
  );
1529
+ /*
1530
+ * SELECT BOX. Measured on `gizmo-select-box.png` (the lit first button, native 2x) and mapped onto
1531
+ * the 16-unit grid with the marquee spanning 1..15: a dashed square, 0.7 units thick, dashes about
1532
+ * 2.3 units with 1.1-unit gaps and an L at each corner, in the selection tools' orange (the
1533
+ * `select` tone, `tonedPath`); inside it a pointer from its tip (6.1, 4.5) down to (6.2, 11.5),
1534
+ * notched at (8.2, 9.4), to (11.2, 9.4).
1535
+ */
1536
+ const MARQUEE_T = 0.7;
1537
+ const marquee =
1538
+ // corners: an L each
1539
+ rrect(1, 1, 2.7, MARQUEE_T) + rrect(1, 1, MARQUEE_T, 2.5) +
1540
+ rrect(12.3, 1, 2.7, MARQUEE_T) + rrect(14.3, 1, MARQUEE_T, 2.4) +
1541
+ rrect(1, 14.3, 2.5, MARQUEE_T) + rrect(1, 12.4, MARQUEE_T, 2.6) +
1542
+ rrect(12.3, 14.3, 2.7, MARQUEE_T) + rrect(14.3, 12.4, MARQUEE_T, 2.6) +
1543
+ // the dashes between
1544
+ rrect(5, 1, 2.2, MARQUEE_T) + rrect(8.4, 1, 2.3, MARQUEE_T) +
1545
+ rrect(5.5, 14.3, 2.4, MARQUEE_T) + rrect(9, 14.3, 2.4, MARQUEE_T) +
1546
+ rrect(1, 4.6, MARQUEE_T, 2.1) + rrect(1, 8, MARQUEE_T, 2.1) +
1547
+ rrect(14.3, 4.6, MARQUEE_T, 2.1) + rrect(14.3, 8, MARQUEE_T, 2.1);
1548
+ g(
1549
+ 'tool-select-box',
1550
+ poly([
1551
+ [6.1, 4.5],
1552
+ [11.2, 9.4],
1553
+ [8.2, 9.4],
1554
+ [6.2, 11.5],
1555
+ ]),
1556
+ { tone: 'select', tonedPath: marquee },
1557
+ );
1490
1558
  g(
1491
1559
  /**
1492
1560
  * TRANSFORM (all handles). Blender's (button 6): a solid square from
@@ -2442,7 +2510,7 @@ g(
2442
2510
  /** The union `IconCategoryTone` declares (`@vgai/editor-sdk/looks`), and the
2443
2511
  * reason `blender.style.ts` may narrow this JSON with an assertion: a tone
2444
2512
  * outside it never reaches the artifact. */
2445
- const TONES = ['object', 'modifier', 'material', 'tool', 'operator', 'data', 'scene', 'collection'];
2513
+ const TONES = ['object', 'modifier', 'material', 'tool', 'operator', 'data', 'scene', 'collection', 'select'];
2446
2514
 
2447
2515
  // The outliner's three names, aliased onto the drawings they share with the
2448
2516
  // generic glyphs. Same picture, its own name, so only the outliner is tinted.
@@ -57,7 +57,7 @@
57
57
  */
58
58
 
59
59
  import type { BlenderOutlinerRow } from '@volter/blender-engine/browser/rna';
60
- import type { ToolObject3DDocumentAuthoringFactory } from '@volter/editor-sdk/contributions';
60
+ import type { ToolObject3DDocumentAuthoringFactory } from '@volter/editor-threejs/object3d-contributions';
61
61
  import { editorHost } from '@volter/editor-sdk/host';
62
62
  import type {
63
63
  AuthoringAdapter,
@@ -75,6 +75,7 @@ import type {
75
75
  WriteAck,
76
76
  } from '@volter/editor-project/adapter';
77
77
  import type * as THREE from 'three';
78
+ import { threeObject } from '@volter/editor-threejs/adapter/three-contract';
78
79
  import { blenderExecute, blenderRnaSet, beginBlenderGesture, endBlenderGesture } from '../host/blender-runtime-host';
79
80
  import {
80
81
  blenderEngineSelection,
@@ -302,13 +303,10 @@ function blenderWorldMatrixRows(object: THREE.Object3D, root: THREE.Object3D): n
302
303
  * implementation of a primitive's defaults, its vertex count or its shading
303
304
  * here; the kind names the operator and the engine does the rest.
304
305
  *
305
- * `location=(0, 0, 0)` IS STATED RATHER THAN DEFAULTED, because Blender's own
306
- * answer is different: `VIEW3D_MT_add` runs every one of these with
307
- * `operator_context = 'EXEC_REGION_WIN'` and each `*_add` operator's `invoke`
308
- * places the new object at the 3D CURSOR. There is no 3D cursor in this editor
309
- * yet — it is not this unit's (WORK.md §B6) — so the origin is what the rows
310
- * below ask for explicitly instead of inheriting whatever `scene.cursor`
311
- * happens to hold. When a cursor arrives, these lose the argument.
306
+ * A NEW OBJECT LANDS AT THE 3D CURSOR, as in Blender: no row states a
307
+ * `location`, and an unset one is `scene->cursor.location`
308
+ * (`editors/object/object_add.cc`, `location_from_view`). The cursor is drawn
309
+ * and placed on the Model document (`blender-runtime-cursor.ts`).
312
310
  *
313
311
  * A ROW THAT NEEDS A FILE IS DRAWN DISABLED WITH THE REASON, never as a
314
312
  * gesture that cannot complete (the shape B6 set for Set Origin ▸ Origin to 3D
@@ -343,12 +341,6 @@ export interface BlenderCreatableKind {
343
341
  readonly group: string | null;
344
342
  /** The call, complete. */
345
343
  readonly call: string;
346
- /** `false` where Blender's operator has NO `location` property to place with:
347
- * the generic add options come from `add_generic_props`
348
- * (`editors/object/object_add.cc:431`), and the two rows here whose operator
349
- * never calls it are `object.quick_fur` and `image.import_as_mesh_planes`.
350
- * Passing one anyway is a `TypeError` from Blender, not a placement. */
351
- readonly placesAtOrigin?: boolean;
352
344
  /** This editor cannot run the row, and this is the sentence saying why — the
353
345
  * row is drawn DISABLED with it as its reason and the operator is never
354
346
  * called. */
@@ -444,7 +436,6 @@ export const CREATABLE_KINDS: readonly BlenderCreatableKind[] = [
444
436
  label: 'Fur',
445
437
  group: 'Curve',
446
438
  call: 'object.quick_fur',
447
- placesAtOrigin: false,
448
439
  },
449
440
  // `VIEW3D_MT_surface_add`, `space_view3d.py:2484-2492`.
450
441
  {
@@ -656,7 +647,6 @@ export const CREATABLE_KINDS: readonly BlenderCreatableKind[] = [
656
647
  label: 'Mesh Plane...',
657
648
  group: 'Image',
658
649
  call: 'image.import_as_mesh_planes',
659
- placesAtOrigin: false,
660
650
  refusal: needsAFilePicker('An image as a mesh plane', 'CANCELLED, with nothing created'),
661
651
  },
662
652
  {
@@ -825,11 +815,10 @@ export function blenderCreatableKinds(): readonly BlenderCreatableKind[] {
825
815
  return [...CREATABLE_KINDS, ...liveCollectionKinds()];
826
816
  }
827
817
 
828
- /** The complete `bpy.ops` expression a kind runs, origin included. */
818
+ /** The complete `bpy.ops` expression a kind runs. */
829
819
  function addCall(kind: BlenderCreatableKind): string {
830
820
  const [call, ...args] = kind.call.split(', ');
831
- const all = kind.placesAtOrigin === false ? args : [...args, 'location=(0, 0, 0)'];
832
- return `bpy.ops.${call}(${all.join(', ')})`;
821
+ return `bpy.ops.${call}(${args.join(', ')})`;
833
822
  }
834
823
 
835
824
  /** A Python string literal for one Blender name. JSON's own escaping is a
@@ -1059,7 +1048,7 @@ export const createBlenderOutlinerAuthoring: ToolObject3DDocumentAuthoringFactor
1059
1048
  */
1060
1049
  const rowIdFor = (id: string): string | null => {
1061
1050
  if (rows().has(id)) return id;
1062
- const object = defaultAdapter.hierarchy.object3D?.(id) ?? null;
1051
+ const object = threeObject(defaultAdapter.hierarchy, id);
1063
1052
  return object === null ? null : rowIdForObject(object);
1064
1053
  };
1065
1054
 
@@ -1096,7 +1085,7 @@ export const createBlenderOutlinerAuthoring: ToolObject3DDocumentAuthoringFactor
1096
1085
  return row === undefined ? null : nodeFor(row, state.parentOf.get(row.id) ?? null);
1097
1086
  },
1098
1087
  object3D: (id) => objectFor(id),
1099
- idForObject3D: (object) => rowIdForObject(object),
1088
+ idForObject3D: (object: THREE.Object3D) => rowIdForObject(object),
1100
1089
  };
1101
1090
 
1102
1091
  /** The Blender object NAMES our selection covers — what the door wants. */
@@ -193,17 +193,13 @@ const MAPPING = {
193
193
  'viewport.active': rgb('space_view3d.active'),
194
194
 
195
195
  // The transform gizmo draws the theme's axis colours as they are
196
- // (`transform_gizmo_3d.cc`, `gizmo_get_axis_color`).
196
+ // (`transform_gizmo_3d.cc`, `gizmo_get_axis_color`), and so does the navigation gizmo, mixed
197
+ // toward the viewport by each ball's depth as it draws (`view3d_gizmo_navigate_type.cc`; the
198
+ // stage computes the mix, so the measured (245,54,81) of `modeling-object-none.png` is `.tui.xaxis`
199
+ // at that ball's depth, not a colour of its own).
197
200
  'gizmo.x': rgb('tui.xaxis'),
198
201
  'gizmo.y': rgb('tui.yaxis'),
199
202
  'gizmo.z': rgb('tui.zaxis'),
200
- 'gizmo.navigationX': held(
201
- '#f53651',
202
- "the navigation gizmo's X ball as Blender draws it, read off `modeling-object-none.png` " +
203
- '(245,54,81): drawn from `.tui.xaxis` but not equal to it — measured.',
204
- ),
205
- 'gizmo.navigationY': held('#6fa41b', 'the same ball for Y (111,164,27) — measured, see navigationX.'),
206
- 'gizmo.navigationZ': held('#2e83e3', 'the same ball for Z (46,131,227) — measured, see navigationX.'),
207
203
 
208
204
  'region.outliner': rgb('space_outliner.back'),
209
205
  'region.properties': rgb('space_properties.back'),
@@ -224,6 +220,11 @@ const MAPPING = {
224
220
  '`DEF_ICON`, drawn in `.text`. Measured on the reference frames, and a level below ' +
225
221
  '`.tui.icon_scene` #cccccc, which is what it sits beside.',
226
222
  ),
223
+ 'category.select': held(
224
+ '#ffaf2a',
225
+ "Blender's Select Box marquee, baked into the tool icon's geometry (`ops.generic.select_box`), " +
226
+ 'not a theme member. Measured on `gizmo-select-box.png`: (255,175,42).',
227
+ ),
227
228
  'category.operator': held(
228
229
  '#95dab2',
229
230
  "Blender's edit-mode tool column tints its creating operators green; no theme member carries " +
@@ -207,6 +207,12 @@ export class BlenderSkinDirector {
207
207
  };
208
208
  }
209
209
 
210
+ /** The frame on screen, or null before the clip door has been read (when `frame()` can only
211
+ * say 1). */
212
+ playhead(): number | null {
213
+ return this.#clip ? this.frame() : null;
214
+ }
215
+
210
216
  /** The mixer's time as a Blender frame. With no clip the scene's own current
211
217
  * frame stands, which is Blender's answer for a file with no animation. */
212
218
  frame(): number {
@@ -25,16 +25,30 @@
25
25
  */
26
26
 
27
27
  // How the `model` stage this document builds behaves (its starting presentation).
28
- import '../src/presentation';
28
+ import { blenderViewFieldOfView } from '../src/presentation';
29
29
  import { blenderModelView } from '@volter/blender-engine/browser/three/blender-runtime-view';
30
30
  import type { ToolContributionProps, ToolDocumentToolbar } from '@volter/editor-sdk/contributions';
31
31
  import { editorHost } from '@volter/editor-sdk/host';
32
- import { useCallback, useEffect, useRef, useState } from 'react';
32
+ import {
33
+ DOCUMENT_STUDIO_PRESET,
34
+ subscribeViewportPresentation,
35
+ viewPresentation,
36
+ } from '@volter/editor-sdk/kit/viewport-presentation';
37
+ import {
38
+ type MouseEvent as ReactMouseEvent,
39
+ type PointerEvent as ReactPointerEvent,
40
+ useCallback,
41
+ useEffect,
42
+ useRef,
43
+ useState,
44
+ useSyncExternalStore,
45
+ } from 'react';
33
46
  import * as THREE from 'three';
34
- import { bindModelDocument, openModelDocumentBlend } from '../host/blender-runtime-host';
47
+ import { bindModelDocument, blenderExecute, openModelDocumentBlend } from '../host/blender-runtime-host';
35
48
  import { BlenderObjectModeHeader } from './blender-header-menus';
36
49
  import { createBlenderOutlinerAuthoring } from './blender-outliner-authoring';
37
50
  import { blenderSkin } from './blender-runtime-skin';
51
+ import { onViewportStages, viewportStages } from '@volter/editor-threejs/viewport-door';
38
52
 
39
53
  export const point = 'workspace.document';
40
54
  export const title = 'Blender Model';
@@ -80,6 +94,51 @@ export const inspectorBuiltins: readonly string[] = [];
80
94
  // Timeline binds a skeleton into the same presented graph and there is exactly
81
95
  // one set of presented objects (`blender-runtime-skin.ts`).
82
96
  const view = blenderModelView;
97
+ /** THE SUBJECT LINE'S FRAME IS THE ONE ON SCREEN: the skin's playhead, which during playback
98
+ * runs ahead of `frame_current` (written once, on pause), as Blender's own `(frame)` does. */
99
+ const subjectOfView = () => {
100
+ const playhead = blenderSkin.playhead();
101
+ return view.subjectLine(playhead === null ? null : Math.floor(playhead));
102
+ };
103
+ const gridScale = (worldPerDevicePixel: number) => view.gridUnitName(worldPerDevicePixel);
104
+ /** BLENDER'S CAMERA VIEW (`blender-runtime-camera-view.ts`): which camera, and how it fits. */
105
+ const cameraView = {
106
+ camera: () => view.cameraViewCamera(),
107
+ zoom: view.cameraViewZoom,
108
+ pan: view.cameraViewPan,
109
+ showing: (camera: string | null) => view.setCameraViewShowing(camera),
110
+ // A LOCKED camera view moves the camera (`ED_view3d_camera_lock_sync`: its scale kept); the
111
+ // navigation's end is the one step Blender's history records.
112
+ setPose: (
113
+ camera: string,
114
+ position: readonly [number, number, number],
115
+ quaternion: readonly [number, number, number, number],
116
+ final: boolean,
117
+ ) => {
118
+ const { location, rotation } = view.blenderPose(position, quaternion);
119
+ return blenderExecute(
120
+ 'from mathutils import Matrix, Quaternion, Vector\n' +
121
+ `import bpy\nobject = bpy.data.objects[${JSON.stringify(camera)}]\n` +
122
+ `object.matrix_world = Matrix.LocRotScale(Vector((${location.join(', ')})), ` +
123
+ `Quaternion((${rotation.join(', ')})), object.matrix_world.to_scale())`,
124
+ final,
125
+ 'Lock Camera to View',
126
+ ).then(
127
+ (answer) => {
128
+ if (answer.error !== null)
129
+ editorHost().console.error(`Blender refused the camera's pose: ${answer.error}`, 'blender-camera');
130
+ },
131
+ (error: unknown) =>
132
+ editorHost().console.error(`The camera's pose was not written to Blender: ${String(error)}`, 'blender-camera'),
133
+ );
134
+ },
135
+ view: (
136
+ camera: string,
137
+ region: { readonly width: number; readonly height: number },
138
+ zoom: number,
139
+ offset: readonly [number, number],
140
+ ) => view.cameraView(camera, region, zoom, offset),
141
+ };
83
142
 
84
143
  export default function BlenderModelDocument(props: ToolContributionProps) {
85
144
  const { active, document, documentId, notify, publishContext } = props;
@@ -153,6 +212,30 @@ function BlenderModelViewport({
153
212
  [],
154
213
  );
155
214
  const blend = document?.source?.path;
215
+ // Re-read on the engine's frames, the skin's publications and every drawn frame of this stage
216
+ // (playback moves the playhead with no publication); the snapshot is a string, so only a
217
+ // changed line re-renders.
218
+ const subscribeSubject = useCallback(
219
+ (listener: () => void) => {
220
+ const stopView = view.onChange(listener);
221
+ const stopSkin = blenderSkin.subscribe(listener);
222
+ let stopFrame: (() => void) | null = null;
223
+ const bind = (): void => {
224
+ stopFrame?.();
225
+ stopFrame = viewportStages().find((one) => one.documentId === documentId)?.onFrame(listener) ?? null;
226
+ };
227
+ bind();
228
+ const stopStages = onViewportStages(bind);
229
+ return () => {
230
+ stopView();
231
+ stopSkin();
232
+ stopStages();
233
+ stopFrame?.();
234
+ };
235
+ },
236
+ [documentId],
237
+ );
238
+ const subject = useSyncExternalStore(subscribeSubject, subjectOfView);
156
239
  /**
157
240
  * THE INSPECTION OVERLAYS ARE HELPERS, and the Helpers menu owns them
158
241
  * (WORK.md §Blender in the tab is Blender, "Inspection parity", I4).
@@ -169,18 +252,17 @@ function BlenderModelViewport({
169
252
  */
170
253
  useEffect(() => {
171
254
  if (!documentId) return;
172
- const { viewport } = editorHost();
173
255
  const groups = view.overlayGroups();
174
256
  const apply = (): void => {
175
- const stage = viewport.stages().find((one) => one.documentId === documentId);
257
+ const stage = viewportStages().find((one) => one.documentId === documentId);
176
258
  if (!stage) return;
177
259
  for (const { kind, object } of groups) stage.setHelper(kind, object);
178
260
  };
179
261
  apply();
180
- const stop = viewport.onStages(apply);
262
+ const stop = onViewportStages(apply);
181
263
  return () => {
182
264
  stop();
183
- const stage = viewport.stages().find((one) => one.documentId === documentId);
265
+ const stage = viewportStages().find((one) => one.documentId === documentId);
184
266
  for (const { kind } of groups) stage?.setHelper(kind, null);
185
267
  };
186
268
  }, [documentId]);
@@ -212,14 +294,125 @@ function BlenderModelViewport({
212
294
  detach?.();
213
295
  };
214
296
  }, [documentId]);
297
+ /**
298
+ * BLENDER'S RENDERED SHADING IS THE SCENE'S OWN LIGHT. When this stage's view lights by the
299
+ * `scene` (the Rendered shading cell, or the Lighting row's Scene), the presenter holds the viewport
300
+ * in the lighting its render photographs with (`BlenderRuntimeView.holdRendered`): the
301
+ * scene's lights, its World, `hide_render` and shadows, through the camera the stage draws
302
+ * with, re-applied from the stage's frame loop when that camera or the World changes. Any
303
+ * other source returns it to modeling. The stage itself draws a `scene` source unlit
304
+ * (`standard-viewport-dressing.ts`), because Blender's lights live in the presenter.
305
+ */
306
+ useEffect(() => {
307
+ if (!documentId) return;
308
+ const stageOf = () => viewportStages().find((one) => one.documentId === documentId);
309
+ let framed: ReturnType<typeof stageOf> = undefined;
310
+ let stopFrame: (() => void) | null = null;
311
+ // One getter for the life of the effect, so holding again is not a change; the stand-in is
312
+ // for the moment a stage has left and `onStages` has not yet released the hold.
313
+ const standIn = new THREE.PerspectiveCamera();
314
+ const drawCamera = (): THREE.Camera => stageOf()?.rig().drawCamera() ?? standIn;
315
+ const apply = (): void => {
316
+ const stage = stageOf();
317
+ if (stage !== framed) {
318
+ stopFrame?.();
319
+ stopFrame = stage ? stage.onFrame(() => view.refreshRendered()) : null;
320
+ framed = stage;
321
+ }
322
+ const { lighting } = viewPresentation(documentId);
323
+ view.holdRendered(lighting.source === 'scene' && stage ? drawCamera : null);
324
+ // BLENDER'S SOLID IS BLENDER'S OWN FUNCTION: while the stage lights by Blender's studio, the
325
+ // presenter draws every surface by it (`blender-workbench-material.ts`).
326
+ view.setWorkbench(lighting.source === 'studio' && lighting.studioPreset === DOCUMENT_STUDIO_PRESET.id);
327
+ };
328
+ apply();
329
+ const stopPresentation = subscribeViewportPresentation(apply);
330
+ const stopStages = onViewportStages(apply);
331
+ return () => {
332
+ stopPresentation();
333
+ stopStages();
334
+ stopFrame?.();
335
+ view.holdRendered(null);
336
+ view.setWorkbench(false);
337
+ };
338
+ }, [documentId]);
339
+ /**
340
+ * SHIFT+RIGHT-CLICK PLACES THE 3D CURSOR, Blender's own chord for
341
+ * `view3d.cursor3d` (`blender_default.py`, `params.cursor_set_event`). The
342
+ * press is taken before the stage's orbit sees it, and a press that moves
343
+ * is not a click. A press that never came back (released outside the page,
344
+ * cancelled) is dropped by the next press or the cancel, so it can never
345
+ * swallow a later orbit's release. The drawn cursor moves at once (`placeCursor`); Blender
346
+ * then writes `Scene.cursor` with no history step, as its operator pushes
347
+ * no undo.
348
+ */
349
+ const cursorPress = useRef<{ id: number; x: number; y: number } | null>(null);
350
+ const cursorChord = (event: ReactPointerEvent | ReactMouseEvent): boolean =>
351
+ event.button === 2 && event.shiftKey && event.target instanceof HTMLCanvasElement;
352
+ const onPointerDownCapture = (event: ReactPointerEvent): void => {
353
+ if (!cursorChord(event)) {
354
+ cursorPress.current = null;
355
+ return;
356
+ }
357
+ event.preventDefault();
358
+ event.stopPropagation();
359
+ cursorPress.current = { id: event.pointerId, x: event.clientX, y: event.clientY };
360
+ // A real pointer is captured so its release comes back here; a synthetic one cannot be.
361
+ if (event.isTrusted) (event.target as Element).setPointerCapture(event.pointerId);
362
+ };
363
+ const dropCursorPress = (): void => {
364
+ cursorPress.current = null;
365
+ };
366
+ const onPointerUpCapture = (event: ReactPointerEvent): void => {
367
+ const press = cursorPress.current;
368
+ if (press === null || event.pointerId !== press.id || event.button !== 2) return;
369
+ cursorPress.current = null;
370
+ event.preventDefault();
371
+ event.stopPropagation();
372
+ if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > 3) return;
373
+ const placed = view.placeCursor(event.clientX, event.clientY, event.target);
374
+ if (placed === null) return;
375
+ const [x, y, z] = placed.location;
376
+ const [qw, qx, qy, qz] = placed.rotation;
377
+ void blenderExecute(
378
+ 'from mathutils import Matrix, Quaternion, Vector\n' +
379
+ `bpy.context.scene.cursor.matrix = Matrix.LocRotScale(Vector((${x}, ${y}, ${z})), ` +
380
+ `Quaternion((${qw}, ${qx}, ${qy}, ${qz})), None)`,
381
+ false,
382
+ 'Set 3D Cursor',
383
+ ).then(
384
+ (answer) => {
385
+ if (answer.error !== null)
386
+ editorHost().console.error(`Blender refused the 3D cursor: ${answer.error}`, 'blender-cursor');
387
+ },
388
+ (error: unknown) =>
389
+ editorHost().console.error(`The 3D cursor was not written to Blender: ${String(error)}`, 'blender-cursor'),
390
+ );
391
+ };
392
+ const onContextMenuCapture = (event: ReactMouseEvent): void => {
393
+ if (cursorChord(event)) event.preventDefault();
394
+ };
215
395
  if (!documentId) return null;
216
396
  const Surface = surfaces.Object3DAuthoring;
217
397
  return (
398
+ <div
399
+ style={{ display: 'contents' }}
400
+ onPointerDownCapture={onPointerDownCapture}
401
+ onPointerUpCapture={onPointerUpCapture}
402
+ onPointerCancelCapture={dropCursorPress}
403
+ onLostPointerCaptureCapture={dropCursorPress}
404
+ onContextMenuCapture={onContextMenuCapture}
405
+ >
218
406
  <Surface
219
407
  active={active ?? true}
220
408
  documentId={documentId}
221
409
  sourcePath={blend ?? 'blender:runtime'}
222
410
  displayName={document?.label ?? 'Model'}
411
+ // THE OVERLAY'S SUBJECT AND GRID LINES ARE BLENDER'S: the engine composes the subject from
412
+ // the scene (`session.py` `_subject_line`) and names the grid's step in the scene's units.
413
+ {...(subject === null ? {} : { subject })}
414
+ gridScale={gridScale}
415
+ cameraView={cameraView}
223
416
  build={build}
224
417
  audit={false}
225
418
  // ON A MODEL DOCUMENT THE HIERARCHY IS BLENDER'S OUTLINER, for the same
@@ -232,12 +425,17 @@ function BlenderModelViewport({
232
425
  // "Inspection parity", I3).
233
426
  authoring={createBlenderOutlinerAuthoring}
234
427
  cameraDirection={[0.8187, 0.4458, 0.3617]}
235
- // AND AS FAR BACK AS BLENDER'S STARTUP VIEW STANDS. The direction alone
236
- // put the cube where Blender's is but FILLING the frame: the factory
237
- // view is 18.39 units from a 2 m cube (`region_3d.view_distance` at
238
- // `--factory-startup`), about a third of the size a bare fit gives, which
239
- // is the number `openingFit` was written for and nothing was passing.
428
+ // FOR A FILE THAT SAVED NO 3D VIEW: Blender's factory Modeling direction, standing back
429
+ // three fits.
240
430
  openingFit={3}
431
+ // WHERE BLENDER OPENS THE FILE: its own saved 3D View, when it holds one; the direction and
432
+ // fit above are the fallback for a file that saved none.
433
+ openingView={view.savedView()}
434
+ // The file's own lens, as the document's presentation (Blender's arithmetic in degrees).
435
+ presentation={(() => {
436
+ const saved = view.savedView();
437
+ return saved ? { camera: { fov: blenderViewFieldOfView(saved.lens) } } : null;
438
+ })()}
241
439
  // Every entry this document opens is a `model` stage, the standing `blender:runtime`
242
440
  // address included (its id carries no `model:` prefix).
243
441
  stageKind={documentKind}
@@ -254,22 +452,15 @@ function BlenderModelViewport({
254
452
  // The BACKGROUND stays the dressing's, because the Blender palette
255
453
  // already paints the viewport its own flat grey.
256
454
  //
257
- // AND THE VIEW TRANSFORM IS BLENDER'S. Blender's factory scene is AgX
258
- // (`view_settings.view_transform`), and this document's own RENDER path
259
- // already photographs through `THREE.AgXToneMapping` (`presenter.ts`),
260
- // so the viewport was the one surface in the chain running a different
261
- // curve from the thing it frames. MEASURED on the factory cube, our own
262
- // radiance through each operator against Blender's (141,143,145)/
263
- // (129,131,131)/(111,112,113): ACES lands the three faces within 6
264
- // levels with a spread of 40 where Blender's is 30 — the curve, not the
265
- // lights, is what was left of row 1's spread — and AgX within 3 at a
266
- // spread of 26.
455
+ // THE VIEW TRANSFORM IS EACH SHADING MODE'S OWN (`src/presentation.ts`: Standard for Solid,
456
+ // AgX for Material Preview and Rendered), so the dressing states none: a document mapper
457
+ // would outrank every mode's.
267
458
  dressing={{
268
459
  environment: false,
269
460
  keyLight: false,
270
- toneMapping: THREE.AgXToneMapping,
271
461
  viewLocked: view.studioLights(),
272
462
  }}
273
463
  />
464
+ </div>
274
465
  );
275
466
  }
@@ -911,10 +911,10 @@
911
911
  "path": "M1.5 6.9A5.4 5.4 0 1 0 12.3 6.9A5.4 5.4 0 1 0 1.5 6.9ZM3.28 6.9A3.63 3.63 0 1 1 10.53 6.9A3.63 3.63 0 1 1 3.28 6.9ZM9.64 10.99L13.67 15.02A0.96 0.96 0 0 0 15.02 13.67L10.99 9.64A0.96 0.96 0 0 0 9.64 10.99Z"
912
912
  },
913
913
  "magnifying-glass-minus": {
914
- "path": "M1.72 6.9A5.19 5.19 0 1 0 12.09 6.9A5.19 5.19 0 1 0 1.72 6.9ZM4.58 7.57L4.58 6.23L9.22 6.23L9.22 7.57ZM8.62 10.55L13.62 15.55A1.37 1.37 0 0 0 15.55 13.62L10.55 8.62A1.37 1.37 0 0 0 8.62 10.55Z"
914
+ "path": "M1.72 6.9A5.19 5.19 0 1 0 12.09 6.9A5.19 5.19 0 1 0 1.72 6.9ZM4.1 7.34L4.1 6.46L9.71 6.46L9.71 7.34ZM8.62 10.55L13.62 15.55A1.37 1.37 0 0 0 15.55 13.62L10.55 8.62A1.37 1.37 0 0 0 8.62 10.55Z"
915
915
  },
916
916
  "magnifying-glass-plus": {
917
- "path": "M1.72 6.9A5.19 5.19 0 1 0 12.09 6.9A5.19 5.19 0 1 0 1.72 6.9ZM6.23 4.58L7.57 4.58L7.57 9.22L6.23 9.22ZM4.58 7.57L4.58 6.23L9.22 6.23L9.22 7.57ZM8.62 10.55L13.62 15.55A1.37 1.37 0 0 0 15.55 13.62L10.55 8.62A1.37 1.37 0 0 0 8.62 10.55Z"
917
+ "path": "M1.72 6.9A5.19 5.19 0 1 0 12.09 6.9A5.19 5.19 0 1 0 1.72 6.9ZM6.46 4.1L7.34 4.1L7.34 6.46L9.71 6.46L9.71 7.34L7.34 7.34L7.34 9.71L6.46 9.71L6.46 7.34L4.1 7.34L4.1 6.46L6.46 6.46ZM8.62 10.55L13.62 15.55A1.37 1.37 0 0 0 15.55 13.62L10.55 8.62A1.37 1.37 0 0 0 8.62 10.55Z"
918
918
  },
919
919
  "maximize": {
920
920
  "path": "M5.8 0.28L1.17 0.28A0.89 0.89 0 0 0 1.17 2.06L5.8 2.06A0.89 0.89 0 0 0 5.8 0.28ZM0.28 1.17L0.28 5.8A0.89 0.89 0 0 0 2.06 5.8L2.06 1.17A0.89 0.89 0 0 0 0.28 1.17ZM10.2 2.06L14.83 2.06A0.89 0.89 0 0 0 14.83 0.28L10.2 0.28A0.89 0.89 0 0 0 10.2 2.06ZM13.94 1.17L13.94 5.8A0.89 0.89 0 0 0 15.72 5.8L15.72 1.17A0.89 0.89 0 0 0 13.94 1.17ZM5.8 13.94L1.17 13.94A0.89 0.89 0 0 0 1.17 15.72L5.8 15.72A0.89 0.89 0 0 0 5.8 13.94ZM2.06 14.83L2.06 10.2A0.89 0.89 0 0 0 0.28 10.2L0.28 14.83A0.89 0.89 0 0 0 2.06 14.83ZM10.2 15.72L14.83 15.72A0.89 0.89 0 0 0 14.83 13.94L10.2 13.94A0.89 0.89 0 0 0 10.2 15.72ZM15.72 14.83L15.72 10.2A0.89 0.89 0 0 0 13.94 10.2L13.94 14.83A0.89 0.89 0 0 0 15.72 14.83Z"
@@ -1207,6 +1207,11 @@
1207
1207
  "tool-scale": {
1208
1208
  "path": "M1.17 1.1L14.83 1.1A0.55 0.55 0 0 0 14.83 0.01L1.17 0.01A0.55 0.55 0 0 0 1.17 1.1ZM14.29 0.56L14.29 14.22A0.55 0.55 0 0 0 15.38 14.22L15.38 0.56A0.55 0.55 0 0 0 14.29 0.56ZM14.83 13.68L1.17 13.68A0.55 0.55 0 0 0 1.17 14.77L14.83 14.77A0.55 0.55 0 0 0 14.83 13.68ZM1.71 14.22L1.71 0.56A0.55 0.55 0 0 0 0.62 0.56L0.62 14.22A0.55 0.55 0 0 0 1.71 14.22ZM0.31 7.51A0.24 0.24 0 0 0 0.07 7.76L0.07 14.95A0.24 0.24 0 0 0 0.31 15.2L7.51 15.2A0.24 0.24 0 0 0 7.76 14.95L7.76 7.76A0.24 0.24 0 0 0 7.51 7.51ZM9.19 7.12L11.75 4.55A0.65 0.65 0 0 0 10.84 3.64L8.27 6.2A0.65 0.65 0 0 0 9.19 7.12ZM9.59 3.12L12.27 5.8L13.37 2.02Z"
1209
1209
  },
1210
+ "tool-select-box": {
1211
+ "path": "M5.8 12.27L8.24 9.71L11.9 9.71L5.68 3.73Z",
1212
+ "tone": "select",
1213
+ "tonedPath": "M-0.54 0.31L2.75 0.31L2.75 -0.54L-0.54 -0.54ZM-0.54 2.51L0.31 2.51L0.31 -0.54L-0.54 -0.54ZM13.25 0.31L16.54 0.31L16.54 -0.54L13.25 -0.54ZM15.69 2.39L16.54 2.39L16.54 -0.54L15.69 -0.54ZM-0.54 16.54L2.51 16.54L2.51 15.69L-0.54 15.69ZM-0.54 16.54L0.31 16.54L0.31 13.37L-0.54 13.37ZM13.25 16.54L16.54 16.54L16.54 15.69L13.25 15.69ZM15.69 16.54L16.54 16.54L16.54 13.37L15.69 13.37ZM4.34 0.31L7.02 0.31L7.02 -0.54L4.34 -0.54ZM8.49 0.31L11.29 0.31L11.29 -0.54L8.49 -0.54ZM4.95 16.54L7.88 16.54L7.88 15.69L4.95 15.69ZM9.22 16.54L12.15 16.54L12.15 15.69L9.22 15.69ZM-0.54 6.41L0.31 6.41L0.31 3.85L-0.54 3.85ZM-0.54 10.56L0.31 10.56L0.31 8L-0.54 8ZM15.69 6.41L16.54 6.41L16.54 3.85L15.69 3.85ZM15.69 10.56L16.54 10.56L16.54 8L15.69 8Z"
1214
+ },
1210
1215
  "tool-transform": {
1211
1216
  "path": "M4.83 4.52A0.31 0.31 0 0 0 4.52 4.83L4.52 11.17A0.31 0.31 0 0 0 4.83 11.48L11.17 11.48A0.31 0.31 0 0 0 11.48 11.17L11.48 4.83A0.31 0.31 0 0 0 11.17 4.52ZM8 -1.64L5.93 1.41L10.07 1.41ZM8 17.64L10.07 14.59L5.93 14.59ZM-1.64 8L1.41 10.07L1.41 5.93ZM17.64 8L14.59 5.93L14.59 10.07ZM15.55 10.75L15.09 11.77L14.5 12.72L13.78 13.58L12.95 14.33L12.02 14.96L11.01 15.45L11.48 16.61L12.65 16.05L13.72 15.32L14.68 14.45L15.52 13.46L16.2 12.36L16.73 11.18ZM15.51 10.96A0.63 0.63 0 1 0 16.77 10.96A0.63 0.63 0 1 0 15.51 10.96ZM10.62 16.03A0.63 0.63 0 1 0 11.87 16.03A0.63 0.63 0 1 0 10.62 16.03ZM5.25 15.55L4.23 15.09L3.28 14.5L2.42 13.78L1.67 12.95L1.04 12.02L0.55 11.01L-0.61 11.48L-0.05 12.65L0.68 13.72L1.55 14.68L2.54 15.52L3.64 16.2L4.82 16.73ZM4.41 16.14A0.63 0.63 0 1 0 5.67 16.14A0.63 0.63 0 1 0 4.41 16.14ZM-0.66 11.24A0.63 0.63 0 1 0 0.6 11.24A0.63 0.63 0 1 0 -0.66 11.24ZM0.45 5.25L0.91 4.23L1.5 3.28L2.22 2.42L3.05 1.67L3.98 1.04L4.99 0.55L4.52 -0.61L3.35 -0.05L2.28 0.68L1.32 1.55L0.48 2.54L-0.2 3.64L-0.73 4.82ZM-0.77 5.04A0.63 0.63 0 1 0 0.49 5.04A0.63 0.63 0 1 0 -0.77 5.04ZM4.13 -0.03A0.63 0.63 0 1 0 5.38 -0.03A0.63 0.63 0 1 0 4.13 -0.03ZM10.75 0.45L11.77 0.91L12.72 1.5L13.58 2.22L14.33 3.05L14.96 3.98L15.45 4.99L16.61 4.52L16.05 3.35L15.32 2.28L14.45 1.32L13.46 0.48L12.36 -0.2L11.18 -0.73ZM10.33 -0.14A0.63 0.63 0 1 0 11.59 -0.14A0.63 0.63 0 1 0 10.33 -0.14ZM15.4 4.76A0.63 0.63 0 1 0 16.66 4.76A0.63 0.63 0 1 0 15.4 4.76Z"
1212
1217
  },
@@ -1225,6 +1230,21 @@
1225
1230
  "users": {
1226
1231
  "path": "M1.78 4.34A3.05 3.05 0 1 0 7.88 4.34A3.05 3.05 0 1 0 1.78 4.34ZM0.31 13.61L0.37 12.91L0.53 12.22L0.81 11.56L1.18 10.96L1.64 10.42L2.17 9.96L2.78 9.59L3.43 9.32L4.12 9.15L4.83 9.1L5.53 9.15L6.22 9.32L6.88 9.59L7.48 9.96L8.02 10.42L8.48 10.96L8.85 11.56L9.12 12.22L9.29 12.91L9.34 13.61L11.05 13.61L10.97 12.64L10.75 11.69L10.37 10.79L9.86 9.95L9.23 9.21L8.49 8.58L7.65 8.07L6.75 7.69L5.8 7.47L4.83 7.39L3.85 7.47L2.91 7.69L2 8.07L1.17 8.58L0.43 9.21L-0.21 9.95L-0.72 10.79L-1.09 11.69L-1.32 12.64L-1.39 13.61ZM-1.39 13.61A0.85 0.85 0 1 0 0.31 13.61A0.85 0.85 0 1 0 -1.39 13.61ZM9.34 13.61A0.85 0.85 0 1 0 11.05 13.61A0.85 0.85 0 1 0 9.34 13.61ZM11.54 3.16L11.8 3.09L12.07 3.05L12.35 3.06L12.61 3.11L12.87 3.21L13.11 3.33L13.33 3.5L13.52 3.7L13.67 3.92L13.79 4.16L13.88 4.42L13.92 4.69L13.92 4.96L13.88 5.23L13.79 5.49L13.67 5.74L13.52 5.96L13.33 6.16L13.11 6.32L12.87 6.45L12.61 6.54L12.35 6.59L12.07 6.6L11.8 6.57L11.54 6.5L11 7.97L11.5 8.11L12.01 8.17L12.52 8.15L13.03 8.06L13.51 7.89L13.96 7.64L14.37 7.33L14.73 6.96L15.02 6.54L15.25 6.08L15.41 5.59L15.49 5.08L15.49 4.57L15.41 4.06L15.25 3.57L15.02 3.11L14.73 2.69L14.37 2.32L13.96 2.01L13.51 1.77L13.03 1.6L12.52 1.5L12.01 1.48L11.5 1.54L11 1.68ZM10.49 2.42A0.79 0.79 0 1 0 12.06 2.42A0.79 0.79 0 1 0 10.49 2.42ZM10.49 7.24A0.79 0.79 0 1 0 12.06 7.24A0.79 0.79 0 1 0 10.49 7.24ZM9.04 11.66L9.26 11.16L9.56 10.71L9.92 10.3L10.34 9.95L10.81 9.68L11.32 9.47L11.85 9.35L12.39 9.31L12.94 9.35L13.47 9.47L13.97 9.68L14.44 9.95L14.86 10.3L15.23 10.71L15.52 11.16L15.75 11.66L17.29 11.1L16.96 10.37L16.53 9.71L16 9.12L15.38 8.61L14.7 8.21L13.96 7.91L13.18 7.73L12.39 7.67L11.6 7.73L10.82 7.91L10.09 8.21L9.4 8.61L8.79 9.12L8.26 9.71L7.82 10.37L7.49 11.1ZM7.45 11.38A0.82 0.82 0 1 0 9.08 11.38A0.82 0.82 0 1 0 7.45 11.38ZM15.7 11.38A0.82 0.82 0 1 0 17.34 11.38A0.82 0.82 0 1 0 15.7 11.38Z"
1227
1232
  },
1233
+ "viewport-camera": {
1234
+ "path": "M4 12L4 10.5C4 10.22 4.22 10 4.5 10L10.5 10C10.78 10 11 10.22 11 10.5L11 15.5C11 15.78 10.78 16 10.5 16L5.71 16C5.57 16 5.45 15.95 5.35 15.85L4.15 14.65C4.05 14.55 4 14.43 4 14.29L4 13L3 13L3 13.5C3 13.78 2.78 14 2.5 14C2.22 14 2 13.78 2 13.5L2 11.5C2 11.22 2.22 11 2.5 11C2.78 11 3 11.22 3 11.5L3 12ZM5.91 15L5 14.09L5 11L10 11L10 15ZM13.22 13.5L15 14.59L15 12.41ZM15.15 15.85C15.24 15.94 15.36 16 15.5 16C15.78 16 16 15.78 16 15.5L16 11.5C16 11.22 15.78 11 15.5 11C15.36 11 15.24 11.06 15.15 11.15L12.04 13.04C11.87 13.12 11.75 13.3 11.75 13.5C11.75 13.7 11.87 13.88 12.04 13.96ZM6.38 4.22C6.02 4.3 5.65 4.17 5.41 3.89C4.95 3.34 4.27 3 3.5 3C2.12 3 1 4.12 1 5.5C1 6.88 2.12 8 3.5 8L10.5 8C12.43 8 14 6.43 14 4.5C14 2.57 12.43 1 10.5 1C8.91 1 7.56 2.06 7.14 3.52C7.04 3.88 6.75 4.15 6.38 4.22ZM6.61 2.24C6.42 2.56 6.28 2.89 6.18 3.25C5.94 2.97 5.67 2.73 5.36 2.54C4.82 2.2 4.18 2 3.5 2C1.57 2 0 3.57 0 5.5C0 7.43 1.57 9 3.5 9L10.5 9C12.99 9 15 6.99 15 4.5C15 2.01 12.99 0 10.5 0C8.84 0 7.38 0.9 6.61 2.24ZM10.5 5C10.78 5 11 4.78 11 4.5C11 4.22 10.78 4 10.5 4C10.22 4 10 4.22 10 4.5C10 4.78 10.22 5 10.5 5ZM10.5 6C11.33 6 12 5.33 12 4.5C12 3.67 11.33 3 10.5 3C9.67 3 9 3.67 9 4.5C9 5.33 9.67 6 10.5 6ZM4.5 5.5C4.5 6.05 4.05 6.5 3.5 6.5C2.95 6.5 2.5 6.05 2.5 5.5C2.5 4.95 2.95 4.5 3.5 4.5C4.05 4.5 4.5 4.95 4.5 5.5Z"
1235
+ },
1236
+ "viewport-camera-locked": {
1237
+ "path": "M3 7L3 5C3 2.24 5.24 0 8 0C10.76 0 13 2.24 13 5L13 7L13.5 7C13.78 7 14 7.22 14 7.5L14 15.5C14 15.78 13.78 16 13.5 16L2.5 16C2.22 16 2 15.78 2 15.5L2 7.5C2 7.22 2.22 7 2.5 7ZM4 5C4 2.79 5.79 1 8 1C10.21 1 12 2.79 12 5L12 7L4 7ZM8 10C7.45 10 7 10.45 7 11C7 11.37 7.2 11.69 7.5 11.87L7.5 13C7.5 13.28 7.72 13.5 8 13.5C8.28 13.5 8.5 13.28 8.5 13L8.5 11.87C8.8 11.69 9 11.37 9 11C9 10.45 8.55 10 8 10Z"
1238
+ },
1239
+ "viewport-camera-unlocked": {
1240
+ "path": "M8 10C7.45 10 7 10.45 7 11C7 11.37 7.2 11.69 7.5 11.87L7.5 13C7.5 13.28 7.72 13.5 8 13.5C8.28 13.5 8.5 13.28 8.5 13L8.5 11.87C8.8 11.69 9 11.37 9 11C9 10.45 8.55 10 8 10ZM8 1C5.79 1 4 2.79 4 5L4 7L13.5 7C13.78 7 14 7.22 14 7.5L14 15.5C14 15.78 13.78 16 13.5 16L2.5 16C2.22 16 2 15.78 2 15.5L2 7.5C2 7.22 2.22 7 2.5 7L3 7L3 5C3 2.24 5.24 0 8 0C10.22 0 12.1 1.44 12.75 3.44C12.83 3.69 12.69 3.94 12.45 4.02C12.17 4.11 11.87 3.94 11.77 3.66C11.22 2.11 9.74 1 8 1ZM3 8L3 15L13 15L13 8Z"
1241
+ },
1242
+ "viewport-camera-view": {
1243
+ "path": "M10.5 0C8.36 0 6.69 1.47 6.17 3.24C5.52 2.5 4.66 2 3.5 2C1.57 2 0 3.57 0 5.5C0 7.43 1.57 9 3.5 9L10.5 9C12.98 9 15 6.98 15 4.5C15 2.02 12.98 0 10.5 0ZM10.5 3C11.32 3 12 3.68 12 4.5C12 5.32 11.32 6 10.5 6C9.68 6 9 5.32 9 4.5C9 3.68 9.68 3 10.5 3ZM3.5 4C4.32 4 5 4.68 5 5.5C5 6.32 4.32 7 3.5 7C2.68 7 2 6.32 2 5.5C2 4.68 2.68 4 3.5 4ZM10.5 4C10.22 4 10 4.22 10 4.5C10 4.78 10.22 5 10.5 5C10.78 5 11 4.78 11 4.5C11 4.22 10.78 4 10.5 4ZM3.5 5C3.22 5 3 5.22 3 5.5C3 5.78 3.22 6 3.5 6C3.78 6 4 5.78 4 5.5C4 5.22 3.78 5 3.5 5ZM4.5 10C4.22 10 4 10.22 4 10.5L4 11.5L4 12L3 12L3 11.5C3 11.22 2.77 10.99 2.49 10.99C2.22 10.99 2 11.22 2 11.5L2 12.5L2 13.5C2.01 14.16 2.99 14.16 3 13.5L3 13L4 13L4 13.5L4 14.5C4 14.63 4.05 14.76 4.15 14.85L5.15 15.85C5.24 15.95 5.37 16 5.5 16L10.5 16C10.78 16 11 15.78 11 15.5L11 10.5C11 10.22 10.78 10 10.5 10ZM15.48 11C15.4 11 15.31 11.03 15.24 11.07L11.99 13.07C11.67 13.27 11.67 13.73 11.99 13.93L15.24 15.93C15.57 16.13 16 15.89 16 15.5L16 11.5C16 11.22 15.77 10.99 15.48 11Z"
1244
+ },
1245
+ "viewport-orthographic": {
1246
+ "path": "M1.42 1.93L14.6 1.93A0.51 0.51 0 0 0 14.6 0.92L1.42 0.92A0.51 0.51 0 0 0 1.42 1.93ZM0.92 1.42L0.92 14.6A0.51 0.51 0 0 0 1.93 14.6L1.93 1.42A0.51 0.51 0 0 0 0.92 1.42ZM1.42 6.32L14.6 6.32A0.51 0.51 0 0 0 14.6 5.31L1.42 5.31A0.51 0.51 0 0 0 1.42 6.32ZM5.31 1.42L5.31 14.6A0.51 0.51 0 0 0 6.32 14.6L6.32 1.42A0.51 0.51 0 0 0 5.31 1.42ZM1.42 10.71L14.6 10.71A0.51 0.51 0 0 0 14.6 9.7L1.42 9.7A0.51 0.51 0 0 0 1.42 10.71ZM9.7 1.42L9.7 14.6A0.51 0.51 0 0 0 10.71 14.6L10.71 1.42A0.51 0.51 0 0 0 9.7 1.42ZM1.42 15.11L14.6 15.11A0.51 0.51 0 0 0 14.6 14.09L1.42 14.09A0.51 0.51 0 0 0 1.42 15.11ZM14.09 1.42L14.09 14.6A0.51 0.51 0 0 0 15.11 14.6L15.11 1.42A0.51 0.51 0 0 0 14.09 1.42Z"
1247
+ },
1228
1248
  "viewport-projection": {
1229
1249
  "path": "M4.14 3.27L11.88 3.27A0.51 0.51 0 0 0 11.88 2.26L4.14 2.26A0.51 0.51 0 0 0 4.14 3.27ZM3.5 5.8L12.53 5.8A0.51 0.51 0 0 0 12.53 4.79L3.5 4.79A0.51 0.51 0 0 0 3.5 5.8ZM2.57 9.4L13.45 9.4A0.51 0.51 0 0 0 13.45 8.39L2.57 8.39A0.51 0.51 0 0 0 2.57 9.4ZM1.42 13.85L14.6 13.85A0.51 0.51 0 0 0 14.6 12.84L1.42 12.84A0.51 0.51 0 0 0 1.42 13.85ZM3.66 2.64L0.93 13.22A0.51 0.51 0 0 0 1.91 13.47L4.63 2.89A0.51 0.51 0 0 0 3.66 2.64ZM11.39 2.89L14.11 13.47A0.51 0.51 0 0 0 15.09 13.22L12.37 2.64A0.51 0.51 0 0 0 11.39 2.89ZM6.42 2.71L5.24 13.29A0.51 0.51 0 0 0 6.25 13.4L7.43 2.82A0.51 0.51 0 0 0 6.42 2.71ZM8.6 2.82L9.78 13.4A0.51 0.51 0 0 0 10.78 13.29L9.6 2.71A0.51 0.51 0 0 0 8.6 2.82Z"
1230
1250
  },
@@ -1557,5 +1557,33 @@
1557
1557
  "sha256": "5fcacba19714529d2283550eeff5c052cf59b190aa32403d2e692dc1ca424b07",
1558
1558
  "box": [3, 3, 13, 13],
1559
1559
  "path": "M4.03 3C3.47 3 3 3.47 3 4.03L3 11.97C3 12.53 3.47 13 4.03 13L11.97 13C12.53 13 13 12.53 13 11.97L13 4.03C13 3.47 12.53 3 11.97 3ZM4.03 4L11.97 4C11.99 4 12 4 12 4.03L12 11.97C12 11.99 11.99 12 11.97 12L4.03 12C4.01 12 4 11.99 4 11.97L4 4.03C4 4.01 4.01 4 4.03 4Z"
1560
+ },
1561
+ "viewport-camera": {
1562
+ "icon": "ICON_VIEW_CAMERA_UNSELECTED",
1563
+ "source": "release/datafiles/icons_svg/view_camera_unselected.svg",
1564
+ "sha256": "a286ff50999057585a5b45714bf98d2a6706f60be114b7b1386fb33d63a2cef3",
1565
+ "box": [0, 0, 16, 16],
1566
+ "path": "M4 12L4 10.5C4 10.22 4.22 10 4.5 10L10.5 10C10.78 10 11 10.22 11 10.5L11 15.5C11 15.78 10.78 16 10.5 16L5.71 16C5.57 16 5.45 15.95 5.35 15.85L4.15 14.65C4.05 14.55 4 14.43 4 14.29L4 13L3 13L3 13.5C3 13.78 2.78 14 2.5 14C2.22 14 2 13.78 2 13.5L2 11.5C2 11.22 2.22 11 2.5 11C2.78 11 3 11.22 3 11.5L3 12ZM5.91 15L5 14.09L5 11L10 11L10 15ZM13.22 13.5L15 14.59L15 12.41ZM15.15 15.85C15.24 15.94 15.36 16 15.5 16C15.78 16 16 15.78 16 15.5L16 11.5C16 11.22 15.78 11 15.5 11C15.36 11 15.24 11.06 15.15 11.15L12.04 13.04C11.87 13.12 11.75 13.3 11.75 13.5C11.75 13.7 11.87 13.88 12.04 13.96ZM6.38 4.22C6.02 4.3 5.65 4.17 5.41 3.89C4.95 3.34 4.27 3 3.5 3C2.12 3 1 4.12 1 5.5C1 6.88 2.12 8 3.5 8L10.5 8C12.43 8 14 6.43 14 4.5C14 2.57 12.43 1 10.5 1C8.91 1 7.56 2.06 7.14 3.52C7.04 3.88 6.75 4.15 6.38 4.22ZM6.61 2.24C6.42 2.56 6.28 2.89 6.18 3.25C5.94 2.97 5.67 2.73 5.36 2.54C4.82 2.2 4.18 2 3.5 2C1.57 2 0 3.57 0 5.5C0 7.43 1.57 9 3.5 9L10.5 9C12.99 9 15 6.99 15 4.5C15 2.01 12.99 0 10.5 0C8.84 0 7.38 0.9 6.61 2.24ZM10.5 5C10.78 5 11 4.78 11 4.5C11 4.22 10.78 4 10.5 4C10.22 4 10 4.22 10 4.5C10 4.78 10.22 5 10.5 5ZM10.5 6C11.33 6 12 5.33 12 4.5C12 3.67 11.33 3 10.5 3C9.67 3 9 3.67 9 4.5C9 5.33 9.67 6 10.5 6ZM4.5 5.5C4.5 6.05 4.05 6.5 3.5 6.5C2.95 6.5 2.5 6.05 2.5 5.5C2.5 4.95 2.95 4.5 3.5 4.5C4.05 4.5 4.5 4.95 4.5 5.5Z"
1567
+ },
1568
+ "viewport-camera-view": {
1569
+ "icon": "ICON_VIEW_CAMERA",
1570
+ "source": "release/datafiles/icons_svg/view_camera.svg",
1571
+ "sha256": "17fd5360f46a00bcf2c3d72f115f017ee693775dd3af44fab89e1a939a879e07",
1572
+ "box": [0, 0, 16, 16.13],
1573
+ "path": "M10.5 0C8.36 0 6.69 1.47 6.17 3.24C5.52 2.5 4.66 2 3.5 2C1.57 2 0 3.57 0 5.5C0 7.43 1.57 9 3.5 9L10.5 9C12.98 9 15 6.98 15 4.5C15 2.02 12.98 0 10.5 0ZM10.5 3C11.32 3 12 3.68 12 4.5C12 5.32 11.32 6 10.5 6C9.68 6 9 5.32 9 4.5C9 3.68 9.68 3 10.5 3ZM3.5 4C4.32 4 5 4.68 5 5.5C5 6.32 4.32 7 3.5 7C2.68 7 2 6.32 2 5.5C2 4.68 2.68 4 3.5 4ZM10.5 4C10.22 4 10 4.22 10 4.5C10 4.78 10.22 5 10.5 5C10.78 5 11 4.78 11 4.5C11 4.22 10.78 4 10.5 4ZM3.5 5C3.22 5 3 5.22 3 5.5C3 5.78 3.22 6 3.5 6C3.78 6 4 5.78 4 5.5C4 5.22 3.78 5 3.5 5ZM4.5 10C4.22 10 4 10.22 4 10.5L4 11.5L4 12L3 12L3 11.5C3 11.22 2.77 10.99 2.49 10.99C2.22 10.99 2 11.22 2 11.5L2 12.5L2 13.5C2.01 14.16 2.99 14.16 3 13.5L3 13L4 13L4 13.5L4 14.5C4 14.63 4.05 14.76 4.15 14.85L5.15 15.85C5.24 15.95 5.37 16 5.5 16L10.5 16C10.78 16 11 15.78 11 15.5L11 10.5C11 10.22 10.78 10 10.5 10ZM15.48 11C15.4 11 15.31 11.03 15.24 11.07L11.99 13.07C11.67 13.27 11.67 13.73 11.99 13.93L15.24 15.93C15.57 16.13 16 15.89 16 15.5L16 11.5C16 11.22 15.77 10.99 15.48 11Z"
1574
+ },
1575
+ "viewport-camera-locked": {
1576
+ "icon": "ICON_VIEW_LOCKED",
1577
+ "source": "release/datafiles/icons_svg/view_locked.svg",
1578
+ "sha256": "8b84ee3bb6920f48eebc0e43116321977e18a09931879081550c9c4564fe4ec1",
1579
+ "box": [2, 0, 14, 16],
1580
+ "path": "M3 7L3 5C3 2.24 5.24 0 8 0C10.76 0 13 2.24 13 5L13 7L13.5 7C13.78 7 14 7.22 14 7.5L14 15.5C14 15.78 13.78 16 13.5 16L2.5 16C2.22 16 2 15.78 2 15.5L2 7.5C2 7.22 2.22 7 2.5 7ZM4 5C4 2.79 5.79 1 8 1C10.21 1 12 2.79 12 5L12 7L4 7ZM8 10C7.45 10 7 10.45 7 11C7 11.37 7.2 11.69 7.5 11.87L7.5 13C7.5 13.28 7.72 13.5 8 13.5C8.28 13.5 8.5 13.28 8.5 13L8.5 11.87C8.8 11.69 9 11.37 9 11C9 10.45 8.55 10 8 10Z"
1581
+ },
1582
+ "viewport-camera-unlocked": {
1583
+ "icon": "ICON_VIEW_UNLOCKED",
1584
+ "source": "release/datafiles/icons_svg/view_unlocked.svg",
1585
+ "sha256": "1a70d7dfe4a77e6a628426f6400b0e273e2b543bda53d967f7d4c0da3268bd52",
1586
+ "box": [2, 0, 14, 16],
1587
+ "path": "M8 10C7.45 10 7 10.45 7 11C7 11.37 7.2 11.69 7.5 11.87L7.5 13C7.5 13.28 7.72 13.5 8 13.5C8.28 13.5 8.5 13.28 8.5 13L8.5 11.87C8.8 11.69 9 11.37 9 11C9 10.45 8.55 10 8 10ZM8 1C5.79 1 4 2.79 4 5L4 7L13.5 7C13.78 7 14 7.22 14 7.5L14 15.5C14 15.78 13.78 16 13.5 16L2.5 16C2.22 16 2 15.78 2 15.5L2 7.5C2 7.22 2.22 7 2.5 7L3 7L3 5C3 2.24 5.24 0 8 0C10.22 0 12.1 1.44 12.75 3.44C12.83 3.69 12.69 3.94 12.45 4.02C12.17 4.11 11.87 3.94 11.77 3.66C11.22 2.11 9.74 1 8 1ZM3 8L3 15L13 15L13 8Z"
1560
1588
  }
1561
1589
  }
@@ -35,5 +35,42 @@ export const keymap: KeymapContribution = {
35
35
  'edit.deselectAll': [{ key: 'a', alt: true }],
36
36
  'viewport.frameSelection': [{ key: '.' }, { key: '', code: 'NumpadDecimal' }],
37
37
  'viewport.cyclePivot': [{ key: ',' }],
38
+ // Blender's G is Grab (`transform.translate` above); a 2D view's modes keep their buttons.
39
+ 'canvas.panMode': [],
40
+ 'canvas.rulerMode': [],
41
+ 'canvas.toggleGrid': [],
42
+ // `km_view3d` — `view3d.view_persportho` on NUMPAD_5; Blender has no isometric preset key.
43
+ 'view.perspective': [],
44
+ 'view.projection': [{ key: '', code: 'Numpad5' }],
45
+ // `km_view3d` — `view3d.view_axis` with Ctrl for the opposite side, `view3d.view_orbit` on
46
+ // NUMPAD_2/4/6/8 (15°, `pad_rot_angle`) and NUMPAD_9 (π), `view3d.view_roll` on Shift+4/6.
47
+ // Blender's Ctrl is the Control key on every platform, macOS included (`ctrl`, not `mod`).
48
+ 'view.back': [{ key: '', code: 'Numpad1', ctrl: true }],
49
+ 'view.left': [{ key: '', code: 'Numpad3', ctrl: true }],
50
+ 'view.bottom': [{ key: '', code: 'Numpad7', ctrl: true }],
51
+ 'view.orbitLeft': [{ key: '', code: 'Numpad4' }],
52
+ 'view.orbitRight': [{ key: '', code: 'Numpad6' }],
53
+ 'view.orbitUp': [{ key: '', code: 'Numpad8' }],
54
+ 'view.orbitDown': [{ key: '', code: 'Numpad2' }],
55
+ 'view.opposite': [{ key: '', code: 'Numpad9' }],
56
+ 'view.rollLeft': [{ key: '', code: 'Numpad4', shift: true }],
57
+ 'view.rollRight': [{ key: '', code: 'Numpad6', shift: true }],
58
+ // `view3d.view_all` on HOME; `view3d.zoom` on NUMPAD_PLUS/NUMPAD_MINUS and Ctrl+=/Ctrl+-.
59
+ 'view.all': [{ key: '', code: 'Home' }],
60
+ 'view.zoomIn': [{ key: '', code: 'NumpadAdd' }, { key: '', code: 'Equal', ctrl: true }],
61
+ 'view.zoomOut': [{ key: '', code: 'NumpadSubtract' }, { key: '', code: 'Minus', ctrl: true }],
62
+ },
63
+ // `km_view3d` — `view3d.rotate` on MIDDLEMOUSE and `view3d.move` on Shift+MIDDLEMOUSE; the
64
+ // right button is the context menu's, and Shift+Right places the 3D cursor. The factory
65
+ // rotation is Turntable at 0.4° a pixel (`view_rotate_method`,
66
+ // `view_rotate_sensitivity_turntable`, read back from Blender 5.2; a pixel over `UI_SCALE_FAC`,
67
+ // so one CSS pixel on any display), with Auto Perspective on (`use_auto_perspective`).
68
+ // Zoom: a key steps the distance by 1.2 (`view_zoom_apply_step`), and Ctrl+middle drags in the
69
+ // factory Dolly style (`U.viewzoom`, `viewzoom_scale_value`).
70
+ navigation: {
71
+ orbit: 'middle',
72
+ turntable: { degreesPerPixel: 0.4 },
73
+ autoPerspective: true,
74
+ zoom: { step: 1.2, drag: 'dolly' },
38
75
  },
39
76
  };
@@ -56,10 +56,7 @@
56
56
  "gizmo": {
57
57
  "x": "#ff3352",
58
58
  "y": "#8bdc00",
59
- "z": "#2890ff",
60
- "navigationX": "#f53651",
61
- "navigationY": "#6fa41b",
62
- "navigationZ": "#2e83e3"
59
+ "z": "#2890ff"
63
60
  },
64
61
  "region": {
65
62
  "outliner": "#282828",
@@ -73,6 +70,7 @@
73
70
  "scene": "#cccccc",
74
71
  "collection": "#ffffff",
75
72
  "tool": "#cbcbcb",
73
+ "select": "#ffaf2a",
76
74
  "operator": "#95dab2"
77
75
  },
78
76
  "semantic": {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/editor-blender",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "AGPL-3.0-only AND GPL-3.0-or-later",
5
- "version": "0.1.9",
5
+ "version": "0.1.11",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -42,7 +42,6 @@
42
42
  "./contributions/blender.keymap.ts",
43
43
  "./contributions/blender.status.tsx",
44
44
  "./contributions/blender.style.ts",
45
- "./contributions/material-preview.view.ts",
46
45
  "./contributions/model.layout.ts",
47
46
  "./contributions/models.finder.ts",
48
47
  "./contributions/properties-bone-constraints.inspector.tsx",
@@ -70,14 +69,14 @@
70
69
  },
71
70
  "dependencies": {
72
71
  "@modelcontextprotocol/sdk": "1.30.0",
73
- "@volter/blender-engine": "0.1.8",
74
- "@volter/editor-threejs": "0.5.66",
72
+ "@volter/blender-engine": "0.1.9",
73
+ "@volter/editor-threejs": "0.5.68",
75
74
  "zod": "^4.3.6"
76
75
  },
77
76
  "peerDependencies": {
78
- "@volter/editor-sdk": "0.5.66",
79
- "@volter/editor-live": "0.5.66",
80
- "@volter/editor-project": "0.5.66",
77
+ "@volter/editor-sdk": "0.5.68",
78
+ "@volter/editor-live": "0.5.68",
79
+ "@volter/editor-project": "0.5.68",
81
80
  "react": "^19.0.0",
82
81
  "three": "^0.180.0",
83
82
  "vite": "^6.4.3"
@@ -3,9 +3,31 @@
3
3
  * viewport-presentation`): this package builds the `model` stage, so it states the stage's
4
4
  * function there, beneath every person's choice and never in the look (ARCHITECTURE.md rule 7).
5
5
  */
6
- import { registerStartingPresentation } from '@volter/editor-sdk/kit/viewport-presentation';
6
+ import {
7
+ DOCUMENT_STUDIO_PRESET,
8
+ registerStartingPresentation,
9
+ type ViewportCamera,
10
+ } from '@volter/editor-sdk/kit/viewport-presentation';
11
+
12
+ /**
13
+ * A 3D View's field of view for its lens, as Blender computes it (`BKE_camera_params_from_view3d`,
14
+ * `BKE_camera_params_compute_viewplane`): the 36 mm sensor over the lens, times the viewport's
15
+ * `CAMERA_PARAM_ZOOM_INIT_PERSP` of 2, held on the region's LARGER side (sensor fit AUTO) — so a
16
+ * wider panel sees no more world sideways and a shorter one sees less vertically. The same zoom
17
+ * scales an orthographic view's `dist * sensor / lens`, so the two projections agree at the pivot.
18
+ */
19
+ export function blenderViewFieldOfView(lens: number): ViewportCamera['fov'] {
20
+ const safeLens = Number.isFinite(lens) && lens > 0 ? lens : 50;
21
+ return { degrees: (2 * Math.atan((36 * 2) / (2 * safeLens)) * 180) / Math.PI, axis: 'larger' };
22
+ }
7
23
 
8
24
  const release = registerStartingPresentation('model', {
25
+ // BLENDER'S VIEW: the factory `View3D.lens` of 50 mm (read back from Blender 5.2), a 71.5°
26
+ // angle on the larger side; and its default user perspective, elevation 26.5° and azimuth
27
+ // −23.8° about the up axis, SOLVED from the reference's two floor axes (projected slopes
28
+ // +1.0093 for X and −0.1969 for Y in `modeling-edit-none.png`), in three's Y-up frame. A file's
29
+ // own saved view and lens are its document's (`blender-runtime.document.tsx`).
30
+ camera: { fov: blenderViewFieldOfView(50), opening: [0.8187, 0.4458, 0.3617] },
9
31
  interaction: {
10
32
  // BLENDER'S SHELF OPENS ON SELECT BOX, so a selected object carries no transform gizmo until
11
33
  // one of the four transform tools is armed (`space_toolsystem_toolbar.py`:
@@ -18,6 +40,63 @@ const release = registerStartingPresentation('model', {
18
40
  // under the rectangle, so any drawn part of an object inside it selects that object.
19
41
  boxSelect: 'touch',
20
42
  },
43
+ // BLENDER'S SHADING TYPES EACH KEEP THEIR OWN LIGHTING, and switching the header's shading cell
44
+ // switches to that type's (`View3DShading` keeps `studio_light` for Solid, `studio_light` /
45
+ // `studiolight_*` for Material Preview, and Rendered lights by the scene). So they are stated per
46
+ // draw mode, and a person's Lighting choice is the current mode's (`stageLightsPerMode`).
47
+ // SOLID (`clay`, and Wireframe with it) is lit by Blender's four studio lights, the document's
48
+ // view-locked studio, and drawn under STANDARD: Blender gives Solid the display's default view
49
+ // and none of the scene's settings (`draw_color_management.cc`, `ViewTransform`), and the sRGB
50
+ // display's default is Standard (`config.ocio`). Blender 5.2's Workbench under Standard gives
51
+ // the factory cube 142 / 131 / 112 in the default view and 162 on the front face, agreeing with
52
+ // the viewport's own frames (141 / 129 / 111, 161). The scene's AgX is the two modes' below.
53
+ all: {
54
+ lighting: {
55
+ source: 'studio',
56
+ studioPreset: DOCUMENT_STUDIO_PRESET.id,
57
+ auto: null,
58
+ tone: { mapper: 'none', exposure: 1 },
59
+ },
60
+ // X-Ray per shading type, read back from Blender 5.2's factory View3DShading: Solid's off at
61
+ // 0.5 (`show_xray`, `xray_alpha`), Wireframe's on at 0 (`show_xray_wireframe`,
62
+ // `xray_alpha_wireframe`) — a wireframe with no surface.
63
+ xray: { enabled: false, alpha: 0.5 },
64
+ },
65
+ modes: {
66
+ wireframe: { xray: { enabled: true, alpha: 0 } },
67
+ // MATERIAL PREVIEW: the scene lit by a world studio light alone, Forest at strength 1 and
68
+ // rotation 0, fixed in the world, drawn over the viewport's own colour, in AgX. Read from
69
+ // Blender 5.2's factory View3DShading: `studio_light` Default (forest.exr),
70
+ // `studiolight_intensity` 1, `studiolight_rotate_z` 0, `studiolight_background_alpha` 0,
71
+ // `use_studiolight_view_rotation` (World Space Lighting) on, `use_scene_lights` off
72
+ // (`docs/reference-probes/blender-view3d-shading.py`). The images are `blender.environment.ts`'s.
73
+ // Blender's viewport never changes its shading on its own: a lamp does not take it over.
74
+ preview: {
75
+ lighting: {
76
+ source: 'preview',
77
+ auto: null,
78
+ preview: {
79
+ sceneLights: false,
80
+ sun: { enabled: false },
81
+ environment: { enabled: true, image: 'blender:forest', energy: 1, rotation: 0 },
82
+ },
83
+ tone: { mapper: 'agx', exposure: 1 },
84
+ },
85
+ backdrop: { source: 'fill' },
86
+ },
87
+ // RENDERED: the scene's own lights and World, drawn behind the model, in AgX — the lighting the
88
+ // document's render photographs with (`BlenderRuntimeView.holdRendered`). Not a path tracer:
89
+ // the viewport and a render agree because they are one drawing.
90
+ rendered: {
91
+ lighting: { source: 'scene', auto: null, tone: { mapper: 'agx', exposure: 1 } },
92
+ backdrop: { source: 'scene' },
93
+ },
94
+ },
95
+ overlays: {
96
+ // BLENDER DRAWS THE SELECTED OBJECTS' ORIGINS (Overlays › Origins, on by default): the dot at
97
+ // the default cube's centre in every reference frame (`gizmo-select-box.png`).
98
+ selection: { origins: true },
99
+ },
21
100
  world: {
22
101
  // BLENDER'S WORLD IS Z-UP, and the stage presents it through the signed permutation
23
102
  // `(x, y, z) → (x, z, −y)` the presented root carries (`blender-runtime-view.ts`'s
@@ -1,35 +0,0 @@
1
- /**
2
- * BLENDER'S MATERIAL PREVIEW: the scene lit by a world studio light alone, Forest at strength 1
3
- * and rotation 0, fixed in the world, drawn over the viewport's own colour, in AgX. Read from
4
- * Blender 5.2's factory View3DShading: `studio_light` Default (forest.exr),
5
- * `studiolight_intensity` 1, `studiolight_rotate_z` 0, `studiolight_background_alpha` 0,
6
- * `use_studiolight_view_rotation` (World Space Lighting) on, `use_scene_lights` off.
7
- * Measured against Blender 5.2's own EEVEE render of the default cube under the same world
8
- * with its lamp removed.
9
- * A named view (`@volter/editor-sdk/kit/viewport-presentation` `ViewPreset`); the images are
10
- * `blender.environment.ts`'s.
11
- */
12
- import type { ViewPreset } from '@volter/editor-sdk/kit/viewport-presentation';
13
-
14
- export const point = 'workspace.view';
15
- export const view: ViewPreset = {
16
- id: 'blender-material-preview',
17
- title: 'Material Preview',
18
- layer: {
19
- all: {
20
- lighting: {
21
- source: 'preview',
22
- // Blender's viewport never changes its shading on its own: a lamp in the scene does not
23
- // take Material Preview over.
24
- auto: null,
25
- preview: {
26
- sceneLights: false,
27
- sun: { enabled: false },
28
- environment: { enabled: true, image: 'blender:forest', energy: 1, rotation: 0 },
29
- },
30
- tone: { mapper: 'agx', exposure: 1 },
31
- },
32
- backdrop: { source: 'fill' },
33
- },
34
- },
35
- };