@volter/editor-blender 0.1.150 → 0.1.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +26 -56
  13. package/contributions/blender-node-editor.document.tsx +30 -181
  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 +99 -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 +59 -91
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +39 -181
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +170 -618
  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 +145 -333
  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 +45 -31
  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 +64 -0
  40. package/contributions/blender.status.tsx +8 -150
  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 +3 -30
  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 -32
  64. package/host/blender-runtime-host.ts +795 -412
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +52 -95
  67. package/package.json +1221 -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 -48
  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/node-view-state.ts +3 -25
  78. package/src/play-mode.ts +13 -42
  79. package/src/play-tool.ts +44 -0
  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
@@ -1,45 +1,6 @@
1
- /**
2
- * The BLENDER style bundle — Blender's greys and widget blue as a palette
3
- * document (`blender.palette.json`, the same v3 shape a person imports), its
4
- * own opaque material, and Blender's chrome regions
5
- * (`@volter/sdk/looks`, a `workspace.style` contribution).
6
- *
7
- * The material transcribes Blender's default theme: widgets round at 0.2 of
8
- * their height (4px on a 20px widget), areas and panels are flat with no
9
- * shadow, and only menus and popovers cast one (`menu_shadow_fac` 0.5 over
10
- * `menu_shadow_width` 4). The `large` radius is our reading — Blender rounds
11
- * its popovers the same as its menus. The density is measured from Blender
12
- * 5.2's own frames at 1x:
13
- * 26px top bar and area headers, 24px status bar, and — re-measured against
14
- * the frames widget by widget — EVERY widget 20px tall, button/field/toggle
15
- * alike, so all three control sizes are 20 rather than a 18/20/24 ramp
16
- * Blender does not have. Its UI face measures 11px (an 8px cap+ascender on
17
- * "Add Modifier"); the rest of the type scale is our reading.
18
- *
19
- * THE ICON SET (`blender.icons.json`) IS PART BLENDER'S, AND THAT IS WHY THIS
20
- * PACKAGE CARRIES TWO LICENCES. Of its 346 glyphs, 197 are machine-TRACED —
21
- * byte for byte — from Blender's own vector sources
22
- * (`release/datafiles/icons_svg/*.svg`, 188 distinct files at the engine's
23
- * pin): an affine transform of Blender's outline onto a 16-unit grid, nothing
24
- * re-proportioned or re-centred, so a traced glyph IS Blender's drawing and a
25
- * derivative of GPL-2.0-or-later artwork. Their provenance — `ICON_*` name,
26
- * source file and sha256 per glyph — is `blender.icons.traced.json`, written
27
- * by `blender-icon-trace.mjs`. The other 149 are our own drawings in Blender's
28
- * idiom: a 16-unit grid, monochrome filled silhouettes, ~1.3-unit round-ended
29
- * strokes, no outline around a fill.
30
- *
31
- * So `@volter/editor-blender` is `AGPL-3.0-only AND GPL-3.0-or-later` — our code AGPL,
32
- * Blender's artwork GPL, combined under each licence's §13
33
- * (`packages/blender/LICENSE` carries the notice and both texts). NOTHING
34
- * TRACED MAY BE COPIED INTO AN APACHE-2.0 OR MIT PART OF THIS REPO;
35
- * `packages/editor`'s own icon set in particular stays free of it.
36
- *
37
- * A glyph is EDITED in `blender-icons.source.mjs` and the JSON re-emitted by
38
- * running it; a path `d` is not a thing a person edits by hand.
39
- */
1
+ /** Blender’s workspace style supplies theme colours, opaque material, typography and region geometry. Widgets round at 0.2 of their height; menus and popovers carry the theme shadow. blender-icons.source.mjs generates the icon set. Traced Blender artwork is GPL-3.0-or-later, combined with this package’s AGPL-3.0-only code under both licences’ section 13; preserve LICENSE and per-glyph provenance in blender.icons.traced.json. Do not copy traced artwork into Apache-2.0 or MIT packages. */
40
2
  import type { IconSetContribution, StyleContribution } from '@volter/sdk/looks';
41
3
  import iconsJson from './blender.icons.json';
42
- import palette from './blender.palette.json';
43
4
 
44
5
  /**
45
6
  * `resolveJsonModule` widens every string in a JSON import, so each glyph's
@@ -54,7 +15,8 @@ export const point = 'workspace.style';
54
15
  export const style: StyleContribution = {
55
16
  id: 'blender',
56
17
  title: 'Blender',
57
- paletteId: 'blender',
18
+ // Theme role values come from userdef_default_theme.c through scripts/workbench/generate-blender-theme.mjs. JSON carries the palette; constants below define this style’s chrome geometry.
19
+ colorTheme: 'blender',
58
20
  materialId: 'blender',
59
21
  material: {
60
22
  id: 'blender',
@@ -69,139 +31,47 @@ export const style: StyleContribution = {
69
31
  density: {
70
32
  control: { compact: 20, default: 20, comfortable: 20 },
71
33
  font: { xs: 9, sm: 10, base: 11, md: 11, lg: 12 },
72
- // Blender's glyphs, measured at matched scale: 14 px through the
73
- // chrome (the Properties tab rail in `properties-data-edit.png`, the
74
- // Outliner's row and toggle marks in `outliner.png`).
75
- //
76
- // `2xl` is now MEASURED too, and it came down from the extrapolated 20.
77
- // Its only consumer is the viewport navigation cluster
78
- // (`ViewportFurniture.tsx`), and Blender's own cluster in
79
- // `modeling-object-none.png` draws 15x15 px of INK at 1x. These glyphs
80
- // ink at ~0.85 of their box (`blender-icons.source.mjs`'s `S` dial), so
81
- // 15 px of ink is an 18 px box — at 20 the cluster measured 17x17,
82
- // two pixels heavier than the frame it is copying.
83
- //
84
- // `xs` IS MEASURED NOW, and the note it replaces was wrong about the
85
- // frames: they do show glyphs below the 14 px chrome rung — the claim
86
- // had only ever looked at the status bar. The whole sub-14 inventory,
87
- // ink bounding boxes in CSS px (device halved), all at native 2x:
88
- //
89
- // `+` Add Modifier `properties-modifier.png` 9.0 x 9.0
90
- // `+` list add `properties-data-edit.png` 9.0 x 9.0
91
- // chevron, list button `properties-data-edit.png` 9.0 x 5.0
92
- // chevron, panel band `properties-data-edit.png` 9.0 x 5.0
93
- // caret, datablock well `properties-data-edit.png` 6.5 x 4.0
94
- // caret, editor-type `properties-modifier.png` 6.5 x 4.0
95
- // grip `::::` `properties-data-edit.png` 10.0 x 4.0
96
- // triangle, list row `properties-data-edit.png` 6.0 x 5.0
97
- //
98
- // FOUR of those agree at 9.0 wide and that is the rung. Solved against
99
- // the one glyph both sides draw on the same fill (`#535353`, so the
100
- // threshold is identical): Blender's `+` arms measure 17.43 and 17.08
101
- // device px by sub-pixel coverage, mean 8.63 CSS; ours at `xs` 12
102
- // measure 20.75 device (10.38 CSS, against the path's own geometric
103
- // bbox of 10.48 — they agree to a tenth). Our glyphs ink 0.865 of the
104
- // box, so the box that inks 8.63 is 9.98. Two other estimators on the
105
- // same pair — ink MASS (mass scales as the square, ratio 0.8171) and
106
- // the pixel bbox (18/22 = 0.818) — give 9.81 and 9.82. The rung is 10.
107
- //
108
- // THE RUNG WAS CHECKED AGAINST A SECOND GLYPH FAMILY AFTER LANDING,
109
- // and it holds: at 10 the dock well's caret inks 5.84 x 3.71 CSS
110
- // against Blender's editor-type caret at 6.0 x 4.0 (12 x 8 device,
111
- // `properties-modifier.png` x 62..73, y 23..30). Our chevron inks 0.58
112
- // of its box where the `+` inks 0.87, so the two agree on the rung
113
- // only because each was solved against its own counterpart — which is
114
- // why both were measured rather than one inferred from the other.
115
- //
116
- // RESIDUE, measured not closed: Blender has TWO chevron sizes, the
117
- // well caret at 6.0 wide and the panel-band / list-button chevron at
118
- // 9.0, and they are not a uniform scale of each other (0.67 wide
119
- // against 0.80 tall). One rung cannot serve both; our chevron lands on
120
- // the well caret, so a band chevron drawn at `xs` would ink 5.84 where
121
- // Blender's inks 9.0. Closing that needs a second glyph, not a rung —
122
- // and no expanded band is on screen in the Model document to confirm
123
- // it against.
34
+ // Icon boxes account for each glyph’s optical extent; small controls, ordinary chrome and viewport navigation use distinct rungs.
124
35
  icon: { xs: 10, sm: 14, md: 14, lg: 16, xl: 16, '2xl': 18 },
125
36
  chrome: {
37
+ // The top bar's height, traced from Blender 5.2's own frames at 1x.
126
38
  commandBar: 26,
127
39
  panelHeader: 26,
128
40
  localToolbar: 26,
41
+ // Blender’s Outliner row pitch is 20 CSS pixels.
129
42
  treeRow: 20,
130
- // The Outliner's tree is a SQUARE grid — one level steps by the row's
131
- // own height. Measured on the native 2x `outliner.png`: the type
132
- // glyphs of Scene Collection, Collection and Camera centre at x 60.5,
133
- // 100.5 and 140.5 device px (30.25 / 50.25 / 70.25 CSS) and each
134
- // row's chevron sits one cell to the left of its glyph (Collection's
135
- // at 30.25, Camera's at 50.25), so the step is 20 and the chevron and
136
- // the glyph each own one cell of it.
43
+ // Outliner depth steps by one row-height cell; each row’s chevron occupies the cell before its type glyph.
137
44
  treeIndent: 20,
138
45
  statusBar: 24,
139
- // The toolbar, RE-MEASURED 2026-09-18 on the native 2x frame
140
- // (`modeling-edit-none.png`, column x=25 and row y=400): a tool is a
141
- // 40x35 BORDER BOX — 38x34 of `widget.menu` fill inside a one-pixel
142
- // boundary — and the boxes are CONTIGUOUS within a group, sharing that
143
- // one pixel, so the pitch down a strip is 35. Plain viewport shows
144
- // only BETWEEN groups (four times in Blender's twenty-one-tool rail).
145
- // These three numbers are the border box, because that is what the
146
- // rail's CSS sets and what the collapse (`margin-block-start: -1px`
147
- // in `workspace-dock.css`) measures against: 36 - 1 = the 35 pitch.
148
- //
149
- // `toolGap` RE-RE-MEASURED 2026-09-19 on the same frame, BOX EDGE TO
150
- // BOX EDGE rather than fill to fill: group 1's border box ends at
151
- // device y 312 and group 2's begins at 323, so the separation is 10
152
- // device px — 5 CSS, not the 7 or 8 the two earlier readings gave.
153
- // Both were counting the group's own 1 CSS px emboss (device rows
154
- // 313-314, luminance 53 over the viewport's 63) and the two borders
155
- // as part of the bare viewport between the groups.
46
+ // Toolbar dimensions are border boxes. Adjacent tools share a one-pixel boundary; toolGap separates groups.
156
47
  toolSize: 36,
157
48
  toolWidth: 40,
158
49
  toolGap: 5,
159
- // THE AREA SEAM, measured on the native 2x `modeling.png`: the
160
- // Outliner-to-Properties groove (median over x 2900..3400) runs
161
- // y 419..425 as (21,21,21) and the viewport-to-Properties groove
162
- // runs x 2836..2842 as the same value — 7 device px, 3.5 CSS px,
163
- // both times with a one-pixel lighter emboss on each side. The
164
- // colour is the palette's `color.boundary.area`.
50
+ // The area seam is a 3.5-CSS-px dark groove between docked areas.
165
51
  areaSeam: 3.5,
166
- // THE EMBOSS on each side of that groove, as percent of white mixed
167
- // into the area's OWN fill. SOLVED on the native 2x `modeling.png`
168
- // from four fills and their light bands — Properties 47 -> 64
169
- // (x=3200, y 426..427), Outliner 39 -> 57/56 (y=300, x 2843..2844),
170
- // the Properties tab rail 23 -> 43/42 (y=1500, same columns), the
171
- // Outliner's alternate row 42 -> 59/60 (x=3200, y 417..418) — whose
172
- // individual alphas are 8.17/8.10/8.62/8.45%, mean 8.14; 8.2 is the
173
- // one value that renders all four measured integers. The viewport's
174
- // 63 -> 93/94 and the 3D View header's 52 -> 86 are this same lift
175
- // applied TWICE; see `theme.ts`'s `areaEmbossValue`.
52
+ // Emboss mixes white into each area’s own fill; overlapping viewport passes apply the lift twice.
176
53
  areaEmboss: 8.2,
54
+ // Viewport navigation uses its own 28-px button size rather than the 20-px menu/header control height.
55
+ navigationControl: 28,
56
+ // Center the collapsed 4.5×9 and expanded 8.5×5 CSS-pixel carets in an 18-pixel cell.
57
+ treeCaret: 18,
58
+ // Outliner glyph plates use a 5-CSS-px corner radius.
59
+ treeGlyphPlateRadius: 5,
60
+ // Composite Outliner glyphs at 0.80 over the row fill.
61
+ treeGlyphOpacity: 0.8,
177
62
  },
178
63
  },
179
64
  stage: {
180
- // BLENDER'S GIZMO IS A CONSTANT SCREEN SIZE (owner, 2026-09-21: "is
181
- // gizmo perhaps the wrong size?"). `U.gizmo_size` is 75
182
- // (`DNA_userdef_types.h:1095`; Preferences > Viewport > Gizmos > Size,
183
- // `rna_userdef.cc:5415-5419`, range 10..200), and it is px PER GIZMO
184
- // UNIT: `wm_gizmo.cc:450-474` sets `scale_final = scale_basis *
185
- // UI_SCALE_FAC * U.gizmo_size * ED_view3d_pixel_size_no_ui_scale(...)`,
186
- // whose last term is world units per DEVICE px against a
187
- // `UI_SCALE_FAC` of device px per UI px — so one gizmo unit is 75 CSS
188
- // px whatever the display scale (read back from the engine's own pin
189
- // on this box: `ui_scale=2.0 pixel_size=2.0 gizmo_size=75`).
65
+ // Blender gizmo units stay 75 CSS px at every display scale, using world units per device pixel and UI_SCALE_FAC.
190
66
  gizmoSize: 75,
191
67
  // A resting handle at 0.6 opacity, a highlighted one opaque in its own colour
192
68
  // (`transform_gizmo_3d.cc`, `gizmo_get_axis_color`).
193
69
  gizmoOpacity: 0.6,
194
- // BLENDER'S OUTLINE IS A HARD LINE, and hidden parts are not drawn: four device pixels
195
- // of orange around the cube in `modeling-object-selected.png` (at 2x), no halo.
70
+ // Blender’s selection outline is a hard 2-CSS-pixel line without a halo; hidden parts are not drawn.
196
71
  outlineStyle: 'crisp',
197
72
  outlineWidth: 4,
198
73
  outlineHidden: false,
199
- // BLENDER'S FLOOR LINES, in device pixels, measured across one scanline of
200
- // `modeling-object-none.png` at device resolution: the 1 m line is 4 px at half rise and
201
- // plateaus at 83, the 10 m line is 6 px and plateaus at 101 — so the major line is wider
202
- // (2.25 against 1.5) and carried (102 − 63) / (84 − 63) past the minor's colour from the
203
- // background they share. Stated here, the look's, since the editor's own floor is a
204
- // single hairline level.
74
+ // The Blender floor uses distinct major/minor line widths and colour strengths over the shared background.
205
75
  gridLineWidth: 1.5,
206
76
  gridMajorWidth: 2.25,
207
77
  gridMajorContrast: 39 / 21,
@@ -215,5 +85,8 @@ export const style: StyleContribution = {
215
85
  },
216
86
  },
217
87
  icons,
218
- palette,
88
+ typography: {
89
+ sans: "'Inter', 'DejaVu Sans', system-ui, sans-serif",
90
+ mono: "'DejaVu Sans Mono', Menlo, monospace",
91
+ },
219
92
  };
@@ -0,0 +1,52 @@
1
+ /** Session Blender verbs relay to volter.<verb> commands registered by the page half. Each manifest entry owns its timeout and derived-refresh policy. Mutating commands present a frame and refresh the document; handlers use only SDK host doors. */
2
+ import type { VerbHandler } from '@volter/sdk/commands';
3
+ import { handleBlenderCommand } from '../host/blender-runtime-host';
4
+
5
+ /** The lane's one handler, for every verb. */
6
+ const handle: VerbHandler = (cmd) => handleBlenderCommand(cmd as { type: string; [key: string]: unknown });
7
+
8
+ export const blenderVerbs: Readonly<Record<string, VerbHandler>> = {
9
+ // Presentation-only comparison, no engine mutation or derived frame refresh.
10
+ 'blender-draw-batching': handle,
11
+ 'blender-start': handle,
12
+ // Stop drains accepted modeling work and persists it before teardown.
13
+ 'blender-stop': handle,
14
+ 'blender-execute': handle,
15
+ 'blender-native-preview': handle,
16
+ 'blender-scene-info': handle,
17
+ 'blender-object-info': handle,
18
+ 'blender-screenshot-view': handle,
19
+ 'blender-read-file': handle,
20
+ 'blender-write-file': handle,
21
+ // File listing uses the worker-read timeout while presentation and derived reads finish.
22
+ 'blender-list-files': handle,
23
+ // RNA reads expose properties and context without presenting; RNA writes mutate, present and derive.
24
+ 'blender-rna': handle,
25
+ 'blender-rna-context': handle,
26
+ 'blender-rna-set': handle,
27
+ // Outliner reads expose the View Layer tree; restriction writes mutate, present and derive.
28
+ 'blender-outliner': handle,
29
+ // The node-tree door is read-only and requires no derivation.
30
+ 'blender-node-tree': handle,
31
+ // The UV door reads one mesh layout; no UV writer is registered here.
32
+ 'blender-uv-layout': handle,
33
+ // THE RIG AND CLIP DOORS (the Timeline). Both READS — the skin binding a
34
+ // `THREE.SkinnedMesh` is built from and the action as three.js tracks — so
35
+ // neither presents and neither owes a derivation. There is deliberately no
36
+ // writer beside them: keying, moving a key and setting a range are edits,
37
+ // and the ONE write the Timeline makes (`scene.frame_current`, once on
38
+ // pause and at scrub-end) goes through `blender-rna-set`, which already
39
+ // exists and already presents.
40
+ 'blender-action-clip': handle,
41
+ // Node-view commands inspect and navigate view state. Editing gestures return the same named refusal as pointer handlers.
42
+ 'blender-node-view': handle,
43
+ 'blender-outliner-set': handle,
44
+ // A FILM, RENDERED (`cyclotron render-movie`, a film's Blender shots): started, answering at once, then followed
45
+ // until it ends. The frames are Blender's own renders; each chunk puts the scene's render settings and frame back, so
46
+ // none presents a change to the model.
47
+ 'blender-render-movie': handle,
48
+ 'blender-render-status': handle,
49
+ // A read of whether this tab HAS a session; it never starts one — and the
50
+ // one verb that answers before a project session exists.
51
+ 'blender-status': handle,
52
+ };
@@ -1,10 +1,4 @@
1
- /**
2
- * The MODEL workspace — Blender-shaped modeling: a dominant document, outliner
3
- * above properties (`@volter/sdk/looks`, a `workspace.layout`
4
- * contribution). Its arrangement carries Blender 5.2's own MODELING-workspace
5
- * proportions; `../src/layouts.tsx` states the measurement and the host's
6
- * fixed-pane clamp that bounds it.
7
- */
1
+ /** Model workspace: a dominant document, outliner above Properties and a lower Timeline area. The workbench owns pane geometry. */
8
2
 
9
3
  import type { WorkspaceLayoutContribution } from '@volter/sdk/looks';
10
4
  import { BLENDER_REGIONS } from '../src/regions';
@@ -26,32 +20,6 @@ export const layout: WorkspaceLayoutContribution = {
26
20
  inspector: 'properties',
27
21
  drawer: 'hidden',
28
22
  },
29
- // THE TIMELINE IS THE BOTTOM AREA (owner, 2026-09-20: "doesn't blender have
30
- // a timeline at the bottom?"). It does — and the workspace this Model
31
- // document stands in is Blender's LAYOUT screen for us, not its Modeling
32
- // one: Layout is the four-area screen with the Timeline, and Modeling is the
33
- // same screen WITHOUT it (measured below).
34
- //
35
- // THE PROPORTION IS BLENDER'S OWN, measured through bpy on the installed
36
- // Blender 5.2.1 at factory settings in a 1920-wide window
37
- // (`bpy.data.workspaces['Layout'].screens[0].areas`):
38
- //
39
- // PROPERTIES x1579 y23 w339 h839
40
- // OUTLINER x1579 y865 w339 h189
41
- // DOPESHEET_EDITOR x2 y23 w1574 h74 ui_type TIMELINE ← this area
42
- // VIEW_3D x2 y100 w1574 h954
43
- //
44
- // and the same screen at a 1600×929 window gives the Timeline 63 of an
45
- // 880-px centre column. Both are the same ratio to three places: 74/1029 =
46
- // 0.0719 and 63/880 = 0.0716. The Modeling screen's own reading, for the
47
- // contrast: VIEW_3D `x2 y23 w1574 h1031` and no DOPESHEET area at all.
48
- //
49
- // It is stated to four places because it is a measurement, not a preference;
50
- // the dock uses it once, to size the group the first time this workspace
51
- // stands up, and a capture of the settled grid wins afterwards.
52
- //
53
- // IN GAME MODE THE SAME AREA HOLDS THE GAME PANEL (`../src/play-mode.ts`):
54
- // the document below draws the Timeline or the panel by the model's mode,
55
- // so the area, its proportion and a person's resize all carry over.
23
+ // The Model workspace’s bottom editor area hosts the Timeline or Game panel. Initial Timeline ratio is Blender Layout’s 74/1029 ≈ 0.0719; a person’s later resize takes precedence.
56
24
  areas: [{ id: 'timeline', document: 'blender-timeline.document', place: 'below', ratio: 0.0719 }],
57
25
  };
@@ -1,33 +1,4 @@
1
- /**
2
- * The `modelsFromBlendFiles` FINDER (ARCHITECTURE-CORE §The project model,
3
- * "A model is Blender data, and a prefab is not a model"): every `.blend`
4
- * under the selection's `include` globs is a `model` document — a Blender
5
- * file the engine in the tab opens, edits and saves. The bpy scripts that
6
- * authored it are ordinary project files beside it (`src/models/<name>.py`);
7
- * this document does not run them, a person or an agent does, through the
8
- * session's Blender (`volter blender-mcp`).
9
- *
10
- * It replaced `modelsFromMeshModules`, which listed every module exporting
11
- * `build(): THREE.BufferGeometry` (owner ruling 2026-09-19, "do the delete":
12
- * the TypeScript mesh kit was the modeling engine while Blender could not run
13
- * in the tab; it now does, and a model is Blender data).
14
- *
15
- * Select it in `editor/volter.adapter.ts`:
16
- *
17
- * documents: { find: [{ finder: 'modelsFromBlendFiles', include: ['src/models/**\/*.blend'] }] }
18
- *
19
- * THE SESSION'S OWN MODEL. A project that holds no `.blend` of its own still
20
- * needs ONE model — it is what `blender-start` presents into and what a first
21
- * bpy call models in, saved to the session's default `models/model.blend` — so
22
- * when the selection finds no file, the finder lists the standing
23
- * `blender:runtime` entry instead. Never beside a real model.
24
- *
25
- * This module is a plain object the HOST registers; it mounts no UI. It reads
26
- * the project only through `input.files` — the PATHS matching those globs.
27
- * Deliberately not `input.sources`: a `.blend` is binary, there is nothing in
28
- * it a text walk could honestly read, and the entry is the file itself. It says
29
- * so (`reads: 'files'`), and the host then lists the files without fetching them.
30
- */
1
+
31
2
 
32
3
  import { z } from 'zod';
33
4
 
@@ -62,6 +33,8 @@ export const finder = {
62
33
  authorable: true,
63
34
  reach: { kind: 'root-mount' as const },
64
35
  source: { path },
36
+ // (the Model document opens a `.blend` not written yet as Blender's empty file, and its first save writes it)
37
+ opensUnwritten: true,
65
38
  });
66
39
  }
67
40
  if (entries.length === 0) {
@@ -1,19 +1,4 @@
1
- /**
2
- * BLENDER'S BONE CONSTRAINTS TAB, as a Properties section (WORK.md §Blender in
3
- * the tab is Blender, "Inspection parity", I2): the active POSE BONE's
4
- * constraint stack, with the generic view beneath under "All properties".
5
- *
6
- * It is the Object Constraints tab over a different datablock, and Blender
7
- * says so itself: `properties_constraint.py` registers
8
- * `BONE_PT_constraints` beside `OBJECT_PT_constraints` with the same
9
- * `template_constraints()` body, differing only in reading
10
- * `context.pose_bone.constraints`. So it shares the header list that module
11
- * exports rather than restating it — a second copy is a second thing to keep
12
- * in step.
13
- *
14
- * It stands when: `buttons_context_path_pose_bone` — a pose channel for the
15
- * active bone, and never in Edit mode.
16
- */
1
+ /** Blender’s Bone Constraints Properties tab follows scripts/startup/bl_ui/properties_constraint.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
17
2
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
18
3
  import type { BlenderCuratedPanel } from './blender-properties-view';
19
4
  import { CONSTRAINT_HEADER } from './properties-constraints.inspector';
@@ -1,26 +1,4 @@
1
- /**
2
- * BLENDER'S BONE TAB, as a Properties section (WORK.md §Blender in the tab is
3
- * Blender, "Inspection parity", I2). Blender's own panels over the RNA door,
4
- * with the generic view beneath under "All properties".
5
- *
6
- * THE SPECIFICATION IS `scripts/startup/bl_ui/properties_data_bone.py` at the
7
- * engine's pin: its `classes` tuple is the panel order, `bl_label` the title,
8
- * each `layout.prop(bone, "x")` an entry. Blender's UI layer is never run,
9
- * ported or recorded.
10
- *
11
- * TWO DATABLOCKS, AND THAT IS THE WHOLE STRUCTURE OF THIS TAB. Blender's panels
12
- * read `bone` (the armature's `Bone`, or the `EditBone` in Edit mode) for the
13
- * rest pose — head, tail, roll, envelope, collections, display — and `pchan`
14
- * (the `PoseBone`) for the POSE: its location/rotation/scale and its inverse
15
- * kinematics. `buttons_context_path_bone` and `_pose_bone` build both, and the
16
- * door hands this tab both as "Bone" and "Pose Bone", so each panel names the
17
- * one Blender's draw function reads. That is why Transform below is split in
18
- * two the way `BONE_PT_transform` (:39) is: its first half is `pchan`, its
19
- * second `bone`.
20
- *
21
- * It stands when: `buttons_context_path_bone` — an armature with an ACTIVE bone
22
- * (the edit bone in Edit mode).
23
- */
1
+ /** Blender’s Bone Properties tab follows scripts/startup/bl_ui/properties_data_bone.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
24
2
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
25
3
  import type { BlenderCuratedPanel } from './blender-properties-view';
26
4
 
@@ -44,14 +22,7 @@ const CURATED: readonly BlenderCuratedPanel[] = [
44
22
  ],
45
23
  },
46
24
  {
47
- // BONE_PT_transform, :39, rest half (:104-110) — the bone's own geometry.
48
- // NO MATRICES. The first cut named `matrix` and `matrix_local` beside them
49
- // and the live read showed why Blender does not: a 3x3 and a 4x4 draw as
50
- // twenty-five numbered rows, which buries the five values the panel is
51
- // about. They are still in "All properties" beneath, where a matrix is
52
- // something you go looking for. (`roll` is an EditBone property, so it
53
- // draws in Edit mode and not in Object mode — the datablock's answer, not
54
- // a condition of ours.)
25
+ // BONE_PT_transform rest geometry excludes matrices; those remain under All properties. roll exists only on EditBone and draws only when that datablock exposes it.
55
26
  title: 'Transform',
56
27
  from: 'Bone',
57
28
  properties: ['head', 'tail', 'roll', 'length', 'lock'],
@@ -1,37 +1,4 @@
1
- /**
2
- * BLENDER'S COLLECTION TAB, as a Properties section (WORK.md §Blender in the
3
- * tab is Blender, "Inspection parity", I2). Blender's own panels over the RNA
4
- * door, with the generic view beneath under "All properties".
5
- *
6
- * THE SPECIFICATION IS `scripts/startup/bl_ui/properties_collection.py` at the
7
- * engine's pin — `classes` (`:146-155`) is the panel order and `bl_label` the
8
- * title. Blender's UI layer is never run, ported or recorded.
9
- *
10
- * THE FIRST PANEL IS CALLED "VISIBILITY" AT THIS PIN, not "Restrictions":
11
- * `COLLECTION_PT_collection_flags.bl_label` is `"Visibility"` (`:32`). Its
12
- * child View Layer (`:47`) does not read the collection at all — it reads
13
- * `view_layer.active_layer_collection` (`:56`), a different datablock, which
14
- * the context door now names beside the collection as this tab's second path.
15
- *
16
- * TWO CHECKBOXES BLENDER DRAWS INVERTED. `hide_select` is labelled
17
- * "Selectable" with `invert_checkbox=True` (`:41`) and `exclude` is labelled
18
- * "Include" the same way (`:60`). What stands here is the RNA's own value
19
- * under the RNA's own name, because inverting a value in the presentation is
20
- * exactly the fabrication the anti-shim rule forbids — the same reading the
21
- * Object tab's Visibility panel already made.
22
- *
23
- * WHAT THE TRANSCRIPTION DELIBERATELY DROPS: `COLLECTION_PT_importer`
24
- * (`:65`), whose poll is a PREFERENCE (`prefs.experimental.use_collection_
25
- * importer`) and whose body is `template_collection_importer()` — an operator
26
- * template. Exporters (`:80`) is the same widget over `collection.exporters`,
27
- * and the collection itself is real RNA (`rna_collection.cc:954`), so its row
28
- * stands. `COLLECTION_PT_collection_custom_props` (`:143`) is IDProperties
29
- * rather than RNA.
30
- *
31
- * It stands when: `buttons_context_path_collection` — the view layer's ACTIVE
32
- * collection, and never the scene's master collection, which only the engine
33
- * knows.
34
- */
1
+ /** Blender’s Collection Properties tab follows scripts/startup/bl_ui/properties_collection.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
35
2
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
36
3
  import type { BlenderCuratedPanel } from './blender-properties-view';
37
4
 
@@ -1,24 +1,4 @@
1
- /**
2
- * BLENDER'S OBJECT CONSTRAINTS TAB, as a Properties section (WORK.md §Blender
3
- * in the tab is Blender, "Inspection parity", I2): the STACK, each constraint
4
- * its own panel with its own settings, the generic view beneath under "All
5
- * properties".
6
- *
7
- * THE SPECIFICATION IS `scripts/startup/bl_ui/properties_constraint.py`, and
8
- * it has the same shape `properties_data_modifier.py` does: `draw` is
9
- * `layout.template_constraints()` and the per-type panels are built in C from
10
- * each constraint's RNA. So the stack is derived the same way — the shared
11
- * `Constraint` header properties named, then everything THIS constraint's own
12
- * struct declares (`andDeclared`, which reads `BlenderRnaRow.group`). A Copy
13
- * Location constraint gets `CopyLocationConstraint`'s target, subtarget, use_x
14
- * / use_y / use_z, invert flags and offset, in RNA's order.
15
- *
16
- * The ACTIVE constraint (`buttons_context_path_constraint`, reported as
17
- * `activeConstraint`) is marked.
18
- *
19
- * It stands when: `buttons_context_path_object` — Constraints shares Object's
20
- * path, so the engine grants it whenever it grants Object.
21
- */
1
+ /** Blender’s Object Constraints Properties tab follows scripts/startup/bl_ui/properties_constraint.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
22
2
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
23
3
  import type { BlenderCuratedPanel } from './blender-properties-view';
24
4
 
@@ -1,36 +1,4 @@
1
- /**
2
- * BLENDER'S OBJECT DATA TAB, as a Properties section (WORK.md §Blender in the
3
- * tab is Blender, "Inspection parity", I2). Blender's own panels over the RNA
4
- * door, with the generic view of the whole datablock beneath under "All
5
- * properties".
6
- *
7
- * THE SPECIFICATION IS `scripts/startup/bl_ui/properties_data_mesh.py`, read
8
- * at the engine's pin: its `classes` tuple is the panel ORDER, each class's
9
- * `bl_label` the title, each `layout.prop(me, "x")` an entry in the order the
10
- * draw function lists it, `bl_options = {'DEFAULT_CLOSED'}` a `closed`, and a
11
- * `template_list` a `collection` panel. Blender's UI layer is never run,
12
- * ported or recorded.
13
- *
14
- * ONE TAB, MANY DATA TYPES, ONE LIST. `buttons_context_path_data` builds this
15
- * tab for whatever the object's data IS, so the same section draws a Mesh, an
16
- * Armature, a Camera or a Light — and the panels for all of them live in one
17
- * list here, because a panel draws only where its properties exist. A Camera's
18
- * data carries no `remesh_mode`, so the Remesh panel finds nothing and renders
19
- * nothing; an Armature's carries no `uv_layers` and every mesh panel stands
20
- * down for it. That is the same answer `MeshButtonsPanel.poll` /
21
- * `ArmatureButtonsPanel.poll` give, from the datablock rather than from a copy
22
- * of their conditions — which is why there is no per-type branch in this file
23
- * and no second module per data type.
24
- *
25
- * NO "NORMALS" PANEL: the brief expected one and 5.2 does not have it. The
26
- * mesh's `DATA_PT_normals` was folded away before this pin — `has_custom_normals`
27
- * survives as the Geometry Data panel's operator condition (`:456`), and the
28
- * shading-per-face data now lives on the Attributes list. Stated rather than
29
- * invented.
30
- *
31
- * It stands when: `buttons_context_path_data` — the object HAS data (an Empty
32
- * usually has none).
33
- */
1
+ /** Blender’s Object Data Properties tab follows scripts/startup/bl_ui/properties_data_mesh.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
34
2
  import type { ToolContributionNode } from '@volter/sdk/contributions';
35
3
  import { blenderPropertiesState, resolveBlenderSubject } from './blender-properties-model';
36
4
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
@@ -93,12 +61,7 @@ const CURATED: readonly BlenderCuratedPanel[] = [
93
61
  ],
94
62
  },
95
63
  {
96
- // DATA_PT_customdata, :435, titled "Geometry Data". Its body is FIVE
97
- // OPERATORS and one condition (`has_custom_normals`, :456) — no
98
- // properties at all. Inspection parity is not editing parity
99
- // (ARCHITECTURE-CORE §Blender north star), so what stands here is the
100
- // condition the operators branch on, which is the only thing in the panel
101
- // a reader can learn something from.
64
+ // Geometry Data exposes the condition and selection counts; its editing operators are not drawn here.
102
65
  title: 'Geometry Data',
103
66
  closed: true,
104
67
  properties: ['has_custom_normals', 'total_vert_sel', 'total_edge_sel', 'total_face_sel'],
@@ -1,34 +1,4 @@
1
- /**
2
- * BLENDER'S MATERIAL TAB, as a Properties section (WORK.md §Blender in the tab
3
- * is Blender, "Inspection parity", I2). Blender's own panels over the RNA
4
- * door, with the generic view beneath under "All properties".
5
- *
6
- * THE SPECIFICATION IS `scripts/startup/bl_ui/properties_material.py` at the
7
- * engine's pin — its `classes` tuple is the panel order, `bl_label` the
8
- * title, each `layout.prop(mat, "x")` an entry. Blender's UI layer is never
9
- * run, ported or recorded.
10
- *
11
- * THE SLOTS LIST IS FIRST, and it is `MATERIAL_UL_matslots` over
12
- * `ob.material_slots` (`:76`, `EEVEE_MATERIAL_PT_context_material`, whose
13
- * `bl_label` is empty because in Blender it is the tab's header block rather
14
- * than a panel). The context door hands it to this tab as its own path, with
15
- * the ACTIVE slot named separately (`buttons_context_path_material`:
16
- * `BKE_object_material_get(ob, ob->actcol)`), so the list marks it.
17
- *
18
- * SURFACE IS A NODE TREE, AND THIS IS NOT THE NODE VIEW. Blender's Surface
19
- * panel (`:164`) calls `panel_node_draw`, which walks the material output
20
- * node's `Surface` input back to the shader that feeds it and draws THAT
21
- * node's inputs by name. Showing those values as a flat list needs the node
22
- * tree walked, which is I5's read-only node view (§Inspection parity, I5) —
23
- * and a half version of it here would be a second, worse implementation of
24
- * the same walk. What stands in this panel instead is what the MATERIAL
25
- * itself holds: `use_nodes` and the `node_tree` pointer, which drills into
26
- * the generic view, plus the settings Blender's own EEVEE panels read off the
27
- * material datablock directly.
28
- *
29
- * It stands when: `buttons_context_path_material` — the object's data holds a
30
- * `materials` collection (the tab stands even when the active slot is empty).
31
- */
1
+ /** Blender’s Material Properties tab follows scripts/startup/bl_ui/properties_material.py at the engine pin. Curated panels read the RNA context’s datablocks; the generic view remains under All properties. Match tab availability and engine-specific panels against the live context. */
32
2
  import { blenderPropertiesTabMatch, blenderPropertiesTabSection } from './blender-properties-tab';
33
3
  import type { BlenderCuratedPanel } from './blender-properties-view';
34
4
 
@@ -44,8 +14,7 @@ const CURATED: readonly BlenderCuratedPanel[] = [
44
14
  properties: ['name', 'material', 'link', 'slot_index'],
45
15
  },
46
16
  {
47
- // EEVEE_MATERIAL_PT_surface, :164 — see the header for why this is the
48
- // material's own two node properties rather than the shader's inputs.
17
+ // EEVEE_MATERIAL_PT_surface, :164. Read the material’s node properties; shader inputs belong to its node tree.
49
18
  title: 'Surface',
50
19
  from: 'Material',
51
20
  properties: ['use_nodes', 'node_tree', 'surface_render_method', 'use_backface_culling'],