@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.
Files changed (93) hide show
  1. package/LICENSE +18 -9
  2. package/README.md +5 -5
  3. package/contributions/blender-animation-editors.tsx +17 -69
  4. package/contributions/blender-display-settings.ts +36 -0
  5. package/contributions/blender-export-gltf.ts +228 -0
  6. package/contributions/blender-export.verbs.ts +39 -0
  7. package/contributions/blender-gltf-export.verbs.ts +312 -0
  8. package/contributions/blender-header-menus.tsx +37 -133
  9. package/contributions/blender-icon-trace.mjs +8 -98
  10. package/contributions/blender-icons.source.mjs +96 -860
  11. package/contributions/blender-mixer-pose.ts +369 -0
  12. package/contributions/blender-model-opening.tsx +3 -7
  13. package/contributions/blender-node-editor.document.tsx +28 -180
  14. package/contributions/blender-node-geometry.ts +44 -230
  15. package/contributions/blender-node-panels.source.mjs +7 -121
  16. package/contributions/blender-outliner-authoring.ts +61 -415
  17. package/contributions/blender-outliner-model.ts +15 -112
  18. package/contributions/blender-play-area.tsx +100 -0
  19. package/contributions/blender-playback-movie.ts +409 -0
  20. package/contributions/blender-pose.ts +18 -368
  21. package/contributions/blender-properties-context.ts +1 -1
  22. package/contributions/blender-properties-model.ts +15 -90
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +15 -147
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +217 -339
  28. package/contributions/blender-scene-mixer.ts +51 -0
  29. package/contributions/blender-screenshot.service.ts +28 -0
  30. package/contributions/blender-timeline-geometry.ts +16 -136
  31. package/contributions/blender-timeline.document.tsx +142 -332
  32. package/contributions/blender-uv-editor.document.tsx +48 -112
  33. package/contributions/blender-uv-geometry.ts +14 -179
  34. package/contributions/blender-version.status.tsx +43 -23
  35. package/contributions/blender.environment.ts +2 -2
  36. package/contributions/blender.icons.json +0 -9
  37. package/contributions/blender.keymap.ts +9 -51
  38. package/contributions/blender.node-panels.json +4 -13
  39. package/contributions/blender.page.ts +63 -0
  40. package/contributions/blender.status.tsx +9 -145
  41. package/contributions/blender.style.ts +25 -152
  42. package/contributions/blender.verbs.ts +52 -0
  43. package/contributions/model.layout.ts +2 -34
  44. package/contributions/models.finder.ts +12 -32
  45. package/contributions/properties-bone-constraints.inspector.tsx +1 -16
  46. package/contributions/properties-bone.inspector.tsx +2 -31
  47. package/contributions/properties-collection.inspector.tsx +1 -34
  48. package/contributions/properties-constraints.inspector.tsx +1 -21
  49. package/contributions/properties-data.inspector.tsx +2 -39
  50. package/contributions/properties-material.inspector.tsx +2 -33
  51. package/contributions/properties-modifiers.inspector.tsx +2 -30
  52. package/contributions/properties-object.inspector.tsx +2 -44
  53. package/contributions/properties-output.inspector.tsx +1 -27
  54. package/contributions/properties-particles.inspector.tsx +2 -46
  55. package/contributions/properties-physics.inspector.tsx +2 -41
  56. package/contributions/properties-render.inspector.tsx +1 -45
  57. package/contributions/properties-scene.inspector.tsx +1 -35
  58. package/contributions/properties-texture.inspector.tsx +1 -49
  59. package/contributions/properties-view-layer.inspector.tsx +1 -29
  60. package/contributions/properties-world.inspector.tsx +2 -39
  61. package/contributions/shading.layout.ts +3 -78
  62. package/contributions/uv-editing.layout.ts +3 -70
  63. package/dist-node/serving.mjs +13 -30
  64. package/host/blender-runtime-host.ts +541 -372
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +49 -93
  67. package/package.json +1209 -46
  68. package/producticons/blender-icons.otf +0 -0
  69. package/producticons/blender-product-icon-theme.json +129 -0
  70. package/serving/blender-routes.ts +9 -43
  71. package/serving/blender-wasm-artifact.ts +29 -166
  72. package/serving/index.ts +1 -1
  73. package/src/api.ts +9 -0
  74. package/src/layouts.tsx +1 -17
  75. package/src/looks.ts +1 -12
  76. package/src/model-document-saving.ts +25 -0
  77. package/src/model-play-extension.ts +173 -0
  78. package/src/node-view-state.ts +3 -25
  79. package/src/play-mode.ts +64 -38
  80. package/src/presentation.ts +6 -18
  81. package/src/regions.ts +3 -43
  82. package/src/start-document.ts +15 -0
  83. package/src/timeline-view-state.ts +4 -39
  84. package/src/uv-view-state.ts +4 -20
  85. package/themes/blender.json +299 -0
  86. package/view/blender-view-routes.ts +1 -1
  87. package/contributions/blender-game-panel.tsx +0 -809
  88. package/contributions/blender-palette.source.mjs +0 -350
  89. package/contributions/blender-play-skin.ts +0 -508
  90. package/contributions/blender-screenshot.menu.ts +0 -32
  91. package/contributions/blender.command.ts +0 -109
  92. package/contributions/blender.palette.json +0 -102
  93. 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
+ }
@@ -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
- export interface DrawnNode {
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
- /** SOCKET PANELS, as drawn: how many the node stood and how many of those
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
- documentPlayExtension,
31
- subscribeDocumentPlayExtensions,
32
- } from '@volter/sdk/kit/document-play-extension';
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 model has a play script. A
116
- * script not yet looked for counts as none, so a document opens on the Timeline and turns to
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 = documentPlayExtension('model');
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-ran a snapshot that had not changed, so React kept the old render — a
139
- * document with a play script stayed in Animation, and the Game panel kept showing Stop after the game
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 ??= subscribeDocumentPlayExtensions(publish);
172
+ stopToolWatch ??= subscribeModelPlayExtension(publish);
147
173
  return () => {
148
174
  listeners.delete(listener);
149
175
  if (listeners.size === 0 && stopToolWatch) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HOW THE BLENDER STAGE BEHAVES, as its starting presentation (`@volter/sdk/kit/
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/kit/viewport-presentation';
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
- // BLENDER'S VIEW: the factory `View3D.lens` of 50 mm (read back from Blender 5.2), a 71.5°
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
- // BLENDER'S SHELF OPENS ON SELECT BOX, so a selected object carries no transform gizmo until
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`). Blender 5.2's Workbench under Standard gives
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
- // BLENDER DRAWS THE SELECTED OBJECTS' ORIGINS (Overlays › Origins, on by default): the dot at
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
- // Measured on `modeling.png`: Blender's top-right corner carries two quiet
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
- // Measured across all three Outliner frames — `outliner.png` (edit mode),
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
- // Measured on `outliner.png`: every object row carries an EYE (CSS ink
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
- export interface TimelineTransform {
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
- export interface TimelineDrawn {
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). */
@@ -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
- export interface UvViewTransform {
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
- export interface UvViewDrawn {
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's mode at the read — named, never required (ruling 1). */
37
+ /** Blender’s mode at the read. */
54
38
  readonly mode: string;
55
39
  readonly loops: number;
56
40
  readonly polygons: number;