@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.
- 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 +16 -5
- package/contributions/blender-properties-view.tsx +28 -9
- package/contributions/blender-runtime-skin.ts +6 -0
- package/contributions/blender-runtime.document.tsx +219 -23
- package/contributions/blender.command.ts +5 -1
- package/contributions/blender.environment.ts +32 -0
- package/contributions/blender.icons.json +22 -2
- package/contributions/blender.icons.traced.json +28 -0
- package/contributions/blender.keymap.ts +33 -0
- package/contributions/blender.palette.json +6 -0
- package/contributions/blender.status.tsx +2 -6
- package/contributions/blender.style.ts +30 -89
- package/contributions/model.layout.ts +7 -7
- package/contributions/models.finder.ts +16 -0
- package/contributions/sculpt.layout.ts +3 -0
- package/contributions/shading.layout.ts +3 -0
- package/contributions/studiolights/world/city.exr +0 -0
- package/contributions/studiolights/world/courtyard.exr +0 -0
- package/contributions/studiolights/world/forest.exr +0 -0
- package/contributions/studiolights/world/interior.exr +0 -0
- package/contributions/studiolights/world/license.txt +15 -0
- package/contributions/studiolights/world/night.exr +0 -0
- package/contributions/studiolights/world/studio.exr +0 -0
- package/contributions/studiolights/world/sunrise.exr +0 -0
- package/contributions/studiolights/world/sunset.exr +0 -0
- package/contributions/uv-editing.layout.ts +3 -0
- package/dist-node/serving.mjs +624 -0
- package/host/blender-runtime-host.ts +11 -1
- package/node/blender-mcp.ts +2 -2
- package/package.json +20 -10
- package/serving/blender-routes.ts +427 -0
- package/serving/blender-wasm-artifact.ts +449 -0
- package/serving/index.ts +10 -0
- package/src/layouts.tsx +4 -5
- package/src/presentation.ts +111 -0
- package/src/regions.ts +59 -0
|
@@ -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. */
|
|
@@ -187,14 +187,20 @@ const MAPPING = {
|
|
|
187
187
|
'#69aa15',
|
|
188
188
|
'the same blend over `.tui.yaxis` #8bdc00 — measured, see axisX.',
|
|
189
189
|
),
|
|
190
|
-
// There is no `viewport.axisZ`:
|
|
191
|
-
//
|
|
192
|
-
// draws two axis lines. `.tui.zaxis` #2890ff is the member a third would
|
|
193
|
-
// cite when one exists — a palette key no reader reads is a name with no
|
|
194
|
-
// caller.
|
|
190
|
+
// There is no `viewport.axisZ`: the grid plane draws two axis lines. The
|
|
191
|
+
// gizmos carry all three (`gizmo.*`).
|
|
195
192
|
'viewport.selection': rgb('space_view3d.select'),
|
|
196
193
|
'viewport.active': rgb('space_view3d.active'),
|
|
197
194
|
|
|
195
|
+
// The transform gizmo draws the theme's axis colours as they are
|
|
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).
|
|
200
|
+
'gizmo.x': rgb('tui.xaxis'),
|
|
201
|
+
'gizmo.y': rgb('tui.yaxis'),
|
|
202
|
+
'gizmo.z': rgb('tui.zaxis'),
|
|
203
|
+
|
|
198
204
|
'region.outliner': rgb('space_outliner.back'),
|
|
199
205
|
'region.properties': rgb('space_properties.back'),
|
|
200
206
|
|
|
@@ -214,6 +220,11 @@ const MAPPING = {
|
|
|
214
220
|
'`DEF_ICON`, drawn in `.text`. Measured on the reference frames, and a level below ' +
|
|
215
221
|
'`.tui.icon_scene` #cccccc, which is what it sits beside.',
|
|
216
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
|
+
),
|
|
217
228
|
'category.operator': held(
|
|
218
229
|
'#95dab2',
|
|
219
230
|
"Blender's edit-mode tool column tints its creating operators green; no theme member carries " +
|
|
@@ -383,9 +383,27 @@ function Numeric({
|
|
|
383
383
|
);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
/**
|
|
387
|
-
*
|
|
388
|
-
|
|
386
|
+
/** An array component's letter, as `RNA_property_array_item_char` gives it:
|
|
387
|
+
* W/X/Y/Z for a quaternion or axis-angle (the angle first), X/Y/Z/W for the
|
|
388
|
+
* vector subtypes, R/G/B/A for a colour, and none otherwise, where the
|
|
389
|
+
* component is numbered. */
|
|
390
|
+
const VECTOR_SUBTYPES = new Set([
|
|
391
|
+
'TRANSLATION',
|
|
392
|
+
'DIRECTION',
|
|
393
|
+
'XYZ',
|
|
394
|
+
'XYZ_LENGTH',
|
|
395
|
+
'EULER',
|
|
396
|
+
'VELOCITY',
|
|
397
|
+
'ACCELERATION',
|
|
398
|
+
'COORDS',
|
|
399
|
+
]);
|
|
400
|
+
function arrayItemChar(subtype: string, index: number): string | undefined {
|
|
401
|
+
if (subtype === 'QUATERNION' || subtype === 'AXISANGLE') return 'WXYZ'[index];
|
|
402
|
+
if (index >= 4) return undefined;
|
|
403
|
+
if (VECTOR_SUBTYPES.has(subtype)) return 'XYZW'[index];
|
|
404
|
+
if (subtype === 'COLOR' || subtype === 'COLOR_GAMMA') return 'RGBA'[index];
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
389
407
|
|
|
390
408
|
/** A field's WHOLE name, for the control's `aria-label` — the property's UI
|
|
391
409
|
* name (its RNA identifier when it has none), plus the array component's
|
|
@@ -393,7 +411,7 @@ const ARRAY_LETTERS = ['X', 'Y', 'Z', 'W'];
|
|
|
393
411
|
* component after the first. */
|
|
394
412
|
function fieldName(row: BlenderRnaRow, index?: number): string {
|
|
395
413
|
const name = row.name || row.identifier;
|
|
396
|
-
return index === undefined ? name : `${name} ${
|
|
414
|
+
return index === undefined ? name : `${name} ${arrayItemChar(row.subtype, index) ?? index}`;
|
|
397
415
|
}
|
|
398
416
|
|
|
399
417
|
function colourOf(values: readonly number[]): string {
|
|
@@ -499,8 +517,8 @@ function ArrayRows({ row, label, title, write }: WidgetProps & { label: string;
|
|
|
499
517
|
<>
|
|
500
518
|
{values.map((value, index) => (
|
|
501
519
|
<Row
|
|
502
|
-
key={`${row.identifier}-${
|
|
503
|
-
label={componentLabel(label, index,
|
|
520
|
+
key={`${row.identifier}-${index}`}
|
|
521
|
+
label={componentLabel(label, index, row.subtype)}
|
|
504
522
|
title={title}
|
|
505
523
|
readonly={row.readonly}
|
|
506
524
|
identifier={`${row.identifier}[${index}]`}
|
|
@@ -526,9 +544,10 @@ function ArrayRows({ row, label, title, write }: WidgetProps & { label: string;
|
|
|
526
544
|
);
|
|
527
545
|
}
|
|
528
546
|
|
|
529
|
-
function componentLabel(label: string, index: number,
|
|
530
|
-
|
|
531
|
-
return
|
|
547
|
+
function componentLabel(label: string, index: number, subtype: string): string {
|
|
548
|
+
const letter = arrayItemChar(subtype, index);
|
|
549
|
+
if (letter === undefined) return index === 0 ? label : `${label} ${index}`;
|
|
550
|
+
return `${index === 0 ? `${label} ` : ''}${letter}`;
|
|
532
551
|
}
|
|
533
552
|
|
|
534
553
|
function widgetFor(props: WidgetProps): React.ReactNode {
|
|
@@ -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 {
|