@volter/editor-blender 0.1.8 → 0.1.10

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.
Files changed (39) hide show
  1. package/contributions/blender-header-menus.tsx +4 -9
  2. package/contributions/blender-icons.source.mjs +74 -6
  3. package/contributions/blender-outliner-authoring.ts +10 -21
  4. package/contributions/blender-palette.source.mjs +16 -5
  5. package/contributions/blender-properties-view.tsx +28 -9
  6. package/contributions/blender-runtime-skin.ts +6 -0
  7. package/contributions/blender-runtime.document.tsx +219 -23
  8. package/contributions/blender.command.ts +5 -1
  9. package/contributions/blender.environment.ts +32 -0
  10. package/contributions/blender.icons.json +22 -2
  11. package/contributions/blender.icons.traced.json +28 -0
  12. package/contributions/blender.keymap.ts +33 -0
  13. package/contributions/blender.palette.json +6 -0
  14. package/contributions/blender.status.tsx +2 -6
  15. package/contributions/blender.style.ts +30 -89
  16. package/contributions/model.layout.ts +7 -7
  17. package/contributions/models.finder.ts +16 -0
  18. package/contributions/sculpt.layout.ts +3 -0
  19. package/contributions/shading.layout.ts +3 -0
  20. package/contributions/studiolights/world/city.exr +0 -0
  21. package/contributions/studiolights/world/courtyard.exr +0 -0
  22. package/contributions/studiolights/world/forest.exr +0 -0
  23. package/contributions/studiolights/world/interior.exr +0 -0
  24. package/contributions/studiolights/world/license.txt +15 -0
  25. package/contributions/studiolights/world/night.exr +0 -0
  26. package/contributions/studiolights/world/studio.exr +0 -0
  27. package/contributions/studiolights/world/sunrise.exr +0 -0
  28. package/contributions/studiolights/world/sunset.exr +0 -0
  29. package/contributions/uv-editing.layout.ts +3 -0
  30. package/dist-node/serving.mjs +624 -0
  31. package/host/blender-runtime-host.ts +11 -1
  32. package/node/blender-mcp.ts +2 -2
  33. package/package.json +20 -10
  34. package/serving/blender-routes.ts +427 -0
  35. package/serving/blender-wasm-artifact.ts +449 -0
  36. package/serving/index.ts +10 -0
  37. package/src/layouts.tsx +4 -5
  38. package/src/presentation.ts +111 -0
  39. package/src/regions.ts +59 -0
@@ -20,19 +20,35 @@
20
20
  *
21
21
  * WITHOUT AN ENTRY it is still the document `blender-start` presents into: a
22
22
  * project that lists no `.blend` of its own gets one Model document at the
23
- * standing `blender:runtime` address (`project-adapter.ts` injects it), and
23
+ * standing `blender:runtime` address (`models.finder.ts` lists it), and
24
24
  * the session opens its default `models/model.blend`.
25
25
  */
26
26
 
27
+ // How the `model` stage this document builds behaves (its starting presentation).
28
+ import { blenderViewFieldOfView } from '../src/presentation';
27
29
  import { blenderModelView } from '@volter/blender-engine/browser/three/blender-runtime-view';
28
30
  import type { ToolContributionProps, ToolDocumentToolbar } from '@volter/editor-sdk/contributions';
29
31
  import { editorHost } from '@volter/editor-sdk/host';
30
- 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';
31
46
  import * as THREE from 'three';
32
- import { bindModelDocument, openModelDocumentBlend } from '../host/blender-runtime-host';
47
+ import { bindModelDocument, blenderExecute, openModelDocumentBlend } from '../host/blender-runtime-host';
33
48
  import { BlenderObjectModeHeader } from './blender-header-menus';
34
49
  import { createBlenderOutlinerAuthoring } from './blender-outliner-authoring';
35
50
  import { blenderSkin } from './blender-runtime-skin';
51
+ import { onViewportStages, viewportStages } from '@volter/editor-threejs/viewport-door';
36
52
 
37
53
  export const point = 'workspace.document';
38
54
  export const title = 'Blender Model';
@@ -78,6 +94,51 @@ export const inspectorBuiltins: readonly string[] = [];
78
94
  // Timeline binds a skeleton into the same presented graph and there is exactly
79
95
  // one set of presented objects (`blender-runtime-skin.ts`).
80
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
+ };
81
142
 
82
143
  export default function BlenderModelDocument(props: ToolContributionProps) {
83
144
  const { active, document, documentId, notify, publishContext } = props;
@@ -151,6 +212,30 @@ function BlenderModelViewport({
151
212
  [],
152
213
  );
153
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);
154
239
  /**
155
240
  * THE INSPECTION OVERLAYS ARE HELPERS, and the Helpers menu owns them
156
241
  * (WORK.md §Blender in the tab is Blender, "Inspection parity", I4).
@@ -167,18 +252,17 @@ function BlenderModelViewport({
167
252
  */
168
253
  useEffect(() => {
169
254
  if (!documentId) return;
170
- const { viewport } = editorHost();
171
255
  const groups = view.overlayGroups();
172
256
  const apply = (): void => {
173
- const stage = viewport.stages().find((one) => one.documentId === documentId);
257
+ const stage = viewportStages().find((one) => one.documentId === documentId);
174
258
  if (!stage) return;
175
259
  for (const { kind, object } of groups) stage.setHelper(kind, object);
176
260
  };
177
261
  apply();
178
- const stop = viewport.onStages(apply);
262
+ const stop = onViewportStages(apply);
179
263
  return () => {
180
264
  stop();
181
- const stage = viewport.stages().find((one) => one.documentId === documentId);
265
+ const stage = viewportStages().find((one) => one.documentId === documentId);
182
266
  for (const { kind } of groups) stage?.setHelper(kind, null);
183
267
  };
184
268
  }, [documentId]);
@@ -210,14 +294,125 @@ function BlenderModelViewport({
210
294
  detach?.();
211
295
  };
212
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
+ };
213
395
  if (!documentId) return null;
214
396
  const Surface = surfaces.Object3DAuthoring;
215
397
  return (
398
+ <div
399
+ style={{ display: 'contents' }}
400
+ onPointerDownCapture={onPointerDownCapture}
401
+ onPointerUpCapture={onPointerUpCapture}
402
+ onPointerCancelCapture={dropCursorPress}
403
+ onLostPointerCaptureCapture={dropCursorPress}
404
+ onContextMenuCapture={onContextMenuCapture}
405
+ >
216
406
  <Surface
217
407
  active={active ?? true}
218
408
  documentId={documentId}
219
409
  sourcePath={blend ?? 'blender:runtime'}
220
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}
221
416
  build={build}
222
417
  audit={false}
223
418
  // ON A MODEL DOCUMENT THE HIERARCHY IS BLENDER'S OUTLINER, for the same
@@ -230,12 +425,20 @@ function BlenderModelViewport({
230
425
  // "Inspection parity", I3).
231
426
  authoring={createBlenderOutlinerAuthoring}
232
427
  cameraDirection={[0.8187, 0.4458, 0.3617]}
233
- // AND AS FAR BACK AS BLENDER'S STARTUP VIEW STANDS. The direction alone
234
- // put the cube where Blender's is but FILLING the frame: the factory
235
- // view is 18.39 units from a 2 m cube (`region_3d.view_distance` at
236
- // `--factory-startup`), about a third of the size a bare fit gives, which
237
- // 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.
238
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
+ })()}
439
+ // Every entry this document opens is a `model` stage, the standing `blender:runtime`
440
+ // address included (its id carries no `model:` prefix).
441
+ stageKind={documentKind}
239
442
  // SOLID SHADING IS BLENDER'S, and Blender's Solid mode has NO world
240
443
  // light: `light_ambient` is (0,0,0), there is no IBL, and the model is
241
444
  // lit by four studio lights stated in VIEW space. So this document
@@ -249,22 +452,15 @@ function BlenderModelViewport({
249
452
  // The BACKGROUND stays the dressing's, because the Blender palette
250
453
  // already paints the viewport its own flat grey.
251
454
  //
252
- // AND THE VIEW TRANSFORM IS BLENDER'S. Blender's factory scene is AgX
253
- // (`view_settings.view_transform`), and this document's own RENDER path
254
- // already photographs through `THREE.AgXToneMapping` (`presenter.ts`),
255
- // so the viewport was the one surface in the chain running a different
256
- // curve from the thing it frames. MEASURED on the factory cube, our own
257
- // radiance through each operator against Blender's (141,143,145)/
258
- // (129,131,131)/(111,112,113): ACES lands the three faces within 6
259
- // levels with a spread of 40 where Blender's is 30 — the curve, not the
260
- // lights, is what was left of row 1's spread — and AgX within 3 at a
261
- // 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.
262
458
  dressing={{
263
459
  environment: false,
264
460
  keyLight: false,
265
- toneMapping: THREE.AgXToneMapping,
266
461
  viewLocked: view.studioLights(),
267
462
  }}
268
463
  />
464
+ </div>
269
465
  );
270
466
  }
@@ -60,7 +60,11 @@ export const commands: CommandContribution['commands'] = {
60
60
  'blender-screenshot-view': verb('always', 60_000),
61
61
  'blender-read-file': verb('none', 60_000),
62
62
  'blender-write-file': verb('none', 60_000),
63
- 'blender-list-files': verb('none'),
63
+ // Listing waits on the worker like a read: after an execute the MCP mirror lists
64
+ // while the page is still presenting the frame and refreshing its RNA and
65
+ // outliner reads, which on a large document outlasts the generic 5s default and
66
+ // turned a successful execute into a timeout.
67
+ 'blender-list-files': verb('none', 60_000),
64
68
  // THE RNA DOOR (WORK.md §Blender in the tab is Blender, "Inspection
65
69
  // parity", I1). Two READS — a datablock's `bl_rna.properties` and the
66
70
  // Properties context — and one WRITE. The reads present nothing, so they
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BLENDER'S WORLD STUDIO LIGHTS: the eight HDRIs Blender ships for Material Preview
3
+ * (`datafiles/studiolights/world`, copied from Blender 5.2), registered as environment images
4
+ * (`@volter/editor-sdk/kit/environment-images`) so a view can light by one and draw it behind
5
+ * the scene. Material Preview opens on Forest. All CC0, by Greg Zaal (Poly Haven); the terms
6
+ * and each original are in `studiolights/world/license.txt`.
7
+ */
8
+ import type { EnvironmentImageSet } from '@volter/editor-sdk/kit/environment-images';
9
+ import city from './studiolights/world/city.exr?url';
10
+ import courtyard from './studiolights/world/courtyard.exr?url';
11
+ import forest from './studiolights/world/forest.exr?url';
12
+ import interior from './studiolights/world/interior.exr?url';
13
+ import night from './studiolights/world/night.exr?url';
14
+ import studio from './studiolights/world/studio.exr?url';
15
+ import sunrise from './studiolights/world/sunrise.exr?url';
16
+ import sunset from './studiolights/world/sunset.exr?url';
17
+
18
+ export const point = 'workspace.environment';
19
+ export const environment: EnvironmentImageSet = {
20
+ id: 'blender-studiolights-world',
21
+ source: "Blender's world studio lights, CC0 (Poly Haven); studiolights/world/license.txt",
22
+ images: [
23
+ { id: 'blender:forest', title: 'Forest', url: forest, format: 'exr' },
24
+ { id: 'blender:city', title: 'City', url: city, format: 'exr' },
25
+ { id: 'blender:courtyard', title: 'Courtyard', url: courtyard, format: 'exr' },
26
+ { id: 'blender:interior', title: 'Interior', url: interior, format: 'exr' },
27
+ { id: 'blender:night', title: 'Night', url: night, format: 'exr' },
28
+ { id: 'blender:studio', title: 'Studio', url: studio, format: 'exr' },
29
+ { id: 'blender:sunrise', title: 'Sunrise', url: sunrise, format: 'exr' },
30
+ { id: 'blender:sunset', title: 'Sunset', url: sunset, format: 'exr' },
31
+ ],
32
+ };
@@ -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,38 @@ 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
+ // `km_view3d` — `view3d.view_persportho` on NUMPAD_5; Blender has no isometric preset key.
39
+ 'view.perspective': [],
40
+ 'view.projection': [{ key: '', code: 'Numpad5' }],
41
+ // `km_view3d` — `view3d.view_axis` with Ctrl for the opposite side, `view3d.view_orbit` on
42
+ // NUMPAD_2/4/6/8 (15°, `pad_rot_angle`) and NUMPAD_9 (π), `view3d.view_roll` on Shift+4/6.
43
+ // Blender's Ctrl is the Control key on every platform, macOS included (`ctrl`, not `mod`).
44
+ 'view.back': [{ key: '', code: 'Numpad1', ctrl: true }],
45
+ 'view.left': [{ key: '', code: 'Numpad3', ctrl: true }],
46
+ 'view.bottom': [{ key: '', code: 'Numpad7', ctrl: true }],
47
+ 'view.orbitLeft': [{ key: '', code: 'Numpad4' }],
48
+ 'view.orbitRight': [{ key: '', code: 'Numpad6' }],
49
+ 'view.orbitUp': [{ key: '', code: 'Numpad8' }],
50
+ 'view.orbitDown': [{ key: '', code: 'Numpad2' }],
51
+ 'view.opposite': [{ key: '', code: 'Numpad9' }],
52
+ 'view.rollLeft': [{ key: '', code: 'Numpad4', shift: true }],
53
+ 'view.rollRight': [{ key: '', code: 'Numpad6', shift: true }],
54
+ // `view3d.view_all` on HOME; `view3d.zoom` on NUMPAD_PLUS/NUMPAD_MINUS and Ctrl+=/Ctrl+-.
55
+ 'view.all': [{ key: '', code: 'Home' }],
56
+ 'view.zoomIn': [{ key: '', code: 'NumpadAdd' }, { key: '', code: 'Equal', ctrl: true }],
57
+ 'view.zoomOut': [{ key: '', code: 'NumpadSubtract' }, { key: '', code: 'Minus', ctrl: true }],
58
+ },
59
+ // `km_view3d` — `view3d.rotate` on MIDDLEMOUSE and `view3d.move` on Shift+MIDDLEMOUSE; the
60
+ // right button is the context menu's, and Shift+Right places the 3D cursor. The factory
61
+ // rotation is Turntable at 0.4° a pixel (`view_rotate_method`,
62
+ // `view_rotate_sensitivity_turntable`, read back from Blender 5.2; a pixel over `UI_SCALE_FAC`,
63
+ // so one CSS pixel on any display), with Auto Perspective on (`use_auto_perspective`).
64
+ // Zoom: a key steps the distance by 1.2 (`view_zoom_apply_step`), and Ctrl+middle drags in the
65
+ // factory Dolly style (`U.viewzoom`, `viewzoom_scale_value`).
66
+ navigation: {
67
+ orbit: 'middle',
68
+ turntable: { degreesPerPixel: 0.4 },
69
+ autoPerspective: true,
70
+ zoom: { step: 1.2, drag: 'dolly' },
38
71
  },
39
72
  };
@@ -53,6 +53,11 @@
53
53
  "selection": "#ed5700",
54
54
  "active": "#ffa028"
55
55
  },
56
+ "gizmo": {
57
+ "x": "#ff3352",
58
+ "y": "#8bdc00",
59
+ "z": "#2890ff"
60
+ },
56
61
  "region": {
57
62
  "outliner": "#282828",
58
63
  "properties": "#303030"
@@ -65,6 +70,7 @@
65
70
  "scene": "#cccccc",
66
71
  "collection": "#ffffff",
67
72
  "tool": "#cbcbcb",
73
+ "select": "#ffaf2a",
68
74
  "operator": "#95dab2"
69
75
  },
70
76
  "semantic": {
@@ -4,9 +4,7 @@
4
4
  * Blender's bottom row opens with three tiny mouse glyphs, each with one
5
5
  * button lit and the action it performs beside it — `Select`, `Rotate View`,
6
6
  * `Options` in the reference (`modeling.png`). It is the one piece of chrome
7
- * that tells a newcomer what the three buttons DO without a menu, and the
8
- * gap a human build session measured here was exactly that question ("how do
9
- * I move the camera", six times — `viewport-controls-hint.ts`).
7
+ * that tells a newcomer what the three buttons DO without a menu.
10
8
  *
11
9
  * THEY ARE HOVER-DRIVEN THERE, AND ALWAYS-ON HERE. Measured across the whole
12
10
  * reference set: `modeling.png` is the ONE frame with any ink left of centre
@@ -33,9 +31,7 @@
33
31
  * `editor-viewport.ts:682-686` sets `orbitControls.mouseButtons` to
34
32
  * `{ LEFT: -1, MIDDLE: PAN, RIGHT: ROTATE }`, and the left button is left
35
33
  * free for the selection raycast (`editor-viewport.ts:3782`,
36
- * `if (e.button === 0 && !e.altKey)`). The editor's own prose says the
37
- * same thing (`ViewportControlsHint.tsx:22`: "Orbit: right-drag · Pan:
38
- * middle-drag").
34
+ * `if (e.button === 0 && !e.altKey)`).
39
35
  *
40
36
  * That contract is a constant of the host, not of any document, so the table
41
37
  * below is a constant too — with the citations above as its provenance. When