@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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ONE MIXER PER PRESENTED SCENE (`docs/SCENE-ANIMATION.md`, steps 3 and 4).
3
+ *
4
+ * Every armature's layers (`blender-mixer-pose.ts`, each bound to its own rig so bones of the same
5
+ * name in two characters never meet) and the scene's movie (`blender-playback-movie.ts`, its objects and
6
+ * cameras) are actions on ONE `THREE.AnimationMixer` rooted at the view's root, on one clock. A
7
+ * frame is placed in three steps for the Timeline preview:
8
+ *
9
+ * 1. PLACE: each player sets its actions' times and weights for the frame (`place`).
10
+ * 2. EVALUATE: the mixer applies every enabled action at once (`evaluate`), once per frame however
11
+ * many characters there are.
12
+ * 3. SETTLE: what the mixer does not do on its own, after it (`settle`): Damped Track and Track To,
13
+ * and the fidelity check against Blender's own pose.
14
+ *
15
+ * An action left enabled keeps writing its last placement every evaluation, so a player that is not
16
+ * placed this frame (the movie while the Timeline stands at Blender's own frame) disables its actions
17
+ * rather than leaving them behind.
18
+ */
19
+ import * as THREE from 'three';
20
+
21
+ export class SceneMixer {
22
+ readonly mixer: THREE.AnimationMixer;
23
+
24
+ constructor(readonly root: THREE.Object3D) {
25
+ this.mixer = new THREE.AnimationMixer(root);
26
+ }
27
+
28
+ /** Apply every placed action, all at once. */
29
+ evaluate(): void {
30
+ this.mixer.update(0);
31
+ this.root.updateMatrixWorld(true);
32
+ }
33
+
34
+ dispose(): void {
35
+ this.mixer.stopAllAction();
36
+ this.mixer.uncacheRoot(this.root);
37
+ }
38
+ }
39
+
40
+ const mixers = new WeakMap<object, SceneMixer>();
41
+
42
+ /** The one mixer of a presented view: made on first use,
43
+ * and made again when the view's root is a new object (a new present). */
44
+ export function sceneMixer(view: { readonly root: THREE.Object3D }): SceneMixer {
45
+ const held = mixers.get(view);
46
+ if (held && held.root === view.root) return held;
47
+ held?.dispose();
48
+ const made = new SceneMixer(view.root);
49
+ mixers.set(view, made);
50
+ return made;
51
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Save Screenshot (Editor)… — Blender's own Window menu row (`TOPBAR_MT_window`,
3
+ * `screen.screenshot_area`), the person's door to the photograph an agent takes with
4
+ * `get_viewport_screenshot`. It is this package's command `volter.blender.saveScreenshotEditor`
5
+ * (`contributes.commands`, in the palette); this runs it.
6
+ *
7
+ * Blender asks for a path in its file browser; the files door has no file picker, so the picture
8
+ * goes to the project's `screenshots/` folder and a notification says which file it is. Like every
9
+ * photograph of the document it flashes the document and shows itself in the corner.
10
+ */
11
+ import { bytesFromBase64 } from '@volter/blender-engine/browser/three/blender-base64';
12
+ import { editorHost } from '@volter/sdk/host';
13
+ import { announcePhotograph } from '@volter/sdk/photograph-notice';
14
+
15
+ export const point = 'workspace.service';
16
+
17
+ async function saveScreenshot(): Promise<void> {
18
+ const host = editorHost();
19
+ const capture = await host.documents.captureActive();
20
+ announcePhotograph(capture.base64);
21
+ const path = `screenshots/screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
22
+ await host.files.write(path, bytesFromBase64(capture.base64));
23
+ host.notify({ id: 'blender-screenshot', tone: 'info', title: 'Saved the screenshot', detail: path });
24
+ }
25
+
26
+ export function start(): () => void {
27
+ return editorHost().keyboard.bindActions([{ id: 'blender.saveScreenshotEditor', run: () => void saveScreenshot() }]);
28
+ }
@@ -1,54 +1,15 @@
1
- /**
2
- * THE TIMELINE'S CONSTANTS — every one read from Blender's source at the
3
- * engine's pin (5.2.0, `fbe6228777e7`) AND checked against a PIXEL in Blender's
4
- * own frame (5.2.1 on this box, `scripts/blender-reference-frames.py`).
5
- *
6
- * This is the first unit of the inspection arc with a sighted read behind it.
7
- * I1-I5 each recorded that the reference frames were not at hand, so each was graded
8
- * against Blender's SOURCE alone; Blender is installed here, the script above
9
- * drives its real GUI, and the numbers below carry three things each: the
10
- * source that defines them, the pixel that confirms them, and what we draw.
11
- *
12
- * ## The screenshot reads ONE 8-BIT LEVEL BELOW the theme byte
13
- *
14
- * Measured on every flat fill in the frame: the theme's `#303030` reads
15
- * `#2f2f2f`, `#1d1d1d` reads `#1c1c1c`, `#161616` reads `#151515`,
16
- * `#4772b3` reads `#4671b2`, `#ffbe33` reads `#ffbd32`. It is uniform and it
17
- * is the screenshot path's, not the theme's — so the values below are the
18
- * THEME BYTES (what Blender means) and a reader comparing them against a
19
- * reference PNG should expect that −1.
20
- *
21
- * ## The theme's own table disagrees with the source's default in one place,
22
- * and the running theme wins
23
- *
24
- * `userdef_default_theme.c:490` declares `.space_action.back` as
25
- * `RGBA(0x30303000)` — alpha ZERO. Read off the RUNNING factory theme
26
- * (`bpy.context.preferences.themes[0].dopesheet_editor.space.back`) it is
27
- * `#303030ff`. The difference is load-bearing: `ANIM_draw_framerange` paints
28
- * the out-of-range area with `TH_BACK` shaded −25 at alpha −100, which at
29
- * alpha 0 would draw NOTHING and at alpha 255 draws a visible darkening. The
30
- * frame settles it — outside the range is `#202020` against `#2f2f2f` inside,
31
- * which is exactly `48 + (23 − 48) × 155/255 = 32.8`. The screenshot wins.
32
- */
1
+ /** Timeline geometry and theme constants follow Blender at the engine pin. Use the running factory theme’s opaque space_action.back, which controls out-of-range shading. */
33
2
 
34
- /** `U.widget_unit` — `wm_window.cc:779`, `int(roundf(18 * scale_factor)) + 2 *
35
- * pixelsize` = 20 at scale 1. Confirmed in the frame: a transport button
36
- * block of six is 120 px wide (x 697…816), so each is 20. */
3
+ /** U.widget_unit is 20 at scale 1 (wm_window.cc:779). */
37
4
  export const WIDGET_UNIT = 20;
38
5
 
39
- /** `UI_TIME_SCRUB_MARGIN_Y` — `UI_view2d.hh:491`, `23 * UI_SCALE_FAC`. The
40
- * strip the ruler and the playhead's number pill live in. CONFIRMED: the
41
- * Timeline area's `.regions.scrubbing.back` band is exactly 23 px tall
42
- * (image rows 26…48 under a 26-px header). */
6
+ /** UI_TIME_SCRUB_MARGIN_Y is 23 × UI_SCALE_FAC (UI_view2d.hh:491). */
43
7
  export const SCRUB_HEIGHT = 23;
44
8
 
45
- /** `HEADERY` — `DNA_screen_types.h:602`, `20 + HEADER_PADDING_Y`. CONFIRMED
46
- * at 26 px, with its 20-px widgets centred (3 px above, 3 below). */
9
+ /** HEADERY is 20 + HEADER_PADDING_Y = 26 (DNA_screen_types.h:602). */
47
10
  export const HEADER_HEIGHT = 26;
48
11
 
49
- /** `ANIM_UI_get_channel_height` — `anim_channels_defines.cc:5147-5150`,
50
- * `0.8 * keyframe_scale_fac * widget_unit`; `keyframe_scale_fac` is 1.0
51
- * (`userdef_default_theme.c:508`, and read back 1.0 off the running theme). */
12
+ /** Channel height is 0.8 × keyframe_scale_fac × widget_unit; the theme scale is 1 (anim_channels_defines.cc:5147-5150). */
52
13
  export const CHANNEL_HEIGHT = 16;
53
14
  /** `ANIM_UI_get_channel_skip` — `:5152-5155`, `0.1 * widget_unit`. */
54
15
  export const CHANNEL_SKIP = 2;
@@ -56,24 +17,6 @@ export const CHANNEL_SKIP = 2;
56
17
  * `-UI_TIME_SCRUB_MARGIN_Y - skip` below the region's top edge. */
57
18
  export const FIRST_CHANNEL_TOP = SCRUB_HEIGHT + CHANNEL_SKIP;
58
19
 
59
- /**
60
- * THE TIMELINE DRAWS EXACTLY ONE CHANNEL ROW, and that is a source fact rather
61
- * than a layout choice: `action_create` sets `ADS_FLAG_SUMMARY_COLLAPSED` when
62
- * the area's subtype is `SACTCONT_TIMELINE` (`space_action.cc:80-83`), and
63
- * `animdata_filter_dopesheet` returns 0 right after adding the summary channel
64
- * when that flag is set (`anim_filter.cc:3845-3849`). The channel-NAMES region
65
- * is not drawn at all in Timeline mode
66
- * (`action_region_poll_hide_in_timeline`, `space_action.cc:785-790`).
67
- *
68
- * THE ROW HAS NO BACKDROP EITHER. `draw_backdrops` branches on
69
- * `ELEM(ac->datatype, ANIMCONT_ACTION, ANIMCONT_DOPESHEET, ANIMCONT_SHAPEKEY)`
70
- * and then on GPENCIL and MASK (`action_draw.cc:218-300`); `ANIMCONT_TIMELINE`
71
- * matches none of them, so the summary row is drawn straight onto the region's
72
- * own clear. CONFIRMED: the row is `#2f2f2f` inside the frame range, the same
73
- * value as the rest of the region.
74
- */
75
- export const TIMELINE_ROWS = 1;
76
-
77
20
  /** `MIN_MAJOR_LINE_DISTANCE` — `view2d_draw.cc:41`, `U.v2d_min_gridsize *
78
21
  * UI_SCALE_FAC`, default 35 (`versioning_userdef.cc:1028-1029`). The real
79
22
  * minimum is `max(35, label width + 6)` (`get_min_line_distance_x`, `:485`,
@@ -152,27 +95,20 @@ export function minorStep(major: number): number | null {
152
95
  * running theme is what draws, and the disagreement is named.
153
96
  */
154
97
  export const TIMELINE_THEME = {
155
- /** `.space_action.back` — `userdef_default_theme.c:490` declares
156
- * `0x30303000`; the RUNNING theme is `#303030ff` (see the module header).
157
- * The region's clear, `frame_buffer_clear(TH_BACK)`. */
98
+ /** space_action.back is #303030ff in the running factory theme; frame_buffer_clear uses its RGB. */
158
99
  back: '#303030',
159
- /** `.space_action.header` `0x303030b3` (`:494`) over the window. Reads
160
- * `#2f2f2f` in the frame, which at alpha 0xb3 over `#2d2d2d`-ish window
161
- * chrome is what this value resolves to; drawn flat here. */
100
+ /** space_action.header from userdef_default_theme.c, drawn flat. */
162
101
  header: '#303030',
163
102
  /** `.space_action.grid` — `:498`. The MAJOR frame lines. */
164
103
  grid: '#161616',
165
- /** `get_color_shade_3ubv(TH_GRID, 16)` — `view2d_draw.cc:266`. The MINOR
166
- * lines, exactly 16 levels lighter. CONFIRMED `#252525` in the frame
167
- * against the major's `#151515`. */
104
+ /** Minor grid lines use TH_GRID shaded 16 levels lighter (view2d_draw.cc). */
168
105
  gridMinor: '#262626',
169
106
  /** `.regions.scrubbing.back` — `userdef_default_theme.c:300`. */
170
107
  scrubBack: '#1d1d1d',
171
108
  /** `.regions.scrubbing.text` — `:301`, the ruler's numbers
172
109
  * (`TH_TIME_SCRUB_TEXT`, passed by `ED_time_scrub_draw`). */
173
110
  scrubText: '#808080',
174
- /** `.common.anim.playhead` (`TH_CFRAME`) — `:312`. The stalk, the pill and
175
- * its tip. CONFIRMED `#4671b2` in the frame. */
111
+ /** TH_CFRAME supplies the playhead stalk, pill and tip. */
176
112
  playhead: '#4772b3',
177
113
  /** `TH_HEADER_TEXT_HI` — the pill's number (`draw_playhead_box`,
178
114
  * `time_scrub_ui.cc:182`); `.space_action.header_text_hi` `:496`. */
@@ -187,8 +123,7 @@ export const TIMELINE_THEME = {
187
123
  longKeyAlpha: 0x1f / 255,
188
124
  longKeySelected: '#ff8c00',
189
125
  longKeySelectedAlpha: 0x99 / 255,
190
- /** `.space_action.keyborder` / `keyborder_select` — `:502-503`, BOTH black
191
- * (confirmed off the running theme: `#000000ff` each). */
126
+ /** space_action.keyborder and keyborder_select are both black. */
192
127
  keyBorder: '#000000',
193
128
  } as const;
194
129
 
@@ -215,58 +150,13 @@ export const KEY_SIZE_FACTOR: Record<string, number> = {
215
150
  JITTER: 0.8,
216
151
  GENERATED: 0.75,
217
152
  };
218
-
219
- /**
220
- * THE DIAMOND, and this is the one number a lane must not re-derive by eye.
221
- *
222
- * `icon_size = widget_unit * 0.5 * yscale_fac` = 10 (`keyframes_draw.cc:218`),
223
- * and the SHAPE is the shader's arithmetic
224
- * (`gpu_shader_keyframe_shape_vert.glsl:46-66`, `outline_scale` 1 at its one
225
- * caller `keyframes_draw.cc:660`):
226
- *
227
- * half_width = 0.06 + (size − 10) × 0.04 → 0.06
228
- * line_width = half_width + line_falloff(1.0) → 1.06
229
- * thresholds = (max(0, line_width − 1), line_width) → (0.06, 1.06)
230
- * ext_radius = round(0.5 × size) + thresholds.x → 5.06
231
- * pointSize = ceil(ext_radius + thresholds.y) × 2 + 1 → 15
232
- *
233
- * and the fragment's diamond test is an L1 ball: `radius = (|x| + |y|) ×
234
- * √0.5` against `radii[0] = ext_radius × √0.5`, with
235
- * `alpha = 1 − smoothstep(thresholds.x, thresholds.y, |outline_dist|)`. So the
236
- * OUTLINE is a band around L1 = 5.06 whose PERPENDICULAR half-extent is
237
- * `line_width` = 1.06 px, and the fill shows inside L1 ≈ 3.56.
238
- *
239
- * CONFIRMED PIXEL FOR PIXEL in Blender's frame: the sprite is 15 rows tall
240
- * (image rows 51…65 for a key centred at 58), the filled core is 7 rows
241
- * (55…61 — L1 ≤ 3.5), and the black band reaches L1 ≈ 6.5.
242
- *
243
- * WE DRAW IT as an SVG polygon of half-diagonal `DIAMOND_RADIUS` with a
244
- * `DIAMOND_STROKE`-wide stroke straddling it, which reproduces both extents
245
- * exactly — 3.56 for the fill's edge and 6.56 for the outline's. The stated
246
- * difference: Blender's band is a 0.06 → 1.06 smoothstep in each direction and
247
- * SVG has no such ramp, so the browser antialiases its own hard edges instead.
248
- * The INK-weighted equivalent would be 1.12 px (the integral of
249
- * `1 − smoothstep(0,1,u)` is exactly ½, so each side deposits 0.06 + 0.5), and
250
- * it would put the fill 0.7 px further out than Blender's — the extents are
251
- * what a parity read measures, so the extent is what is matched.
252
- */
253
- export const KEY_ICON_SIZE = 10;
254
153
  export const DIAMOND_RADIUS = 5.06;
255
154
  export const DIAMOND_STROKE = 2.12;
256
155
  /** `gl_PointSize` — the sprite Blender rasterises the shape into. Reported in
257
156
  * the view's `state` so a parity reading can check it. */
258
157
  export const DIAMOND_SPRITE = 15;
259
158
 
260
- /**
261
- * THE PLAYHEAD — `get_playhead_dimensions` (`time_scrub_ui.cc:96-116`) and
262
- * `draw_playhead_box` / `draw_playhead_tip` (`:161-206`).
263
- *
264
- * CONFIRMED: a 24-px pill (22 px of flat `#4671b2` plus a pixel of
265
- * antialiasing each side) starting 2 px below the scrub strip's top, and a
266
- * 3-px stalk (`draw_playhead_stalk`'s `rect` at `UI_SCALE_FAC ≥ 0.91`:
267
- * `floor(x − 1) − 1` to `floor(x + 2) + 1`, of which the shadow is the outer
268
- * pixel on each side).
269
- */
159
+ /** Playhead dimensions follow get_playhead_dimensions and draw_playhead_box/tip in time_scrub_ui.cc. */
270
160
  export const PLAYHEAD = {
271
161
  /** `box_min_width = 24 * UI_SCALE_FAC` (`:109`); a wider number grows it to
272
162
  * `text_width + 2 * text_padding`. */
@@ -280,35 +170,25 @@ export const PLAYHEAD = {
280
170
  /** `tri_half_width` / `tri_height` = `6 * UI_SCALE_FAC` (`:113-114`). */
281
171
  tipHalfWidth: 6,
282
172
  tipHeight: 6,
283
- /** The stalk's drawn core, measured: 3 px. */
173
+ /** The stalk’s core is 3 CSS px. */
284
174
  stalkWidth: 3,
285
175
  } as const;
286
176
 
287
- /**
288
- * `ANIM_draw_framerange` — `anim_draw.cc:172-190`: the area OUTSIDE
289
- * `scene.frame_start … frame_end` takes `TH_BACK` shaded −25 with alpha −100.
290
- * With the running theme's `#303030ff` that resolves to
291
- * `48 + (23 − 48) × 155/255 = 32.8`, and the frame reads `#202020` (32).
292
- * Drawn here as one flat colour rather than a blend, because the blend has one
293
- * possible answer over the region's own clear.
294
- */
177
+ /** Out-of-range fill is TH_BACK shaded −25 at alpha −100 (ANIM_draw_framerange), resolved once over the opaque region clear. */
295
178
  export const OUT_OF_RANGE = '#212121';
296
179
 
297
180
  /** `.space_action.text` — `userdef_default_theme.c:492`, the status line's. */
298
181
  export const TIMELINE_CHROME = {
299
182
  text: '#a6a6a6',
300
183
  textHi: '#ffffff',
301
- /** `ui.editor_border` — `#161616`, read back off the running theme. */
184
+ /** ui.editor_border from the running factory theme. */
302
185
  rule: '#161616',
303
- /** `wcol_tool.inner` / `.text` / `.outline`, read back off the running
304
- * theme: `#545454` / `#e6e6e6` / `#3d3d3d`, roundness 0.4. A 20-px widget
305
- * therefore has a 4-px corner radius. */
186
+ /** wcol_tool inner/text/outline and roundness 0.4; a 20-px widget has a 4-px corner radius. */
306
187
  widget: '#545454',
307
188
  widgetText: '#e6e6e6',
308
189
  widgetOutline: '#3d3d3d',
309
190
  widgetRadius: 4,
310
- /** `UI_UNIT_X` / `UI_UNIT_Y` — one `widget_unit`. Confirmed: the six
311
- * transport buttons span exactly 120 px. */
191
+ /** UI_UNIT_X and UI_UNIT_Y equal one widget_unit. */
312
192
  unit: WIDGET_UNIT,
313
193
  refusal: '#ffaf23',
314
194
  /** The header's own padding and the gap between its widget groups —