@volter/editor-blender 0.1.151 → 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/contributions/blender-model-opening.tsx +25 -51
- package/contributions/blender-node-editor.document.tsx +2 -1
- package/contributions/blender-play-area.tsx +3 -4
- package/contributions/blender-properties-model.ts +44 -1
- package/contributions/blender-properties-view.tsx +24 -34
- package/contributions/blender-runtime.document.tsx +59 -385
- package/contributions/blender-timeline.document.tsx +6 -4
- package/contributions/blender-version.status.tsx +2 -8
- package/contributions/blender.page.ts +12 -11
- package/contributions/blender.status.tsx +1 -7
- package/contributions/models.finder.ts +4 -11
- package/dist-node/serving.mjs +0 -2
- package/host/blender-runtime-host.ts +290 -76
- package/node/blender-mcp.ts +3 -2
- package/package.json +56 -44
- package/serving/blender-routes.ts +0 -5
- package/src/api.ts +2 -2
- package/src/play-mode.ts +10 -65
- package/src/play-tool.ts +44 -0
- package/src/model-play-extension.ts +0 -173
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { ModelDocumentPreview } from '../src/model-document-preview';
|
|
2
2
|
import { modelOpeningErrorMessage } from '../src/model-opening-error';
|
|
3
|
-
import { Button,
|
|
3
|
+
import { Button, EditorBanner, StateSurface, themeVars } from '@volter/sdk/widgets';
|
|
4
4
|
import type { AuthoringAdapter, EditorNode } from '@volter/project/adapter';
|
|
5
5
|
import { AssetEditorSubject } from '@volter/sdk/components/AssetEditorShell';
|
|
6
|
-
import {
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export function BlenderModelOpening({ documentId, path, error, preview, retry, returnToPreview, preparingView = false
|
|
8
|
+
/**
|
|
9
|
+
* A model that is not open yet: its empty frame while it opens (the previous model's last view,
|
|
10
|
+
* when there is one), and what failed once it has failed. Its wait is the document's
|
|
11
|
+
* (`blender-runtime.document.tsx`). It has no authoring stage and publishes no model.
|
|
12
|
+
*/
|
|
13
|
+
export function BlenderModelOpening({ documentId, path, error, preview, retry, returnToPreview, preparingView = false }: {
|
|
14
14
|
readonly documentId: string;
|
|
15
15
|
readonly path: string;
|
|
16
16
|
readonly error: string | null;
|
|
@@ -18,63 +18,37 @@ export function BlenderModelOpening({ documentId, path, error, preview, retry, r
|
|
|
18
18
|
readonly retry: () => void;
|
|
19
19
|
readonly returnToPreview: () => void;
|
|
20
20
|
readonly preparingView?: boolean;
|
|
21
|
-
/** What a slow open is waiting on — progress beside the spinner, not an error. */
|
|
22
|
-
readonly note?: string | null;
|
|
23
21
|
}) {
|
|
24
|
-
const title = error ? 'Could not open model' : preparingView ? 'Preparing model view' : 'Opening model';
|
|
25
|
-
// A stalled open offers the same retry as an error.
|
|
26
|
-
const [startedAt] = useState(() => Date.now());
|
|
27
|
-
const [now, setNow] = useState(() => Date.now());
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (error) return;
|
|
30
|
-
const timer = setInterval(() => setNow(Date.now()), 5_000);
|
|
31
|
-
return () => clearInterval(timer);
|
|
32
|
-
}, [error]);
|
|
33
|
-
const waited = now - startedAt;
|
|
34
|
-
const stalled = !error && waited >= STALL_AFTER_MS;
|
|
35
22
|
// This document has not published an authoring stage yet. Its panels still
|
|
36
23
|
// belong to it, rather than the empty project's "Declare a root" fallback.
|
|
37
24
|
// A boundary row is informational; no old model objects remain editable.
|
|
25
|
+
const label = error ? 'Could not open model' : path;
|
|
38
26
|
const adapter = useMemo<AuthoringAdapter>(() => {
|
|
39
|
-
const node: EditorNode = { id: `opening:${documentId}`, label
|
|
27
|
+
const node: EditorNode = { id: `opening:${documentId}`, label, ...(error ? { secondaryLabel: path } : {}),
|
|
40
28
|
role: 'boundary', kind: 'object', parentId: null, childIds: [] };
|
|
41
29
|
return { capabilities: { transform: false, inspectorFields: false, persist: false },
|
|
42
30
|
hierarchy: { roots: () => [node], node: id => id === node.id ? node : null },
|
|
43
31
|
subscribe: () => () => {} };
|
|
44
|
-
}, [documentId,
|
|
32
|
+
}, [documentId, error, label, path]);
|
|
45
33
|
return (
|
|
46
34
|
<div data-testid="blender-model-opening" aria-busy={!error}
|
|
47
|
-
style={{ position: 'absolute', inset: 0,
|
|
48
|
-
{!preparingView && <AssetEditorSubject
|
|
49
|
-
status={error ? modelOpeningErrorMessage(error) :
|
|
35
|
+
style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', background: themeVars.surface.panel }}>
|
|
36
|
+
{!preparingView && <AssetEditorSubject documentId={documentId} type="Model" title={path}
|
|
37
|
+
status={error ? modelOpeningErrorMessage(error) : undefined}
|
|
50
38
|
selection={() => ({ adapter, nodeId: null })} />}
|
|
51
|
-
{preview && <
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
description={<>
|
|
61
|
-
<div style={{ color: themeVars.content.primary, marginBottom: spaceVar[3] }}>{path}</div>
|
|
62
|
-
{error ? modelOpeningErrorMessage(error) : preparingView ? 'The file is open. Preparing its viewport.' : preview ? 'The previous model stays visible while this file opens.' : 'Preparing Blender and opening this file. The model will appear here when it is ready.'}
|
|
63
|
-
{/* Show the exact cause as selectable text where the person is looking. */}
|
|
64
|
-
{stalled && <div data-testid="blender-model-opening-stalled" style={{ marginTop: spaceVar[3], color: themeVars.content.primary }}>
|
|
65
|
-
Still opening after {Math.floor(waited / 60_000)} minutes. It may have stalled: retry opening, or reload the page.
|
|
66
|
-
</div>}
|
|
67
|
-
{!error && note && <div data-testid="blender-model-opening-note" style={{ marginTop: spaceVar[3], color: themeVars.content.muted, userSelect: 'text' }}>{note}</div>}
|
|
68
|
-
{error && <div data-testid="blender-model-opening-detail"
|
|
69
|
-
style={{ marginTop: spaceVar[3], fontFamily: fontMono, fontSize: fontSizeVar.sm, color: themeVars.content.muted, whiteSpace: 'pre-wrap', userSelect: 'text', textAlign: 'left' }}>
|
|
70
|
-
{error}
|
|
71
|
-
</div>}
|
|
72
|
-
</>}
|
|
73
|
-
action={error || stalled ? <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: spaceVar[3] }}>
|
|
39
|
+
{preview && <EditorBanner tone="info">Previous model: {preview.path} · Preview only</EditorBanner>}
|
|
40
|
+
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
|
41
|
+
{preview && <img src={preview.image} alt={`Last view of ${preview.path}`} draggable={false}
|
|
42
|
+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain', pointerEvents: 'none' }} />}
|
|
43
|
+
{error && <StateSurface tone="error" fill data-testid="blender-model-opening-error"
|
|
44
|
+
title="Could not open model"
|
|
45
|
+
description={<>{path}<br />{modelOpeningErrorMessage(error)}</>}
|
|
46
|
+
detail={error}
|
|
47
|
+
action={<>
|
|
74
48
|
{preview && <Button onClick={returnToPreview}>Return to previous model</Button>}
|
|
75
49
|
<Button variant="secondary" onClick={retry}>Retry opening</Button>
|
|
76
|
-
|
|
77
|
-
/>
|
|
50
|
+
</>}
|
|
51
|
+
/>}
|
|
78
52
|
</div>
|
|
79
53
|
</div>
|
|
80
54
|
);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { BlenderNode, BlenderNodeTree } from '@volter/blender-engine/browser/rna';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
5
|
+
import { StateSurface } from '@volter/sdk/widgets';
|
|
5
6
|
import {
|
|
6
7
|
blenderNodeTree,
|
|
7
8
|
blenderRnaVersion,
|
|
@@ -1181,7 +1182,7 @@ function NodePanel({
|
|
|
1181
1182
|
>
|
|
1182
1183
|
<div style={{ opacity: 0.6, marginBottom: 'var(--volter-space-2)' }}>Node</div>
|
|
1183
1184
|
{!node ? (
|
|
1184
|
-
<
|
|
1185
|
+
<StateSurface inline title="No node looked at." />
|
|
1185
1186
|
) : (
|
|
1186
1187
|
<>
|
|
1187
1188
|
<Row label="Name" value={node.name} onRefuse={onRefuse} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** The bottom area shows
|
|
1
|
+
/** The bottom area shows the Play tool’s Panel, or below a model its animation editors. PlayModeSwitch stays in the header of either view, and exists only with a Play tool and a model in front. Each switch applies the mode’s area ratio; the person’s sash resize holds between switches. */
|
|
2
2
|
import { type MouseEvent, type ReactNode, useSyncExternalStore } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { playTool, subscribePlayTool } from '../src/play-tool';
|
|
4
4
|
import { blenderSkin } from './blender-runtime-skin';
|
|
5
5
|
import { type ModelPlayMode, modelPlayMode, playModeVersion, servedModelDocument, setChosenPlayMode, subscribePlayMode } from '../src/play-mode';
|
|
6
6
|
import { TIMELINE_CHROME, TIMELINE_THEME } from './blender-timeline-geometry';
|
|
@@ -8,7 +8,6 @@ import { TIMELINE_CHROME, TIMELINE_THEME } from './blender-timeline-geometry';
|
|
|
8
8
|
/** Reserve 20% of the column for the Game panel; the person can resize its sash. */
|
|
9
9
|
export const PLAY_PANEL_RATIO = 0.2;
|
|
10
10
|
|
|
11
|
-
const extension = () => modelPlayExtension();
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* FLIP A DOCUMENT BETWEEN GAME AND ANIMATION. A running game keeps running when Animation is
|
|
@@ -76,7 +75,7 @@ export function PlayAreaButton({ label, testId, disabled, pressed, wide, onClick
|
|
|
76
75
|
*/
|
|
77
76
|
export function PlayModeSwitch({ title }: { readonly title: string }) {
|
|
78
77
|
useSyncExternalStore(subscribePlayMode, playModeVersion, playModeVersion);
|
|
79
|
-
const found = useSyncExternalStore(
|
|
78
|
+
const found = useSyncExternalStore(subscribePlayTool, playTool, () => null);
|
|
80
79
|
const documentId = servedModelDocument();
|
|
81
80
|
if (found === null || documentId === null) return <span style={{ color: TIMELINE_CHROME.text }}>{title}</span>;
|
|
82
81
|
const mode = modelPlayMode(documentId);
|
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
blenderRna,
|
|
9
9
|
blenderRnaContext,
|
|
10
10
|
blenderRnaSet,
|
|
11
|
+
blenderModelChanging,
|
|
12
|
+
blenderModelConflict,
|
|
11
13
|
blenderSessionStarted,
|
|
12
14
|
noteBlenderRnaChanged,
|
|
13
15
|
subscribeBlenderRna,
|
|
16
|
+
subscribeBlenderStarting,
|
|
14
17
|
} from '../host/blender-runtime-host';
|
|
15
18
|
import { blenderOutlinerState } from './blender-outliner-model';
|
|
16
19
|
|
|
@@ -143,6 +146,8 @@ let contextRequest = 0;
|
|
|
143
146
|
/** Views read at the current generation. A held view outside this set is
|
|
144
147
|
* STALE: still shown, re-read on the next ask. */
|
|
145
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>();
|
|
146
151
|
|
|
147
152
|
function publish(next: Partial<BlenderPropertiesState>, tabsMayHaveChanged = false): void {
|
|
148
153
|
state = { ...state, ...next };
|
|
@@ -179,6 +184,7 @@ function onEngineMoved(): void {
|
|
|
179
184
|
readGeneration += 1;
|
|
180
185
|
inFlight.clear();
|
|
181
186
|
fresh.clear();
|
|
187
|
+
answeredNothing.clear();
|
|
182
188
|
if (subject !== null) scheduleContextRead(subject, true);
|
|
183
189
|
else publish({});
|
|
184
190
|
}
|
|
@@ -229,6 +235,7 @@ async function readContext(key: string, refresh = false): Promise<void> {
|
|
|
229
235
|
readGeneration += 1;
|
|
230
236
|
inFlight.clear();
|
|
231
237
|
fresh.clear();
|
|
238
|
+
answeredNothing.clear();
|
|
232
239
|
}
|
|
233
240
|
const generation = readGeneration;
|
|
234
241
|
const request = ++contextRequest;
|
|
@@ -247,6 +254,18 @@ async function readContext(key: string, refresh = false): Promise<void> {
|
|
|
247
254
|
publish({ context, loading: false, error: null }, true);
|
|
248
255
|
} catch (error) {
|
|
249
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
|
+
}
|
|
250
269
|
publish({ context: null, loading: false, error: describe(error) }, true);
|
|
251
270
|
}
|
|
252
271
|
}
|
|
@@ -279,7 +298,11 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
279
298
|
if (generation !== readGeneration) return;
|
|
280
299
|
inFlight.delete(path);
|
|
281
300
|
fresh.add(path);
|
|
282
|
-
if (view === null)
|
|
301
|
+
if (view === null) {
|
|
302
|
+
answeredNothing.add(path);
|
|
303
|
+
publish({});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
283
306
|
const views = new Map(state.views);
|
|
284
307
|
views.set(path, view);
|
|
285
308
|
publish({ views, error: null });
|
|
@@ -287,11 +310,30 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
287
310
|
.catch((error: unknown) => {
|
|
288
311
|
if (generation !== readGeneration) return;
|
|
289
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;
|
|
290
316
|
publish({ error: describe(error) });
|
|
291
317
|
});
|
|
292
318
|
return held;
|
|
293
319
|
}
|
|
294
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
|
+
|
|
295
337
|
/**
|
|
296
338
|
* WRITE ONE PROPERTY. The engine refuses a read-only one by name and this
|
|
297
339
|
* surfaces that refusal verbatim; a successful write presents, and the frame
|
|
@@ -309,6 +351,7 @@ export async function writeBlenderRnaProperty(
|
|
|
309
351
|
// The write presented; a re-read of this one datablock closes the loop
|
|
310
352
|
// even if the frame listener has not been taken yet.
|
|
311
353
|
fresh.delete(path);
|
|
354
|
+
answeredNothing.delete(path);
|
|
312
355
|
publish({ error: null });
|
|
313
356
|
return true;
|
|
314
357
|
} catch (error) {
|
|
@@ -8,12 +8,15 @@ import type {
|
|
|
8
8
|
BlenderRnaView,
|
|
9
9
|
} from '@volter/blender-engine/browser/rna';
|
|
10
10
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
11
|
+
import { Progress } from '@volter/sdk/progress';
|
|
11
12
|
import { faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
|
|
12
|
-
import { ColorPicker, EditorIcon, hexToRgb, parseAlpha, toHex } from '@volter/sdk/widgets';
|
|
13
|
+
import { ColorPicker, EditorIcon, hexToRgb, parseAlpha, StateSurface, toHex } from '@volter/sdk/widgets';
|
|
13
14
|
import {
|
|
14
15
|
type BlenderSubject,
|
|
15
16
|
blenderPropertiesState,
|
|
16
17
|
blenderPropertiesVersion,
|
|
18
|
+
blenderRnaUnavailable,
|
|
19
|
+
subscribeBlenderRnaAvailability,
|
|
17
20
|
blenderRnaViewFor,
|
|
18
21
|
showBlenderSubject,
|
|
19
22
|
subscribeBlenderProperties,
|
|
@@ -38,9 +41,6 @@ const SUB_PANEL_BACK = 'rgba(0,0,0,0.12)';
|
|
|
38
41
|
const WIDGET_INNER = '#545454';
|
|
39
42
|
const WIDGET_OUTLINE = '#3d3d3d';
|
|
40
43
|
const WIDGET_ITEM = '#4772b3';
|
|
41
|
-
/** `wcol_state.error` (`userdef_default_theme.c:234`) — the ink Blender puts
|
|
42
|
-
* on a widget whose state is an error. The door's refusals are drawn in it. */
|
|
43
|
-
const ERROR_INK = '#991616';
|
|
44
44
|
/** The collapse triangle's optical size and the gap after it. Blender draws
|
|
45
45
|
* the triangle as an icon inside `panel_label_offset`'s UI_UNIT_X lead-in
|
|
46
46
|
* (`interface_panel.cc:1049-1060`) rather than as text; 8 is that mark's
|
|
@@ -66,6 +66,14 @@ export function useBlenderProperties() {
|
|
|
66
66
|
|
|
67
67
|
// ---- Blender's panel -------------------------------------------------------
|
|
68
68
|
|
|
69
|
+
/** A datablock still being read: the Properties view's wait, or, when Blender holds no model to
|
|
70
|
+
* read it from, that failure said in its place. */
|
|
71
|
+
function Reading({ path }: { readonly path: string }) {
|
|
72
|
+
const unavailable = useSyncExternalStore(subscribeBlenderRnaAvailability, () => blenderRnaUnavailable(path), () => null);
|
|
73
|
+
if (unavailable !== null) return <StateSurface inline tone="error" title={unavailable} />;
|
|
74
|
+
return <Progress view="properties" title={"Reading " + path} />;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
function Panel({
|
|
70
78
|
title,
|
|
71
79
|
sub = false,
|
|
@@ -845,21 +853,19 @@ function CuratedCollectionPanel({
|
|
|
845
853
|
if (view === undefined)
|
|
846
854
|
return (
|
|
847
855
|
<Panel title={panel.title} closed={panel.closed === true}>
|
|
848
|
-
<
|
|
856
|
+
<Reading path={path} />
|
|
849
857
|
</Panel>
|
|
850
858
|
);
|
|
851
859
|
if (view.kind !== 'collection')
|
|
852
860
|
return (
|
|
853
861
|
<Panel title={panel.title} closed={panel.closed === true} note={path}>
|
|
854
|
-
<
|
|
855
|
-
{path} is not a collection — this panel is declared `collection: true`.
|
|
856
|
-
</div>
|
|
862
|
+
<StateSurface inline tone="error" title={<>{path} is not a collection — this panel is declared <code>collection: true</code>.</>} />
|
|
857
863
|
</Panel>
|
|
858
864
|
);
|
|
859
865
|
return (
|
|
860
866
|
<Panel title={panel.title} closed={panel.closed === true} note={path} count={view.count}>
|
|
861
867
|
{view.items.length === 0 ? (
|
|
862
|
-
<
|
|
868
|
+
<StateSurface inline title="None" />
|
|
863
869
|
) : null}
|
|
864
870
|
{view.items.map((item, index) => (
|
|
865
871
|
<Panel
|
|
@@ -870,9 +876,7 @@ function CuratedCollectionPanel({
|
|
|
870
876
|
sub
|
|
871
877
|
>
|
|
872
878
|
{item.path === null ? (
|
|
873
|
-
<
|
|
874
|
-
{item.type} — RNA gives this member no address.
|
|
875
|
-
</div>
|
|
879
|
+
<StateSurface inline title={<>{item.type} — RNA gives this member no address.</>} />
|
|
876
880
|
) : (
|
|
877
881
|
<CuratedRows
|
|
878
882
|
path={item.path}
|
|
@@ -903,9 +907,7 @@ function CuratedRows({
|
|
|
903
907
|
useBlenderProperties();
|
|
904
908
|
const view = blenderRnaViewFor(path);
|
|
905
909
|
if (view === undefined)
|
|
906
|
-
return
|
|
907
|
-
<div style={{ color: INK, opacity: READONLY_ALPHA, fontSize: FONT }}>Reading {path}…</div>
|
|
908
|
-
);
|
|
910
|
+
return <Reading path={path} />;
|
|
909
911
|
const rows = view.kind === 'struct' ? view.groups.flatMap((group) => group.rows) : [];
|
|
910
912
|
const byIdentifier = new Map(rows.map((row) => [row.identifier, row]));
|
|
911
913
|
// A collection member is addressed by its own path, so a qualified entry is
|
|
@@ -1024,9 +1026,7 @@ function CuratedPanel({
|
|
|
1024
1026
|
if (view === undefined)
|
|
1025
1027
|
return (
|
|
1026
1028
|
<Panel title={panel.title} closed={panel.closed === true}>
|
|
1027
|
-
<
|
|
1028
|
-
Reading {entry.path}…
|
|
1029
|
-
</div>
|
|
1029
|
+
<Reading path={entry.path} />
|
|
1030
1030
|
</Panel>
|
|
1031
1031
|
);
|
|
1032
1032
|
if (found.length === 0 && sub.length === 0) return null;
|
|
@@ -1072,25 +1072,17 @@ function DatablockView({
|
|
|
1072
1072
|
useBlenderProperties();
|
|
1073
1073
|
const view: BlenderRnaView | undefined = blenderRnaViewFor(path);
|
|
1074
1074
|
if (view === undefined)
|
|
1075
|
-
return
|
|
1076
|
-
<div style={{ color: INK, opacity: READONLY_ALPHA, fontSize: FONT, padding: PANEL_PAD }}>
|
|
1077
|
-
Reading {path}…
|
|
1078
|
-
</div>
|
|
1079
|
-
);
|
|
1075
|
+
return <Reading path={path} />;
|
|
1080
1076
|
if (view.kind === 'collection')
|
|
1081
1077
|
return (
|
|
1082
1078
|
<>
|
|
1083
1079
|
{view.items.length === 0 ? (
|
|
1084
|
-
<
|
|
1085
|
-
{view.count === 0 ? 'None' : `${view.count} items`}
|
|
1086
|
-
</div>
|
|
1080
|
+
<StateSurface inline title={view.count === 0 ? 'None' : <>{view.count} items</>} />
|
|
1087
1081
|
) : null}
|
|
1088
1082
|
{view.items.map((item) => (
|
|
1089
1083
|
<Panel key={item.path ?? item.name ?? item.type} title={item.name ?? item.type} sub>
|
|
1090
1084
|
{item.path === null ? (
|
|
1091
|
-
<
|
|
1092
|
-
{item.type} — RNA gives this member no address.
|
|
1093
|
-
</div>
|
|
1085
|
+
<StateSurface inline title={<>{item.type} — RNA gives this member no address.</>} />
|
|
1094
1086
|
) : (
|
|
1095
1087
|
<DatablockView path={item.path} onOpen={onOpen} />
|
|
1096
1088
|
)}
|
|
@@ -1144,13 +1136,11 @@ export function BlenderPropertiesSection({
|
|
|
1144
1136
|
|
|
1145
1137
|
if (state.error !== null)
|
|
1146
1138
|
return (
|
|
1147
|
-
<
|
|
1139
|
+
<StateSurface inline tone="error" title="Blender did not answer for these properties" detail={state.error} />
|
|
1148
1140
|
);
|
|
1149
1141
|
if (tab === null)
|
|
1150
|
-
return (
|
|
1151
|
-
<
|
|
1152
|
-
{state.loading ? 'Reading the engine…' : 'Blender shows no properties here.'}
|
|
1153
|
-
</div>
|
|
1142
|
+
return state.loading ? <Progress view="properties" title="Reading the engine" /> : (
|
|
1143
|
+
<StateSurface inline title="Blender shows no properties here." />
|
|
1154
1144
|
);
|
|
1155
1145
|
|
|
1156
1146
|
const open = (path: string) => setDrill((held) => [...held, path]);
|