@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MODEL DOCUMENT'S PLAY, which editor-blender owns and offers in its API (`registerModelPlay`,
|
|
3
|
+
* `docs/adr/plugin-apis.md`). A Play tool owns its controls, state and runner; the model document
|
|
4
|
+
* lends that runner its detached stage. play registers its tool from its own `activate`, through
|
|
5
|
+
* the API; the host hears only whether a document plays (`@volter/sdk/document-play-extension`).
|
|
6
|
+
*/
|
|
7
|
+
import { setDocumentPlaySignal } from '@volter/sdk/document-play-extension';
|
|
8
|
+
import type { ComponentType } from 'react';
|
|
9
|
+
|
|
10
|
+
export interface DocumentPlayControlProps {
|
|
11
|
+
readonly documentId: string | undefined;
|
|
12
|
+
readonly onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
/** Where the document's own area stands while its game runs (`DocumentPlayStage.edit`): a box is in fractions of the
|
|
15
|
+
* stage. */
|
|
16
|
+
export type DocumentPlayEditArea = 'live' | 'hidden' | { readonly left: number; readonly top: number; readonly width: number; readonly height: number };
|
|
17
|
+
export interface DocumentPlayStage {
|
|
18
|
+
readonly documentId: string;
|
|
19
|
+
readonly sourcePath: string;
|
|
20
|
+
readonly report: (title: string, detail: string) => void;
|
|
21
|
+
/** Where the tool draws its game, over the document's own area. */
|
|
22
|
+
readonly container: HTMLElement;
|
|
23
|
+
/** The game's first frame stands. */
|
|
24
|
+
readonly ready: () => void;
|
|
25
|
+
/** THE DOCUMENT'S OWN AREA WHILE THE GAME RUNS ON: `live` brings the editing area back in front, live for editing
|
|
26
|
+
* (its keys the editor's); a box shows it drawing in that part of the stage, taking no press (a tile among the
|
|
27
|
+
* game's views); `hidden` puts the game in front again. */
|
|
28
|
+
readonly edit?: (area: DocumentPlayEditArea) => void;
|
|
29
|
+
/** A layer over the editing area and the play area alike, for the tool's view controls, which stand whichever area
|
|
30
|
+
* is in front; it takes no press itself. */
|
|
31
|
+
readonly overlay?: HTMLElement;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A RUNNING GAME'S CLOCK, as the Play tool keeps it: the game's own seconds and frames, which stand still while paused
|
|
36
|
+
* and run at `speed` times the page's own clock otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export interface DocumentPlayClock {
|
|
39
|
+
/** The game's seconds since this run began. */
|
|
40
|
+
readonly time: number;
|
|
41
|
+
/** The frames the game has run since this run began. */
|
|
42
|
+
readonly tick: number;
|
|
43
|
+
readonly paused: boolean;
|
|
44
|
+
/** Simulation seconds per real second; one of the transport's `speeds`. */
|
|
45
|
+
readonly speed: number;
|
|
46
|
+
/** While playing with no game running because the game failed to start or threw: why. The
|
|
47
|
+
* run stays playing (a save of the game retries it); a running game clears it. */
|
|
48
|
+
readonly failure?: string | null;
|
|
49
|
+
/** A game has started in this run and is running (its first update ran). False while the run
|
|
50
|
+
* is still preparing, and while it plays with a `failure`. A reader that must know whether Play
|
|
51
|
+
* WORKED (the CLI's `play`) waits for this or for `failure`, never for "playing" alone. */
|
|
52
|
+
readonly running: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* THE GAME'S TRANSPORT — what a Play tool offers beyond Play and Stop, for a layout that draws a
|
|
56
|
+
* game panel (Cyclotron's Game mode, `@volter/editor-blender`). Optional: a tool without
|
|
57
|
+
* one still plays and stops, and a layout draws only the controls a tool answers.
|
|
58
|
+
*/
|
|
59
|
+
export interface DocumentPlayTransport {
|
|
60
|
+
/** The speeds `setSpeed` accepts, slowest first. */
|
|
61
|
+
readonly speeds: readonly number[];
|
|
62
|
+
clock(documentId: string): DocumentPlayClock;
|
|
63
|
+
/** The clock moves every played frame, so it has a subscription of its own: `subscribe` would
|
|
64
|
+
* re-render everything that only asks whether a document plays. */
|
|
65
|
+
subscribeClock(listener: () => void): () => void;
|
|
66
|
+
/** Hold or release the game; the answer settles once the game has. */
|
|
67
|
+
setPaused(documentId: string, paused: boolean): void | Promise<void>;
|
|
68
|
+
/** `frames` frames (one when absent), pausing the game first. */
|
|
69
|
+
step(documentId: string, frames?: number): void | Promise<void>;
|
|
70
|
+
setSpeed(documentId: string, speed: number): void | Promise<void>;
|
|
71
|
+
/** Begin the run again on a FRESH detached copy, the clock at zero. */
|
|
72
|
+
restart(documentId: string): void;
|
|
73
|
+
/** Changes on every restart. The document that lends the stage keys its detached copy on it,
|
|
74
|
+
* so a new generation is a new copy; the change is announced through `subscribe`. */
|
|
75
|
+
generation(documentId: string): number;
|
|
76
|
+
/** Whether the running game's own bot drives the player in view, and the goals it offers; a change is announced
|
|
77
|
+
* through `subscribeClock`. Absent, the tool has no autoplay. */
|
|
78
|
+
autoplay?(documentId: string): DocumentPlayAutoplay;
|
|
79
|
+
/** Switch the bot on (it must be offered, and the game playing) or off. On plays `goal` (the game's own when
|
|
80
|
+
* absent); the answer settles once the goal has started. */
|
|
81
|
+
setAutoplay?(documentId: string, on: boolean, by: 'panel' | 'cli', goal?: string): void | Promise<void>;
|
|
82
|
+
/** While stopped, ask the next start to switch the bot on with `goal` as soon as its player joins. Absent, autoplay
|
|
83
|
+
* can only be switched while running. */
|
|
84
|
+
armAutoplay?(documentId: string, armed: boolean, goal?: string): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* AUTOPLAY, AS THE EDITOR OWNS IT: a game offers a bot and its goals, the editor decides whether it drives the player
|
|
88
|
+
* in view. A person's own press in the game takes it back (`takeover`).
|
|
89
|
+
*/
|
|
90
|
+
export interface DocumentPlayAutoplay {
|
|
91
|
+
readonly on: boolean;
|
|
92
|
+
/** The bot can be switched on for the player in view. */
|
|
93
|
+
readonly available: boolean;
|
|
94
|
+
/** Why the bot cannot be switched on now, in the tool's words (no goals, a view with no player, a player not yet
|
|
95
|
+
* joined): the Game panel and `play state` say it. */
|
|
96
|
+
readonly unavailable?: string | null;
|
|
97
|
+
/** Who made the last change. */
|
|
98
|
+
readonly by: 'panel' | 'cli' | 'takeover' | null;
|
|
99
|
+
/** Armed while stopped (`armAutoplay`): the next start turns it on once its player joins. */
|
|
100
|
+
readonly armed?: boolean;
|
|
101
|
+
/** The goals the game's bot offers, and the one driving, armed, or the game's own. */
|
|
102
|
+
readonly behaviors?: readonly string[];
|
|
103
|
+
readonly behavior?: string | null;
|
|
104
|
+
/** A goal played where the person is not looking (a std game's headless play), and how it stands: `<goal>:
|
|
105
|
+
* playing`, `<goal>: achieved` or `<goal>: failed — why`; null before any. */
|
|
106
|
+
readonly state?: string | null;
|
|
107
|
+
}
|
|
108
|
+
export interface DocumentPlayExtension {
|
|
109
|
+
readonly Control: ComponentType<DocumentPlayControlProps>;
|
|
110
|
+
readonly Menu: ComponentType<DocumentPlayControlProps>;
|
|
111
|
+
/** The run's panel, which a document's layout shows in its bottom area while it plays, when the tool has one. */
|
|
112
|
+
readonly Panel?: ComponentType;
|
|
113
|
+
playing(documentId: string): boolean;
|
|
114
|
+
/** Intended live frame shape; absent/null lets the document fill its area. */
|
|
115
|
+
aspectRatio?(documentId: string): number | null;
|
|
116
|
+
setPlaying(documentId: string, value: boolean): void;
|
|
117
|
+
escape?(documentId: string): void;
|
|
118
|
+
subscribe(listener: () => void): () => void;
|
|
119
|
+
run(stage: DocumentPlayStage): () => void;
|
|
120
|
+
/** Pause, step, speed, restart and the clock, when the tool keeps them. */
|
|
121
|
+
readonly transport?: DocumentPlayTransport;
|
|
122
|
+
/** What this tool runs to play a document's source file (a game's dev server command). */
|
|
123
|
+
scriptPath?(sourcePath: string): string;
|
|
124
|
+
/** Whether the document's source has a game to play: `null` while the tool has not looked yet. A change is announced
|
|
125
|
+
* through `subscribe`. A layout uses it to open a document as a game or as a model. */
|
|
126
|
+
hasScript?(sourcePath: string): boolean | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let registered: DocumentPlayExtension | null = null;
|
|
130
|
+
const listeners = new Set<() => void>();
|
|
131
|
+
function publish(): void { for (const listener of listeners) listener(); }
|
|
132
|
+
|
|
133
|
+
/** The registered Play tool, or `null` while none is. */
|
|
134
|
+
export function modelPlayExtension(): DocumentPlayExtension | null {
|
|
135
|
+
return registered;
|
|
136
|
+
}
|
|
137
|
+
/** A registration, and every change the registered tool announces. */
|
|
138
|
+
export function subscribeModelPlayExtension(listener: () => void): () => void {
|
|
139
|
+
listeners.add(listener);
|
|
140
|
+
return () => { listeners.delete(listener); };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Whether a document's Play runs and is paused, for the host. A pause is a clock change, and the
|
|
144
|
+
* clock moves every played frame, so its subscribers are readers whose snapshot changes rarely. */
|
|
145
|
+
function signal(extension: DocumentPlayExtension) {
|
|
146
|
+
return {
|
|
147
|
+
state(documentId: string): 'playing' | 'paused' | 'stopped' {
|
|
148
|
+
if (!extension.playing(documentId)) return 'stopped';
|
|
149
|
+
return extension.transport?.clock(documentId).paused ? 'paused' : 'playing';
|
|
150
|
+
},
|
|
151
|
+
subscribe(listener: () => void): () => void {
|
|
152
|
+
const stopPlay = extension.subscribe(listener);
|
|
153
|
+
const stopClock = extension.transport?.subscribeClock(listener);
|
|
154
|
+
return () => { stopPlay(); stopClock?.(); };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The API's `registerModelPlay`: one Play tool for model documents at a time. */
|
|
160
|
+
export function registerModelPlay(extension: DocumentPlayExtension): () => void {
|
|
161
|
+
if (registered) throw new Error('A Play tool already plays model documents.');
|
|
162
|
+
registered = extension;
|
|
163
|
+
const stop = extension.subscribe(publish);
|
|
164
|
+
setDocumentPlaySignal('model', signal(extension));
|
|
165
|
+
publish();
|
|
166
|
+
return () => {
|
|
167
|
+
stop();
|
|
168
|
+
if (registered !== extension) return;
|
|
169
|
+
registered = null;
|
|
170
|
+
setDocumentPlaySignal('model', null);
|
|
171
|
+
publish();
|
|
172
|
+
};
|
|
173
|
+
}
|
package/src/node-view-state.ts
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE NODE VIEW'S OWN STATE, and the reason it is a module rather than a
|
|
3
|
-
* `useState` inside the component.
|
|
4
|
-
*
|
|
5
|
-
* `editor.document.*` is scoped to the ACTIVE CENTER DOCUMENT by design — its
|
|
6
|
-
* own header says so: "a selector resolving outside that document's container
|
|
7
|
-
* is refused by a message naming the scope". The node editor is a DRAWER
|
|
8
|
-
* utility, so nothing in the product could read or drive it: the sighted door
|
|
9
|
-
* (`editor.captureEditorChrome`, `cyclotron screenshot editor`) photographs it, and
|
|
10
|
-
* a photograph cannot click.
|
|
11
|
-
*
|
|
12
|
-
* So the view's actions are SESSION VERBS, which is the pattern the keyboard
|
|
13
|
-
* ruling already asks for — a `volter.*` command per action rather than a raw
|
|
14
|
-
* listener — and this module is the one place their state lives. The component
|
|
15
|
-
* subscribes to it; `blender-node-view` (the command) reads and writes it. One
|
|
16
|
-
* store, two readers, no second copy.
|
|
17
|
-
*
|
|
18
|
-
* It imports NOTHING: the host handler and the contribution both reach it, and
|
|
19
|
-
* a store either of them could not import would not be one store.
|
|
20
|
-
*/
|
|
1
|
+
/** Shared node-view store for the component and blender-node-view command. Keep pan, zoom, focus, drawing measurements and refusal in one React-free module. */
|
|
21
2
|
|
|
22
3
|
export interface NodeViewTransform {
|
|
23
4
|
/** The tree-space point at the view's centre. */
|
|
@@ -48,7 +29,7 @@ export interface NodeViewState {
|
|
|
48
29
|
readonly drawn: readonly DrawnNode[];
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
|
|
32
|
+
interface DrawnNode {
|
|
52
33
|
readonly name: string;
|
|
53
34
|
readonly x: number;
|
|
54
35
|
readonly yTop: number;
|
|
@@ -62,10 +43,7 @@ export interface DrawnNode {
|
|
|
62
43
|
readonly firstSocketBelowTop: number | null;
|
|
63
44
|
/** The header's colour, as drawn. */
|
|
64
45
|
readonly header: string;
|
|
65
|
-
/**
|
|
66
|
-
* are collapsed. A node that declares panels and drew them FLAT instead
|
|
67
|
-
* carries the reason in `panelFallback` — the ruling's own condition,
|
|
68
|
-
* readable rather than merely displayed. */
|
|
46
|
+
/** Count drawn socket panels and collapsed panels; panelFallback names a flat-layout limitation. */
|
|
69
47
|
readonly panels: number;
|
|
70
48
|
readonly collapsedPanels: number;
|
|
71
49
|
readonly panelFallback: string | null;
|
package/src/play-mode.ts
CHANGED
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GAME OR ANIMATION — what a model document's bottom area shows. It is not mutually exclusive:
|
|
3
|
-
* animation is used by games too.
|
|
4
|
-
*
|
|
5
|
-
* - **Animation** is Blender's animation editors: the Timeline, the Dope Sheet's Action Editor and
|
|
6
|
-
* the NLA editor (`../contributions/blender-animation-editors.tsx`). While a game runs they show
|
|
7
|
-
* what the game is animating, read-only.
|
|
8
|
-
* - **Game** is the Game panel (`../contributions/blender-game-panel.tsx`): the ONE Play, and the
|
|
9
|
-
* run's pause, step, speed, restart and clock.
|
|
10
|
-
*
|
|
11
|
-
* They are two views of one bottom area, not two exclusive states: switching to Animation leaves a
|
|
12
|
-
* running game running.
|
|
13
|
-
*
|
|
14
|
-
* The switch is the bottom area's own, at the leading edge of its header row in the Game panel
|
|
15
|
-
* and the Timeline alike (`PlayModeSwitch`, `blender-game-panel.tsx`); this module is the state
|
|
16
|
-
* both sides read. A document opens as a
|
|
17
|
-
* Game when its model has a play script (`*.play.ts` beside the `.blend`, as the Play tool finds
|
|
18
|
-
* it — `DocumentPlayExtension.hasScript`) and in Animation otherwise, and the person's own pick
|
|
19
|
-
* then stands for that document for the browser session, kept the way `area-split.ts` keeps the
|
|
20
|
-
* split, per document id — in session storage rather than local, because the default is what a
|
|
21
|
-
* fresh session should start from.
|
|
22
|
-
*
|
|
23
|
-
* THE BOTTOM AREA SERVES ONE DOCUMENT, the model document on screen. The Timeline was always
|
|
24
|
-
* that (it binds the presented-view singleton, and cannot name a document), but a game panel
|
|
25
|
-
* needs the id to drive a run, and clicking into the panel makes the PANEL the active workspace
|
|
26
|
-
* document — so the model document announces itself here ({@link noteModelDocument}) rather
|
|
27
|
-
* than the panel asking the workspace which document is active.
|
|
28
|
-
*/
|
|
1
|
+
/** The model’s bottom area shows animation editors or the Play tool’s Game panel. Switching views leaves a run active. Remember the person’s mode per document for this browser session; otherwise use hasScript to choose the default. The Model announces its identity so focusing the bottom-area document does not change the run’s target. */
|
|
29
2
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from '
|
|
3
|
+
modelPlayExtension,
|
|
4
|
+
subscribeModelPlayExtension,
|
|
5
|
+
} from './model-play-extension';
|
|
33
6
|
|
|
34
7
|
export type ModelPlayMode = 'game' | 'animation';
|
|
35
8
|
|
|
@@ -40,6 +13,8 @@ export function playModeOf(value: unknown): ModelPlayMode | null {
|
|
|
40
13
|
|
|
41
14
|
const STORAGE_PREFIX = 'volter.blender.play-mode:';
|
|
42
15
|
const listeners = new Set<() => void>();
|
|
16
|
+
/** Verbs waiting for a document to be on screen (`documentOnScreen`), told at every change. */
|
|
17
|
+
const waiters = new Set<() => void>();
|
|
43
18
|
/** The person's pick per document; `null` once storage was read and held none. */
|
|
44
19
|
const chosen = new Map<string, ModelPlayMode | null>();
|
|
45
20
|
/** Each model document's `.blend`, as the document reported it. */
|
|
@@ -50,6 +25,7 @@ let version = 0;
|
|
|
50
25
|
function publish(): void {
|
|
51
26
|
version += 1;
|
|
52
27
|
for (const listener of [...listeners]) listener();
|
|
28
|
+
for (const waiter of [...waiters]) waiter();
|
|
53
29
|
}
|
|
54
30
|
|
|
55
31
|
function stored(documentId: string): ModelPlayMode | null {
|
|
@@ -105,6 +81,57 @@ export function servedModelDocument(): string | null {
|
|
|
105
81
|
return served;
|
|
106
82
|
}
|
|
107
83
|
|
|
84
|
+
/** Model documents on screen whose file is still opening (read and first presented), the newest last. */
|
|
85
|
+
const opening: string[] = [];
|
|
86
|
+
|
|
87
|
+
/** An opening Model document does not serve the bottom area until presentation completes. Return its withdrawal function; documentOnScreen waits for it rather than reporting no model. */
|
|
88
|
+
export function noteModelOpening(documentId: string): () => void {
|
|
89
|
+
opening.push(documentId);
|
|
90
|
+
publish();
|
|
91
|
+
return () => {
|
|
92
|
+
const at = opening.lastIndexOf(documentId);
|
|
93
|
+
if (at < 0) return;
|
|
94
|
+
opening.splice(at, 1);
|
|
95
|
+
publish();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The model document still opening on screen, or `null` with none. */
|
|
100
|
+
export function openingModelDocument(): string | null {
|
|
101
|
+
return opening.at(-1) ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* THE MODEL DOCUMENT ON SCREEN, once it is: the one served now, else the one opening, waited for at most `waitMs`.
|
|
106
|
+
* Answers `{ served }` once one serves the bottom area, `{ opening }` when one is still opening at the end of the wait
|
|
107
|
+
* (the caller asks again: an open can take minutes), and `null` when none is on screen or opening.
|
|
108
|
+
*/
|
|
109
|
+
export function documentOnScreen(waitMs: number): Promise<{ readonly served: string } | { readonly opening: string } | null> {
|
|
110
|
+
const now = (): { readonly served: string } | { readonly opening: string } | null =>
|
|
111
|
+
served !== null ? { served } : opening.length > 0 ? { opening: opening.at(-1)! } : null;
|
|
112
|
+
const first = now();
|
|
113
|
+
if (first === null || 'served' in first || waitMs <= 0) return Promise.resolve(first);
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
const settle = (answer: ReturnType<typeof now>): void => {
|
|
117
|
+
waiters.delete(check);
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
clearTimeout(recheck);
|
|
120
|
+
resolve(answer);
|
|
121
|
+
};
|
|
122
|
+
let recheck: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
function check(): void {
|
|
124
|
+
const answer = now();
|
|
125
|
+
if (answer !== null && 'served' in answer) { clearTimeout(recheck); settle(answer); return; }
|
|
126
|
+
// (an opening withdrawn is read again a task later: the document's own commit withdraws it before its
|
|
127
|
+
// viewport announces itself, both in one commit, and only an open that ended with nothing on screen is none)
|
|
128
|
+
if (answer === null) { clearTimeout(recheck); recheck = setTimeout(() => { const later = now(); if (later === null || 'served' in later) settle(later); }, 0); }
|
|
129
|
+
}
|
|
130
|
+
waiters.add(check);
|
|
131
|
+
timer = setTimeout(() => settle(now()), waitMs);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
/** The `.blend` a model document reported, if it has a file of its own. */
|
|
109
136
|
export function modelDocumentSource(documentId: string): string | undefined {
|
|
110
137
|
return sources.get(documentId);
|
|
@@ -112,12 +139,12 @@ export function modelDocumentSource(documentId: string): string | undefined {
|
|
|
112
139
|
|
|
113
140
|
/**
|
|
114
141
|
* THE MODE A DOCUMENT IS IN: Animation whenever no Play tool is installed (there is no game to
|
|
115
|
-
* switch to), else the person's pick, else Game exactly when the
|
|
116
|
-
*
|
|
142
|
+
* switch to), else the person's pick, else Game exactly when the level has a game. A
|
|
143
|
+
* game not yet looked for counts as none, so a document opens on the Timeline and turns to
|
|
117
144
|
* the Game panel when the answer arrives.
|
|
118
145
|
*/
|
|
119
146
|
export function modelPlayMode(documentId: string): ModelPlayMode {
|
|
120
|
-
const extension =
|
|
147
|
+
const extension = modelPlayExtension();
|
|
121
148
|
if (extension === null) return 'animation';
|
|
122
149
|
const pick = chosenPlayMode(documentId);
|
|
123
150
|
if (pick !== null) return pick;
|
|
@@ -135,15 +162,14 @@ export function playModeVersion(): number {
|
|
|
135
162
|
* THE PLAY TOOL'S ANSWERS MOVE THE VERSION TOO, because the version is the snapshot every reader
|
|
136
163
|
* takes (`useSyncExternalStore(subscribePlayMode, playModeVersion)`): a script found after the
|
|
137
164
|
* first render, a run starting or stopping, a restart. Handing the tool's publications straight to
|
|
138
|
-
* the reader's listener re-
|
|
139
|
-
*
|
|
140
|
-
* had stopped. One subscription to the tool's registry, held while anything reads this store,
|
|
165
|
+
* the reader's listener would re-run a snapshot that had not changed, so React would keep the old
|
|
166
|
+
* render. One subscription to the tool's registry, held while anything reads this store,
|
|
141
167
|
* bumps the version and then tells the readers.
|
|
142
168
|
*/
|
|
143
169
|
let stopToolWatch: (() => void) | null = null;
|
|
144
170
|
export function subscribePlayMode(listener: () => void): () => void {
|
|
145
171
|
listeners.add(listener);
|
|
146
|
-
stopToolWatch ??=
|
|
172
|
+
stopToolWatch ??= subscribeModelPlayExtension(publish);
|
|
147
173
|
return () => {
|
|
148
174
|
listeners.delete(listener);
|
|
149
175
|
if (listeners.size === 0 && stopToolWatch) {
|
package/src/presentation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HOW THE BLENDER STAGE BEHAVES, as its starting presentation (`@volter/sdk/
|
|
2
|
+
* HOW THE BLENDER STAGE BEHAVES, as its starting presentation (`@volter/sdk/
|
|
3
3
|
* viewport-presentation`): this package builds the `model` stage, so it states the stage's
|
|
4
4
|
* function there, beneath every person's choice and never in the look (ARCHITECTURE.md rule 7).
|
|
5
5
|
*/
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
DOCUMENT_STUDIO_PRESET,
|
|
8
8
|
registerStartingPresentation,
|
|
9
9
|
type ViewportCamera,
|
|
10
|
-
} from '@volter/sdk/
|
|
10
|
+
} from '@volter/sdk/viewport-presentation';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A 3D View's field of view for its lens, as Blender computes it (`BKE_camera_params_from_view3d`,
|
|
@@ -22,19 +22,10 @@ export function blenderViewFieldOfView(lens: number): ViewportCamera['fov'] {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const release = registerStartingPresentation('model', {
|
|
25
|
-
//
|
|
26
|
-
// angle on the larger side; and its default user perspective, elevation 26.5° and azimuth
|
|
27
|
-
// −23.8° about the up axis, SOLVED from the reference's two floor axes (projected slopes
|
|
28
|
-
// +1.0093 for X and −0.1969 for Y in `modeling-edit-none.png`), in three's Y-up frame. A file's
|
|
29
|
-
// own saved view and lens are its document's (`blender-runtime.document.tsx`).
|
|
25
|
+
// The factory Blender lens is 50 mm. The default view uses the reference floor-axis orientation in the presenter’s Y-up frame; a saved document supplies its own view and lens.
|
|
30
26
|
camera: { fov: blenderViewFieldOfView(50), opening: [0.8187, 0.4458, 0.3617] },
|
|
31
27
|
interaction: {
|
|
32
|
-
//
|
|
33
|
-
// one of the four transform tools is armed (`space_toolsystem_toolbar.py`:
|
|
34
|
-
// `_defs_view3d_generic.select_box` is the first entry of the Object Mode `_tools_default`
|
|
35
|
-
// tuple). Photographed at the engine's pin: `gizmo-select-box.png` has Select Box lit in the
|
|
36
|
-
// shelf, the default cube selected and outlined, and nothing at its origin but the 3D cursor
|
|
37
|
-
// and the object dot; `gizmo-move.png` has Move lit in the same frame.
|
|
28
|
+
// Open on Select Box, the first Object Mode shelf tool in space_toolsystem_toolbar.py. Show the transform gizmo only when a transform tool is armed.
|
|
38
29
|
bootTool: 'select',
|
|
39
30
|
// BLENDER'S SELECT BOX IS A TOUCH TEST: its object-mode box select reads the object-id buffer
|
|
40
31
|
// under the rectangle, so any drawn part of an object inside it selects that object.
|
|
@@ -47,9 +38,7 @@ const release = registerStartingPresentation('model', {
|
|
|
47
38
|
// SOLID (`clay`, and Wireframe with it) is lit by Blender's four studio lights, the document's
|
|
48
39
|
// view-locked studio, and drawn under STANDARD: Blender gives Solid the display's default view
|
|
49
40
|
// and none of the scene's settings (`draw_color_management.cc`, `ViewTransform`), and the sRGB
|
|
50
|
-
// display's default is Standard (`config.ocio`).
|
|
51
|
-
// the factory cube 142 / 131 / 112 in the default view and 162 on the front face, agreeing with
|
|
52
|
-
// the viewport's own frames (141 / 129 / 111, 161). The scene's AgX is the two modes' below.
|
|
41
|
+
// display's default is Standard (`config.ocio`). Material Preview and Rendered use AgX below.
|
|
53
42
|
all: {
|
|
54
43
|
lighting: {
|
|
55
44
|
source: 'studio',
|
|
@@ -93,8 +82,7 @@ const release = registerStartingPresentation('model', {
|
|
|
93
82
|
},
|
|
94
83
|
},
|
|
95
84
|
overlays: {
|
|
96
|
-
//
|
|
97
|
-
// the default cube's centre in every reference frame (`gizmo-select-box.png`).
|
|
85
|
+
// Draw the selected objects’ origins when the Origins overlay is enabled.
|
|
98
86
|
selection: { origins: true },
|
|
99
87
|
},
|
|
100
88
|
world: {
|
package/src/regions.ts
CHANGED
|
@@ -7,53 +7,13 @@ import type { WorkspaceLayoutContribution } from '@volter/sdk/looks';
|
|
|
7
7
|
|
|
8
8
|
export const BLENDER_REGIONS: NonNullable<WorkspaceLayoutContribution['regions']> = {
|
|
9
9
|
workspaceTabs: 'shown',
|
|
10
|
-
// Blender has no document tab strip over its areas (the host's own note
|
|
11
|
-
// in `workspace-presets.ts` says so; the bundle contradicted it).
|
|
12
10
|
header: 'shown',
|
|
13
11
|
shelf: 'shown',
|
|
14
12
|
inspector: 'properties',
|
|
15
|
-
//
|
|
16
|
-
// wells (Scene, ViewLayer) and nothing brighter than the 216 its labels
|
|
17
|
-
// ink at. Ours drew a yellow FPS/ms readout, an orange sparkline and a lit
|
|
18
|
-
// audio meter there — the brightest, highest-chroma thing in the frame and
|
|
19
|
-
// the first place the eye lands. The numbers keep their door (the Profiler
|
|
20
|
-
// utility, which the readout's own tooltip names).
|
|
21
|
-
telemetry: 'hidden',
|
|
22
|
-
// Measured on `outliner.png`: Scene Collection, Collection, Camera, Cube
|
|
23
|
-
// and Light each carry their NAME and nothing after it — no type text
|
|
24
|
-
// anywhere in the frame. The type is the GLYPH, which is why the glyph is
|
|
25
|
-
// coloured by category in the first place; a row that also spelled the
|
|
26
|
-
// type would say it twice. Ours printed `Hero Box ·HeroBox` at
|
|
27
|
-
// `content.dim` in a 10px face, one `space-2` past the name.
|
|
13
|
+
// Hide hierarchy type suffixes; glyph categories identify row types.
|
|
28
14
|
hierarchyTypeSuffix: 'hidden',
|
|
29
|
-
//
|
|
30
|
-
// `modeling-object-none.png` (object mode, nothing selected) and
|
|
31
|
-
// `modeling-object-selected.png` (object mode, everything selected):
|
|
32
|
-
// Blender draws NO rule, dotted or solid, under any name in that panel, in
|
|
33
|
-
// any selection state. Ours drew an orange dotted rule under every
|
|
34
|
-
// component-instance name — six of this scaffold's eleven rows — which is a
|
|
35
|
-
// mark with no counterpart in the frame at all. The instance fact keeps its
|
|
36
|
-
// doors (the label's tooltip, Go to Callsite / Open Component Source, the
|
|
37
|
-
// inspector's component sections); only the paint yields.
|
|
15
|
+
// Hide component-instance underline marks; tooltips, source navigation and inspector sections retain the instance information.
|
|
38
16
|
hierarchyInstanceRule: 'hidden',
|
|
39
|
-
//
|
|
40
|
-
// 257.5-270.5, 32 px in from the area's right edge) and a render CAMERA
|
|
41
|
-
// (277.5-290.5, 12 px in) on a 20 px column pitch, and no lock glyph
|
|
42
|
-
// anywhere — Blender's Disable Selection column is off by default. Ours
|
|
43
|
-
// drew a lock 29-41 px in and the eye 6-18 px in, so the LOCK was sitting
|
|
44
|
-
// in Blender's eye column and the eye outboard of its camera column. The
|
|
45
|
-
// lock goes, and the render column is reserved blank so the eye lands on
|
|
46
|
-
// Blender's own; `workspace-regions.ts` carries why it stays blank.
|
|
17
|
+
// Hide the selection-lock column and reserve the render-camera column so the eye occupies Blender’s column.
|
|
47
18
|
hierarchyRestrictions: 'viewport+render',
|
|
48
|
-
// Measured at native 2x on `outliner.png` AND on the Properties area of
|
|
49
|
-
// `modeling-edit-none.png` — device x relative to each area's own left
|
|
50
|
-
// edge, and the two agree to half a pixel: every Blender area begins with
|
|
51
|
-
// an EDITOR-TYPE WELL at x 17..80, i.e. 8.5 CSS in and 32 CSS wide, 20
|
|
52
|
-
// tall, its glyph and a chevron inside a 1 px `#3c3c3c` outline over a
|
|
53
|
-
// `#272727` inner. It is there whether the area holds one editor or a
|
|
54
|
-
// dozen, and its dropdown is how an area changes editor. Ours began with
|
|
55
|
-
// a 203 px strip of WORDS, which is what squeezed the Outliner's search
|
|
56
|
-
// to 64 px against Blender's 115, truncated the third tab to `Libr`, and
|
|
57
|
-
// left the Properties area — a lone panel declaring no header controls —
|
|
58
|
-
// with no header band at all.
|
|
59
19
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** blender-start uses an explicit file first, otherwise the active Model file before the bound pane. During Save As, the active tab can lead pane binding. An unlisted-file follow keeps the session’s new file on the tab it left. */
|
|
2
|
+
export function startDocument(input: {
|
|
3
|
+
/** The file the start names, if any. */
|
|
4
|
+
readonly requested: string | undefined;
|
|
5
|
+
/** The file the editor is showing as the Model: the active tab's, else the bound pane's. */
|
|
6
|
+
readonly shown: string | undefined;
|
|
7
|
+
/** A Save As to an unlisted file, from the tab it left, for this session. */
|
|
8
|
+
readonly follow: { readonly from: string; readonly document: string } | null;
|
|
9
|
+
/** The standing Model's file, for a project with no `.blend` of its own. */
|
|
10
|
+
readonly fallback: string;
|
|
11
|
+
}): string {
|
|
12
|
+
if (input.requested !== undefined) return input.requested;
|
|
13
|
+
const shown = input.shown ?? input.fallback;
|
|
14
|
+
return input.follow !== null && input.follow.from === shown ? input.follow.document : shown;
|
|
15
|
+
}
|
|
@@ -1,32 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE TIMELINE'S OWN VIEW STATE — the pan/zoom of its ruler, the last refusal,
|
|
3
|
-
* the measured box, and what it drew.
|
|
4
|
-
*
|
|
5
|
-
* THE PLAYBACK STATE IS NOT HERE. The frame, the play/pause and the keyframe
|
|
6
|
-
* columns live in the presenter's `BlenderSkinDirector`
|
|
7
|
-
* (`../contributions/blender-runtime-skin.ts`), because that object OWNS the
|
|
8
|
-
* `AnimationMixer` and a second copy of "what frame is it" is exactly the
|
|
9
|
-
* disagreement the whole design exists to avoid. This module holds only what
|
|
10
|
-
* is about the DRAWING.
|
|
11
|
-
*
|
|
12
|
-
* WHAT DRIVES IT IS NOT A SESSION VERB. U8's ruling 1 (2026-09-19): every view
|
|
13
|
-
* publishes `volter.<view>.<verb>` commands through `@volter/sdk/views`, one
|
|
14
|
-
* table behind the frame's command service and standalone `cyclotron edit`'s
|
|
15
|
-
* session verb — so this view adds no `blender-*` verb of its own.
|
|
16
|
-
*
|
|
17
|
-
* It imports NOTHING, for the reason the node and UV stores do: the view and
|
|
18
|
-
* the verb table both reach it, and a store either of them could not import
|
|
19
|
-
* would not be one store.
|
|
20
|
-
*/
|
|
1
|
+
/** React-free Timeline drawing state: ruler pan/zoom, refusal, measured box and drawn geometry. Playback belongs to BlenderSkinDirector. The page registers actions as volter.<view>.<verb> commands. */
|
|
21
2
|
|
|
22
|
-
|
|
3
|
+
interface TimelineTransform {
|
|
23
4
|
/** The FRAME at the view's left edge, and the CSS pixels per frame. Blender's
|
|
24
5
|
* own `View2D.cur` for a time editor is exactly this pair. */
|
|
25
6
|
readonly startFrame: number;
|
|
26
7
|
readonly pixelsPerFrame: number;
|
|
27
8
|
}
|
|
28
9
|
|
|
29
|
-
|
|
10
|
+
interface TimelineDrawn {
|
|
30
11
|
/** The action being played, and whose keys the summary row draws. */
|
|
31
12
|
readonly action: string | null;
|
|
32
13
|
readonly object: string | null;
|
|
@@ -72,23 +53,7 @@ export interface TimelineViewState {
|
|
|
72
53
|
readonly size: { readonly w: number; readonly h: number };
|
|
73
54
|
readonly framedAt: number;
|
|
74
55
|
readonly drawn: TimelineDrawn | null;
|
|
75
|
-
/**
|
|
76
|
-
* BLENDER'S `show_keys_from_selected_only`, and it is VIEW state here for a
|
|
77
|
-
* reason a headless Blender forces: the flag Blender's Timeline reads is
|
|
78
|
-
* `Scene.flag & SCE_KEYS_NO_SELONLY` (`anim_filter.cc:254-270`), and this
|
|
79
|
-
* surface is an INSPECTION surface that does not write the file — so the
|
|
80
|
-
* filter's position is ours to hold and the data it filters on
|
|
81
|
-
* (`Object.select_get()`) stays Blender's.
|
|
82
|
-
*
|
|
83
|
-
* DEFAULTS ON, as Blender's does: `SCE_KEYS_NO_SELONLY` is a NEGATIVE flag
|
|
84
|
-
* and a new scene does not carry it, so a factory Blender's Timeline shows
|
|
85
|
-
* the selected objects' keys only. The reference read recorded exactly that
|
|
86
|
-
* — the probe's own Timeline drew an EMPTY summary row with the rig
|
|
87
|
-
* unselected while its Dope Sheet drew all five keys.
|
|
88
|
-
*
|
|
89
|
-
* IT DECIDES WHAT THE ROW DRAWS, NEVER WHAT PLAYS. Playback moves every
|
|
90
|
-
* object regardless, in Blender and here.
|
|
91
|
-
*/
|
|
56
|
+
/** Only Show Selected is Timeline view state; keyframe subjects still come from Blender’s selection. Default it on, matching Blender’s scene flag, without writing the inspection filter into the file. */
|
|
92
57
|
readonly onlySelected: boolean;
|
|
93
58
|
/** Which of Blender's animation editors the bottom area shows in Animation mode: the
|
|
94
59
|
* Timeline, the Dope Sheet's Action Editor, or the NLA editor (Blender's editor-type menu). */
|
package/src/uv-view-state.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE UV VIEW'S OWN STATE, and the reason it is a module rather than a
|
|
3
|
-
* `useState` inside the component — the same reason `node-view-state.ts`
|
|
4
|
-
* records: `editor.document.*` reaches only the ACTIVE CENTRE DOCUMENT by its
|
|
5
|
-
* own contract, and this view is not one, so a `useState` here would be a
|
|
6
|
-
* surface nothing in the product could read or drive.
|
|
7
|
-
*
|
|
8
|
-
* WHAT DRIVES IT IS NOT A SESSION VERB. U8's ruling 1 (2026-09-19): every view
|
|
9
|
-
* publishes `volter.<view>.<verb>` commands through `@volter/sdk/views`,
|
|
10
|
-
* one table behind the frame's command service and standalone `cyclotron edit`'s
|
|
11
|
-
* session verb — so this view adds NO `blender-*` verb of its own, which is
|
|
12
|
-
* exactly what the ruling asked the remaining I5 views to stop paying for.
|
|
13
|
-
*
|
|
14
|
-
* It imports NOTHING, for the reason the node view's store does: the view and
|
|
15
|
-
* the verb table both reach it, and a store either of them could not import
|
|
16
|
-
* would not be one store.
|
|
17
|
-
*/
|
|
1
|
+
/** React-free UV drawing state shared by the view and page commands. Actions are registered as volter.blender-uv-view.<verb>. */
|
|
18
2
|
|
|
19
|
-
|
|
3
|
+
interface UvViewTransform {
|
|
20
4
|
/** The UV-space point at the view's centre. UV space is Y-UP (v increases
|
|
21
5
|
* upward, as Blender's image space does); the view flips it once, at the
|
|
22
6
|
* SVG transform, so nothing downstream carries a sign. */
|
|
@@ -45,12 +29,12 @@ export interface UvViewState {
|
|
|
45
29
|
readonly drawn: UvViewDrawn | null;
|
|
46
30
|
}
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
interface UvViewDrawn {
|
|
49
33
|
readonly object: string | null;
|
|
50
34
|
readonly mesh: string | null;
|
|
51
35
|
readonly layer: string | null;
|
|
52
36
|
readonly layers: readonly string[];
|
|
53
|
-
/** Blender
|
|
37
|
+
/** Blender’s mode at the read. */
|
|
54
38
|
readonly mode: string;
|
|
55
39
|
readonly loops: number;
|
|
56
40
|
readonly polygons: number;
|