@volter/editor-blender 0.1.150 → 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,18 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WHAT THE OUTLINER IS LOOKING AT — the page-side half of the TREE door
|
|
3
|
-
* (WORK.md §Blender in the tab is Blender, "Inspection parity", I3).
|
|
4
|
-
*
|
|
5
|
-
* The same shape as `blender-properties-model.ts`, and deliberately: a plain
|
|
6
|
-
* React-free store with `subscribe` / `version` / `state`, one read driven by
|
|
7
|
-
* the thing that displays it, re-read on every PRESENT because every mutation
|
|
8
|
-
* presents (`session.py::dispatch`) and a frame is the tab's one signal that
|
|
9
|
-
* what it is showing is stale.
|
|
10
|
-
*
|
|
11
|
-
* WHAT IT IS NOT. It holds no tree of its own making: `rows` is the engine's
|
|
12
|
-
* answer, kept verbatim, and the only thing this module derives is the two
|
|
13
|
-
* indexes a hierarchy provider needs — a row by id, and a row's parent — so
|
|
14
|
-
* `HierarchyProvider.node(id)` is a lookup rather than a walk.
|
|
15
|
-
*/
|
|
1
|
+
/** Page-side Outliner store: retain the engine’s rows verbatim and derive indexes by row id and parent id. subscribe/version/state serve React and the adapter; frames and restriction writes invalidate the tree. */
|
|
16
2
|
|
|
17
3
|
import type { BlenderOutlinerRow, BlenderOutlinerTree } from '@volter/blender-engine/browser/rna';
|
|
18
4
|
import { editorHost } from '@volter/sdk/host';
|
|
@@ -24,10 +10,7 @@ import {
|
|
|
24
10
|
blenderSessionStarted,
|
|
25
11
|
} from '../host/blender-runtime-host';
|
|
26
12
|
|
|
27
|
-
/** The
|
|
28
|
-
* typed for the same reason `blender-properties-model.ts` narrows it: the
|
|
29
|
-
* document that published it and the reader are two modules meeting over one
|
|
30
|
-
* object, and `@volter/editor-blender` holds no host import. */
|
|
13
|
+
/** The Model context fields this SDK-host reader uses, validated at publication. */
|
|
31
14
|
export interface PresentedView {
|
|
32
15
|
readonly root: THREE.Object3D;
|
|
33
16
|
subscribeFrames(listener: () => void): () => void;
|
|
@@ -45,7 +28,7 @@ export interface PresentedView {
|
|
|
45
28
|
setStageMode(mode: string | null): void;
|
|
46
29
|
/** A pose moved mid-gesture, drawn by the other views of the model — see
|
|
47
30
|
* `BlenderRuntimeView.mirrorPose`. */
|
|
48
|
-
mirrorPose
|
|
31
|
+
mirrorPose(object: THREE.Object3D): void;
|
|
49
32
|
}
|
|
50
33
|
|
|
51
34
|
const isPresentedView = (value: unknown): value is PresentedView =>
|
|
@@ -145,14 +128,7 @@ export function blenderOutlinerState(): BlenderOutlinerState {
|
|
|
145
128
|
return state;
|
|
146
129
|
}
|
|
147
130
|
|
|
148
|
-
/**
|
|
149
|
-
* THE READ NEVER STARTS INSIDE A RENDER, and that is I1's correction applied
|
|
150
|
-
* here rather than rediscovered: the hierarchy asks for the tree from inside
|
|
151
|
-
* React's render pass, and the read's first act is to publish `loading`, which
|
|
152
|
-
* is a `setState` in every subscriber ("Cannot update a component … while
|
|
153
|
-
* rendering a different component"). The WANT is recorded synchronously so a
|
|
154
|
-
* second ask in the same pass queues nothing; the read runs on the next turn.
|
|
155
|
-
*/
|
|
131
|
+
/** Retain the latest selection and defer its tree read outside React render; newer requests supersede it. */
|
|
156
132
|
function scheduleRead(key: string): void {
|
|
157
133
|
if (scheduled === key) return;
|
|
158
134
|
scheduled = key;
|
|
@@ -168,17 +144,10 @@ function scheduleRead(key: string): void {
|
|
|
168
144
|
*
|
|
169
145
|
* Two reads can be in flight at once: a frame arrives and schedules one
|
|
170
146
|
* (`onFrame`), and a structural verb asks for one of its own
|
|
171
|
-
* ({@link refreshBlenderOutliner}).
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* `readFor`, so the other one's answer is thrown away even when it is newer.
|
|
176
|
-
*
|
|
177
|
-
* MEASURED 2026-09-21: Add ▸ Mesh ▸ UV Sphere created the sphere, the verb's
|
|
178
|
-
* own read was discarded by that comparison, and the verb then looked for the
|
|
179
|
-
* new row in the tree from BEFORE the add and found none — so nothing was
|
|
180
|
-
* selected. The frame's read landed a moment later and the row appeared, which
|
|
181
|
-
* is why the defect looked like "selection only".
|
|
147
|
+
* ({@link refreshBlenderOutliner}). Judging staleness by comparing `readFor`
|
|
148
|
+
* against the read's own key answers a different question — "is this still the
|
|
149
|
+
* selection anyone wants?" — and gets the ordering wrong when the two reads
|
|
150
|
+
* carry DIFFERENT keys, throwing away a newer answer.
|
|
182
151
|
*
|
|
183
152
|
* The sequence says exactly what the guard means: the LAST READ STARTED is the
|
|
184
153
|
* one whose answer the panel shows. {@link readFor} keeps its other job, which
|
|
@@ -216,17 +185,7 @@ async function read(selected: readonly string[]): Promise<void> {
|
|
|
216
185
|
}
|
|
217
186
|
}
|
|
218
187
|
|
|
219
|
-
/**
|
|
220
|
-
* A frame arrived: the tree the engine holds may be a different tree (an object
|
|
221
|
-
* added, a modifier stacked, a collection excluded). Re-read it.
|
|
222
|
-
*
|
|
223
|
-
* IT RE-READS THE LAST SELECTION, NOT `readFor`, and that distinction is the
|
|
224
|
-
* second half of the defect above: `readFor` is the IDEMPOTENCE marker and a
|
|
225
|
-
* successful column write clears it deliberately (the value it holds is now
|
|
226
|
-
* stale), so a frame arriving after one found nothing to re-read and the eye
|
|
227
|
-
* it had just written never moved. The selection the tree was last read for is
|
|
228
|
-
* its own fact and outlives both.
|
|
229
|
-
*/
|
|
188
|
+
/** A presented frame invalidates the tree. Re-read lastSelectionKey; readFor only tracks idempotence and can be cleared by a write. */
|
|
230
189
|
function onFrame(): void {
|
|
231
190
|
readFor = null;
|
|
232
191
|
scheduleRead(lastSelectionKey);
|
|
@@ -254,27 +213,9 @@ export function onBlenderFrame(listener: () => void): () => void {
|
|
|
254
213
|
};
|
|
255
214
|
}
|
|
256
215
|
|
|
257
|
-
/**
|
|
258
|
-
* TAKE THE FRAME SUBSCRIPTION, AND WAIT FOR THE DOCUMENT IF IT IS NOT THERE
|
|
259
|
-
* YET — which is the shape of the defect the I3 walk found: the hierarchy asks
|
|
260
|
-
* for the tree the moment the panel first renders, and the Model document
|
|
261
|
-
* publishes its context on its own mount, so the FIRST ask can legitimately
|
|
262
|
-
* find no view. Retrying "next time someone asks" is not enough, because
|
|
263
|
-
* {@link showBlenderOutliner} is idempotent by selection: nothing asks again
|
|
264
|
-
* until the selection moves, so a scene mutated by a script updated the
|
|
265
|
-
* viewport and left the Outliner showing the old tree for good (measured
|
|
266
|
-
* 2026-09-19 — `outliner_set` hid an object and its row's eye never moved).
|
|
267
|
-
* `documents.waitForContext` is the door for exactly this.
|
|
268
|
-
*/
|
|
216
|
+
/** Subscribe to frames after the document publishes its context. The first hierarchy read can precede mount, so waitForContext completes that subscription. */
|
|
269
217
|
let awaitingView = false;
|
|
270
|
-
/**
|
|
271
|
-
* remounts, or an engine restarted with `blender-start --fresh`, PUBLISHES A
|
|
272
|
-
* NEW `BlenderRuntimeView`; the old one's `subscribeFrames` set goes with it,
|
|
273
|
-
* so a subscription held against it is a subscription to nothing and every
|
|
274
|
-
* later present reaches no reader — the tree, and now the selection, freeze
|
|
275
|
-
* on what they last read while the viewport goes on drawing the new frames.
|
|
276
|
-
* Holding the view this subscription belongs to is what makes the identity
|
|
277
|
-
* check below possible; `frameSubscription !== null` alone cannot see it. */
|
|
218
|
+
/** The view identity owning frameSubscription. Replace the subscription when a document remounts or its engine restarts. */
|
|
278
219
|
let subscribedView: unknown = null;
|
|
279
220
|
|
|
280
221
|
function watchFrames(): void {
|
|
@@ -317,40 +258,13 @@ export function showBlenderOutliner(selected: readonly string[]): void {
|
|
|
317
258
|
scheduleRead(key);
|
|
318
259
|
}
|
|
319
260
|
|
|
320
|
-
/**
|
|
321
|
-
* RE-READ THE TREE NOW, AND ANSWER WHEN IT HAS LANDED — the awaited half of
|
|
322
|
-
* {@link showBlenderOutliner}, for a caller that has just CHANGED the tree and
|
|
323
|
-
* must address a row that did not exist a moment ago.
|
|
324
|
-
*
|
|
325
|
-
* The structural verbs are the callers (`blender-outliner-authoring.ts`'s
|
|
326
|
-
* `StructureProvider`): `bpy.ops.mesh.primitive_uv_sphere_add()` returns, the
|
|
327
|
-
* present that follows ships a frame, {@link onFrame} schedules a read on the
|
|
328
|
-
* next turn — and the verb's contract is to hand its caller the new row's id
|
|
329
|
-
* and select it, which it cannot do against the tree from before the add. So
|
|
330
|
-
* it asks for the read itself and awaits it, instead of polling for a row to
|
|
331
|
-
* appear.
|
|
332
|
-
*
|
|
333
|
-
* It is the SAME read the panel drives, not a second one: `readFor` and
|
|
334
|
-
* `lastSelectionKey` move with it, so the frame's own scheduled read finds the
|
|
335
|
-
* selection already current and does nothing.
|
|
336
|
-
*/
|
|
261
|
+
/** Cancel a queued read and await the current tree, following any newer read until its winner publishes. */
|
|
337
262
|
export async function refreshBlenderOutliner(selected: readonly string[]): Promise<void> {
|
|
338
263
|
if (!blenderSessionStarted()) return;
|
|
339
264
|
// Any read already QUEUED is superseded by this one before it starts.
|
|
340
265
|
scheduled = null;
|
|
341
266
|
let awaited = startRead([...selected].sort());
|
|
342
|
-
//
|
|
343
|
-
// read can start WHILE this one is in flight and take the sequence, in which
|
|
344
|
-
// case this one's answer is dropped and the caller must still not look at
|
|
345
|
-
// the tree until the winner has published (see {@link readSeq}).
|
|
346
|
-
//
|
|
347
|
-
// THE CHECK IS AFTER THE AWAIT, not before it, and that is the whole
|
|
348
|
-
// mechanism — a pre-test loop finds nothing newer at entry (the frame's read
|
|
349
|
-
// is scheduled on a later turn), exits at once, and returns having published
|
|
350
|
-
// nothing. Measured 2026-09-21: the first cut of this loop was pre-test, and
|
|
351
|
-
// Add ▸ Mesh ▸ Monkey still resolved its new row against the tree from
|
|
352
|
-
// before the add. It ends because each turn follows a read that started
|
|
353
|
-
// strictly later than the last.
|
|
267
|
+
// After awaiting, follow any newer read until its winner publishes. Each iteration joins a strictly later sequence, so the caller sees the current tree.
|
|
354
268
|
for (;;) {
|
|
355
269
|
await awaited;
|
|
356
270
|
if (latestRead === awaited) return;
|
|
@@ -358,13 +272,7 @@ export async function refreshBlenderOutliner(selected: readonly string[]): Promi
|
|
|
358
272
|
}
|
|
359
273
|
}
|
|
360
274
|
|
|
361
|
-
/**
|
|
362
|
-
* WRITE ONE RESTRICTION COLUMN. The engine refuses a column it draws on no row
|
|
363
|
-
* of that type BY NAME and this surfaces the refusal verbatim; a successful
|
|
364
|
-
* write presents, and the frame that comes back re-reads the whole tree
|
|
365
|
-
* through {@link onFrame} — so the eye shows the state the ENGINE holds rather
|
|
366
|
-
* than the one that was clicked.
|
|
367
|
-
*/
|
|
275
|
+
/** Write one restriction column and schedule a tree read even when unchanged frame bytes emit no signal. */
|
|
368
276
|
export async function writeBlenderOutlinerColumn(
|
|
369
277
|
path: string,
|
|
370
278
|
column: string,
|
|
@@ -372,12 +280,7 @@ export async function writeBlenderOutlinerColumn(
|
|
|
372
280
|
): Promise<boolean> {
|
|
373
281
|
try {
|
|
374
282
|
await blenderOutlinerSet(path, column, value);
|
|
375
|
-
//
|
|
376
|
-
// presented. A present is not a reliable signal for a COLUMN: `hide_render`
|
|
377
|
-
// changes nothing the presenter draws, so the frame is identical and the
|
|
378
|
-
// session ships nothing — measured 2026-09-19, the render camera toggled in
|
|
379
|
-
// the engine and the row never moved. The write's own outcome is what the
|
|
380
|
-
// row has to show, so the write asks for it.
|
|
283
|
+
// Re-read after a successful column write. A change such as hide_render can leave the rendered frame identical and emit no frame signal.
|
|
381
284
|
readFor = null;
|
|
382
285
|
scheduleRead(lastSelectionKey);
|
|
383
286
|
return true;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** The bottom area shows animation editors or the model Play tool’s Panel. PlayModeSwitch stays in the header of either view, and exists only with a Play tool and model. Each switch applies the mode’s area ratio; the person’s sash resize holds between switches. */
|
|
2
|
+
import { type MouseEvent, type ReactNode, useSyncExternalStore } from 'react';
|
|
3
|
+
import { modelPlayExtension, subscribeModelPlayExtension } from '../src/model-play-extension';
|
|
4
|
+
import { blenderSkin } from './blender-runtime-skin';
|
|
5
|
+
import { type ModelPlayMode, modelPlayMode, playModeVersion, servedModelDocument, setChosenPlayMode, subscribePlayMode } from '../src/play-mode';
|
|
6
|
+
import { TIMELINE_CHROME, TIMELINE_THEME } from './blender-timeline-geometry';
|
|
7
|
+
|
|
8
|
+
/** Reserve 20% of the column for the Game panel; the person can resize its sash. */
|
|
9
|
+
export const PLAY_PANEL_RATIO = 0.2;
|
|
10
|
+
|
|
11
|
+
const extension = () => modelPlayExtension();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FLIP A DOCUMENT BETWEEN GAME AND ANIMATION. A running game keeps running when Animation is
|
|
15
|
+
* chosen: the animation editors then show what it animates. The file's animation playing in the
|
|
16
|
+
* Timeline when Game is chosen is paused (Game has no Timeline transport).
|
|
17
|
+
*/
|
|
18
|
+
export function switchPlayMode(documentId: string, mode: ModelPlayMode): void {
|
|
19
|
+
if (mode === 'game' && blenderSkin.transport?.snapshot().playbackState === 'playing') blenderSkin.transport.pause();
|
|
20
|
+
setChosenPlayMode(documentId, mode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A panel button keeps the keyboard where it was. Focus left on a button would turn the
|
|
24
|
+
* player's next Space — a jump, in most games — into a second click on Pause. */
|
|
25
|
+
const keepFocus = (event: MouseEvent): void => event.preventDefault();
|
|
26
|
+
|
|
27
|
+
/** The play area's button: the Timeline header's widget, at its height, keeping the keyboard where it was. */
|
|
28
|
+
export function PlayAreaButton({ label, testId, disabled, pressed, wide, onClick, children }: {
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly testId: string;
|
|
31
|
+
readonly disabled?: boolean;
|
|
32
|
+
readonly pressed?: boolean;
|
|
33
|
+
readonly wide?: boolean;
|
|
34
|
+
readonly onClick: () => void;
|
|
35
|
+
readonly children: ReactNode;
|
|
36
|
+
}) {
|
|
37
|
+
const unit = TIMELINE_CHROME.unit;
|
|
38
|
+
return (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
data-testid={testId}
|
|
42
|
+
title={label}
|
|
43
|
+
aria-label={label}
|
|
44
|
+
aria-pressed={pressed}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
onMouseDown={keepFocus}
|
|
47
|
+
onClick={onClick}
|
|
48
|
+
style={{
|
|
49
|
+
minWidth: unit,
|
|
50
|
+
height: unit,
|
|
51
|
+
padding: wide ? '0 6px' : 0,
|
|
52
|
+
border: `1px solid ${TIMELINE_CHROME.widgetOutline}`,
|
|
53
|
+
background: pressed ? TIMELINE_THEME.playhead : TIMELINE_CHROME.widget,
|
|
54
|
+
color: TIMELINE_CHROME.widgetText,
|
|
55
|
+
font: 'inherit',
|
|
56
|
+
display: 'inline-flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
gap: 4,
|
|
60
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
61
|
+
opacity: disabled ? 0.4 : 1,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* GAME | ANIMATION, at the leading edge of the bottom area's header row, in the run's panel and the
|
|
71
|
+
* Timeline alike (the head of this file says why it is here). Two joined cells with the current
|
|
72
|
+
* one lit in the playhead's blue — the same widgets as the transport beside it, at the same
|
|
73
|
+
* height, so it reads as one of this row's controls and not a smaller one. It serves the model
|
|
74
|
+
* document the bottom area serves. With no Play tool installed, or no model on screen, there is
|
|
75
|
+
* nothing to switch, and the row keeps its `title` instead.
|
|
76
|
+
*/
|
|
77
|
+
export function PlayModeSwitch({ title }: { readonly title: string }) {
|
|
78
|
+
useSyncExternalStore(subscribePlayMode, playModeVersion, playModeVersion);
|
|
79
|
+
const found = useSyncExternalStore(subscribeModelPlayExtension, extension, () => null);
|
|
80
|
+
const documentId = servedModelDocument();
|
|
81
|
+
if (found === null || documentId === null) return <span style={{ color: TIMELINE_CHROME.text }}>{title}</span>;
|
|
82
|
+
const mode = modelPlayMode(documentId);
|
|
83
|
+
const cell = (value: ModelPlayMode, label: string, tip: string) => (
|
|
84
|
+
<PlayAreaButton
|
|
85
|
+
testId={`model-play-mode-${value}`}
|
|
86
|
+
label={tip}
|
|
87
|
+
pressed={mode === value}
|
|
88
|
+
wide
|
|
89
|
+
onClick={() => switchPlayMode(documentId, value)}
|
|
90
|
+
>
|
|
91
|
+
{label}
|
|
92
|
+
</PlayAreaButton>
|
|
93
|
+
);
|
|
94
|
+
return (
|
|
95
|
+
<div role="group" aria-label="Game or Animation" data-testid="model-play-mode" style={{ display: 'flex' }}>
|
|
96
|
+
{cell('game', 'Game', 'Game: this area is the Game panel, with Play, pause, step, speed, restart and autoplay')}
|
|
97
|
+
{cell('animation', 'Animation', 'Animation: this area is Blender’s animation editors (Timeline, Action Editor, NLA); a running game keeps running and they show what it animates')}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|