@volter/editor-blender 0.1.150 → 0.1.152
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 +26 -56
- package/contributions/blender-node-editor.document.tsx +30 -181
- 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 +99 -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 +59 -91
- package/contributions/blender-properties-tab.tsx +3 -50
- package/contributions/blender-properties-view.tsx +39 -181
- package/contributions/blender-remedy.ts +14 -0
- package/contributions/blender-runtime-skin.ts +103 -99
- package/contributions/blender-runtime.document.tsx +170 -618
- 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 +145 -333
- package/contributions/blender-uv-editor.document.tsx +48 -112
- package/contributions/blender-uv-geometry.ts +14 -179
- package/contributions/blender-version.status.tsx +45 -31
- 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 +64 -0
- package/contributions/blender.status.tsx +8 -150
- 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 +3 -30
- 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 -32
- package/host/blender-runtime-host.ts +795 -412
- package/host/scene-sound.ts +143 -0
- package/node/blender-mcp.ts +52 -95
- package/package.json +1221 -46
- package/producticons/blender-icons.otf +0 -0
- package/producticons/blender-product-icon-theme.json +129 -0
- package/serving/blender-routes.ts +9 -48
- 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/node-view-state.ts +3 -25
- package/src/play-mode.ts +13 -42
- package/src/play-tool.ts +44 -0
- 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,37 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WHAT THE PROPERTIES SECTIONS ARE LOOKING AT — the page-side half of the RNA
|
|
3
|
-
* door (WORK.md §Blender in the tab is Blender, "Inspection parity", I1).
|
|
4
|
-
*
|
|
5
|
-
* React-free ON PURPOSE: this module is a plain store with `subscribe` /
|
|
6
|
-
* `version` / `state`, so the host's Blender side can import it without
|
|
7
|
-
* pulling React into its closure, and the sections read it through one
|
|
8
|
-
* `useSyncExternalStore` (`blender-properties-view.tsx`).
|
|
9
|
-
*
|
|
10
|
-
* THE SUBJECT IS THE PRESENTED BLENDER CONTEXT. The person clicks a node
|
|
11
|
-
* in our viewport; the adapter hands that node's `THREE.Object3D`; the
|
|
12
|
-
* presented view answers which Blender object that three object IS
|
|
13
|
-
* (`BlenderRuntimeView.blenderObjectName`, a lookup by object identity in the
|
|
14
|
-
* frame's own table — never a name match). That name goes to
|
|
15
|
-
* `rna_context(object=…)`, which builds the Properties context around it
|
|
16
|
-
* exactly as `buttons_context.cc` builds one around
|
|
17
|
-
* `BKE_view_layer_active_object_get`. Reading our selection never WRITES the
|
|
18
|
-
* engine's active object: that is a mutation, and the document would save it.
|
|
19
|
-
* With no selected node, the presented frame's active object remains the
|
|
20
|
-
* Properties subject; no active object resolves the scene instead.
|
|
21
|
-
*
|
|
22
|
-
* WHEN IT RE-READS. Three times, and each is a signal rather than a poll:
|
|
23
|
-
* - a PRESENT (`BlenderRuntimeView.subscribeFrames`) — every mutation
|
|
24
|
-
* presents (`session.py::dispatch`), so a frame is "what you are showing is
|
|
25
|
-
* stale";
|
|
26
|
-
* - the RNA DOOR'S OWN VERSION (`subscribeBlenderRna`, ruling 3 of
|
|
27
|
-
* 2026-09-19) — because a write can change what RNA answers and nothing the
|
|
28
|
-
* presenter draws, so no frame ships: `Node.panel_states[n].is_collapsed`
|
|
29
|
-
* and `Object.hide_render` are the two measured cases;
|
|
30
|
-
* - the SELECTION moving to another datablock.
|
|
31
|
-
* A re-read that changes which TABS exist also calls the host door's
|
|
32
|
-
* `documents.contextChanged`, because the tab rail IS the section list and
|
|
33
|
-
* nothing in a host store moved.
|
|
34
|
-
*/
|
|
1
|
+
/** React-free Properties store over the RNA door. Resolve the subject from the presented Blender context without mutating selection. Re-read on presented frames, RNA writes and selection changes; notify documents.contextChanged whenever the resolved tab context changes. */
|
|
35
2
|
|
|
36
3
|
import type { BlenderRnaContext, BlenderRnaView } from '@volter/blender-engine/browser/rna';
|
|
37
4
|
import { editorHost } from '@volter/sdk/host';
|
|
@@ -41,9 +8,12 @@ import {
|
|
|
41
8
|
blenderRna,
|
|
42
9
|
blenderRnaContext,
|
|
43
10
|
blenderRnaSet,
|
|
11
|
+
blenderModelChanging,
|
|
12
|
+
blenderModelConflict,
|
|
44
13
|
blenderSessionStarted,
|
|
45
14
|
noteBlenderRnaChanged,
|
|
46
15
|
subscribeBlenderRna,
|
|
16
|
+
subscribeBlenderStarting,
|
|
47
17
|
} from '../host/blender-runtime-host';
|
|
48
18
|
import { blenderOutlinerState } from './blender-outliner-model';
|
|
49
19
|
|
|
@@ -65,13 +35,10 @@ const isPresentedView = (value: unknown): value is PresentedView =>
|
|
|
65
35
|
typeof (value as PresentedView).blenderObjectName === 'function' &&
|
|
66
36
|
typeof (value as PresentedView).blenderSelection === 'function';
|
|
67
37
|
|
|
68
|
-
/** The
|
|
69
|
-
* adapter is deliberately `unknown` at the contribution point (adapter-native
|
|
70
|
-
* API, never a fabricated projection), and `@volter/editor-blender` holds no host
|
|
71
|
-
* import — so the shape it needs is stated here and checked at runtime. */
|
|
38
|
+
/** The adapter-native fields this SDK-host reader uses, checked at runtime. */
|
|
72
39
|
interface Object3DAuthoringLike {
|
|
73
40
|
readonly documentId: string;
|
|
74
|
-
readonly hierarchy: {
|
|
41
|
+
readonly hierarchy: { nativeObject(id: string): THREE.Object3D | null };
|
|
75
42
|
readonly documentRootObject: THREE.Object3D | null;
|
|
76
43
|
}
|
|
77
44
|
|
|
@@ -79,7 +46,7 @@ const isObject3DAuthoring = (value: unknown): value is Object3DAuthoringLike =>
|
|
|
79
46
|
typeof value === 'object' &&
|
|
80
47
|
value !== null &&
|
|
81
48
|
typeof (value as Object3DAuthoringLike).documentId === 'string' &&
|
|
82
|
-
typeof (value as Object3DAuthoringLike).hierarchy?.
|
|
49
|
+
typeof (value as Object3DAuthoringLike).hierarchy?.nativeObject === 'function';
|
|
83
50
|
|
|
84
51
|
function presentedView(): PresentedView | null {
|
|
85
52
|
const published = editorHost().documents.context(blenderPresentationDocumentId());
|
|
@@ -127,20 +94,14 @@ export function resolveBlenderSubject(
|
|
|
127
94
|
if (node.id === adapter.documentId)
|
|
128
95
|
return adapter.documentRootObject === view.root ? { kind: 'scene' } : null;
|
|
129
96
|
// A COLLECTION ROW HAS NO OBJECT, so it has to be resolved before the object
|
|
130
|
-
// lookup rather than after it — `
|
|
131
|
-
//
|
|
132
|
-
// rail simply stayed on the previous subject). The row table is this
|
|
97
|
+
// lookup rather than after it — `nativeObject` answers null for one, which would
|
|
98
|
+
// leave the rail on the previous subject. The row table is this
|
|
133
99
|
// package's own (`blender-outliner-model.ts`), so this is a map lookup.
|
|
134
100
|
const row = blenderOutlinerState().byId.get(node.id);
|
|
135
101
|
if (row?.type === 'TSE_LAYER_COLLECTION') return { kind: 'collection', path: row.path };
|
|
136
|
-
const object = adapter.hierarchy.
|
|
102
|
+
const object = adapter.hierarchy.nativeObject(node.id);
|
|
137
103
|
if (object === null) return null;
|
|
138
|
-
//
|
|
139
|
-
// Blender's Outliner, whose root row is the Scene Collection and NOT a
|
|
140
|
-
// document row of the host's — so the row that stands for the whole file is
|
|
141
|
-
// the one whose object is the presented ROOT, and it stands for exactly what
|
|
142
|
-
// the document row did: the tabs that need no object (Render, Output, View
|
|
143
|
-
// Layer, Scene, World, Collection).
|
|
104
|
+
// The Scene Collection root resolves the scene subject and its object-independent Properties tabs.
|
|
144
105
|
if (object === view.root) return { kind: 'scene' };
|
|
145
106
|
const name = view.blenderObjectName(object);
|
|
146
107
|
return name === null ? null : { kind: 'object', name };
|
|
@@ -185,6 +146,8 @@ let contextRequest = 0;
|
|
|
185
146
|
/** Views read at the current generation. A held view outside this set is
|
|
186
147
|
* STALE: still shown, re-read on the next ask. */
|
|
187
148
|
const fresh = new Set<string>();
|
|
149
|
+
/** Paths Blender answered with nothing at the current generation: it holds no model. */
|
|
150
|
+
const answeredNothing = new Set<string>();
|
|
188
151
|
|
|
189
152
|
function publish(next: Partial<BlenderPropertiesState>, tabsMayHaveChanged = false): void {
|
|
190
153
|
state = { ...state, ...next };
|
|
@@ -211,50 +174,30 @@ export function blenderPropertiesState(): BlenderPropertiesState {
|
|
|
211
174
|
return state;
|
|
212
175
|
}
|
|
213
176
|
|
|
214
|
-
/**
|
|
215
|
-
* re-read immediately (the tabs may have changed — a modifier was added, a
|
|
216
|
-
* bone became active); the datablock views are dropped and re-read by
|
|
217
|
-
* whichever section is still showing them.
|
|
218
|
-
*
|
|
219
|
-
* Two things call this, and the second is why it is no longer named
|
|
220
|
-
* `onFrame`: a PRESENTED FRAME, and the RNA door's own version (ruling 3,
|
|
221
|
-
* 2026-09-19 — `blenderRnaVersion`). A write that changes nothing the
|
|
222
|
-
* presenter draws ships no frame, and before this the rail simply kept the
|
|
223
|
-
* values it had. */
|
|
177
|
+
/** Invalidate datablock caches and re-read context on RNA version changes, including writes that produce no presented-frame change. */
|
|
224
178
|
function onEngineMoved(): void {
|
|
225
179
|
// STALE, NOT EMPTY. The generation moves, so every answer already in flight
|
|
226
180
|
// is dropped when it lands (a deleted duplicate's late read cannot overwrite
|
|
227
181
|
// the new selection) -- but the panel keeps showing what it holds until the
|
|
228
|
-
// re-read answers.
|
|
229
|
-
// panel on every edit, because every edit presents a frame.
|
|
182
|
+
// re-read answers. Keep the settled panel visible while each edit presents its frame.
|
|
230
183
|
const subject = readFor;
|
|
231
184
|
readGeneration += 1;
|
|
232
185
|
inFlight.clear();
|
|
233
186
|
fresh.clear();
|
|
187
|
+
answeredNothing.clear();
|
|
234
188
|
if (subject !== null) scheduleContextRead(subject, true);
|
|
235
189
|
else publish({});
|
|
236
190
|
}
|
|
237
191
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// exists from the first import while the document's view arrives later — and a
|
|
241
|
-
// write made before the first present is exactly the case that went unseen.
|
|
192
|
+
// Subscribe on module import so RNA writes before the first presented frame
|
|
193
|
+
// also invalidate the context. Frame changes provide the other signal.
|
|
242
194
|
subscribeBlenderRna(() => {
|
|
243
195
|
onEngineMoved();
|
|
244
196
|
});
|
|
245
197
|
|
|
246
|
-
/**
|
|
247
|
-
* THE READ NEVER STARTS INSIDE A RENDER. `showBlenderSubject` is called from a
|
|
248
|
-
* tab's `match()`, which the host runs while it is RENDERING the workspace —
|
|
249
|
-
* and the read's first act is to publish `loading`, which is a `setState` in
|
|
250
|
-
* every subscribed component. React said so by name ("Cannot update a
|
|
251
|
-
* component (BlenderPropertiesSection) while rendering a different
|
|
252
|
-
* component"), measured live 2026-09-19. The WANT is recorded
|
|
253
|
-
* synchronously so a second matcher in the same pass does not queue a second
|
|
254
|
-
* read; the read itself runs on the next turn of the loop, outside the render.
|
|
255
|
-
*/
|
|
256
198
|
let scheduledKey: string | null = null;
|
|
257
199
|
|
|
200
|
+
/** Queue the context read outside React render; record the request synchronously to deduplicate matchers in the same composition. */
|
|
258
201
|
function scheduleContextRead(key: string, refresh = false): void {
|
|
259
202
|
if (scheduledKey === key) return;
|
|
260
203
|
scheduledKey = key;
|
|
@@ -278,12 +221,7 @@ function watchFrames(): void {
|
|
|
278
221
|
if (frameSubscription !== null) return;
|
|
279
222
|
const view = presentedView();
|
|
280
223
|
if (view === null) return;
|
|
281
|
-
//
|
|
282
|
-
// subscription lives in this module because this module is what the document
|
|
283
|
-
// publishes its view to, but the signal belongs to every view over the
|
|
284
|
-
// engine — so it is minted in ONE place (ruling 3) and this module hears its
|
|
285
|
-
// own frame back through `subscribeBlenderRna` above, exactly as the node
|
|
286
|
-
// view and the UV view do.
|
|
224
|
+
// Report frames through the shared RNA version so Properties, node and UV views receive the same engine-change signal.
|
|
287
225
|
frameSubscription = view.subscribeFrames(noteBlenderRnaChanged);
|
|
288
226
|
}
|
|
289
227
|
|
|
@@ -297,6 +235,7 @@ async function readContext(key: string, refresh = false): Promise<void> {
|
|
|
297
235
|
readGeneration += 1;
|
|
298
236
|
inFlight.clear();
|
|
299
237
|
fresh.clear();
|
|
238
|
+
answeredNothing.clear();
|
|
300
239
|
}
|
|
301
240
|
const generation = readGeneration;
|
|
302
241
|
const request = ++contextRequest;
|
|
@@ -311,17 +250,22 @@ async function readContext(key: string, refresh = false): Promise<void> {
|
|
|
311
250
|
publish({ loading: false, error: null });
|
|
312
251
|
return;
|
|
313
252
|
}
|
|
314
|
-
//
|
|
315
|
-
// convenience: the gate a tab matches on is BOTH the subject this context
|
|
316
|
-
// was read for and the tab list, so comparing only the list missed the
|
|
317
|
-
// case that actually happens — selecting the Cube after the document row
|
|
318
|
-
// reads the SAME thirteen tabs for a different subject, the gated tabs'
|
|
319
|
-
// `state.object !== object` went false, and nothing ever said so. Measured
|
|
320
|
-
// live 2026-09-19: Collection/Modifiers/Particles/Object Data/Material
|
|
321
|
-
// disappeared on the first selection and only came back on a re-select.
|
|
253
|
+
// Notify the tab rail after every context read; both the subject identity and tab list determine matches.
|
|
322
254
|
publish({ context, loading: false, error: null }, true);
|
|
323
255
|
} catch (error) {
|
|
324
256
|
if (generation !== readGeneration || request !== contextRequest) return;
|
|
257
|
+
if (blenderModelChanging()) {
|
|
258
|
+
// Refused while Blender changes its model (a switch): still reading, and read again once the
|
|
259
|
+
// change is over, when the tabs ask for their subject again.
|
|
260
|
+
readFor = null;
|
|
261
|
+
publish({ loading: true, error: null });
|
|
262
|
+
const stop = subscribeBlenderStarting(() => {
|
|
263
|
+
if (blenderModelChanging()) return;
|
|
264
|
+
stop();
|
|
265
|
+
publish({}, true);
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
325
269
|
publish({ context: null, loading: false, error: describe(error) }, true);
|
|
326
270
|
}
|
|
327
271
|
}
|
|
@@ -354,7 +298,11 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
354
298
|
if (generation !== readGeneration) return;
|
|
355
299
|
inFlight.delete(path);
|
|
356
300
|
fresh.add(path);
|
|
357
|
-
if (view === null)
|
|
301
|
+
if (view === null) {
|
|
302
|
+
answeredNothing.add(path);
|
|
303
|
+
publish({});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
358
306
|
const views = new Map(state.views);
|
|
359
307
|
views.set(path, view);
|
|
360
308
|
publish({ views, error: null });
|
|
@@ -362,11 +310,30 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
362
310
|
.catch((error: unknown) => {
|
|
363
311
|
if (generation !== readGeneration) return;
|
|
364
312
|
inFlight.delete(path);
|
|
313
|
+
// Refused while Blender changes its model (a switch): the read waits for it and is asked again
|
|
314
|
+
// once the change is over (`subscribeBlenderRnaAvailability`), rather than failing.
|
|
315
|
+
if (blenderModelChanging()) return;
|
|
365
316
|
publish({ error: describe(error) });
|
|
366
317
|
});
|
|
367
318
|
return held;
|
|
368
319
|
}
|
|
369
320
|
|
|
321
|
+
/** Why `path` cannot be read, in a person's words, or null while its read may still come: Blender is on another
|
|
322
|
+
* document than the editor shows, or holds no model, or answered nothing for it. While Blender starts or changes its
|
|
323
|
+
* model (a switch) the read waits. Its section says the reason rather than waiting on a read that will not come. */
|
|
324
|
+
export function blenderRnaUnavailable(path: string): string | null {
|
|
325
|
+
if (state.views.get(path) !== undefined || blenderModelChanging()) return null;
|
|
326
|
+
const conflict = blenderModelConflict();
|
|
327
|
+
if (conflict !== null) return conflict;
|
|
328
|
+
if (!blenderSessionStarted() || answeredNothing.has(path)) return `Blender holds no model, so ${path} cannot be read.`;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Hear when what {@link blenderRnaUnavailable} answers may have changed. */
|
|
333
|
+
export function subscribeBlenderRnaAvailability(listener: () => void): () => void {
|
|
334
|
+
return subscribeBlenderStarting(listener);
|
|
335
|
+
}
|
|
336
|
+
|
|
370
337
|
/**
|
|
371
338
|
* WRITE ONE PROPERTY. The engine refuses a read-only one by name and this
|
|
372
339
|
* surfaces that refusal verbatim; a successful write presents, and the frame
|
|
@@ -384,6 +351,7 @@ export async function writeBlenderRnaProperty(
|
|
|
384
351
|
// The write presented; a re-read of this one datablock closes the loop
|
|
385
352
|
// even if the frame listener has not been taken yet.
|
|
386
353
|
fresh.delete(path);
|
|
354
|
+
answeredNothing.delete(path);
|
|
387
355
|
publish({ error: null });
|
|
388
356
|
return true;
|
|
389
357
|
} catch (error) {
|
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ONE PROPERTIES TAB IS ONE `selection.inspector` CONTRIBUTION — the factory
|
|
3
|
-
* every `properties-*.inspector.tsx` in this package is built from (WORK.md
|
|
4
|
-
* §Blender in the tab is Blender, "Inspection parity", I1, item 3).
|
|
5
|
-
*
|
|
6
|
-
* WHY A TAB IS A SECTION, and not a rail of our own: under the `properties`
|
|
7
|
-
* presentation the inspector's sections ARE the vertical icon rail, 1:1
|
|
8
|
-
* (`packages/sdk/src/kit/components/InspectionProjection.tsx`,
|
|
9
|
-
* `PropertiesColumn`). So the way to make Blender's tab rail appear is to
|
|
10
|
-
* contribute Blender's tabs as sections — which is also exactly the shape I2
|
|
11
|
-
* needs, because each of these modules then grows a CURATED body over the same
|
|
12
|
-
* door while the generic view stays beneath it.
|
|
13
|
-
*
|
|
14
|
-
* WHICH TABS STAND is `buttons_context.cc`'s answer, computed in the engine
|
|
15
|
-
* (`session.py::rna_context`) and read here. Two kinds of tab:
|
|
16
|
-
*
|
|
17
|
-
* - `standing: 'any' | 'object'` — a tab `buttons_context.cc` builds a path
|
|
18
|
-
* to UNCONDITIONALLY given a scene (`buttons_context_path_scene`,
|
|
19
|
-
* `_view_layer`, `_world`) or given an object at all
|
|
20
|
-
* (`buttons_context_path_object`, of which only the OBJECT tab is standing
|
|
21
|
-
* here — Physics and Constraints share that path but are context-gated,
|
|
22
|
-
* because Blender's Properties editor follows the ACTIVE OBJECT rather
|
|
23
|
-
* than the row that was clicked, and only the door knows there is one).
|
|
24
|
-
* These match from the SELECTION alone, synchronously.
|
|
25
|
-
* - everything else — Modifiers (an object type set), Particles (a mesh),
|
|
26
|
-
* Object Data (the object has data), Bone (an active bone), Material,
|
|
27
|
-
* Collection, Texture — which only the engine can answer, so they match
|
|
28
|
-
* against the context the door last returned.
|
|
29
|
-
*
|
|
30
|
-
* THE STANDING TABS EXIST SO THE RAIL IS NEVER EMPTY: `match()` is
|
|
31
|
-
* synchronous and the first composition has read nothing, so if every tab
|
|
32
|
-
* waited on the door there would be no rail at all. The READ itself is driven
|
|
33
|
-
* from `match` (see the note there), and when its answer lands it calls
|
|
34
|
-
* `documents.contextChanged`, which re-derives the whole inspection and brings
|
|
35
|
-
* the rest of the rail in.
|
|
36
|
-
*/
|
|
1
|
+
/** Build one selection.inspector contribution per Blender Properties tab. Standing scene/object tabs match synchronously; context-dependent tabs use the engine’s answer. Matchers drive the shared read, whose publication recomposes the rail; only the active tab mounts its body. */
|
|
37
2
|
|
|
38
3
|
import type {
|
|
39
4
|
ToolContributionNode,
|
|
@@ -52,11 +17,7 @@ export interface BlenderPropertiesTab {
|
|
|
52
17
|
/** Whether this tab needs the engine's answer to exist, or stands on the
|
|
53
18
|
* selection alone — see the header. */
|
|
54
19
|
readonly standing?: 'any' | 'object';
|
|
55
|
-
/**
|
|
56
|
-
* the matching `scripts/startup/bl_ui/properties_*.py`
|
|
57
|
-
* ({@link BlenderCuratedPanel}). The generic RNA view stays beneath them
|
|
58
|
-
* under "All properties"; a tab that declares none is the generic view
|
|
59
|
-
* alone, which is what I1 shipped. */
|
|
20
|
+
/** Curated panels follow Blender’s panel order. The generic RNA view remains beneath them under All properties. */
|
|
60
21
|
readonly curated?: readonly BlenderCuratedPanel[];
|
|
61
22
|
}
|
|
62
23
|
|
|
@@ -64,15 +25,7 @@ export function blenderPropertiesTabMatch(tab: BlenderPropertiesTab) {
|
|
|
64
25
|
return (node: ToolContributionNode | null, adapter: unknown): boolean => {
|
|
65
26
|
const subject = resolveBlenderSubject(node, adapter);
|
|
66
27
|
if (subject === null) return false;
|
|
67
|
-
//
|
|
68
|
-
// correction the live walk forced: under the `properties` presentation
|
|
69
|
-
// the host mounts ONLY THE ACTIVE TAB's body, so a read driven from a
|
|
70
|
-
// body never happened at all unless the person had already clicked a
|
|
71
|
-
// Blender tab — the rail showed the standing tabs and nothing else, for
|
|
72
|
-
// good (measured 2026-09-19). A matcher runs on every composition,
|
|
73
|
-
// active tab or not, so this is the one place that always sees the
|
|
74
|
-
// subject. It is idempotent by construction: `showBlenderSubject`
|
|
75
|
-
// returns immediately unless the subject moved.
|
|
28
|
+
// Drive the idempotent subject read from match, which runs for every composition even when this tab’s body is unmounted.
|
|
76
29
|
showBlenderSubject(subject);
|
|
77
30
|
if (tab.standing === 'any') return true;
|
|
78
31
|
if (tab.standing === 'object') return subject.kind === 'object';
|