@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.
Files changed (58) hide show
  1. package/LICENSE +1409 -0
  2. package/README.md +17 -0
  3. package/contributions/blender-header-menus.tsx +483 -0
  4. package/contributions/blender-icon-trace.mjs +403 -0
  5. package/contributions/blender-icons.source.mjs +2925 -0
  6. package/contributions/blender-node-editor.document.tsx +1402 -0
  7. package/contributions/blender-node-geometry.ts +1138 -0
  8. package/contributions/blender-node-panels.source.mjs +485 -0
  9. package/contributions/blender-outliner-authoring.ts +1729 -0
  10. package/contributions/blender-outliner-model.ts +389 -0
  11. package/contributions/blender-palette.source.mjs +319 -0
  12. package/contributions/blender-properties-model.ts +351 -0
  13. package/contributions/blender-properties-tab.tsx +100 -0
  14. package/contributions/blender-properties-view.tsx +1191 -0
  15. package/contributions/blender-runtime-skin.ts +619 -0
  16. package/contributions/blender-runtime.document.tsx +232 -0
  17. package/contributions/blender-timeline-geometry.ts +323 -0
  18. package/contributions/blender-timeline.document.tsx +1056 -0
  19. package/contributions/blender-uv-editor.document.tsx +483 -0
  20. package/contributions/blender-uv-geometry.ts +305 -0
  21. package/contributions/blender-version.status.tsx +93 -0
  22. package/contributions/blender.command.ts +102 -0
  23. package/contributions/blender.icons.json +1247 -0
  24. package/contributions/blender.icons.traced.json +1561 -0
  25. package/contributions/blender.keymap.ts +39 -0
  26. package/contributions/blender.node-panels.json +2436 -0
  27. package/contributions/blender.palette.json +93 -0
  28. package/contributions/blender.status.tsx +263 -0
  29. package/contributions/blender.style.ts +271 -0
  30. package/contributions/model.layout.ts +53 -0
  31. package/contributions/models.finder.ts +59 -0
  32. package/contributions/properties-bone-constraints.inspector.tsx +50 -0
  33. package/contributions/properties-bone.inspector.tsx +184 -0
  34. package/contributions/properties-collection.inspector.tsx +96 -0
  35. package/contributions/properties-constraints.inspector.tsx +69 -0
  36. package/contributions/properties-data.inspector.tsx +229 -0
  37. package/contributions/properties-material.inspector.tsx +121 -0
  38. package/contributions/properties-modifiers.inspector.tsx +74 -0
  39. package/contributions/properties-object.inspector.tsx +215 -0
  40. package/contributions/properties-output.inspector.tsx +210 -0
  41. package/contributions/properties-particles.inspector.tsx +494 -0
  42. package/contributions/properties-physics.inspector.tsx +614 -0
  43. package/contributions/properties-render.inspector.tsx +446 -0
  44. package/contributions/properties-scene.inspector.tsx +174 -0
  45. package/contributions/properties-texture.inspector.tsx +300 -0
  46. package/contributions/properties-view-layer.inspector.tsx +145 -0
  47. package/contributions/properties-world.inspector.tsx +130 -0
  48. package/contributions/sculpt.layout.ts +25 -0
  49. package/contributions/shading.layout.ts +99 -0
  50. package/contributions/texture.layout.ts +16 -0
  51. package/contributions/uv-editing.layout.ts +93 -0
  52. package/host/blender-runtime-host.ts +1256 -0
  53. package/package.json +77 -0
  54. package/src/layouts.tsx +48 -0
  55. package/src/looks.ts +14 -0
  56. package/src/node-view-state.ts +125 -0
  57. package/src/timeline-view-state.ts +154 -0
  58. package/src/uv-view-state.ts +125 -0
@@ -0,0 +1,93 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "name": "Blender",
4
+ "theme": {
5
+ "id": "blender",
6
+ "color": {
7
+ "surface": {
8
+ "shell": "#161616",
9
+ "panel": "#303030",
10
+ "chrome": "#1c1c1c",
11
+ "raised": "#545454",
12
+ "inset": "#1d1d1d",
13
+ "overlay": "rgba(48,48,48,0.94)"
14
+ },
15
+ "boundary": {
16
+ "default": "#3d3d3d",
17
+ "strong": "#3d3d3d",
18
+ "area": "#161616",
19
+ "indent": "#656565",
20
+ "divider": "#2e2e2e"
21
+ },
22
+ "content": {
23
+ "primary": "#e6e6e6",
24
+ "muted": "#c3c3c3",
25
+ "dim": "#969696",
26
+ "onAccent": "#ffffff",
27
+ "menu": "#dddddd",
28
+ "status": "#888888",
29
+ "placeholder": "#5e5e5e",
30
+ "active": "#ffa028",
31
+ "selected": "#e96a00",
32
+ "onBright": {
33
+ "primary": "#1d1d1d",
34
+ "muted": "rgba(29,29,29,0.82)",
35
+ "dim": "rgba(29,29,29,0.7)"
36
+ }
37
+ },
38
+ "accent": {
39
+ "default": "#4772b3",
40
+ "muted": "#4772b3"
41
+ },
42
+ "widget": {
43
+ "regular": "#545454",
44
+ "menu": "#282828",
45
+ "field": "#1d1d1d",
46
+ "emboss": "rgba(0,0,0,0.149)"
47
+ },
48
+ "viewport": {
49
+ "background": "#3d3d3d",
50
+ "grid": "#545454",
51
+ "axisX": "#cb293f",
52
+ "axisY": "#69aa15",
53
+ "selection": "#ed5700",
54
+ "active": "#ffa028"
55
+ },
56
+ "region": {
57
+ "outliner": "#282828",
58
+ "properties": "#303030"
59
+ },
60
+ "category": {
61
+ "object": "#e19658",
62
+ "modifier": "#74a2ff",
63
+ "material": "#cc6670",
64
+ "data": "#00d4a3",
65
+ "scene": "#cccccc",
66
+ "collection": "#ffffff",
67
+ "tool": "#cbcbcb",
68
+ "operator": "#95dab2"
69
+ },
70
+ "semantic": {
71
+ "danger": "#e0524a",
72
+ "dangerMuted": "rgba(224,82,74,0.28)",
73
+ "dangerFaint": "rgba(224,82,74,0.12)",
74
+ "warning": "#e5b83c",
75
+ "warningMuted": "rgba(229,184,60,0.28)",
76
+ "success": "#6fbf5f",
77
+ "successMuted": "rgba(111,191,95,0.28)",
78
+ "dynamic": "#e5b83c",
79
+ "dynamicMuted": "rgba(229,184,60,0.18)",
80
+ "instance": "#ffa028"
81
+ },
82
+ "neutralOverlay": {
83
+ "hover": "rgba(255,255,255,0.08)",
84
+ "active": "rgba(255,255,255,0.14)"
85
+ },
86
+ "scrim": "rgba(0,0,0,0.6)"
87
+ },
88
+ "typography": {
89
+ "sans": "'Inter', 'DejaVu Sans', system-ui, sans-serif",
90
+ "mono": "'DejaVu Sans Mono', Menlo, monospace"
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Blender's MOUSE HINTS, at the left of the status bar (`workspace.status`).
3
+ *
4
+ * Blender's bottom row opens with three tiny mouse glyphs, each with one
5
+ * button lit and the action it performs beside it — `Select`, `Rotate View`,
6
+ * `Options` in the reference (`modeling.png`). It is the one piece of chrome
7
+ * that tells a newcomer what the three buttons DO without a menu, and the
8
+ * gap a human build session measured here was exactly that question ("how do
9
+ * I move the camera", six times — `viewport-controls-hint.ts`).
10
+ *
11
+ * THEY ARE HOVER-DRIVEN THERE, AND ALWAYS-ON HERE. Measured across the whole
12
+ * reference set: `modeling.png` is the ONE frame with any ink left of centre
13
+ * in the status band; `layout`, `modeling-edit-none`, `modeling-edit-all`,
14
+ * `modeling-object-selected`, `sculpting` and `texture-paint` all have ZERO
15
+ * ink before device x 3325 — Blender publishes the keymap for whatever the
16
+ * cursor is over, so a still with the pointer outside the 3D View shows
17
+ * nothing. A still therefore cannot say whether Blender's band is empty by
18
+ * design; it can only say what the hints look like WHEN they are drawn, which
19
+ * is what the geometry below is measured from. Ours are unconditional
20
+ * because the editor has no hover-scoped keymap publisher and inventing one
21
+ * to match a screenshot would be a mechanism with no other reader.
22
+ *
23
+ * THE ACTION NAMES ARE OURS, NOT BLENDER'S. What each button does is a fact
24
+ * about THIS editor, so the labels are read from this editor's own contract,
25
+ * never transcribed from the reference:
26
+ *
27
+ * - the KEYMAP (`keymap-presets.ts`, `@volter/editor-sdk/looks`) binds no
28
+ * mouse buttons and structurally cannot: a `KeyChord` is
29
+ * `{ key, code?, mod?, shift?, alt? }` (`editor-sdk/src/looks.ts:76-82`)
30
+ * with no button field, so no keymap — the Blender one included — can
31
+ * move a mouse binding. Nothing to read there.
32
+ * - so the source is the VIEWPORT's own mouse contract:
33
+ * `editor-viewport.ts:682-686` sets `orbitControls.mouseButtons` to
34
+ * `{ LEFT: -1, MIDDLE: PAN, RIGHT: ROTATE }`, and the left button is left
35
+ * free for the selection raycast (`editor-viewport.ts:3782`,
36
+ * `if (e.button === 0 && !e.altKey)`). The editor's own prose says the
37
+ * same thing (`ViewportControlsHint.tsx:22`: "Orbit: right-drag · Pan:
38
+ * middle-drag").
39
+ *
40
+ * That contract is a constant of the host, not of any document, so the table
41
+ * below is a constant too — with the citations above as its provenance. When
42
+ * a surface ever makes a button mean something else, it publishes that and
43
+ * this reads it; it does not get hardcoded here.
44
+ *
45
+ * The inline styles spell the theme tokens directly (`var(--vgai-space-5)`,
46
+ * `var(--vgai-font-md)`) instead of importing the host's `spaceVar` /
47
+ * `fontSizeVar` handles: `@volter/editor-blender` is pinned at ZERO host imports
48
+ * (`validate-editor-closure.mjs`). See `blender-version.status.tsx`.
49
+ */
50
+
51
+ export const point = 'workspace.status';
52
+ export const title = 'Mouse hints';
53
+ export const align = 'left';
54
+ /** Front of the contributed band — see the note at the bottom of this file
55
+ * for why that is not the front of the bar. */
56
+ export const order = 0;
57
+
58
+ /** Which button the glyph lights. */
59
+ type MouseButton = 'left' | 'middle' | 'right';
60
+
61
+ /**
62
+ * The viewport's three buttons, in Blender's order. Sourced from
63
+ * `editor-viewport.ts:682-686` + `:3782` (see the header) — the editor's
64
+ * contract, which is why the middle/right labels are Pan/Orbit and not
65
+ * Blender's own Rotate View/Options.
66
+ */
67
+ const HINTS: readonly { readonly button: MouseButton; readonly label: string }[] = [
68
+ { button: 'left', label: 'Select' },
69
+ { button: 'middle', label: 'Pan View' },
70
+ { button: 'right', label: 'Orbit View' },
71
+ ];
72
+
73
+ /**
74
+ * One mouse glyph in Blender's idiom, TRANSCRIBED FROM THE FRAME rather than
75
+ * estimated: `modeling.png` at native 2x (the only reference frame whose
76
+ * status bar carries the hints — see the note under {@link HINTS}), glyph 1 at
77
+ * device x 12..35, y 2065..2096. Halved, that is a 12 x 16 CSS ink box, and
78
+ * the viewBox IS that box so the SVG needs no scale factor to reason about:
79
+ * one unit is one CSS pixel at the size below.
80
+ *
81
+ * MEASURED BY COVERAGE, NOT BY A THRESHOLD. Every number below is the 50%
82
+ * contour of `(value - 23) / (135 - 23)` — an ink threshold reads a glyph's
83
+ * antialiasing as geometry and got two of these badly wrong on the first cut.
84
+ * Device reading -> CSS (the ink box's left edge is device -0.5, so
85
+ * `css = (device + 0.5) / 2`):
86
+ *
87
+ * shell stroke 1.98 device, flat on every side row -> {@link STROKE} 1
88
+ * button row height 16 of 32 device -> {@link BUTTON_H} 8, exactly half
89
+ * lit button width left fill ends 9.5 / right fill starts 13.5 device
90
+ * -> {@link BUTTON_W} 5, a button at each end
91
+ * divider the shell's top edge stops at 11.98 device in the
92
+ * left-lit glyph and starts there in the right-lit one
93
+ * -> {@link DIVIDER_X} 6.25, ONE boundary, not a gap
94
+ * centred on the middle
95
+ * gap under button 2 device of nothing (fill ends row 15, stroke resumes
96
+ * row 18) -> {@link UNDER_GAP} 1
97
+ * wheel 8 x 14 device at x 8, y 4, ends rounded at r 2.5
98
+ * -> 4 x 7 at 4, 2, rx 1.25
99
+ *
100
+ * THE LIT BUTTON IS NOT AN INSET NUB AND NOT A CLIPPED OVERLAY: in the frame
101
+ * the fill REPLACES the shell over its own quadrant (a mid row reads ten solid
102
+ * device px from the very left edge) and the shell stroke is absent for two
103
+ * device rows beneath it and across the divider on the top edge. So the fill
104
+ * is the outer silhouette intersected with the button rect, and the stroke is
105
+ * MASKED OUT over the button's side of the divider down to the hairline —
106
+ * which is why there is a mask here and not the clip the earlier cut used.
107
+ *
108
+ * (Superseded readings, kept so the next round does not re-derive them: a
109
+ * 13x17 box with a 1.7 stroke and a 0.45 hairline, sized to the text's own
110
+ * font size. Both came from a DPR-1 capture against a downscaled reference,
111
+ * which is the instrument WORK.md records as unable to settle stroke weights;
112
+ * at native 2x the glyph measures 16 CSS tall against 9 CSS of cap height,
113
+ * not the ~1:1 that reading reported.)
114
+ *
115
+ * RESIDUE, measured and not fitted away: Blender's TOP edge integrates 3.0
116
+ * device px of coverage where its sides integrate 1.98. One uniform stroke
117
+ * cannot draw both, and a second stroke for the top would be a constant with
118
+ * no reason behind it; the sides are the ones that set the glyph's weight.
119
+ */
120
+ const STROKE = 1;
121
+ const BUTTON_H = 8;
122
+ const BUTTON_W = 5;
123
+ const UNDER_GAP = 1;
124
+ /**
125
+ * Where the shell's top edge stops for a lit LEFT button and starts again for
126
+ * a lit RIGHT one — ONE boundary, at device 11.98 in both glyphs, not a gap
127
+ * straddling the centre. The button fills stop short of it by 1.25 on the left
128
+ * and start 0.75 past it on the right, which is why modelling this as a
129
+ * symmetric "gap" beside a button could not reproduce either glyph.
130
+ */
131
+ const DIVIDER_X = 6.25;
132
+ /**
133
+ * The shell's OUTER corner radius, and the number two cuts got wrong — first
134
+ * as a capsule (half the width), then as 2 from an ink threshold. SOLVED on
135
+ * the 50% contour of the middle glyph's bottom-left and top-right corners
136
+ * (`modeling.png`, device x 122..145, y 2065..2096): the contour runs 4.04,
137
+ * 2.39, 1.33, 0.55, 0.05, -0.28 device from the straight edge over successive
138
+ * rows, and a circle of radius 7.16 device reproduces all six to within 0.11.
139
+ * The opposite corner independently gives 7.2. Halved, 3.6 — and both wrong
140
+ * answers were visible at full scale as the wrong SHAPE, not the wrong size.
141
+ */
142
+ const RADIUS = 3.6;
143
+ /** The ink box, in CSS px, and the viewBox. */
144
+ const BOX = { width: 12, height: 16 } as const;
145
+
146
+ function MouseGlyph({ button }: { button: MouseButton }) {
147
+ const id = `vgai-mouse-${button}`;
148
+ const rightX = BOX.width - BUTTON_W;
149
+ const fillX = button === 'right' ? rightX : 0;
150
+ // The stroke's own rect runs along the centre line of a 1-unit stroke, so it
151
+ // is inset half a stroke and its radius drops by the same half.
152
+ const half = STROKE / 2;
153
+ const outerRx = RADIUS;
154
+ return (
155
+ <svg
156
+ viewBox={`0 0 ${BOX.width} ${BOX.height}`}
157
+ role="presentation"
158
+ aria-hidden="true"
159
+ focusable="false"
160
+ // 16 CSS px, which is the `lg` icon rung — and Blender's glyph measures
161
+ // exactly 16 CSS tall in the frame. `width: auto` then resolves to 12
162
+ // from the viewBox, the measured width, with no second number to drift.
163
+ style={{ height: 'var(--vgai-icon-lg)', width: 'auto', display: 'block', flex: '0 0 auto' }}
164
+ >
165
+ <title>{button} mouse button</title>
166
+ {/* The outer silhouette: what the fill is intersected with, so the lit
167
+ quadrant follows the shell's rounded shoulder. */}
168
+ <clipPath id={`${id}-outer`}>
169
+ <rect x="0" y="0" width={BOX.width} height={BOX.height} rx={outerRx} />
170
+ </clipPath>
171
+ {/* Where the shell stroke is NOT drawn: the lit button's whole side of
172
+ the divider, down to the hairline beneath the button. */}
173
+ <mask id={`${id}-shell`}>
174
+ <rect x="0" y="0" width={BOX.width} height={BOX.height} fill="white" />
175
+ {button !== 'middle' && (
176
+ <rect
177
+ x={button === 'right' ? DIVIDER_X : -1}
178
+ y={-1}
179
+ width={button === 'right' ? BOX.width - DIVIDER_X + 1 : DIVIDER_X + 1}
180
+ height={BUTTON_H + UNDER_GAP + 1}
181
+ fill="black"
182
+ />
183
+ )}
184
+ </mask>
185
+ <rect
186
+ x={half}
187
+ y={half}
188
+ width={BOX.width - STROKE}
189
+ height={BOX.height - STROKE}
190
+ rx={outerRx - half}
191
+ fill="none"
192
+ stroke="currentColor"
193
+ strokeWidth={STROKE}
194
+ mask={`url(#${id}-shell)`}
195
+ />
196
+ {button !== 'middle' && (
197
+ <rect
198
+ x={fillX}
199
+ y="0"
200
+ width={BUTTON_W}
201
+ height={BUTTON_H}
202
+ fill="currentColor"
203
+ clipPath={`url(#${id}-outer)`}
204
+ />
205
+ )}
206
+ {button === 'middle' && (
207
+ <rect x="4" y="2" width="4" height="7" rx="1.25" fill="currentColor" />
208
+ )}
209
+ </svg>
210
+ );
211
+ }
212
+
213
+ export default function BlenderMouseHintsStatus() {
214
+ return (
215
+ <span
216
+ className="vgai-status-copy"
217
+ data-testid="status-blender-mouse-hints"
218
+ // Blender's hint-to-hint gap is 9.5 CSS (glyph 1 ends at 51.5, glyph 2
219
+ // starts at 61; the second gap repeats it to a tenth). `space-5` is 10.
220
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--vgai-space-5)' }}
221
+ >
222
+ {HINTS.map((hint) => (
223
+ <span
224
+ key={hint.button}
225
+ data-mouse-button={hint.button}
226
+ // NO COLOUR HERE. The band declares its own ink and Blender's status
227
+ // bar is the DIMMEST text in its window — `#878787` (135) flat, on
228
+ // every frame, against the 216 its top-bar menus ink. A contribution
229
+ // that pins `content.muted` (194) makes itself the loudest thing in
230
+ // the quietest band; inheriting is what keeps the band one voice.
231
+ //
232
+ // The SIZE is measured: Blender's status text caps at 9.0 CSS (18
233
+ // device), which is the `md`/`base` rung (11px -> 8.8), not `sm`
234
+ // (10px -> 8.0).
235
+ style={{
236
+ display: 'inline-flex',
237
+ alignItems: 'center',
238
+ gap: 'var(--vgai-space-1)',
239
+ fontSize: 'var(--vgai-font-md)',
240
+ }}
241
+ title={`${hint.label} — the ${hint.button} mouse button in the viewport`}
242
+ >
243
+ <MouseGlyph button={hint.button} />
244
+ {hint.label}
245
+ </span>
246
+ ))}
247
+ </span>
248
+ );
249
+ }
250
+
251
+ /**
252
+ * ORDER, and the one thing this cannot have. Blender's hints are the
253
+ * LEFTMOST thing in its bottom row. A contributed status item cannot be:
254
+ * `tool-loader.ts:1145` registers it at `contributedSectionOrder(order)`,
255
+ * which is `CONTRIBUTED_SECTION_ORDER + (order ?? 999)`
256
+ * (`tool-loader.ts:359-361`) with `CONTRIBUTED_SECTION_ORDER = 5000`
257
+ * (`inspection/model.ts:475`), while the host's own left items register at
258
+ * -90..40 (`components/status-contributions.tsx`). Every contributed item
259
+ * therefore lands BEHIND every core item, by construction — the band exists
260
+ * precisely so a contribution cannot outrank a built-in. `order = 0` is the
261
+ * front of what the contract allows: first among contributed left items,
262
+ * after the host's save/console/play cluster.
263
+ */
@@ -0,0 +1,271 @@
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/editor-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 (`/Volumes/PeakSSD/volter-work/blender-reference`):
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
+ */
40
+ import type { IconSetContribution, StyleContribution } from '@volter/editor-sdk/looks';
41
+ import iconsJson from './blender.icons.json';
42
+ import palette from './blender.palette.json';
43
+
44
+ /**
45
+ * `resolveJsonModule` widens every string in a JSON import, so each glyph's
46
+ * `tone` arrives typed `string` rather than the `IconCategoryTone` union it
47
+ * holds. The generator is what narrows it: `blender-icons.source.mjs` checks
48
+ * every assignment against the union's own member list and throws on a name
49
+ * outside it, so the JSON cannot carry a tone this type does not have.
50
+ */
51
+ const icons = iconsJson as IconSetContribution;
52
+
53
+ export const point = 'workspace.style';
54
+ export const style: StyleContribution = {
55
+ id: 'blender',
56
+ title: 'Blender',
57
+ paletteId: 'blender',
58
+ materialId: 'blender',
59
+ material: {
60
+ id: 'blender',
61
+ title: 'Blender',
62
+ description: 'Flat opaque areas, four-pixel widget corners, shadows under menus only.',
63
+ shape: { small: '4px', medium: '4px', large: '4px', full: '9999px' },
64
+ elevation: {
65
+ small: 'none',
66
+ medium: '0 4px 12px rgba(0,0,0,0.5)',
67
+ large: '0 6px 18px rgba(0,0,0,0.55)',
68
+ },
69
+ density: {
70
+ control: { compact: 20, default: 20, comfortable: 20 },
71
+ 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.
124
+ icon: { xs: 10, sm: 14, md: 14, lg: 16, xl: 16, '2xl': 18 },
125
+ chrome: {
126
+ commandBar: 26,
127
+ panelHeader: 26,
128
+ localToolbar: 26,
129
+ 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.
137
+ treeIndent: 20,
138
+ 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.
156
+ toolSize: 36,
157
+ toolWidth: 40,
158
+ 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`.
165
+ 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`.
176
+ areaEmboss: 8.2,
177
+ },
178
+ viewport: {
179
+ // BLENDER'S GIZMO IS A CONSTANT SCREEN SIZE (owner, 2026-09-21: "is
180
+ // gizmo perhaps the wrong size?"). `U.gizmo_size` is 75
181
+ // (`DNA_userdef_types.h:1095`; Preferences > Viewport > Gizmos > Size,
182
+ // `rna_userdef.cc:5415-5419`, range 10..200), and it is px PER GIZMO
183
+ // UNIT: `wm_gizmo.cc:450-474` sets `scale_final = scale_basis *
184
+ // UI_SCALE_FAC * U.gizmo_size * ED_view3d_pixel_size_no_ui_scale(...)`,
185
+ // whose last term is world units per DEVICE px against a
186
+ // `UI_SCALE_FAC` of device px per UI px — so one gizmo unit is 75 CSS
187
+ // px whatever the display scale (read back from the engine's own pin
188
+ // on this box: `ui_scale=2.0 pixel_size=2.0 gizmo_size=75`).
189
+ gizmoSize: 75,
190
+ // BLENDER'S SHELF OPENS ON SELECT BOX, so a selected object carries no
191
+ // transform gizmo until one of the four transform tools is armed
192
+ // (`space_toolsystem_toolbar.py`: `_defs_view3d_generic.select_box` is
193
+ // the first entry of the Object Mode `_tools_default` tuple).
194
+ // Photographed at the engine's pin rather than remembered:
195
+ // `gizmo-select-box.png` has Select Box lit in the shelf, the default
196
+ // cube selected and outlined orange, and nothing at its origin but the
197
+ // 3D cursor and the object dot; `gizmo-move.png` has Move lit in the
198
+ // same frame. Ours opened on the COMBINED gizmo — Unity's shape, three
199
+ // tools' handles at once, on something merely selected.
200
+ shelfTool: 'select',
201
+ // BLENDER'S WORLD IS Z-UP, and the stage presents it through the exact
202
+ // signed permutation `(x, y, z) → (x, z, −y)` the presented root
203
+ // carries (`blender-runtime-view.ts`'s constructor). So three's Y is
204
+ // Blender's Z and three's Z is Blender's −Y, and the transform gizmo's
205
+ // colours, the side each arm is drawn on, the plane squares and the
206
+ // navigation gizmo's six labels all say so: the arm that points up is
207
+ // Z and it is blue, Y is green and points away.
208
+ upAxis: 'z',
209
+ // BLENDER'S SELECT BOX IS A TOUCH TEST — its object-mode box select
210
+ // reads the object-id buffer under the rectangle, so any drawn part of
211
+ // an object inside it selects that object. The editor's own answer is
212
+ // containment, for its own measured reason; see the field.
213
+ boxSelect: 'touch',
214
+ },
215
+ },
216
+ },
217
+ icons,
218
+ regions: {
219
+ workspaceTabs: 'shown',
220
+ // Blender has no document tab strip over its areas (the host's own note
221
+ // in `workspace-presets.ts` says so; the bundle contradicted it).
222
+ header: 'shown',
223
+ shelf: 'shown',
224
+ inspector: 'properties',
225
+ // Measured on `modeling.png`: Blender's top-right corner carries two quiet
226
+ // wells (Scene, ViewLayer) and nothing brighter than the 216 its labels
227
+ // ink at. Ours drew a yellow FPS/ms readout, an orange sparkline and a lit
228
+ // audio meter there — the brightest, highest-chroma thing in the frame and
229
+ // the first place the eye lands. The numbers keep their door (the Profiler
230
+ // utility, which the readout's own tooltip names).
231
+ telemetry: 'hidden',
232
+ // Measured on `outliner.png`: Scene Collection, Collection, Camera, Cube
233
+ // and Light each carry their NAME and nothing after it — no type text
234
+ // anywhere in the frame. The type is the GLYPH, which is why the glyph is
235
+ // coloured by category in the first place; a row that also spelled the
236
+ // type would say it twice. Ours printed `Hero Box ·HeroBox` at
237
+ // `content.dim` in a 10px face, one `space-2` past the name.
238
+ hierarchyTypeSuffix: 'hidden',
239
+ // Measured across all three Outliner frames — `outliner.png` (edit mode),
240
+ // `modeling-object-none.png` (object mode, nothing selected) and
241
+ // `modeling-object-selected.png` (object mode, everything selected):
242
+ // Blender draws NO rule, dotted or solid, under any name in that panel, in
243
+ // any selection state. Ours drew an orange dotted rule under every
244
+ // component-instance name — six of this scaffold's eleven rows — which is a
245
+ // mark with no counterpart in the frame at all. The instance fact keeps its
246
+ // doors (the label's tooltip, Go to Callsite / Open Component Source, the
247
+ // inspector's component sections); only the paint yields.
248
+ hierarchyInstanceRule: 'hidden',
249
+ // Measured on `outliner.png`: every object row carries an EYE (CSS ink
250
+ // 257.5-270.5, 32 px in from the area's right edge) and a render CAMERA
251
+ // (277.5-290.5, 12 px in) on a 20 px column pitch, and no lock glyph
252
+ // anywhere — Blender's Disable Selection column is off by default. Ours
253
+ // drew a lock 29-41 px in and the eye 6-18 px in, so the LOCK was sitting
254
+ // in Blender's eye column and the eye outboard of its camera column. The
255
+ // lock goes, and the render column is reserved blank so the eye lands on
256
+ // Blender's own; `workspace-regions.ts` carries why it stays blank.
257
+ hierarchyRestrictions: 'viewport+render',
258
+ // Measured at native 2x on `outliner.png` AND on the Properties area of
259
+ // `modeling-edit-none.png` — device x relative to each area's own left
260
+ // edge, and the two agree to half a pixel: every Blender area begins with
261
+ // an EDITOR-TYPE WELL at x 17..80, i.e. 8.5 CSS in and 32 CSS wide, 20
262
+ // tall, its glyph and a chevron inside a 1 px `#3c3c3c` outline over a
263
+ // `#272727` inner. It is there whether the area holds one editor or a
264
+ // dozen, and its dropdown is how an area changes editor. Ours began with
265
+ // a 203 px strip of WORDS, which is what squeezed the Outliner's search
266
+ // to 64 px against Blender's 115, truncated the third tab to `Libr`, and
267
+ // left the Properties area — a lone panel declaring no header controls —
268
+ // with no header band at all.
269
+ },
270
+ palette,
271
+ };