@volter/editor-blender 0.1.150 → 0.1.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +18 -9
  2. package/README.md +5 -5
  3. package/contributions/blender-animation-editors.tsx +17 -69
  4. package/contributions/blender-display-settings.ts +36 -0
  5. package/contributions/blender-export-gltf.ts +228 -0
  6. package/contributions/blender-export.verbs.ts +39 -0
  7. package/contributions/blender-gltf-export.verbs.ts +312 -0
  8. package/contributions/blender-header-menus.tsx +37 -133
  9. package/contributions/blender-icon-trace.mjs +8 -98
  10. package/contributions/blender-icons.source.mjs +96 -860
  11. package/contributions/blender-mixer-pose.ts +369 -0
  12. package/contributions/blender-model-opening.tsx +3 -7
  13. package/contributions/blender-node-editor.document.tsx +28 -180
  14. package/contributions/blender-node-geometry.ts +44 -230
  15. package/contributions/blender-node-panels.source.mjs +7 -121
  16. package/contributions/blender-outliner-authoring.ts +61 -415
  17. package/contributions/blender-outliner-model.ts +15 -112
  18. package/contributions/blender-play-area.tsx +100 -0
  19. package/contributions/blender-playback-movie.ts +409 -0
  20. package/contributions/blender-pose.ts +18 -368
  21. package/contributions/blender-properties-context.ts +1 -1
  22. package/contributions/blender-properties-model.ts +15 -90
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +15 -147
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +217 -339
  28. package/contributions/blender-scene-mixer.ts +51 -0
  29. package/contributions/blender-screenshot.service.ts +28 -0
  30. package/contributions/blender-timeline-geometry.ts +16 -136
  31. package/contributions/blender-timeline.document.tsx +142 -332
  32. package/contributions/blender-uv-editor.document.tsx +48 -112
  33. package/contributions/blender-uv-geometry.ts +14 -179
  34. package/contributions/blender-version.status.tsx +43 -23
  35. package/contributions/blender.environment.ts +2 -2
  36. package/contributions/blender.icons.json +0 -9
  37. package/contributions/blender.keymap.ts +9 -51
  38. package/contributions/blender.node-panels.json +4 -13
  39. package/contributions/blender.page.ts +63 -0
  40. package/contributions/blender.status.tsx +9 -145
  41. package/contributions/blender.style.ts +25 -152
  42. package/contributions/blender.verbs.ts +52 -0
  43. package/contributions/model.layout.ts +2 -34
  44. package/contributions/models.finder.ts +12 -32
  45. package/contributions/properties-bone-constraints.inspector.tsx +1 -16
  46. package/contributions/properties-bone.inspector.tsx +2 -31
  47. package/contributions/properties-collection.inspector.tsx +1 -34
  48. package/contributions/properties-constraints.inspector.tsx +1 -21
  49. package/contributions/properties-data.inspector.tsx +2 -39
  50. package/contributions/properties-material.inspector.tsx +2 -33
  51. package/contributions/properties-modifiers.inspector.tsx +2 -30
  52. package/contributions/properties-object.inspector.tsx +2 -44
  53. package/contributions/properties-output.inspector.tsx +1 -27
  54. package/contributions/properties-particles.inspector.tsx +2 -46
  55. package/contributions/properties-physics.inspector.tsx +2 -41
  56. package/contributions/properties-render.inspector.tsx +1 -45
  57. package/contributions/properties-scene.inspector.tsx +1 -35
  58. package/contributions/properties-texture.inspector.tsx +1 -49
  59. package/contributions/properties-view-layer.inspector.tsx +1 -29
  60. package/contributions/properties-world.inspector.tsx +2 -39
  61. package/contributions/shading.layout.ts +3 -78
  62. package/contributions/uv-editing.layout.ts +3 -70
  63. package/dist-node/serving.mjs +13 -30
  64. package/host/blender-runtime-host.ts +541 -372
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +49 -93
  67. package/package.json +1209 -46
  68. package/producticons/blender-icons.otf +0 -0
  69. package/producticons/blender-product-icon-theme.json +129 -0
  70. package/serving/blender-routes.ts +9 -43
  71. package/serving/blender-wasm-artifact.ts +29 -166
  72. package/serving/index.ts +1 -1
  73. package/src/api.ts +9 -0
  74. package/src/layouts.tsx +1 -17
  75. package/src/looks.ts +1 -12
  76. package/src/model-document-saving.ts +25 -0
  77. package/src/model-play-extension.ts +173 -0
  78. package/src/node-view-state.ts +3 -25
  79. package/src/play-mode.ts +64 -38
  80. package/src/presentation.ts +6 -18
  81. package/src/regions.ts +3 -43
  82. package/src/start-document.ts +15 -0
  83. package/src/timeline-view-state.ts +4 -39
  84. package/src/uv-view-state.ts +4 -20
  85. package/themes/blender.json +299 -0
  86. package/view/blender-view-routes.ts +1 -1
  87. package/contributions/blender-game-panel.tsx +0 -809
  88. package/contributions/blender-palette.source.mjs +0 -350
  89. package/contributions/blender-play-skin.ts +0 -508
  90. package/contributions/blender-screenshot.menu.ts +0 -32
  91. package/contributions/blender.command.ts +0 -109
  92. package/contributions/blender.palette.json +0 -102
  93. package/src/play-live.ts +0 -27
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * The BLENDER keymap (`@volter/sdk/looks`, a `workspace.keymap`
3
- * contribution). Only the chords that differ from the editor's own are
4
- * declared; the host fills the rest from its default table.
5
- *
6
- * Every chord below is Blender's own, cited to its entry in
7
- * `scripts/presets/keyconfig/keymap_data/blender_default.py` at the engine's
8
- * pin (v5.2.0) — the file that IS the "Blender" keyconfig.
3
+ * contribution). Its chords are this package's `contributes.keybindings`,
4
+ * gated on `volter.keymap == 'blender'`: only where they differ from the
5
+ * editor's own, each Blender's in `scripts/presets/keyconfig/keymap_data/
6
+ * blender_default.py` at the engine's pin (v5.2.0), the file that IS the
7
+ * "Blender" keyconfig. A Volter chord Blender leaves free is bound there to
8
+ * `volter.unboundKey`, so it does nothing here as it does in Blender. W arms
9
+ * `transform.select`, the tool that draws no transform gizmo: Blender's Select
10
+ * Box, which the 3D View keymap puts on W (`blender_default.py:1950-1955`,
11
+ * `op_tool_cycle("builtin.select_box", …)`).
9
12
  */
10
13
  import type { KeymapContribution } from '@volter/sdk/looks';
11
14
 
@@ -15,51 +18,6 @@ export const keymap: KeymapContribution = {
15
18
  title: 'Blender',
16
19
  description:
17
20
  'Blender bindings: W select, G/R/S gizmo modes, X delete, Shift+D duplicate, A select all, Alt+A deselect, . frames.',
18
- bindings: {
19
- // `km_view3d`, `:1950-1955` — `op_tool_cycle("builtin.select_box", {"type": 'W'})`,
20
- // bound whenever select is on the left mouse button, which is the default.
21
- 'transform.select': [{ key: 'w' }],
22
- 'transform.translate': [{ key: 'g' }],
23
- 'transform.rotate': [{ key: 'r' }],
24
- 'transform.scale': [{ key: 's' }],
25
- // `km_object_mode`, `:4552` — `object.delete` with `use_global: False`.
26
- // Blender's X raises a confirmation popup and ours does not; the deletion
27
- // itself is `bpy.ops.object.delete()` either way.
28
- 'edit.delete': [{ key: 'x' }],
29
- // `km_object_mode`, `:4563` — `object.duplicate_move` on Shift+D. Blender
30
- // then runs a modal move on the copy; ours leaves it on the original's
31
- // pose for the gizmo to move, which is what the Outliner and the `.blend`
32
- // both read back.
33
- 'edit.duplicate': [{ key: 'd', shift: true }],
34
- 'edit.selectAll': [{ key: 'a' }, { key: 'a', mod: true }],
35
- 'edit.deselectAll': [{ key: 'a', alt: true }],
36
- 'viewport.frameSelection': [{ key: '.' }, { key: '', code: 'NumpadDecimal' }],
37
- 'viewport.cyclePivot': [{ key: ',' }],
38
- // Blender's G is Grab (`transform.translate` above); a 2D view's modes keep their buttons.
39
- 'canvas.panMode': [],
40
- 'canvas.rulerMode': [],
41
- 'canvas.toggleGrid': [],
42
- // `km_view3d` — `view3d.view_persportho` on NUMPAD_5; Blender has no isometric preset key.
43
- 'view.perspective': [],
44
- 'view.projection': [{ key: '', code: 'Numpad5' }],
45
- // `km_view3d` — `view3d.view_axis` with Ctrl for the opposite side, `view3d.view_orbit` on
46
- // NUMPAD_2/4/6/8 (15°, `pad_rot_angle`) and NUMPAD_9 (π), `view3d.view_roll` on Shift+4/6.
47
- // Blender's Ctrl is the Control key on every platform, macOS included (`ctrl`, not `mod`).
48
- 'view.back': [{ key: '', code: 'Numpad1', ctrl: true }],
49
- 'view.left': [{ key: '', code: 'Numpad3', ctrl: true }],
50
- 'view.bottom': [{ key: '', code: 'Numpad7', ctrl: true }],
51
- 'view.orbitLeft': [{ key: '', code: 'Numpad4' }],
52
- 'view.orbitRight': [{ key: '', code: 'Numpad6' }],
53
- 'view.orbitUp': [{ key: '', code: 'Numpad8' }],
54
- 'view.orbitDown': [{ key: '', code: 'Numpad2' }],
55
- 'view.opposite': [{ key: '', code: 'Numpad9' }],
56
- 'view.rollLeft': [{ key: '', code: 'Numpad4', shift: true }],
57
- 'view.rollRight': [{ key: '', code: 'Numpad6', shift: true }],
58
- // `view3d.view_all` on HOME; `view3d.zoom` on NUMPAD_PLUS/NUMPAD_MINUS and Ctrl+=/Ctrl+-.
59
- 'view.all': [{ key: '', code: 'Home' }],
60
- 'view.zoomIn': [{ key: '', code: 'NumpadAdd' }, { key: '', code: 'Equal', ctrl: true }],
61
- 'view.zoomOut': [{ key: '', code: 'NumpadSubtract' }, { key: '', code: 'Minus', ctrl: true }],
62
- },
63
21
  // `km_view3d` — `view3d.rotate` on MIDDLEMOUSE and `view3d.move` on Shift+MIDDLEMOUSE; the
64
22
  // right button is the context menu's, and Shift+Right places the 3D cursor. The factory
65
23
  // rotation is Turntable at 0.4° a pixel (`view_rotate_method`,
@@ -1462,7 +1462,6 @@
1462
1462
  },
1463
1463
  "GeometryNodeForeachGeometryElementOutput": {
1464
1464
  "customSocketOrder": true,
1465
- "conditional": true,
1466
1465
  "items": [
1467
1466
  {
1468
1467
  "kind": "output",
@@ -1546,7 +1545,6 @@
1546
1545
  },
1547
1546
  "GeometryNodeGridToPoints": {
1548
1547
  "customSocketOrder": true,
1549
- "conditional": true,
1550
1548
  "items": [
1551
1549
  {
1552
1550
  "kind": "layout"
@@ -1860,7 +1858,6 @@
1860
1858
  },
1861
1859
  "GeometryNodeTransferAttributes": {
1862
1860
  "customSocketOrder": true,
1863
- "conditional": true,
1864
1861
  "items": [
1865
1862
  {
1866
1863
  "kind": "input",
@@ -1967,7 +1964,6 @@
1967
1964
  },
1968
1965
  "NodeEvaluateClosure": {
1969
1966
  "customSocketOrder": true,
1970
- "conditional": true,
1971
1967
  "items": [
1972
1968
  {
1973
1969
  "kind": "input",
@@ -2019,8 +2015,7 @@
2019
2015
  },
2020
2016
  {
2021
2017
  "kind": "input",
2022
- "name": "Weight",
2023
- "conditional": true
2018
+ "name": "Weight"
2024
2019
  },
2025
2020
  {
2026
2021
  "kind": "panel",
@@ -2087,8 +2082,7 @@
2087
2082
  },
2088
2083
  {
2089
2084
  "kind": "input",
2090
- "name": "Weight",
2091
- "conditional": true
2085
+ "name": "Weight"
2092
2086
  },
2093
2087
  {
2094
2088
  "kind": "panel",
@@ -2144,8 +2138,7 @@
2144
2138
  },
2145
2139
  {
2146
2140
  "kind": "input",
2147
- "name": "Weight",
2148
- "conditional": true
2141
+ "name": "Weight"
2149
2142
  },
2150
2143
  {
2151
2144
  "kind": "panel",
@@ -2309,7 +2302,6 @@
2309
2302
  },
2310
2303
  "ShaderNodeRaycast": {
2311
2304
  "customSocketOrder": true,
2312
- "conditional": true,
2313
2305
  "items": [
2314
2306
  {
2315
2307
  "kind": "layout"
@@ -2375,8 +2367,7 @@
2375
2367
  },
2376
2368
  {
2377
2369
  "kind": "input",
2378
- "name": "Weight",
2379
- "conditional": true
2370
+ "name": "Weight"
2380
2371
  },
2381
2372
  {
2382
2373
  "kind": "panel",
@@ -0,0 +1,63 @@
1
+ /**
2
+ * EDITOR-BLENDER'S PAGE HALF (`contributes.volter.page`, `docs/adr/plugin-apis.md`): what its
3
+ * `activate` returns is its API, which a package reads with
4
+ * `context.extensions.getExtension('volter.@volter/editor-blender')`. It is the model Play seam
5
+ * this package owns, the parts of the Blender document's bottom area that play's Game panel
6
+ * draws with, and two generic doors onto the document's Blender: its saves, each running a script on the
7
+ * scene it wrote whose files reach the project (and a save asked for where it changed), and an asset library in its preferences. Nothing more. Its type is `@volter/editor-blender/api`'s `BlenderPageApi`. It also
8
+ * registers the package's commands: its session verbs and its views' verbs.
9
+ */
10
+ import { registerVerbCommands, registerViewCommands } from '@volter/sdk/commands';
11
+ import { registerHierarchyKinds } from '@volter/sdk/hierarchy-kind-icon';
12
+ import type { PluginContext } from '@volter/sdk/plugins';
13
+ import { flushModelDocument, NODE_VIEW_VERBS, setBlenderAssetLibrary, setModelDocumentSaveScript, subscribeModelDocumentSaved } from '../host/blender-runtime-host';
14
+ import { registerModelPlay } from '../src/model-play-extension';
15
+ import {
16
+ documentOnScreen,
17
+ modelDocumentSource,
18
+ modelPlayMode,
19
+ playModeOf,
20
+ playModeVersion,
21
+ servedModelDocument,
22
+ subscribePlayMode,
23
+ } from '../src/play-mode';
24
+ import { PlayAreaButton, PlayModeSwitch, switchPlayMode } from './blender-play-area';
25
+ import { HEADER_HEIGHT, TIMELINE_CHROME, TIMELINE_THEME } from './blender-timeline-geometry';
26
+ import { TIMELINE_VIEW_VERBS } from './blender-timeline.document';
27
+ import { UV_VIEW_VERBS } from './blender-uv-editor.document';
28
+ import { blenderVerbs } from './blender.verbs';
29
+ import { exportVerbs } from './blender-export.verbs';
30
+ import { gltfExportVerbs } from './blender-gltf-export.verbs';
31
+
32
+ export function activate(context: PluginContext) {
33
+ // The Model documents send Blender's kinds: `blender-*`, each its own Outliner glyph, among them the DATA marks for
34
+ // an object's data, its materials and its shape keys (`tree_element_get_icon_from_id`, `outliner_draw.cc:2479-2610`)
35
+ // and the value-keyed lamp and probe marks and `OUTLINER_DATA_*` family (`:2508-2521`, `:2578-2589`). Only a Model
36
+ // document sends them, and its editor is this package's, so they are registered before any of its modules mount.
37
+ registerHierarchyKinds({
38
+ namespace: 'blender-',
39
+ datablocks: ['blender-material-data', 'blender-shapekey-data', 'blender-texture-data', 'blender-image-data'],
40
+ datablockPrefixes: ['blender-outliner-data-', 'blender-light-', 'blender-lightprobe-'],
41
+ });
42
+ // The session verbs and the views' verbs, as the Code-OSS commands package.json declares.
43
+ registerVerbCommands(context, { ...blenderVerbs, ...exportVerbs, ...gltfExportVerbs });
44
+ registerViewCommands(context, 'blender-node-view', NODE_VIEW_VERBS);
45
+ registerViewCommands(context, 'timeline', TIMELINE_VIEW_VERBS);
46
+ registerViewCommands(context, 'blender-uv-view', UV_VIEW_VERBS);
47
+ return {
48
+ registerModelPlay,
49
+ /** The bottom area's mode, the served model document, its on-screen wait and its source. */
50
+ playMode: { documentOnScreen, modelDocumentSource, modelPlayMode, playModeOf, playModeVersion, servedModelDocument, subscribePlayMode },
51
+ PlayAreaButton,
52
+ PlayModeSwitch,
53
+ switchPlayMode,
54
+ /** The Timeline's chrome, which a panel in the same area draws with. */
55
+ timelineGeometry: { HEADER_HEIGHT, TIMELINE_CHROME, TIMELINE_THEME },
56
+ /** The model document's `.blend` as a file: a script every save runs in its own turn on the scene it wrote, each
57
+ * save that landed it in the project, with what that script did and the carry of its files, and a save of the
58
+ * project's `.blend` where Blender holds it changed (`flush`). */
59
+ modelDocument: { setSaveScript: setModelDocumentSaveScript, subscribeSaved: subscribeModelDocumentSaved, flush: flushModelDocument },
60
+ /** An asset library every Blender session has, until its removal. */
61
+ setAssetLibrary: setBlenderAssetLibrary,
62
+ };
63
+ }
@@ -1,140 +1,29 @@
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.
8
- *
9
- * THEY ARE HOVER-DRIVEN THERE, AND ALWAYS-ON HERE. Measured across the whole
10
- * reference set: `modeling.png` is the ONE frame with any ink left of centre
11
- * in the status band; `layout`, `modeling-edit-none`, `modeling-edit-all`,
12
- * `modeling-object-selected`, `sculpting` and `texture-paint` all have ZERO
13
- * ink before device x 3325 — Blender publishes the keymap for whatever the
14
- * cursor is over, so a still with the pointer outside the 3D View shows
15
- * nothing. A still therefore cannot say whether Blender's band is empty by
16
- * design; it can only say what the hints look like WHEN they are drawn, which
17
- * is what the geometry below is measured from. Ours are unconditional
18
- * because the editor has no hover-scoped keymap publisher and inventing one
19
- * to match a screenshot would be a mechanism with no other reader.
20
- *
21
- * THE ACTION NAMES ARE OURS, NOT BLENDER'S. What each button does is a fact
22
- * about THIS editor, so the labels are read from this editor's own contract,
23
- * never transcribed from the reference:
24
- *
25
- * - the KEYMAP (`keymap-presets.ts`, `@volter/sdk/looks`) binds no
26
- * mouse buttons and structurally cannot: a `KeyChord` is
27
- * `{ key, code?, mod?, shift?, alt? }` (`sdk/src/looks.ts:76-82`)
28
- * with no button field, so no keymap — the Blender one included — can
29
- * move a mouse binding. Nothing to read there.
30
- * - so the source is the VIEWPORT's own mouse contract:
31
- * `editor-viewport.ts:682-686` sets `orbitControls.mouseButtons` to
32
- * `{ LEFT: -1, MIDDLE: PAN, RIGHT: ROTATE }`, and the left button is left
33
- * free for the selection raycast (`editor-viewport.ts:3782`,
34
- * `if (e.button === 0 && !e.altKey)`).
35
- *
36
- * That contract is a constant of the host, not of any document, so the table
37
- * below is a constant too — with the citations above as its provenance. When
38
- * a surface ever makes a button mean something else, it publishes that and
39
- * this reads it; it does not get hardcoded here.
40
- *
41
- * The inline styles spell the theme tokens directly (`var(--volter-space-5)`,
42
- * `var(--volter-font-md)`) instead of importing the host's `spaceVar` /
43
- * `fontSizeVar` handles: `@volter/editor-blender` is pinned at ZERO host imports
44
- * (`validate-editor-closure.mjs`). See `blender-version.status.tsx`.
45
- */
1
+ /** Blender-style status hints describe this editor’s viewport mouse bindings: left selects, middle pans, right rotates. They are always visible because there is no hover-scoped keymap publisher. Use SDK theme variables without importing host implementation. */
46
2
 
47
3
  export const point = 'workspace.status';
48
4
  export const title = 'Mouse hints';
49
5
  export const align = 'left';
50
- /** Front of the contributed band — see the note at the bottom of this file
51
- * for why that is not the front of the bar. */
6
+ /** First among contributed left status items, after the host's built-in items. */
52
7
  export const order = 0;
53
8
 
54
9
  /** Which button the glyph lights. */
55
10
  type MouseButton = 'left' | 'middle' | 'right';
56
11
 
57
- /**
58
- * The viewport's three buttons, in Blender's order. Sourced from
59
- * `editor-viewport.ts:682-686` + `:3782` (see the header) — the editor's
60
- * contract, which is why the middle/right labels are Pan/Orbit and not
61
- * Blender's own Rotate View/Options.
62
- */
12
+ /** Viewport bindings in Blender's button order: left selects, middle pans and right orbits. */
63
13
  const HINTS: readonly { readonly button: MouseButton; readonly label: string }[] = [
64
14
  { button: 'left', label: 'Select' },
65
15
  { button: 'middle', label: 'Pan View' },
66
16
  { button: 'right', label: 'Orbit View' },
67
17
  ];
68
18
 
69
- /**
70
- * One mouse glyph in Blender's idiom, TRANSCRIBED FROM THE FRAME rather than
71
- * estimated: `modeling.png` at native 2x (the only reference frame whose
72
- * status bar carries the hints — see the note under {@link HINTS}), glyph 1 at
73
- * device x 12..35, y 2065..2096. Halved, that is a 12 x 16 CSS ink box, and
74
- * the viewBox IS that box so the SVG needs no scale factor to reason about:
75
- * one unit is one CSS pixel at the size below.
76
- *
77
- * MEASURED BY COVERAGE, NOT BY A THRESHOLD. Every number below is the 50%
78
- * contour of `(value - 23) / (135 - 23)` — an ink threshold reads a glyph's
79
- * antialiasing as geometry and got two of these badly wrong on the first cut.
80
- * Device reading -> CSS (the ink box's left edge is device -0.5, so
81
- * `css = (device + 0.5) / 2`):
82
- *
83
- * shell stroke 1.98 device, flat on every side row -> {@link STROKE} 1
84
- * button row height 16 of 32 device -> {@link BUTTON_H} 8, exactly half
85
- * lit button width left fill ends 9.5 / right fill starts 13.5 device
86
- * -> {@link BUTTON_W} 5, a button at each end
87
- * divider the shell's top edge stops at 11.98 device in the
88
- * left-lit glyph and starts there in the right-lit one
89
- * -> {@link DIVIDER_X} 6.25, ONE boundary, not a gap
90
- * centred on the middle
91
- * gap under button 2 device of nothing (fill ends row 15, stroke resumes
92
- * row 18) -> {@link UNDER_GAP} 1
93
- * wheel 8 x 14 device at x 8, y 4, ends rounded at r 2.5
94
- * -> 4 x 7 at 4, 2, rx 1.25
95
- *
96
- * THE LIT BUTTON IS NOT AN INSET NUB AND NOT A CLIPPED OVERLAY: in the frame
97
- * the fill REPLACES the shell over its own quadrant (a mid row reads ten solid
98
- * device px from the very left edge) and the shell stroke is absent for two
99
- * device rows beneath it and across the divider on the top edge. So the fill
100
- * is the outer silhouette intersected with the button rect, and the stroke is
101
- * MASKED OUT over the button's side of the divider down to the hairline —
102
- * which is why there is a mask here and not the clip the earlier cut used.
103
- *
104
- * (Superseded readings, kept so the next round does not re-derive them: a
105
- * 13x17 box with a 1.7 stroke and a 0.45 hairline, sized to the text's own
106
- * font size. Both came from a DPR-1 capture against a downscaled reference,
107
- * which is the instrument WORK.md records as unable to settle stroke weights;
108
- * at native 2x the glyph measures 16 CSS tall against 9 CSS of cap height,
109
- * not the ~1:1 that reading reported.)
110
- *
111
- * RESIDUE, measured and not fitted away: Blender's TOP edge integrates 3.0
112
- * device px of coverage where its sides integrate 1.98. One uniform stroke
113
- * cannot draw both, and a second stroke for the top would be a constant with
114
- * no reason behind it; the sides are the ones that set the glyph's weight.
115
- */
19
+ /** The mouse glyph’s viewBox uses CSS pixels. Its lit quadrant replaces the shell fill; mask that quadrant’s shell stroke and preserve the divider and gap beneath it. */
116
20
  const STROKE = 1;
117
21
  const BUTTON_H = 8;
118
22
  const BUTTON_W = 5;
119
23
  const UNDER_GAP = 1;
120
- /**
121
- * Where the shell's top edge stops for a lit LEFT button and starts again for
122
- * a lit RIGHT one — ONE boundary, at device 11.98 in both glyphs, not a gap
123
- * straddling the centre. The button fills stop short of it by 1.25 on the left
124
- * and start 0.75 past it on the right, which is why modelling this as a
125
- * symmetric "gap" beside a button could not reproduce either glyph.
126
- */
24
+ /** Shared shell-stroke boundary; button fills end 1.25 units before it on the left and start 0.75 after it on the right. */
127
25
  const DIVIDER_X = 6.25;
128
- /**
129
- * The shell's OUTER corner radius, and the number two cuts got wrong — first
130
- * as a capsule (half the width), then as 2 from an ink threshold. SOLVED on
131
- * the 50% contour of the middle glyph's bottom-left and top-right corners
132
- * (`modeling.png`, device x 122..145, y 2065..2096): the contour runs 4.04,
133
- * 2.39, 1.33, 0.55, 0.05, -0.28 device from the straight edge over successive
134
- * rows, and a circle of radius 7.16 device reproduces all six to within 0.11.
135
- * The opposite corner independently gives 7.2. Halved, 3.6 — and both wrong
136
- * answers were visible at full scale as the wrong SHAPE, not the wrong size.
137
- */
26
+ /** The mouse shell’s outer corner radius is 3.6 CSS px. */
138
27
  const RADIUS = 3.6;
139
28
  /** The ink box, in CSS px, and the viewBox. */
140
29
  const BOX = { width: 12, height: 16 } as const;
@@ -153,9 +42,7 @@ function MouseGlyph({ button }: { button: MouseButton }) {
153
42
  role="presentation"
154
43
  aria-hidden="true"
155
44
  focusable="false"
156
- // 16 CSS px, which is the `lg` icon rung — and Blender's glyph measures
157
- // exactly 16 CSS tall in the frame. `width: auto` then resolves to 12
158
- // from the viewBox, the measured width, with no second number to drift.
45
+ // Use the 16-px icon-lg height; the viewBox supplies the 12-px width.
159
46
  style={{ height: 'var(--volter-icon-lg)', width: 'auto', display: 'block', flex: '0 0 auto' }}
160
47
  >
161
48
  <title>{button} mouse button</title>
@@ -211,23 +98,14 @@ export default function BlenderMouseHintsStatus() {
211
98
  <span
212
99
  className="volter-status-copy"
213
100
  data-testid="status-blender-mouse-hints"
214
- // Blender's hint-to-hint gap is 9.5 CSS (glyph 1 ends at 51.5, glyph 2
215
- // starts at 61; the second gap repeats it to a tenth). `space-5` is 10.
101
+ // The 10px space-5 token approximates Blender's 9.5px hint gap.
216
102
  style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--volter-space-5)' }}
217
103
  >
218
104
  {HINTS.map((hint) => (
219
105
  <span
220
106
  key={hint.button}
221
107
  data-mouse-button={hint.button}
222
- // NO COLOUR HERE. The band declares its own ink and Blender's status
223
- // bar is the DIMMEST text in its window — `#878787` (135) flat, on
224
- // every frame, against the 216 its top-bar menus ink. A contribution
225
- // that pins `content.muted` (194) makes itself the loudest thing in
226
- // the quietest band; inheriting is what keeps the band one voice.
227
- //
228
- // The SIZE is measured: Blender's status text caps at 9.0 CSS (18
229
- // device), which is the `md`/`base` rung (11px -> 8.8), not `sm`
230
- // (10px -> 8.0).
108
+ // Inherit the status band’s colour and base font size.
231
109
  style={{
232
110
  display: 'inline-flex',
233
111
  alignItems: 'center',
@@ -243,17 +121,3 @@ export default function BlenderMouseHintsStatus() {
243
121
  </span>
244
122
  );
245
123
  }
246
-
247
- /**
248
- * ORDER, and the one thing this cannot have. Blender's hints are the
249
- * LEFTMOST thing in its bottom row. A contributed status item cannot be:
250
- * `tool-loader.ts:1145` registers it at `contributedSectionOrder(order)`,
251
- * which is `CONTRIBUTED_SECTION_ORDER + (order ?? 999)`
252
- * (`tool-loader.ts:359-361`) with `CONTRIBUTED_SECTION_ORDER = 5000`
253
- * (`inspection/model.ts:475`), while the host's own left items register at
254
- * -90..40 (`components/status-contributions.tsx`). Every contributed item
255
- * therefore lands BEHIND every core item, by construction — the band exists
256
- * precisely so a contribution cannot outrank a built-in. `order = 0` is the
257
- * front of what the contract allows: first among contributed left items,
258
- * after the host's save/console/play cluster.
259
- */
@@ -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
  };