@volter/editor-blender 0.1.0
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 +1409 -0
- package/README.md +17 -0
- package/contributions/blender-header-menus.tsx +483 -0
- package/contributions/blender-icon-trace.mjs +403 -0
- package/contributions/blender-icons.source.mjs +2925 -0
- package/contributions/blender-node-editor.document.tsx +1402 -0
- package/contributions/blender-node-geometry.ts +1138 -0
- package/contributions/blender-node-panels.source.mjs +485 -0
- package/contributions/blender-outliner-authoring.ts +1729 -0
- package/contributions/blender-outliner-model.ts +389 -0
- package/contributions/blender-palette.source.mjs +319 -0
- package/contributions/blender-properties-model.ts +351 -0
- package/contributions/blender-properties-tab.tsx +100 -0
- package/contributions/blender-properties-view.tsx +1191 -0
- package/contributions/blender-runtime-skin.ts +619 -0
- package/contributions/blender-runtime.document.tsx +232 -0
- package/contributions/blender-timeline-geometry.ts +323 -0
- package/contributions/blender-timeline.document.tsx +1056 -0
- package/contributions/blender-uv-editor.document.tsx +483 -0
- package/contributions/blender-uv-geometry.ts +305 -0
- package/contributions/blender-version.status.tsx +93 -0
- package/contributions/blender.command.ts +102 -0
- package/contributions/blender.icons.json +1247 -0
- package/contributions/blender.icons.traced.json +1561 -0
- package/contributions/blender.keymap.ts +39 -0
- package/contributions/blender.node-panels.json +2436 -0
- package/contributions/blender.palette.json +93 -0
- package/contributions/blender.status.tsx +263 -0
- package/contributions/blender.style.ts +271 -0
- package/contributions/model.layout.ts +53 -0
- package/contributions/models.finder.ts +59 -0
- package/contributions/properties-bone-constraints.inspector.tsx +50 -0
- package/contributions/properties-bone.inspector.tsx +184 -0
- package/contributions/properties-collection.inspector.tsx +96 -0
- package/contributions/properties-constraints.inspector.tsx +69 -0
- package/contributions/properties-data.inspector.tsx +229 -0
- package/contributions/properties-material.inspector.tsx +121 -0
- package/contributions/properties-modifiers.inspector.tsx +74 -0
- package/contributions/properties-object.inspector.tsx +215 -0
- package/contributions/properties-output.inspector.tsx +210 -0
- package/contributions/properties-particles.inspector.tsx +494 -0
- package/contributions/properties-physics.inspector.tsx +614 -0
- package/contributions/properties-render.inspector.tsx +446 -0
- package/contributions/properties-scene.inspector.tsx +174 -0
- package/contributions/properties-texture.inspector.tsx +300 -0
- package/contributions/properties-view-layer.inspector.tsx +145 -0
- package/contributions/properties-world.inspector.tsx +130 -0
- package/contributions/sculpt.layout.ts +25 -0
- package/contributions/shading.layout.ts +99 -0
- package/contributions/texture.layout.ts +16 -0
- package/contributions/uv-editing.layout.ts +93 -0
- package/host/blender-runtime-host.ts +1256 -0
- package/package.json +77 -0
- package/src/layouts.tsx +48 -0
- package/src/looks.ts +14 -0
- package/src/node-view-state.ts +125 -0
- package/src/timeline-view-state.ts +154 -0
- package/src/uv-view-state.ts +125 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/editor-blender",
|
|
3
|
+
"author": "Volter AI, Inc.",
|
|
4
|
+
"license": "AGPL-3.0-only AND GPL-3.0-or-later",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"description": "Blender modeling documents, inspectors, commands and presentation for Volter Editor.",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/volter-ai/editor.git",
|
|
14
|
+
"directory": "packages/editor-blender"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"contributions",
|
|
19
|
+
"host",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./contributions/*": "./contributions/*",
|
|
24
|
+
"./looks": "./src/looks.ts",
|
|
25
|
+
"./layouts": "./src/layouts.tsx",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"vgai": {
|
|
29
|
+
"contributions": [
|
|
30
|
+
"./contributions/blender-node-editor.document.tsx",
|
|
31
|
+
"./contributions/blender-timeline.document.tsx",
|
|
32
|
+
"./contributions/blender-uv-editor.document.tsx",
|
|
33
|
+
"./contributions/blender-runtime.document.tsx",
|
|
34
|
+
"./contributions/blender-version.status.tsx",
|
|
35
|
+
"./contributions/blender.command.ts",
|
|
36
|
+
"./contributions/blender.keymap.ts",
|
|
37
|
+
"./contributions/blender.status.tsx",
|
|
38
|
+
"./contributions/blender.style.ts",
|
|
39
|
+
"./contributions/model.layout.ts",
|
|
40
|
+
"./contributions/models.finder.ts",
|
|
41
|
+
"./contributions/properties-bone-constraints.inspector.tsx",
|
|
42
|
+
"./contributions/properties-bone.inspector.tsx",
|
|
43
|
+
"./contributions/properties-collection.inspector.tsx",
|
|
44
|
+
"./contributions/properties-constraints.inspector.tsx",
|
|
45
|
+
"./contributions/properties-data.inspector.tsx",
|
|
46
|
+
"./contributions/properties-material.inspector.tsx",
|
|
47
|
+
"./contributions/properties-modifiers.inspector.tsx",
|
|
48
|
+
"./contributions/properties-object.inspector.tsx",
|
|
49
|
+
"./contributions/properties-output.inspector.tsx",
|
|
50
|
+
"./contributions/properties-particles.inspector.tsx",
|
|
51
|
+
"./contributions/properties-physics.inspector.tsx",
|
|
52
|
+
"./contributions/properties-render.inspector.tsx",
|
|
53
|
+
"./contributions/properties-scene.inspector.tsx",
|
|
54
|
+
"./contributions/properties-texture.inspector.tsx",
|
|
55
|
+
"./contributions/properties-view-layer.inspector.tsx",
|
|
56
|
+
"./contributions/properties-world.inspector.tsx",
|
|
57
|
+
"./contributions/sculpt.layout.ts",
|
|
58
|
+
"./contributions/shading.layout.ts",
|
|
59
|
+
"./contributions/uv-editing.layout.ts",
|
|
60
|
+
"./contributions/texture.layout.ts"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@volter/blender-engine": "0.1.0",
|
|
65
|
+
"@volter/editor-threejs": "0.5.57",
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"@volter/editor-sdk": "*",
|
|
70
|
+
"@volter/editor-project": "*",
|
|
71
|
+
"react": "^19.0.0",
|
|
72
|
+
"three": "^0.180.0"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/layouts.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BLENDER layout — the composition a models project's adapter declares
|
|
3
|
+
* (`vgai.adapter.ts`: `editor: { Layout: ModelLayout }`, the door
|
|
4
|
+
* `@volter/editor-sdk/layouts` documents). `ModelArrangement` names the Model
|
|
5
|
+
* workspace and the chrome regions it shows; Sculpt and Texture open on it too
|
|
6
|
+
* — their reserved panels (the brush rail, the layer stack) ship WITH their
|
|
7
|
+
* programs, never as empty chrome, and until then nothing about a sculpt
|
|
8
|
+
* arrangement differs from a modeling one.
|
|
9
|
+
*
|
|
10
|
+
* WHERE THE PANELS SIT IS THE WORKBENCH'S. A workspace declares what it SHOWS
|
|
11
|
+
* (`regions`) and which documents stand beside the centre one (`areas`); the
|
|
12
|
+
* sizes, the splits and the tab order are the frame's editor-group state,
|
|
13
|
+
* which VS Code persists itself. A captured grid used to ship here beside the
|
|
14
|
+
* contribution, measured off Blender 5.2's own MODELING workspace; the
|
|
15
|
+
* measurement that outlived it is `contributions/model.layout.ts`'s Timeline
|
|
16
|
+
* RATIO, which is a proportion rather than a geometry and is stated there.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
EditorFooter,
|
|
20
|
+
EditorFrame,
|
|
21
|
+
EditorHeader,
|
|
22
|
+
Workspace,
|
|
23
|
+
type WorkspaceArrangement,
|
|
24
|
+
} from '@volter/editor-sdk/layouts';
|
|
25
|
+
|
|
26
|
+
export const ModelArrangement: WorkspaceArrangement = {
|
|
27
|
+
id: 'model',
|
|
28
|
+
title: 'Model',
|
|
29
|
+
// `tabs` unstated — see `../contributions/model.layout.ts`: the workspace
|
|
30
|
+
// layer wins over the style bundle, so restating the host default shadows it.
|
|
31
|
+
regions: {
|
|
32
|
+
header: 'shown',
|
|
33
|
+
shelf: 'shown',
|
|
34
|
+
inspector: 'properties',
|
|
35
|
+
drawer: 'hidden',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Blender-shaped modeling; the game never takes the workspace over. */
|
|
40
|
+
export function ModelLayout() {
|
|
41
|
+
return (
|
|
42
|
+
<EditorFrame>
|
|
43
|
+
<EditorHeader />
|
|
44
|
+
<Workspace arrangement={ModelArrangement} immersivePlay={false} />
|
|
45
|
+
<EditorFooter />
|
|
46
|
+
</EditorFrame>
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/looks.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Blender skew's LOOK and BINDINGS as importable values — `@volter/editor-blender/looks`,
|
|
3
|
+
* the public surface a project's `vgai.adapter.ts` declares them from
|
|
4
|
+
* (ARCHITECTURE-CORE §Adapters and contributions are code, not configs):
|
|
5
|
+
*
|
|
6
|
+
* editor: { Layout: ModelLayout, style: blenderStyle, keymap: blenderKeymap }
|
|
7
|
+
*
|
|
8
|
+
* The same two objects the package's `workspace.style` / `workspace.keymap`
|
|
9
|
+
* contributions register (`package.json#vgai.contributions`). Registration
|
|
10
|
+
* publishes them to every project that declares this package; the adapter's
|
|
11
|
+
* declaration is what CHOOSES them for one.
|
|
12
|
+
*/
|
|
13
|
+
export { keymap as blenderKeymap } from '../contributions/blender.keymap';
|
|
14
|
+
export { style as blenderStyle } from '../contributions/blender.style';
|
|
@@ -0,0 +1,125 @@
|
|
|
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`, `vgai 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 `vgai.*` 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
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface NodeViewTransform {
|
|
23
|
+
/** The tree-space point at the view's centre. */
|
|
24
|
+
readonly cx: number;
|
|
25
|
+
readonly cy: number;
|
|
26
|
+
/** Tree units per CSS pixel. Blender's node editor draws without DPI
|
|
27
|
+
* (`node_intern.hh:332`), so zoom 1 is one tree unit per pixel. */
|
|
28
|
+
readonly zoom: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NodeViewState {
|
|
32
|
+
readonly transform: NodeViewTransform;
|
|
33
|
+
/** The node being LOOKED AT — inspection state, never `Node.select` in the
|
|
34
|
+
* engine, which writing would be a mutation the document saves. */
|
|
35
|
+
readonly looked: string | null;
|
|
36
|
+
/** The last gesture refused by name, and why. */
|
|
37
|
+
readonly refusal: string | null;
|
|
38
|
+
/** The canvas box the view measured itself at, in CSS px. */
|
|
39
|
+
readonly size: { readonly w: number; readonly h: number };
|
|
40
|
+
/** Set by the view when a `view-all` was asked for and it has framed. */
|
|
41
|
+
readonly framedAt: number;
|
|
42
|
+
/**
|
|
43
|
+
* WHAT THE VIEW ACTUALLY DREW, in tree space — published so the parity
|
|
44
|
+
* table is a MEASUREMENT of the shipped drawing rather than a second run of
|
|
45
|
+
* the same arithmetic. `height` is `yTop - yBottom`, which at zoom 1 is CSS
|
|
46
|
+
* pixels, because the node editor draws without DPI (`node_intern.hh:332`).
|
|
47
|
+
*/
|
|
48
|
+
readonly drawn: readonly DrawnNode[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DrawnNode {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly x: number;
|
|
54
|
+
readonly yTop: number;
|
|
55
|
+
readonly yBottom: number;
|
|
56
|
+
readonly width: number;
|
|
57
|
+
readonly height: number;
|
|
58
|
+
/** How many socket marks the node drew, and where the first one sits
|
|
59
|
+
* relative to the node's top — the number `node_update_basis_socket`'s
|
|
60
|
+
* `locy - NODE_DYS` decides. */
|
|
61
|
+
readonly sockets: number;
|
|
62
|
+
readonly firstSocketBelowTop: number | null;
|
|
63
|
+
/** The header's colour, as drawn. */
|
|
64
|
+
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. */
|
|
69
|
+
readonly panels: number;
|
|
70
|
+
readonly collapsedPanels: number;
|
|
71
|
+
readonly panelFallback: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const INITIAL: NodeViewState = {
|
|
75
|
+
transform: { cx: 0, cy: 0, zoom: 1 },
|
|
76
|
+
looked: null,
|
|
77
|
+
refusal: null,
|
|
78
|
+
size: { w: 0, h: 0 },
|
|
79
|
+
framedAt: 0,
|
|
80
|
+
drawn: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let state: NodeViewState = INITIAL;
|
|
84
|
+
let version = 0;
|
|
85
|
+
const listeners = new Set<() => void>();
|
|
86
|
+
/** A `view-all` ASK, raised by the command and consumed by the view — the one
|
|
87
|
+
* action the store cannot compute, because framing needs the laid-out tree. */
|
|
88
|
+
let viewAllRequest = 0;
|
|
89
|
+
|
|
90
|
+
export function nodeViewState(): NodeViewState {
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function nodeViewVersion(): number {
|
|
95
|
+
return version;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function subscribeNodeView(listener: () => void): () => void {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => {
|
|
101
|
+
listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setNodeViewState(next: Partial<NodeViewState>): void {
|
|
106
|
+
state = { ...state, ...next };
|
|
107
|
+
version += 1;
|
|
108
|
+
for (const listener of [...listeners]) listener();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function requestNodeViewAll(): void {
|
|
112
|
+
viewAllRequest += 1;
|
|
113
|
+
version += 1;
|
|
114
|
+
for (const listener of [...listeners]) listener();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function nodeViewAllRequest(): number {
|
|
118
|
+
return viewAllRequest;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A gesture the view refuses because it would EDIT. One place, so the command
|
|
122
|
+
* door and the pointer handlers cannot drift into two vocabularies. */
|
|
123
|
+
export function refuseNodeViewGesture(text: string): void {
|
|
124
|
+
setNodeViewState({ refusal: text });
|
|
125
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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 `vgai.<view>.<verb>` commands through `@volter/editor-sdk/views`, one
|
|
14
|
+
* table behind the frame's command service and standalone `vgai 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
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface TimelineTransform {
|
|
23
|
+
/** The FRAME at the view's left edge, and the CSS pixels per frame. Blender's
|
|
24
|
+
* own `View2D.cur` for a time editor is exactly this pair. */
|
|
25
|
+
readonly startFrame: number;
|
|
26
|
+
readonly pixelsPerFrame: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TimelineDrawn {
|
|
30
|
+
/** The action being played, and whose keys the summary row draws. */
|
|
31
|
+
readonly action: string | null;
|
|
32
|
+
readonly object: string | null;
|
|
33
|
+
readonly armature: string | null;
|
|
34
|
+
/** The playhead's frame, as the MIXER holds it. */
|
|
35
|
+
readonly frame: number;
|
|
36
|
+
readonly start: number;
|
|
37
|
+
readonly end: number;
|
|
38
|
+
readonly fps: number;
|
|
39
|
+
readonly playing: boolean;
|
|
40
|
+
/** The frames the summary row drew a diamond at — AFTER the selection
|
|
41
|
+
* filter, which is what "what the row drew" means. */
|
|
42
|
+
readonly keyframes: readonly number[];
|
|
43
|
+
/** The filter's position, and the objects whose keys survived it. */
|
|
44
|
+
readonly onlySelected: boolean;
|
|
45
|
+
readonly summaryObjects: readonly string[];
|
|
46
|
+
/** The ruler's major step, in frames, as `calculate_grid_step` answered it
|
|
47
|
+
* for the drawn width — the number a parity read checks. */
|
|
48
|
+
readonly majorStep: number;
|
|
49
|
+
readonly minorStep: number | null;
|
|
50
|
+
/** How many marks of each kind went into the DOM. */
|
|
51
|
+
readonly majorLines: number;
|
|
52
|
+
readonly minorLines: number;
|
|
53
|
+
readonly labels: number;
|
|
54
|
+
readonly diamonds: number;
|
|
55
|
+
/** The diamond's drawn geometry, so the table is a measurement of the
|
|
56
|
+
* shipped drawing rather than a second run of the same arithmetic. */
|
|
57
|
+
readonly diamondRadius: number;
|
|
58
|
+
readonly diamondSprite: number;
|
|
59
|
+
/** Bones the presenter bound, and tracks the mixer holds. */
|
|
60
|
+
readonly bones: number;
|
|
61
|
+
readonly tracks: number;
|
|
62
|
+
/** Engine calls the whole Timeline has made since the session began — a
|
|
63
|
+
* scrub adds NONE, and that is the claim this number is here to prove. */
|
|
64
|
+
readonly engineCalls: number;
|
|
65
|
+
/** Everything this view shows LESS than Blender would, by name. */
|
|
66
|
+
readonly warnings: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TimelineViewState {
|
|
70
|
+
readonly transform: TimelineTransform;
|
|
71
|
+
readonly refusal: string | null;
|
|
72
|
+
readonly size: { readonly w: number; readonly h: number };
|
|
73
|
+
readonly framedAt: number;
|
|
74
|
+
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
|
+
*/
|
|
92
|
+
readonly onlySelected: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const INITIAL: TimelineViewState = {
|
|
96
|
+
transform: { startFrame: -5, pixelsPerFrame: 8 },
|
|
97
|
+
refusal: null,
|
|
98
|
+
size: { w: 0, h: 0 },
|
|
99
|
+
framedAt: 0,
|
|
100
|
+
drawn: null,
|
|
101
|
+
onlySelected: true,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let state: TimelineViewState = INITIAL;
|
|
105
|
+
let version = 0;
|
|
106
|
+
const listeners = new Set<() => void>();
|
|
107
|
+
let viewAllRequest = 0;
|
|
108
|
+
|
|
109
|
+
export function timelineViewState(): TimelineViewState {
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function timelineViewVersion(): number {
|
|
114
|
+
return version;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function subscribeTimelineView(listener: () => void): () => void {
|
|
118
|
+
listeners.add(listener);
|
|
119
|
+
return () => {
|
|
120
|
+
listeners.delete(listener);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function setTimelineViewState(next: Partial<TimelineViewState>): void {
|
|
125
|
+
state = { ...state, ...next };
|
|
126
|
+
version += 1;
|
|
127
|
+
for (const listener of [...listeners]) listener();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Flip the summary row's selection filter (Blender's `View ▸ Only Show
|
|
131
|
+
* Selected`). The row re-filters from data the door already reported; nothing
|
|
132
|
+
* is read from the engine and nothing the mixer plays changes. */
|
|
133
|
+
export function setTimelineOnlySelected(onlySelected: boolean): void {
|
|
134
|
+
if (state.onlySelected === onlySelected) return;
|
|
135
|
+
setTimelineViewState({ onlySelected });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A `view-all` ASK, raised by the verb and consumed by the view — the one
|
|
139
|
+
* action the store cannot compute, because framing needs the measured box. */
|
|
140
|
+
export function requestTimelineViewAll(): void {
|
|
141
|
+
viewAllRequest += 1;
|
|
142
|
+
version += 1;
|
|
143
|
+
for (const listener of [...listeners]) listener();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function timelineViewAllRequest(): number {
|
|
147
|
+
return viewAllRequest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A gesture the view refuses because it would EDIT. One place, so the verb
|
|
151
|
+
* table and the pointer handlers cannot drift into two vocabularies. */
|
|
152
|
+
export function refuseTimelineGesture(text: string): void {
|
|
153
|
+
setTimelineViewState({ refusal: text });
|
|
154
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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 `vgai.<view>.<verb>` commands through `@volter/editor-sdk/views`,
|
|
10
|
+
* one table behind the frame's command service and standalone `vgai 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
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface UvViewTransform {
|
|
20
|
+
/** The UV-space point at the view's centre. UV space is Y-UP (v increases
|
|
21
|
+
* upward, as Blender's image space does); the view flips it once, at the
|
|
22
|
+
* SVG transform, so nothing downstream carries a sign. */
|
|
23
|
+
readonly cx: number;
|
|
24
|
+
readonly cy: number;
|
|
25
|
+
/** CSS pixels per UV unit. At zoom 1 the 0–1 tile is one pixel across, so
|
|
26
|
+
* the view's own `view-all` is what a person actually starts from — the
|
|
27
|
+
* same shape as Blender's `image_view_all` (`image_ops.cc`). */
|
|
28
|
+
readonly zoom: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UvViewState {
|
|
32
|
+
readonly transform: UvViewTransform;
|
|
33
|
+
/** The last gesture refused by name, and why. */
|
|
34
|
+
readonly refusal: string | null;
|
|
35
|
+
/** The canvas box the view measured itself at, in CSS px. */
|
|
36
|
+
readonly size: { readonly w: number; readonly h: number };
|
|
37
|
+
/** Set by the view when a `view-all` was asked for and it has framed. */
|
|
38
|
+
readonly framedAt: number;
|
|
39
|
+
/**
|
|
40
|
+
* WHAT THE VIEW ACTUALLY DREW — published so a parity table is a
|
|
41
|
+
* MEASUREMENT of the shipped drawing rather than a second run of the same
|
|
42
|
+
* arithmetic, which is the reading `node-view-state.ts`'s `drawn` already
|
|
43
|
+
* earns its keep with.
|
|
44
|
+
*/
|
|
45
|
+
readonly drawn: UvViewDrawn | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface UvViewDrawn {
|
|
49
|
+
readonly object: string | null;
|
|
50
|
+
readonly mesh: string | null;
|
|
51
|
+
readonly layer: string | null;
|
|
52
|
+
readonly layers: readonly string[];
|
|
53
|
+
/** Blender's mode at the read — named, never required (ruling 1). */
|
|
54
|
+
readonly mode: string;
|
|
55
|
+
readonly loops: number;
|
|
56
|
+
readonly polygons: number;
|
|
57
|
+
readonly triangles: number;
|
|
58
|
+
/** How many marks of each kind the view put in the DOM. */
|
|
59
|
+
readonly faces: number;
|
|
60
|
+
readonly edges: number;
|
|
61
|
+
readonly verts: number;
|
|
62
|
+
readonly faceDots: number;
|
|
63
|
+
readonly pinned: number;
|
|
64
|
+
/** The UV bounds the layout actually occupies. */
|
|
65
|
+
readonly bounds: readonly [number, number, number, number] | null;
|
|
66
|
+
/** The tile grid, as drawn: the tile count and its subdivision. */
|
|
67
|
+
readonly tiles: readonly [number, number];
|
|
68
|
+
readonly subdiv: readonly [number, number];
|
|
69
|
+
/** The image behind the tile, or the reason there is none. */
|
|
70
|
+
readonly image: string | null;
|
|
71
|
+
/** Everything the view is showing LESS than Blender would, by name. */
|
|
72
|
+
readonly warnings: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const INITIAL: UvViewState = {
|
|
76
|
+
transform: { cx: 0.5, cy: 0.5, zoom: 512 },
|
|
77
|
+
refusal: null,
|
|
78
|
+
size: { w: 0, h: 0 },
|
|
79
|
+
framedAt: 0,
|
|
80
|
+
drawn: null,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let state: UvViewState = INITIAL;
|
|
84
|
+
let version = 0;
|
|
85
|
+
const listeners = new Set<() => void>();
|
|
86
|
+
/** A `view-all` ASK, raised by the verb and consumed by the view — the one
|
|
87
|
+
* action the store cannot compute, because framing needs the read layout. */
|
|
88
|
+
let viewAllRequest = 0;
|
|
89
|
+
|
|
90
|
+
export function uvViewState(): UvViewState {
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function uvViewVersion(): number {
|
|
95
|
+
return version;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function subscribeUvView(listener: () => void): () => void {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => {
|
|
101
|
+
listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setUvViewState(next: Partial<UvViewState>): void {
|
|
106
|
+
state = { ...state, ...next };
|
|
107
|
+
version += 1;
|
|
108
|
+
for (const listener of [...listeners]) listener();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function requestUvViewAll(): void {
|
|
112
|
+
viewAllRequest += 1;
|
|
113
|
+
version += 1;
|
|
114
|
+
for (const listener of [...listeners]) listener();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function uvViewAllRequest(): number {
|
|
118
|
+
return viewAllRequest;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A gesture the view refuses because it would EDIT. One place, so the verb
|
|
122
|
+
* table and the pointer handlers cannot drift into two vocabularies. */
|
|
123
|
+
export function refuseUvViewGesture(text: string): void {
|
|
124
|
+
setUvViewState({ refusal: text });
|
|
125
|
+
}
|