@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.
- package/contributions/blender-header-menus.tsx +4 -9
- package/contributions/blender-icons.source.mjs +74 -6
- package/contributions/blender-outliner-authoring.ts +10 -21
- package/contributions/blender-palette.source.mjs +9 -8
- package/contributions/blender-runtime-skin.ts +6 -0
- package/contributions/blender-runtime.document.tsx +214 -23
- package/contributions/blender.icons.json +22 -2
- package/contributions/blender.icons.traced.json +28 -0
- package/contributions/blender.keymap.ts +37 -0
- package/contributions/blender.palette.json +2 -4
- package/package.json +6 -7
- package/src/presentation.ts +80 -1
- package/contributions/material-preview.view.ts +0 -35
|
@@ -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:
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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-
|
|
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
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* `
|
|
308
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
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 =
|
|
262
|
+
const stop = onViewportStages(apply);
|
|
181
263
|
return () => {
|
|
182
264
|
stop();
|
|
183
|
-
const stage =
|
|
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
|
-
//
|
|
236
|
-
//
|
|
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
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
74
|
-
"@volter/editor-threejs": "0.5.
|
|
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.
|
|
79
|
-
"@volter/editor-live": "0.5.
|
|
80
|
-
"@volter/editor-project": "0.5.
|
|
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"
|
package/src/presentation.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
};
|