@volter/editor-blender 0.1.149 → 0.1.151
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/LICENSE +18 -9
- package/README.md +5 -5
- package/contributions/blender-animation-editors.tsx +17 -69
- package/contributions/blender-display-settings.ts +36 -0
- package/contributions/blender-export-gltf.ts +228 -0
- package/contributions/blender-export.verbs.ts +39 -0
- package/contributions/blender-gltf-export.verbs.ts +312 -0
- package/contributions/blender-header-menus.tsx +37 -133
- package/contributions/blender-icon-trace.mjs +8 -98
- package/contributions/blender-icons.source.mjs +96 -860
- package/contributions/blender-mixer-pose.ts +369 -0
- package/contributions/blender-model-opening.tsx +3 -7
- package/contributions/blender-node-editor.document.tsx +28 -180
- package/contributions/blender-node-geometry.ts +44 -230
- package/contributions/blender-node-panels.source.mjs +7 -121
- package/contributions/blender-outliner-authoring.ts +61 -415
- package/contributions/blender-outliner-model.ts +15 -112
- package/contributions/blender-play-area.tsx +100 -0
- package/contributions/blender-playback-movie.ts +409 -0
- package/contributions/blender-pose.ts +18 -368
- package/contributions/blender-properties-context.ts +1 -1
- package/contributions/blender-properties-model.ts +15 -90
- package/contributions/blender-properties-tab.tsx +3 -50
- package/contributions/blender-properties-view.tsx +15 -147
- package/contributions/blender-remedy.ts +14 -0
- package/contributions/blender-runtime-skin.ts +103 -99
- package/contributions/blender-runtime.document.tsx +217 -339
- package/contributions/blender-scene-mixer.ts +51 -0
- package/contributions/blender-screenshot.service.ts +28 -0
- package/contributions/blender-timeline-geometry.ts +16 -136
- package/contributions/blender-timeline.document.tsx +142 -332
- package/contributions/blender-uv-editor.document.tsx +48 -112
- package/contributions/blender-uv-geometry.ts +14 -179
- package/contributions/blender-version.status.tsx +43 -23
- package/contributions/blender.environment.ts +2 -2
- package/contributions/blender.icons.json +0 -9
- package/contributions/blender.keymap.ts +9 -51
- package/contributions/blender.node-panels.json +4 -13
- package/contributions/blender.page.ts +63 -0
- package/contributions/blender.status.tsx +9 -145
- package/contributions/blender.style.ts +25 -152
- package/contributions/blender.verbs.ts +52 -0
- package/contributions/model.layout.ts +2 -34
- package/contributions/models.finder.ts +12 -32
- package/contributions/properties-bone-constraints.inspector.tsx +1 -16
- package/contributions/properties-bone.inspector.tsx +2 -31
- package/contributions/properties-collection.inspector.tsx +1 -34
- package/contributions/properties-constraints.inspector.tsx +1 -21
- package/contributions/properties-data.inspector.tsx +2 -39
- package/contributions/properties-material.inspector.tsx +2 -33
- package/contributions/properties-modifiers.inspector.tsx +2 -30
- package/contributions/properties-object.inspector.tsx +2 -44
- package/contributions/properties-output.inspector.tsx +1 -27
- package/contributions/properties-particles.inspector.tsx +2 -46
- package/contributions/properties-physics.inspector.tsx +2 -41
- package/contributions/properties-render.inspector.tsx +1 -45
- package/contributions/properties-scene.inspector.tsx +1 -35
- package/contributions/properties-texture.inspector.tsx +1 -49
- package/contributions/properties-view-layer.inspector.tsx +1 -29
- package/contributions/properties-world.inspector.tsx +2 -39
- package/contributions/shading.layout.ts +3 -78
- package/contributions/uv-editing.layout.ts +3 -70
- package/dist-node/serving.mjs +13 -30
- package/host/blender-runtime-host.ts +541 -372
- package/host/scene-sound.ts +143 -0
- package/node/blender-mcp.ts +49 -93
- package/package.json +1209 -46
- package/producticons/blender-icons.otf +0 -0
- package/producticons/blender-product-icon-theme.json +129 -0
- package/serving/blender-routes.ts +9 -43
- package/serving/blender-wasm-artifact.ts +29 -166
- package/serving/index.ts +1 -1
- package/src/api.ts +9 -0
- package/src/layouts.tsx +1 -17
- package/src/looks.ts +1 -12
- package/src/model-document-saving.ts +25 -0
- package/src/model-play-extension.ts +173 -0
- package/src/node-view-state.ts +3 -25
- package/src/play-mode.ts +64 -38
- package/src/presentation.ts +6 -18
- package/src/regions.ts +3 -43
- package/src/start-document.ts +15 -0
- package/src/timeline-view-state.ts +4 -39
- package/src/uv-view-state.ts +4 -20
- package/themes/blender.json +299 -0
- package/view/blender-view-routes.ts +1 -1
- package/contributions/blender-game-panel.tsx +0 -809
- package/contributions/blender-palette.source.mjs +0 -350
- package/contributions/blender-play-skin.ts +0 -508
- package/contributions/blender-screenshot.menu.ts +0 -32
- package/contributions/blender.command.ts +0 -109
- package/contributions/blender.palette.json +0 -102
- package/src/play-live.ts +0 -27
|
@@ -1,60 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* THE OUTLINER IS BLENDER'S DATABLOCK TREE, DRAWN BY OUR HIERARCHY PANEL
|
|
3
|
-
* (ARCHITECTURE-CORE §Blender north star, "Inspection parity, not editing
|
|
4
|
-
* parity"; WORK.md §Blender in the tab is Blender, "Inspection parity", I3).
|
|
5
|
-
*
|
|
6
|
-
* The Model document's rows used to be the PRESENTER's three.js graph — "Model
|
|
7
|
-
* › ModelLighting, Cube, Probe", a reading of Blender's data by the thing that
|
|
8
|
-
* draws the photograph. What Blender's own Outliner shows is the VIEW LAYER:
|
|
9
|
-
* Scene Collection, its collections, their objects, and under each object the
|
|
10
|
-
* datablocks it carries. That tree is the engine's, `rna_outliner` answers it,
|
|
11
|
-
* and this is the `HierarchyProvider` that hands it to `GameHierarchy`.
|
|
12
|
-
*
|
|
13
|
-
* IT REPLACES NOTHING ELSE. `ToolObject3DAuthoringProps.authoring` hands the
|
|
14
|
-
* document the host's DEFAULT adapter for delegation, and everything that is
|
|
15
|
-
* genuinely about the three.js presentation still goes to it: the transform
|
|
16
|
-
* the gizmo reads, the object a raycast hit, the selection the viewport
|
|
17
|
-
* outlines. What this overrides is the TREE and what a row means — because
|
|
18
|
-
* only the engine knows there is a Subdivision modifier on that cube.
|
|
19
|
-
*
|
|
20
|
-
* THE TWO ID SPACES, and the one place they meet. A row's id is Blender's own
|
|
21
|
-
* address (`bpy.data.objects["Cube"].modifiers["Subdivision"]`); the default
|
|
22
|
-
* adapter's ids are the presenter's three ids. They meet at `object3D`: a row
|
|
23
|
-
* that names an OBJECT resolves through the presented frame's own table
|
|
24
|
-
* (`BlenderRuntimeView.objectForBlenderName`, a lookup by the frame's id and
|
|
25
|
-
* never by `object.name`), and every delegation below goes through that one
|
|
26
|
-
* function. A row with no object — a collection, a vertex group, a bone —
|
|
27
|
-
* simply has none, which is the honest answer and the one the shell already
|
|
28
|
-
* treats as "no object here".
|
|
29
|
-
*
|
|
30
|
-
* AND THE SPACES MEET BOTH WAYS, which is the half that was missing.
|
|
31
|
-
* `selection.set` PUBLISHES the presentation's ids into the shell store (so
|
|
32
|
-
* the viewport's own selection machinery keeps working), and the viewport
|
|
33
|
-
* then asks THIS adapter about those ids: `editor-viewport.ts` computes its
|
|
34
|
-
* gizmo subject from `store.selectedEntityId` and runs it through
|
|
35
|
-
* `hierarchy.node` / `transforms.editability` / `_objectForAuthoringId`. So
|
|
36
|
-
* every id arriving from outside is normalized by {@link rowIdFor} — a row id
|
|
37
|
-
* passes through, a presentation id is resolved to its object and mapped back
|
|
38
|
-
* to the row that names it.
|
|
39
|
-
*
|
|
40
|
-
* SELECTING A DATABLOCK SELECTS ITS OBJECT, which is Blender's own behaviour
|
|
41
|
-
* rather than a convenience: `outliner_select.cc`'s activation walks back to
|
|
42
|
-
* the owning object (`outliner_search_back_te(te, ID_OB)`, `:863`) and sets
|
|
43
|
-
* the index on IT, which is why clicking a shape key in Blender moves the
|
|
44
|
-
* Properties editor to that object. The door carries the owning object per row
|
|
45
|
-
* (`BlenderOutlinerRow.object`) and that is what this selects.
|
|
46
|
-
*
|
|
47
|
-
* AND IT WRITES NOW — the "inspection parity, not editing parity" the first
|
|
48
|
-
* line cites is the order the UI was BUILT in, and the editing half of it is
|
|
49
|
-
* on the release's critical path as of the owner's 2026-09-21 amendment to
|
|
50
|
-
* ARCHITECTURE-CORE §Blender north star goal 3 ("the key to the edit is just
|
|
51
|
-
* macro adjustments"). Three providers here write, each one bpy call through a
|
|
52
|
-
* door that already existed: {@link transforms} (`rna-set` of `matrix_world`,
|
|
53
|
-
* B5), the `name` half of {@link inspector} (`rna-set` of `Object.name`), and
|
|
54
|
-
* {@link structure} (`bpy.ops.*` through the script door — add, delete,
|
|
55
|
-
* duplicate). Every one of them is Blender running its own code; none of them
|
|
56
|
-
* reimplements an operator.
|
|
57
|
-
*/
|
|
1
|
+
|
|
58
2
|
|
|
59
3
|
import type { BlenderOutlinerRow } from '@volter/blender-engine/browser/rna';
|
|
60
4
|
import type { ToolObject3DDocumentAuthoringFactory } from '@volter/editor-threejs/object3d-contributions';
|
|
@@ -119,30 +63,19 @@ function isObjectRow(row: BlenderOutlinerRow | undefined): row is BlenderOutline
|
|
|
119
63
|
return row?.type === 'TSE_SOME_ID' && row.struct === 'Object' && row.object !== undefined;
|
|
120
64
|
}
|
|
121
65
|
|
|
122
|
-
/** What the shell calls this row's TYPE on the inspector's identity row.
|
|
123
|
-
* Blender's Outliner prints no type text at all (`blender.style.ts`'s
|
|
124
|
-
* `hierarchyTypeSuffix: 'hidden'` measured that on `outliner.png`), so this
|
|
125
|
-
* is only ever read where a type is genuinely asked for. */
|
|
126
|
-
function typeLabel(row: BlenderOutlinerRow): string | undefined {
|
|
127
|
-
if (row.struct) return row.struct;
|
|
128
|
-
// A base row is a LABEL in Blender ("Modifiers", "Vertex Groups", "Pose"),
|
|
129
|
-
// and `TSE_MODIFIER_BASE` is not a noun a person says. Nothing here.
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
66
|
function nodeFor(row: BlenderOutlinerRow, parentId: string | null): EditorNode {
|
|
134
67
|
return {
|
|
135
68
|
id: row.id,
|
|
136
69
|
label: row.name,
|
|
137
70
|
// EVERY BLENDER ROW IS `entity`, INCLUDING THE COLLECTIONS. `folder` reads
|
|
138
|
-
// as structural to the shell (`
|
|
71
|
+
// as structural to the shell (`Hierarchy.tsx`'s `STRUCTURAL_ROLES`),
|
|
139
72
|
// and a structural row is drawn NO restriction columns — while Blender
|
|
140
73
|
// draws a collection the same eye and camera it draws an object
|
|
141
74
|
// (`outliner_draw.cc:1620-1700`). The role is what the shell branches on;
|
|
142
75
|
// the KIND is what says what this is.
|
|
143
76
|
role: 'entity',
|
|
144
77
|
kind: blenderOutlinerKind(row.icon),
|
|
145
|
-
...(
|
|
78
|
+
...(row.struct ? { typeLabel: row.struct } : {}),
|
|
146
79
|
parentId,
|
|
147
80
|
childIds: row.children.map((child) => child.id),
|
|
148
81
|
// BLENDER'S OWN DEFAULT OPEN STATE, not the shell's shape heuristic: a
|
|
@@ -169,13 +102,13 @@ const CAPABILITIES: AuthoringCapabilities = {
|
|
|
169
102
|
* `documentId` and `documentRootObject` are not on `AuthoringAdapter` — which
|
|
170
103
|
* is exactly why `blender-properties-model.ts`'s `isObject3DAuthoring` checks
|
|
171
104
|
* for them structurally — so they have to be declared somewhere for this
|
|
172
|
-
* literal to typecheck. Here, once, rather than at each getter:
|
|
173
|
-
*
|
|
174
|
-
*
|
|
105
|
+
* literal to typecheck. Here, once, rather than at each getter: a conditional
|
|
106
|
+
* spread in the literal would silently suppress TypeScript's excess-property
|
|
107
|
+
* check on them.
|
|
175
108
|
*/
|
|
176
109
|
interface BlenderOutlinerAdapter extends AuthoringAdapter {
|
|
177
|
-
readonly documentId: string
|
|
178
|
-
readonly documentRootObject: THREE.Object3D | null
|
|
110
|
+
readonly documentId: string;
|
|
111
|
+
readonly documentRootObject: THREE.Object3D | null;
|
|
179
112
|
}
|
|
180
113
|
|
|
181
114
|
/**
|
|
@@ -190,7 +123,7 @@ interface BlenderOutlinerAdapter extends AuthoringAdapter {
|
|
|
190
123
|
* provider's.
|
|
191
124
|
*/
|
|
192
125
|
export interface BlenderOutlinerHandle {
|
|
193
|
-
readonly documentId: string
|
|
126
|
+
readonly documentId: string;
|
|
194
127
|
/** The Blender object NAMES the panel's selection covers — the header's
|
|
195
128
|
* subject for every Object-menu row, and for Select ▸ Invert. */
|
|
196
129
|
selectedObjectNames(): readonly string[];
|
|
@@ -206,27 +139,17 @@ export interface BlenderOutlinerHandle {
|
|
|
206
139
|
* `_rna_address`' own identifier escaping and `_outliner_unique`'s
|
|
207
140
|
* disambiguating suffix, neither of which a caller can reconstruct. */
|
|
208
141
|
rowIdForObject(name: string): string | null;
|
|
209
|
-
readonly structure: StructureProvider
|
|
210
|
-
subscribe(listener: () => void): () => void;
|
|
142
|
+
readonly structure: StructureProvider & Required<Pick<StructureProvider, 'duplicateMany' | 'removeMany'>>;
|
|
211
143
|
}
|
|
212
144
|
|
|
213
145
|
/**
|
|
214
146
|
* THE LIVE OUTLINERS, by the document each is the Outliner of.
|
|
215
147
|
*
|
|
216
|
-
*
|
|
217
|
-
* the default adapter (see {@link BlenderOutlinerAdapter}) and is not
|
|
218
|
-
* necessarily resolved when the factory runs, so there is no key to register
|
|
219
|
-
* under at that moment.
|
|
148
|
+
* Each stage retains its own handle, including stages showing the same document.
|
|
220
149
|
*/
|
|
221
150
|
const liveOutliners = new Set<BlenderOutlinerHandle>();
|
|
222
151
|
|
|
223
|
-
/**
|
|
224
|
-
* A HANDLE ARRIVING IS NEWS. A header drawn before its document's stage made an Outliner has no
|
|
225
|
-
* subject and draws nothing; the tree's own version does not move when a handle registers, so
|
|
226
|
-
* without this it stayed empty until the next edit (measured: a cold load showed no View, Select,
|
|
227
|
-
* Add or Object until a script changed the tree). Notified on the next turn, never inside the
|
|
228
|
-
* render that made the handle.
|
|
229
|
-
*/
|
|
152
|
+
/** Publish handle registration on the next turn so headers waiting for an Outliner subject update outside the render pass. */
|
|
230
153
|
let liveOutlinersVersion = 0;
|
|
231
154
|
const liveOutlinerListeners = new Set<() => void>();
|
|
232
155
|
function liveOutlinersChanged(): void {
|
|
@@ -271,41 +194,7 @@ const NO_TRANSFORM = (): Transform => ({
|
|
|
271
194
|
scale: [1, 1, 1],
|
|
272
195
|
});
|
|
273
196
|
|
|
274
|
-
/**
|
|
275
|
-
* A presented object's pose, back in BLENDER's space and in the shape the RNA
|
|
276
|
-
* door's WRITE half takes.
|
|
277
|
-
*
|
|
278
|
-
* The presented root carries the signed axis permutation P that takes
|
|
279
|
-
* Blender's Z-up into three's Y-up — written as an exact matrix in
|
|
280
|
-
* `blender-runtime-view.ts`'s constructor precisely so it inverts exactly —
|
|
281
|
-
* and every presented object hangs under it. So `P⁻¹ · object.matrixWorld` is
|
|
282
|
-
* the matrix Blender calls `matrix_world`. The root's own WORLD matrix is what
|
|
283
|
-
* is inverted, not the literal, so any transform the stage puts above it is
|
|
284
|
-
* divided out too.
|
|
285
|
-
*
|
|
286
|
-
* THE TWO HALVES OF `matrix_world` USE OPPOSITE ORDERS, and that is measured
|
|
287
|
-
* rather than reasoned (2026-09-21, live, through `blender-rna-set` on the
|
|
288
|
-
* default cube):
|
|
289
|
-
*
|
|
290
|
-
* sent `[[1,0,0,0.5],[0,1,0,0],[0,0,1,0],[0,0,0,1]]` → `location` stayed
|
|
291
|
-
* `[0,0,0]` and the next depsgraph evaluation put `matrix_world` back to
|
|
292
|
-
* identity — Blender read the translation as the projective row, found
|
|
293
|
-
* none to decompose, and recomputed the matrix from loc/rot/scale;
|
|
294
|
-
* sent `[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0.5,0,0,1]]` → `location` reads
|
|
295
|
-
* `[0.5,0,0]` and `matrix_world` reads back `[1,0,0,0.5, …]`.
|
|
296
|
-
*
|
|
297
|
-
* So the WRITE goes into Blender's C `float[4][4]` in C order — entry `i` is
|
|
298
|
-
* the i-th COLUMN of the `mathutils.Matrix` a Python read returns — while the
|
|
299
|
-
* READ (`session.py`'s `_rna_value`, and the frame's
|
|
300
|
-
* `[[float(v) for v in row] for row in obj.matrix_world]`) is that Matrix's
|
|
301
|
-
* ROWS. The frame's reader is right to be row-major
|
|
302
|
-
* (`blender-runtime-view.ts` feeds it to `Matrix4.set`, which is row-major);
|
|
303
|
-
* this writer is its transpose.
|
|
304
|
-
*
|
|
305
|
-
* Which makes the code the simplest form of all: `THREE.Matrix4.elements` is
|
|
306
|
-
* ALREADY column-major, so the four groups of four are the four entries
|
|
307
|
-
* Blender wants, in order.
|
|
308
|
-
*/
|
|
197
|
+
/** Convert a presented world matrix back through the document root’s inverse, removing Blender-to-stage axes and any stage transform. The RNA write consumes columns while the read returns rows; Matrix4.elements already holds the required column-major order. */
|
|
309
198
|
function blenderWorldMatrixRows(object: THREE.Object3D, root: THREE.Object3D): number[][] {
|
|
310
199
|
// The ancestors too: a presented object owns its matrix
|
|
311
200
|
// (`matrixAutoUpdate = false`), so nothing recomposes it for us.
|
|
@@ -319,44 +208,7 @@ function blenderWorldMatrixRows(object: THREE.Object3D, root: THREE.Object3D): n
|
|
|
319
208
|
];
|
|
320
209
|
}
|
|
321
210
|
|
|
322
|
-
/**
|
|
323
|
-
* BLENDER'S ADD MENU, AS KINDS — every row `VIEW3D_MT_add` draws in Object
|
|
324
|
-
* Mode, each with Blender's OWN label and the `bpy.ops` call its row runs
|
|
325
|
-
* (`scripts/startup/bl_ui/space_view3d.py` at the engine's pin, cited per
|
|
326
|
-
* row). The menu's SHAPE — Blender's order, its separators and which rows are
|
|
327
|
-
* submenus — is `blender-header-menus.tsx`'s `ADD_MENU`, which names these
|
|
328
|
-
* kinds; this table is what each one DOES.
|
|
329
|
-
*
|
|
330
|
-
* EVERY ONE IS BLENDER RUNNING ITS OWN OPERATOR. There is no second
|
|
331
|
-
* implementation of a primitive's defaults, its vertex count or its shading
|
|
332
|
-
* here; the kind names the operator and the engine does the rest.
|
|
333
|
-
*
|
|
334
|
-
* A NEW OBJECT LANDS AT THE 3D CURSOR, as in Blender: no row states a
|
|
335
|
-
* `location`, and an unset one is `scene->cursor.location`
|
|
336
|
-
* (`editors/object/object_add.cc`, `location_from_view`). The cursor is drawn
|
|
337
|
-
* and placed on the Model document (`blender-runtime-cursor.ts`).
|
|
338
|
-
*
|
|
339
|
-
* A ROW THAT NEEDS A FILE IS DRAWN DISABLED WITH THE REASON, never as a
|
|
340
|
-
* gesture that cannot complete (the shape B6 set for Set Origin ▸ Origin to 3D
|
|
341
|
-
* Cursor). Blender's Image ▸ Reference/Background/Mesh Plane and Volume ▸
|
|
342
|
-
* Import OpenVDB open a FILE BROWSER; this editor has no door to open one
|
|
343
|
-
* through — `EditorHost.files` (`@volter/sdk/host`) reads and writes
|
|
344
|
-
* project-relative paths and has no "pick a file" member at all — so those
|
|
345
|
-
* rows carry {@link BlenderCreatableKind.refusal} instead. Measured at the
|
|
346
|
-
* engine on 2026-09-21: `object.empty_image_add` answers "poll() failed,
|
|
347
|
-
* context is incorrect / Please select at least one image", and
|
|
348
|
-
* `image.import_as_mesh_planes` and `object.volume_import` both return
|
|
349
|
-
* `CANCELLED` having created nothing — so the alternative was a row that
|
|
350
|
-
* looked like it worked.
|
|
351
|
-
*
|
|
352
|
-
* Blender's OWN conditional rows are read as conditions, not copied: `Search…`
|
|
353
|
-
* (`:2689`) draws only under `EXEC_REGION_WIN` and opens a menu-search dialog
|
|
354
|
-
* this editor does not have; Armature and Camera draw as SUBMENUS only when an
|
|
355
|
-
* add-on has extended them (`is_extended()`, `:2713`/`:2732`) and this engine
|
|
356
|
-
* loads none, so both are the flat row Blender falls through to; Curve's
|
|
357
|
-
* `Random` row (`:2471`) stands on `preferences.experimental.use_new_curves_tools`
|
|
358
|
-
* and this build reports `bpy.app.build_options.experimental_features` False.
|
|
359
|
-
*/
|
|
211
|
+
/** The Add table maps Blender’s Object Mode menu kinds to bpy operators. Blender supplies defaults and active-collection placement at the 3D cursor. File-browser operators carry a named refusal because this host has no file-picker door. Read Blender’s conditional rows against the engine’s available add-ons and experimental features. */
|
|
360
212
|
export interface BlenderCreatableKind {
|
|
361
213
|
/** The `kind` string the shell round-trips into {@link StructureProvider.create}. */
|
|
362
214
|
readonly kind: string;
|
|
@@ -594,13 +446,7 @@ export const CREATABLE_KINDS: readonly BlenderCreatableKind[] = [
|
|
|
594
446
|
// `VIEW3D_MT_add`, `space_view3d.py:2716` — the flat row Blender falls
|
|
595
447
|
// through to when no add-on has extended `VIEW3D_MT_armature_add`.
|
|
596
448
|
{ kind: 'armature', label: 'Armature', group: null, call: 'object.armature_add' },
|
|
597
|
-
//
|
|
598
|
-
// GENERIC add operator with a type, not a `lattice_add` of its own. Its kind
|
|
599
|
-
// is `lattice.lattice` and not `lattice` because the header stamps a row's
|
|
600
|
-
// test id from its kind and a submenu's from its group label — measured
|
|
601
|
-
// 2026-09-21: a bare `lattice` made `blender-add-lattice` name BOTH the
|
|
602
|
-
// submenu trigger and the row inside it, and the row's own click re-opened
|
|
603
|
-
// the submenu and added nothing.
|
|
449
|
+
// VIEW3D_MT_lattice_add uses the generic add operator. lattice.lattice gives the row a distinct test id from its lattice submenu.
|
|
604
450
|
{
|
|
605
451
|
kind: 'lattice.lattice',
|
|
606
452
|
label: 'Lattice',
|
|
@@ -849,10 +695,7 @@ function addCall(kind: BlenderCreatableKind): string {
|
|
|
849
695
|
return `bpy.ops.${call}(${args.join(', ')})`;
|
|
850
696
|
}
|
|
851
697
|
|
|
852
|
-
/**
|
|
853
|
-
* subset of Python's for every character a datablock name can hold, which is
|
|
854
|
-
* why the transform writer above addresses `bpy.data.objects[…]` the same
|
|
855
|
-
* way. */
|
|
698
|
+
/** Encode a Blender datablock name as a Python string literal. */
|
|
856
699
|
function py(name: string): string {
|
|
857
700
|
return JSON.stringify(name);
|
|
858
701
|
}
|
|
@@ -861,11 +704,10 @@ function py(name: string): string {
|
|
|
861
704
|
* THE STEP'S LABEL IS THE OPERATOR'S OWN NAME, the one Blender's Edit menu shows: Add › Cone is
|
|
862
705
|
* "Undo Add Cone", not "Undo Blender Python". Blender's names do not follow the menu rows (a Point
|
|
863
706
|
* light is "Add Light"), so they are Blender's, read from this build with
|
|
864
|
-
* `bpy.ops.<id>.get_rna_type().name` (`ot->name`) for
|
|
865
|
-
*
|
|
707
|
+
* `bpy.ops.<id>.get_rna_type().name` (`ot->name`) for the operators these menus run.
|
|
708
|
+
* An id not here keeps the script door's label.
|
|
866
709
|
*
|
|
867
|
-
*
|
|
868
|
-
* time for a name that never changes within a build.
|
|
710
|
+
* Cache these build-stable operator names instead of querying Blender on each click.
|
|
869
711
|
*/
|
|
870
712
|
const OPERATOR_NAMES: Readonly<Record<string, string>> = {
|
|
871
713
|
'curve.primitive_bezier_circle_add': "Add B\u00e9zier Circle",
|
|
@@ -873,7 +715,6 @@ const OPERATOR_NAMES: Readonly<Record<string, string>> = {
|
|
|
873
715
|
'curve.primitive_nurbs_circle_add': "Add Nurbs Circle",
|
|
874
716
|
'curve.primitive_nurbs_curve_add': "Add Nurbs Curve",
|
|
875
717
|
'curve.primitive_nurbs_path_add': "Add Path",
|
|
876
|
-
'image.import_as_mesh_planes': "Import Images as Planes",
|
|
877
718
|
'mesh.primitive_circle_add': "Add Circle",
|
|
878
719
|
'mesh.primitive_cone_add': "Add Cone",
|
|
879
720
|
'mesh.primitive_cube_add': "Add Cube",
|
|
@@ -886,41 +727,25 @@ const OPERATOR_NAMES: Readonly<Record<string, string>> = {
|
|
|
886
727
|
'mesh.primitive_uv_sphere_add': "Add UV Sphere",
|
|
887
728
|
'object.armature_add': "Add Armature",
|
|
888
729
|
'object.camera_add': "Add Camera",
|
|
889
|
-
'object.collection_add': "Add to Collection",
|
|
890
730
|
'object.collection_instance_add': "Add Collection Instance",
|
|
891
|
-
'object.constraint_add': "Add Constraint",
|
|
892
731
|
'object.curves_empty_hair_add': "Add Empty Curves",
|
|
893
|
-
'object.curves_random_add': "Add Random Curves",
|
|
894
|
-
'object.data_instance_add': "Add Object Data Instance",
|
|
895
|
-
'object.delete': "Delete",
|
|
896
|
-
'object.duplicate': "Duplicate Objects",
|
|
897
732
|
'object.effector_add': "Add Effector",
|
|
898
733
|
'object.empty_add': "Add Empty",
|
|
899
|
-
'object.empty_image_add': "Add Empty Image/Drop Image to Empty",
|
|
900
734
|
'object.grease_pencil_add': "Add Grease Pencil",
|
|
901
|
-
'object.grease_pencil_dash_modifier_segment_add': "Add Segment",
|
|
902
|
-
'object.grease_pencil_time_modifier_segment_add': "Add Segment",
|
|
903
735
|
'object.lattice_add_to_selected': "Add Lattice Deformer",
|
|
904
736
|
'object.light_add': "Add Light",
|
|
905
737
|
'object.lightprobe_add': "Add Light Probe",
|
|
906
738
|
'object.location_clear': "Clear Location",
|
|
907
|
-
'object.material_slot_add': "Add Material Slot",
|
|
908
739
|
'object.metaball_add': "Add Metaball",
|
|
909
|
-
'object.modifier_add': "Add Modifier",
|
|
910
740
|
'object.origin_set': "Set Origin",
|
|
911
|
-
'object.particle_system_add': "Add Particle System Slot",
|
|
912
741
|
'object.pointcloud_random_add': "Add Point Cloud",
|
|
913
742
|
'object.quick_fur': "Quick Fur",
|
|
914
743
|
'object.rotation_clear': "Clear Rotation",
|
|
915
744
|
'object.scale_clear': "Clear Scale",
|
|
916
|
-
'object.shaderfx_add': "Add Effect",
|
|
917
|
-
'object.shape_key_add': "Add Shape Key",
|
|
918
745
|
'object.speaker_add': "Add Speaker",
|
|
919
746
|
'object.text_add': "Add Text",
|
|
920
747
|
'object.transform_apply': "Apply Object Transform",
|
|
921
|
-
'object.vertex_group_add': "Add Vertex Group",
|
|
922
748
|
'object.volume_add': "Add Volume",
|
|
923
|
-
'object.volume_import': "Import OpenVDB Volume",
|
|
924
749
|
'surface.primitive_nurbs_surface_circle_add': "Add Surface Circle",
|
|
925
750
|
'surface.primitive_nurbs_surface_curve_add': "Add Surface Curve",
|
|
926
751
|
'surface.primitive_nurbs_surface_cylinder_add': "Add Surface Cylinder",
|
|
@@ -972,35 +797,7 @@ async function runBlenderOperator(
|
|
|
972
797
|
return { made, error: null };
|
|
973
798
|
}
|
|
974
799
|
|
|
975
|
-
|
|
976
|
-
* THE SELECTION IS BLENDER'S, AND THIS IS THE WRITE (WORK.md §THE BLENDER
|
|
977
|
-
* RELEASE, B11; ARCHITECTURE-CORE §Blender north star goal 1 — *"through the
|
|
978
|
-
* Blender MCP interface an agent cannot tell it is using anything other than
|
|
979
|
-
* real Blender"*).
|
|
980
|
-
*
|
|
981
|
-
* Every selection gesture on the Model document ends here: a viewport click, a
|
|
982
|
-
* box, an Outliner row, Select ▸ All/None/Invert, and the selection an
|
|
983
|
-
* operator leaves behind. ONE script per gesture, because a selection is one
|
|
984
|
-
* act — `select_set` per object in the view layer and one assignment to
|
|
985
|
-
* `view_layer.objects.active` — and because the script door PRESENTS when it
|
|
986
|
-
* returns, so the frame that comes back already carries the new `selected` and
|
|
987
|
-
* `active` for {@link blenderEngineSelection} to read.
|
|
988
|
-
*
|
|
989
|
-
* WHY IT IS A WRITE AT ALL, when `rna_outliner` deliberately takes the
|
|
990
|
-
* caller's selection instead of making one: reading a tree must not mutate the
|
|
991
|
-
* document, and that rule stands. This is not a read — it is the gesture
|
|
992
|
-
* itself. Before it, WALK 5 measured a click that picked the cube in this
|
|
993
|
-
* editor while `bpy.context.active_object` still answered `Point`, and the
|
|
994
|
-
* Shader Editor and UV Editing — which key on Blender's ACTIVE object and
|
|
995
|
-
* nothing else — drew a cube's material as "No material" and its unwrap as
|
|
996
|
-
* blank. There is no second selection to reconcile now: the engine holds it.
|
|
997
|
-
*
|
|
998
|
-
* THE LOOP IS OVER `view_layer.objects`, not `bpy.data.objects`: an object
|
|
999
|
-
* outside the view layer has no selection flag to set and `select_set` refuses
|
|
1000
|
-
* it by name. `bpy.data.objects.get` for the active is the same care — a name
|
|
1001
|
-
* the tree still holds but the engine has deleted answers None rather than
|
|
1002
|
-
* raising, and None is what Blender's own "no active object" is.
|
|
1003
|
-
*/
|
|
800
|
+
|
|
1004
801
|
async function writeBlenderSelection(
|
|
1005
802
|
names: readonly string[],
|
|
1006
803
|
active: string | null,
|
|
@@ -1030,12 +827,7 @@ async function writeBlenderSelection(
|
|
|
1030
827
|
* menu runs (`blender-header-menus.tsx`: Apply, Clear, Set Origin), and the
|
|
1031
828
|
* same shape {@link StructureProvider}'s own delete and duplicate use.
|
|
1032
829
|
*
|
|
1033
|
-
*
|
|
1034
|
-
* `context.selected_objects` (and `selected_editable_objects`); the engine runs
|
|
1035
|
-
* headless with no 3D view, and its selection is not the panel's, so the
|
|
1036
|
-
* subject is STATED per call rather than written into the engine first — a
|
|
1037
|
-
* write the document would then save, and the one `rna_outliner` already
|
|
1038
|
-
* refuses to make for the same reason.
|
|
830
|
+
* Supply the named objects through a context override for the headless operator.
|
|
1039
831
|
*
|
|
1040
832
|
* `call` is the bare `bpy.ops.…(…)` expression, so a caller reads exactly what
|
|
1041
833
|
* Blender will run and the ack can quote it.
|
|
@@ -1069,9 +861,7 @@ const PROVENANCE: AuthoringProvenance = {
|
|
|
1069
861
|
'Editing the model is a bpy call; the eye and render columns write the engine directly.',
|
|
1070
862
|
};
|
|
1071
863
|
|
|
1072
|
-
/**
|
|
1073
|
-
* say which column did. The checkbox is beside it now (I3 follow-up (b)), so
|
|
1074
|
-
* this names it rather than apologising for its absence. */
|
|
864
|
+
/** An eye refusal names the restriction column that excludes the collection; the checkbox is beside it. */
|
|
1075
865
|
const EXCLUDED_REASON =
|
|
1076
866
|
'This collection is excluded from the view layer (LayerCollection.exclude), so its viewport ' +
|
|
1077
867
|
'visibility is not what is hiding it. Clear the Exclude checkbox on this row first.';
|
|
@@ -1080,7 +870,7 @@ const EXCLUDED_REASON =
|
|
|
1080
870
|
* THE OUTLINER AUTHORING FOR A STAGE DRAWING `presented`: the rows are Blender's either way, and
|
|
1081
871
|
* the view is where a row meets the three object its stage picks, outlines and moves. The Model
|
|
1082
872
|
* document's own area draws the published view ({@link createBlenderOutlinerAuthoring}); a split
|
|
1083
|
-
* area draws a follower of it (`BlenderRuntimeView.follow
|
|
873
|
+
* area draws a follower of it (`BlenderRuntimeView.follow`).
|
|
1084
874
|
*/
|
|
1085
875
|
export function blenderOutlinerAuthoringFor(
|
|
1086
876
|
presented: () => PresentedView | null,
|
|
@@ -1121,7 +911,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1121
911
|
const rows = (): ReadonlyMap<string, BlenderOutlinerRow> => blenderOutlinerState().byId;
|
|
1122
912
|
|
|
1123
913
|
/** The object a ROW belongs to, as a three object — the place the two id
|
|
1124
|
-
* spaces meet
|
|
914
|
+
* spaces meet. */
|
|
1125
915
|
const objectForRow = (id: string): THREE.Object3D | null => {
|
|
1126
916
|
const row = rows().get(id);
|
|
1127
917
|
const view = presented();
|
|
@@ -1130,8 +920,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1130
920
|
return row.object === undefined ? null : view.objectForBlenderName(row.object);
|
|
1131
921
|
};
|
|
1132
922
|
|
|
1133
|
-
/**
|
|
1134
|
-
* is a map lookup rather than a walk. */
|
|
923
|
+
/** Find the object row whose presented Blender object has this identity. */
|
|
1135
924
|
const rowIdForObject = (object: THREE.Object3D): string | null => {
|
|
1136
925
|
const view = presented();
|
|
1137
926
|
if (view === null) return null;
|
|
@@ -1149,19 +938,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1149
938
|
return null;
|
|
1150
939
|
};
|
|
1151
940
|
|
|
1152
|
-
/**
|
|
1153
|
-
* THE SAME MEETING POINT, RUN BACKWARDS — normalize an id that arrived from
|
|
1154
|
-
* outside into one of OUR rows (see the header's "both ways").
|
|
1155
|
-
*
|
|
1156
|
-
* A row id passes straight through. Anything else is the PRESENTATION's id,
|
|
1157
|
-
* because that is what `selection.set` below publishes into the shell store
|
|
1158
|
-
* — and the gizmo block reads `store.selectedEntityId`, so the viewport asks
|
|
1159
|
-
* this adapter about ids it minted itself. Measured 2026-09-21 on a cold
|
|
1160
|
-
* model scaffold: without this, `adapterOnlyToolOwner` (which gates on
|
|
1161
|
-
* `hierarchy.node(id) !== null`) answered null for every selection, so the
|
|
1162
|
-
* 3D tool context never claimed the surface and the gizmo stayed detached
|
|
1163
|
-
* even with `capabilities.transform` true.
|
|
1164
|
-
*/
|
|
941
|
+
/** Normalize external presentation ids to Outliner row ids. Existing row ids pass through; foreign ids resolve through their presented object. */
|
|
1165
942
|
const rowIdFor = (id: string): string | null => {
|
|
1166
943
|
if (rows().has(id)) return id;
|
|
1167
944
|
const object = threeObject(defaultAdapter.hierarchy, id);
|
|
@@ -1177,14 +954,12 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1177
954
|
/** An id in the DEFAULT adapter's space, for the reads it owns. */
|
|
1178
955
|
const presentationIdFor = (id: string): string | null => {
|
|
1179
956
|
const object = objectFor(id);
|
|
1180
|
-
return object === null ? null :
|
|
957
|
+
return object === null ? null : defaultAdapter.hierarchy.idForNativeObject(object);
|
|
1181
958
|
};
|
|
1182
959
|
|
|
1183
960
|
const hierarchy: HierarchyProvider = {
|
|
1184
961
|
roots: () => {
|
|
1185
|
-
//
|
|
1186
|
-
// thing that always runs, and `showBlenderOutliner` is idempotent and
|
|
1187
|
-
// schedules its work outside the render pass.
|
|
962
|
+
// Drive the idempotent Outliner read from the panel; it schedules work outside the render pass.
|
|
1188
963
|
showBlenderOutliner(selectedObjects());
|
|
1189
964
|
const state = blenderOutlinerState();
|
|
1190
965
|
return (state.tree?.rows ?? []).map((row) => nodeFor(row, null));
|
|
@@ -1200,8 +975,8 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1200
975
|
const row = rowId === null ? undefined : state.byId.get(rowId);
|
|
1201
976
|
return row === undefined ? null : nodeFor(row, state.parentOf.get(row.id) ?? null);
|
|
1202
977
|
},
|
|
1203
|
-
|
|
1204
|
-
|
|
978
|
+
nativeObject: (id) => objectFor(id),
|
|
979
|
+
idForNativeObject: (object: THREE.Object3D) => rowIdForObject(object),
|
|
1205
980
|
};
|
|
1206
981
|
|
|
1207
982
|
/** The Blender object NAMES our selection covers — what the door wants. */
|
|
@@ -1217,16 +992,16 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1217
992
|
|
|
1218
993
|
/** Keep the PRESENTATION in step: the viewport's selection outline and the
|
|
1219
994
|
* gizmo bind to the default adapter's own ids, so a Blender row's object is
|
|
1220
|
-
* pushed through its `
|
|
995
|
+
* pushed through its `idForNativeObject`. The one place that push happens — a
|
|
1221
996
|
* gesture and a frame both end here. */
|
|
1222
997
|
const publishPresentation = (): void => {
|
|
1223
998
|
const mapped: string[] = [];
|
|
1224
999
|
for (const id of selected) {
|
|
1225
1000
|
const object = objectFor(id);
|
|
1226
|
-
const mappedId = object === null ? null : defaultAdapter.hierarchy.
|
|
1001
|
+
const mappedId = object === null ? null : defaultAdapter.hierarchy.idForNativeObject(object);
|
|
1227
1002
|
if (mappedId) mapped.push(mappedId);
|
|
1228
1003
|
}
|
|
1229
|
-
defaultAdapter.selection
|
|
1004
|
+
defaultAdapter.selection.set(mapped, { intent: 'exact' });
|
|
1230
1005
|
notify();
|
|
1231
1006
|
};
|
|
1232
1007
|
|
|
@@ -1248,9 +1023,9 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1248
1023
|
const syncFromEngine = (): void => {
|
|
1249
1024
|
if (disposed || pendingSelection !== null) return;
|
|
1250
1025
|
const engine = blenderEngineSelection();
|
|
1251
|
-
const key = `${blenderOutlinerVersion()}
|
|
1026
|
+
const key = `${blenderOutlinerVersion()}\0${engine.active ?? ''}\0${[...engine.selected]
|
|
1252
1027
|
.sort()
|
|
1253
|
-
.join('
|
|
1028
|
+
.join('\u0001')}`;
|
|
1254
1029
|
if (key === lastEngineKey) return;
|
|
1255
1030
|
lastEngineKey = key;
|
|
1256
1031
|
const names = engine.selected.filter((name) => name !== engine.active);
|
|
@@ -1267,22 +1042,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1267
1042
|
if (disposed) return;
|
|
1268
1043
|
const request = Symbol('Blender selection');
|
|
1269
1044
|
pendingSelection = request;
|
|
1270
|
-
//
|
|
1271
|
-
// ways"). A hierarchy click and this adapter's own verbs speak ROW ids;
|
|
1272
|
-
// the viewport's MARQUEE speaks the presentation's, because it picks
|
|
1273
|
-
// three objects and hands back whatever the default adapter calls them.
|
|
1274
|
-
//
|
|
1275
|
-
// MEASURED 2026-09-21: a box select across the scene left `selection`
|
|
1276
|
-
// holding nine `source-object:…` ids, so every row in the Outliner read
|
|
1277
|
-
// as unselected, the Properties rail had no Blender subject, and the
|
|
1278
|
-
// gizmo's own `rowIdFor` was the only thing in the file still working.
|
|
1279
|
-
// `rowIdFor` is the one function that answers for either space, and this
|
|
1280
|
-
// is the other end of the same seam `hierarchy.node` already normalizes.
|
|
1281
|
-
// An id that resolves to no row is DROPPED rather than stored: a row id
|
|
1282
|
-
// is this provider's whole currency and a foreign one is not a selection.
|
|
1283
|
-
// The WRITE below still names a presented object that has no row yet.
|
|
1284
|
-
// One row lookup per id: `rowIdForObject` scans the object rows, and a marquee over a
|
|
1285
|
-
// large scene passes every object it covers.
|
|
1045
|
+
// Normalize row and presentation ids through rowIdFor once per id. Drop ids with no row; the write can still name a presented object awaiting its tree row.
|
|
1286
1046
|
const rowIds = ids.map(rowIdFor);
|
|
1287
1047
|
selected = rowIds.filter((id): id is string => id !== null);
|
|
1288
1048
|
publishPresentation();
|
|
@@ -1311,12 +1071,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1311
1071
|
else if (!names.includes(name)) names.push(name);
|
|
1312
1072
|
return;
|
|
1313
1073
|
}
|
|
1314
|
-
// A
|
|
1315
|
-
// answers the first `_OUTLINER_PAGE` rows of every child list, so in a larger
|
|
1316
|
-
// scene the viewport picks objects that have no row yet (Canyon Comet: 1,146 rows
|
|
1317
|
-
// under its one collection, 64 answered, all six karts among the rest). The view
|
|
1318
|
-
// names the object by identity; the tree read for the frame this write presents
|
|
1319
|
-
// keeps the selection's rows (`page_tree`), and `syncFromEngine` takes the row up.
|
|
1074
|
+
// A presented object outside the Outliner page remains selectable by frame identity. The next tree read includes the selected rows, and syncFromEngine maps them into selection.
|
|
1320
1075
|
const object = threeObject(defaultAdapter.hierarchy, id);
|
|
1321
1076
|
const name = object === null ? null : (view?.blenderObjectName(object) ?? null);
|
|
1322
1077
|
if (name === null) unnamed.push(id);
|
|
@@ -1360,12 +1115,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1360
1115
|
if (restrict === undefined) return [];
|
|
1361
1116
|
const row = rows().get(id);
|
|
1362
1117
|
const properties: PropertyDescriptor[] = [
|
|
1363
|
-
//
|
|
1364
|
-
// datablock on a double-click or F2 in its own Outliner, and the
|
|
1365
|
-
// "this Outliner inspects" refusal this replaced stopped being true
|
|
1366
|
-
// when the gizmo started writing. {@link InspectorProvider.editability}
|
|
1367
|
-
// is where the per-row answer is, and it is what a COLLECTION row
|
|
1368
|
-
// still refuses.
|
|
1118
|
+
// Object rows support rename. InspectorProvider.editability supplies the per-row refusal for other datablocks.
|
|
1369
1119
|
{ path: 'name', label: 'Name', type: 'string', readonly: !isObjectRow(row) },
|
|
1370
1120
|
];
|
|
1371
1121
|
// The RESERVED paths the hierarchy's restriction columns read
|
|
@@ -1375,11 +1125,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1375
1125
|
properties.push({ path: 'visible', label: 'Show in Viewport', type: 'boolean' });
|
|
1376
1126
|
if (restrict.render !== undefined)
|
|
1377
1127
|
properties.push({ path: 'renderVisible', label: 'Show in Renders', type: 'boolean' });
|
|
1378
|
-
//
|
|
1379
|
-
// collections, and only under the View Layer display
|
|
1380
|
-
// (`outliner_draw.cc:1634-1653`, guarded by `SO_RESTRICT_ENABLE`, which
|
|
1381
|
-
// is in the default set at `space_outliner.cc:399`). The door already
|
|
1382
|
-
// answered `exclude` for those rows; what was missing was the column.
|
|
1128
|
+
// Draw the exclude checkbox only on layer collections in the View Layer display (outliner_draw.cc, SO_RESTRICT_ENABLE).
|
|
1383
1129
|
if (restrict.exclude !== undefined)
|
|
1384
1130
|
properties.push({ path: 'exclude', label: 'Exclude from View Layer', type: 'boolean' });
|
|
1385
1131
|
return properties;
|
|
@@ -1403,15 +1149,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1403
1149
|
const row = rows().get(id);
|
|
1404
1150
|
if (row === undefined) return { writable: false };
|
|
1405
1151
|
if (path === 'name') {
|
|
1406
|
-
//
|
|
1407
|
-
// F2 (`outliner_item_rename`, `outliner_tools.cc`) — and the refusal
|
|
1408
|
-
// that used to stand here ("this Outliner inspects") stopped being
|
|
1409
|
-
// true the moment the transform gizmo started writing `matrix_world`.
|
|
1410
|
-
// It is withdrawn for OBJECT rows, and only those: a collection, a
|
|
1411
|
-
// modifier, a vertex group and a bone each rename through a different
|
|
1412
|
-
// datablock's own `name`, and this unit does not reach them (WORK.md
|
|
1413
|
-
// §THE BLENDER RELEASE, B6 — "Collections stay read-only for this
|
|
1414
|
-
// unit, by name").
|
|
1152
|
+
// Object rows rename through their object datablock. Other row types require their own datablock writer and remain read-only.
|
|
1415
1153
|
if (isObjectRow(row)) return { writable: true };
|
|
1416
1154
|
return {
|
|
1417
1155
|
writable: false,
|
|
@@ -1445,10 +1183,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1445
1183
|
const name = String(value);
|
|
1446
1184
|
if (name === row.name || name.length === 0) return;
|
|
1447
1185
|
await blenderRnaSet(`bpy.data.objects[${py(row.object)}]`, 'name', name);
|
|
1448
|
-
//
|
|
1449
|
-
// address (`bpy.data.objects["Cube"]`), so a rename changes it. Re-read
|
|
1450
|
-
// and re-publish the selection, or the panels go on addressing a
|
|
1451
|
-
// datablock `bpy.data.objects` no longer has.
|
|
1186
|
+
// RNA-address row ids change on rename. Re-read the tree and re-publish selection against the new addresses.
|
|
1452
1187
|
await refreshBlenderOutliner([name]);
|
|
1453
1188
|
const renamed = rowIdForName(name);
|
|
1454
1189
|
if (renamed !== null) selection.set([renamed]);
|
|
@@ -1464,40 +1199,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1464
1199
|
},
|
|
1465
1200
|
};
|
|
1466
1201
|
|
|
1467
|
-
/**
|
|
1468
|
-
* THE GIZMO IS ONE MORE bpy WRITER (owner, 2026-09-21: *"I don't see the
|
|
1469
|
-
* gizmo in the blender app. Why not?"*).
|
|
1470
|
-
*
|
|
1471
|
-
* The precedent is the Object tab's Transform panel, transcribed by METHOD:
|
|
1472
|
-
* a field there writes through `blender-properties-model.ts`'s
|
|
1473
|
-
* `writeBlenderRnaProperty` → `blenderRnaSet` → `session.py::rna_set`, and
|
|
1474
|
-
* the write PRESENTS, so the picture that comes back is the engine's own
|
|
1475
|
-
* reading rather than ours. This provider is that same door driven by a
|
|
1476
|
-
* drag: `apply` moves the PRESENTED object so the picture follows the
|
|
1477
|
-
* pointer, and `endEdit` makes exactly ONE write when the gesture closes.
|
|
1478
|
-
*
|
|
1479
|
-
* WHAT IS WRITTEN IS `matrix_world`, not `location`/`rotation_euler`/`scale`.
|
|
1480
|
-
* Blender's own setter (`rna_Object_matrix_world_update` →
|
|
1481
|
-
* `BKE_object_apply_mat4`) decomposes a world matrix through the parent,
|
|
1482
|
-
* `matrix_parent_inverse`, the delta transforms and the object's
|
|
1483
|
-
* `rotation_mode`. That is Blender's math and it stays Blender's; we hand it
|
|
1484
|
-
* the matrix.
|
|
1485
|
-
*
|
|
1486
|
-
* THE ORIENTATION IS GATED BY THE ROUND TRIP, not by a claim. `blenderRnaSet`
|
|
1487
|
-
* bumps the RNA version and the write presents, so the frame that follows
|
|
1488
|
-
* re-sets this object's matrix from the engine — if the rows/columns or the
|
|
1489
|
-
* axis permutation were wrong, the object would JUMP the moment it came
|
|
1490
|
-
* back. No jump is the proof.
|
|
1491
|
-
*
|
|
1492
|
-
* LOCKS ARE NOT READ, and that is stated rather than silently skipped:
|
|
1493
|
-
* `lock_location`/`lock_rotation`/`lock_scale` constrain Blender's own
|
|
1494
|
-
* interactive transform operators, not a matrix assignment, and the Outliner
|
|
1495
|
-
* rows this adapter draws do not carry them.
|
|
1496
|
-
*
|
|
1497
|
-
* There is deliberately no `remove`: a Blender object has no absent
|
|
1498
|
-
* transform channel to drop, so the protocol's removal door refuses by name
|
|
1499
|
-
* instead of falling back to a value write.
|
|
1500
|
-
*/
|
|
1202
|
+
/** The gizmo previews changes on the presented object and writes matrix_world once at endEdit through the RNA door. Blender decomposes it through parent, delta transforms and rotation mode; the returned frame supplies the authoritative pose. Matrix assignment does not enforce interactive transform locks. Transform-channel removal refuses by name. */
|
|
1501
1203
|
const transforms: TransformProvider = {
|
|
1502
1204
|
get: (id) => {
|
|
1503
1205
|
// THE DEFAULT ADAPTER'S OWN READER, on the mapped id. A presented object
|
|
@@ -1506,14 +1208,12 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1506
1208
|
// decomposes instead — inventing a second reader here is how the two
|
|
1507
1209
|
// would drift.
|
|
1508
1210
|
const mapped = presentationIdFor(id);
|
|
1509
|
-
return (
|
|
1510
|
-
(mapped === null ? undefined : defaultAdapter.transforms?.get(mapped)) ?? NO_TRANSFORM()
|
|
1511
|
-
);
|
|
1211
|
+
return mapped === null ? NO_TRANSFORM() : defaultAdapter.transforms.get(mapped);
|
|
1512
1212
|
},
|
|
1513
1213
|
/** WHAT THIS DOES NOT ASK: the object's `lock_location`/`lock_rotation`/
|
|
1514
1214
|
* `lock_scale`. Those constrain Blender's own interactive operators, not a
|
|
1515
1215
|
* matrix assignment, and an Outliner row does not carry them — so a locked
|
|
1516
|
-
* channel still moves here.
|
|
1216
|
+
* channel still moves here. */
|
|
1517
1217
|
editability: (id) => {
|
|
1518
1218
|
const rowId = rowIdFor(id);
|
|
1519
1219
|
const row = rowId === null ? undefined : rows().get(rowId);
|
|
@@ -1531,7 +1231,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1531
1231
|
* without writing — `dragging-changed` fires on pointerdown, and a write
|
|
1532
1232
|
* that changes nothing still presents and still dirties the `.blend`.
|
|
1533
1233
|
* Keyed by the OBJECT rather than the id, because the viewport addresses
|
|
1534
|
-
* one gesture in both id spaces
|
|
1234
|
+
* one gesture in both id spaces. */
|
|
1535
1235
|
beginEdit: (id) => {
|
|
1536
1236
|
const object = objectFor(id);
|
|
1537
1237
|
if (object !== null && !gestureStart.has(object)) {
|
|
@@ -1554,8 +1254,9 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1554
1254
|
// `matrixWorld`, would sit still with them.
|
|
1555
1255
|
object.updateMatrix();
|
|
1556
1256
|
object.updateMatrixWorld(true);
|
|
1557
|
-
// And every other area drawing the model moves with it, as Blender redraws them all
|
|
1558
|
-
|
|
1257
|
+
// And every other area drawing the model moves with it, as Blender redraws them all; the
|
|
1258
|
+
// move is signalled as a change of what the views draw, this one's included.
|
|
1259
|
+
presented()?.mirrorPose(object);
|
|
1559
1260
|
},
|
|
1560
1261
|
endEdit: async (id): Promise<WriteAck | undefined> => {
|
|
1561
1262
|
const object = gestureObjects.get(id) ?? objectFor(id);
|
|
@@ -1613,41 +1314,13 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1613
1314
|
* is what keeps the Outliner, the Properties rail and the viewport's gizmo
|
|
1614
1315
|
* looking at one subject.
|
|
1615
1316
|
*/
|
|
1616
|
-
const selectAfterOperator = async (made: readonly string[]): Promise<
|
|
1317
|
+
const selectAfterOperator = async (made: readonly string[]): Promise<void> => {
|
|
1617
1318
|
await refreshBlenderOutliner(made);
|
|
1618
1319
|
const ids = made.map(rowIdForName).filter((id): id is string => id !== null);
|
|
1619
1320
|
if (ids.length > 0) selection.set(ids);
|
|
1620
|
-
return ids[0] ?? '';
|
|
1621
1321
|
};
|
|
1622
1322
|
|
|
1623
|
-
|
|
1624
|
-
* ADD, DELETE, DUPLICATE — BLENDER'S OWN OPERATORS, NAMED HERE AND RUN THERE
|
|
1625
|
-
* (WORK.md §THE BLENDER RELEASE, B6; ARCHITECTURE-CORE §Blender north star
|
|
1626
|
-
* goal 3 as amended: "can make MACRO adjustments at the screen — move,
|
|
1627
|
-
* rotate, scale a part, add or remove one, rename it … each a bpy call
|
|
1628
|
-
* through the doors that exist").
|
|
1629
|
-
*
|
|
1630
|
-
* Every verb is one `bpy.ops` call through the script door
|
|
1631
|
-
* (`blenderExecute`), which presents afterwards, so what comes back is the
|
|
1632
|
-
* engine's reading and never ours. The operators carry semantics we would
|
|
1633
|
-
* otherwise have to reimplement and get subtly wrong — `object.delete`
|
|
1634
|
-
* unparents a deleted object's children the way Blender does, which
|
|
1635
|
-
* `bpy.data.objects.remove` does not; `object.duplicate` copies the object's
|
|
1636
|
-
* data, its modifiers, its material slots and its parent relationship under
|
|
1637
|
-
* the user's own duplication preferences — so naming the operator IS the
|
|
1638
|
-
* implementation.
|
|
1639
|
-
*
|
|
1640
|
-
* THE CONTEXT OVERRIDE IS WHAT MAKES A ROW THE SUBJECT. `object.delete` and
|
|
1641
|
-
* `object.duplicate` read `context.selected_objects`; the engine runs
|
|
1642
|
-
* headless with no view3d and its selection is not the panel's, so each verb
|
|
1643
|
-
* states its subject with `bpy.context.temp_override(...)` rather than
|
|
1644
|
-
* writing the engine's selection first — a write the document would then
|
|
1645
|
-
* save.
|
|
1646
|
-
*
|
|
1647
|
-
* The script door checkpoints Blender's native undo stack and records its
|
|
1648
|
-
* callback in Code-OSS. These operators are undone by restoring Blender's
|
|
1649
|
-
* state, never by constructing an inverse operator or replaying the script.
|
|
1650
|
-
*/
|
|
1323
|
+
|
|
1651
1324
|
/**
|
|
1652
1325
|
* ADD ▸ CAMERA LOOKS THE WAY THE VIEW LOOKS, AND IS THE SCENE'S CAMERA WHEN THERE IS NONE: what
|
|
1653
1326
|
* `object_camera_add_exec` does with the 3D View it runs in (`object_add.cc`: `align` forced to
|
|
@@ -1657,7 +1330,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1657
1330
|
* location (the 3D cursor) kept, and the scene camera after it.
|
|
1658
1331
|
*/
|
|
1659
1332
|
const cameraAddCall = (): string => {
|
|
1660
|
-
const areaId =
|
|
1333
|
+
const areaId = defaultAdapter.documentId.replace(/^object3d-document:/, '');
|
|
1661
1334
|
const camera = viewportStages().find((one) => one.documentId === areaId)?.rig().drawCamera() ?? null;
|
|
1662
1335
|
const view = presented();
|
|
1663
1336
|
const lines = ['bpy.ops.object.camera_add()', 'added = bpy.context.view_layer.objects.active'];
|
|
@@ -1671,7 +1344,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1671
1344
|
return lines.join('\n');
|
|
1672
1345
|
};
|
|
1673
1346
|
|
|
1674
|
-
const structure:
|
|
1347
|
+
const structure: BlenderOutlinerHandle['structure'] = {
|
|
1675
1348
|
/**
|
|
1676
1349
|
* BLENDER'S ADD MENU, FOR EVERY PARENT — `parentId` is deliberately not
|
|
1677
1350
|
* read. Blender's Add adds to the ACTIVE COLLECTION, never under whatever
|
|
@@ -1708,12 +1381,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1708
1381
|
ack: (async (): Promise<WriteAck> => {
|
|
1709
1382
|
const { made, error } = await runBlenderOperator(call);
|
|
1710
1383
|
if (error !== null) return refuse(error);
|
|
1711
|
-
//
|
|
1712
|
-
// Blender's file-browser operators answer `{'CANCELLED'}` — a status
|
|
1713
|
-
// the script door reports no error for — with `bpy.data.objects`
|
|
1714
|
-
// unchanged, which is exactly the silent degrade `_absent_capability`
|
|
1715
|
-
// exists to stop (`session.py`'s own note). The set difference this
|
|
1716
|
-
// door already computes is what catches it.
|
|
1384
|
+
// Treat an operator with no new object as a refusal even when Blender returns CANCELLED without a script error.
|
|
1717
1385
|
if (made.length === 0)
|
|
1718
1386
|
return refuse(
|
|
1719
1387
|
`Blender ran ${call} and created nothing. The operator reported no error, so it ` +
|
|
@@ -1728,7 +1396,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1728
1396
|
})(),
|
|
1729
1397
|
};
|
|
1730
1398
|
},
|
|
1731
|
-
remove: (id) => structure.removeMany
|
|
1399
|
+
remove: (id) => structure.removeMany([id]),
|
|
1732
1400
|
/**
|
|
1733
1401
|
* BLENDER'S OWN DELETE, for one row or a whole selection. `object.delete`
|
|
1734
1402
|
* is the operator the X key runs (`blender_default.py:4552`, with
|
|
@@ -1740,7 +1408,8 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1740
1408
|
for (const id of ids) {
|
|
1741
1409
|
const name = objectNameFor(id);
|
|
1742
1410
|
if (name === null) {
|
|
1743
|
-
const
|
|
1411
|
+
const rowId = rowIdFor(id);
|
|
1412
|
+
const row = rowId === null ? undefined : rows().get(rowId);
|
|
1744
1413
|
return refuse(
|
|
1745
1414
|
`Only an object can be deleted here; this row is a ${row?.struct ?? row?.type ?? 'row'}. ` +
|
|
1746
1415
|
'Deleting a collection, a modifier or a vertex group is that datablock’s own bpy call.',
|
|
@@ -1824,12 +1493,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1824
1493
|
persisted: true,
|
|
1825
1494
|
};
|
|
1826
1495
|
},
|
|
1827
|
-
/**
|
|
1828
|
-
* a gesture of its own with a type to choose (Object, Object Keep
|
|
1829
|
-
* Transform, Armature Deform, …) and a `matrix_parent_inverse` to settle.
|
|
1830
|
-
* A drag in a tree cannot mean one of those without saying which, so this
|
|
1831
|
-
* unit does not guess (WORK.md §THE BLENDER RELEASE, B6 — parenting is
|
|
1832
|
-
* not in it). */
|
|
1496
|
+
/** Refuse tree reparenting: Blender parenting requires an explicit operator type and matrix_parent_inverse choice. */
|
|
1833
1497
|
reparent: () =>
|
|
1834
1498
|
refuse(
|
|
1835
1499
|
"Reparenting is Blender's `bpy.ops.object.parent_set`, which needs a parent TYPE " +
|
|
@@ -1839,22 +1503,12 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1839
1503
|
};
|
|
1840
1504
|
|
|
1841
1505
|
const adapter: BlenderOutlinerAdapter = {
|
|
1842
|
-
//
|
|
1843
|
-
// walk caught first: this package's Properties sections resolve their
|
|
1844
|
-
// subject by asking the ACTIVE adapter for `documentId`, `hierarchy.object3D`
|
|
1845
|
-
// and `documentRootObject` (`blender-properties-model.ts`,
|
|
1846
|
-
// `isObject3DAuthoring`) — a structural check, because a contributed
|
|
1847
|
-
// package holds no host type. An adapter that overrides the tree and drops
|
|
1848
|
-
// those two members is a DIFFERENT adapter to that check, and the whole
|
|
1849
|
-
// Properties rail silently stopped matching: selecting the Rig drew the
|
|
1850
|
-
// host's bare identity row instead of Blender's tabs (measured 2026-09-19).
|
|
1851
|
-
// They are getters rather than copies because the default adapter answers
|
|
1852
|
-
// both from live state.
|
|
1506
|
+
// Delegate the document identity and root; the hierarchy resolves each row's presented object.
|
|
1853
1507
|
get documentId() {
|
|
1854
|
-
return
|
|
1508
|
+
return defaultAdapter.documentId;
|
|
1855
1509
|
},
|
|
1856
1510
|
get documentRootObject() {
|
|
1857
|
-
return
|
|
1511
|
+
return defaultAdapter.documentRootObject;
|
|
1858
1512
|
},
|
|
1859
1513
|
capabilities: CAPABILITIES,
|
|
1860
1514
|
provenance: PROVENANCE,
|
|
@@ -1866,7 +1520,7 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1866
1520
|
subscribe: (listener) => {
|
|
1867
1521
|
listeners.add(listener);
|
|
1868
1522
|
const stopTree = subscribeBlenderOutliner(listener);
|
|
1869
|
-
const stopDefault = defaultAdapter.subscribe
|
|
1523
|
+
const stopDefault = defaultAdapter.subscribe(listener);
|
|
1870
1524
|
return () => {
|
|
1871
1525
|
listeners.delete(listener);
|
|
1872
1526
|
stopTree();
|
|
@@ -1895,14 +1549,6 @@ export function blenderOutlinerAuthoringFor(
|
|
|
1895
1549
|
},
|
|
1896
1550
|
rowIdForObject: (name) => rowIdForName(name),
|
|
1897
1551
|
structure,
|
|
1898
|
-
subscribe: (listener) => {
|
|
1899
|
-
listeners.add(listener);
|
|
1900
|
-
const stopTree = subscribeBlenderOutliner(listener);
|
|
1901
|
-
return () => {
|
|
1902
|
-
listeners.delete(listener);
|
|
1903
|
-
stopTree();
|
|
1904
|
-
};
|
|
1905
|
-
},
|
|
1906
1552
|
};
|
|
1907
1553
|
liveOutliners.add(handle);
|
|
1908
1554
|
// A factory is called during the stage's React render. Notify the other roots
|