@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.
- package/LICENSE +18 -9
- package/README.md +5 -5
- package/contributions/blender-animation-editors.tsx +17 -69
- package/contributions/blender-display-settings.ts +36 -0
- package/contributions/blender-export-gltf.ts +228 -0
- package/contributions/blender-export.verbs.ts +39 -0
- package/contributions/blender-gltf-export.verbs.ts +312 -0
- package/contributions/blender-header-menus.tsx +37 -133
- package/contributions/blender-icon-trace.mjs +8 -98
- package/contributions/blender-icons.source.mjs +96 -860
- package/contributions/blender-mixer-pose.ts +369 -0
- package/contributions/blender-model-opening.tsx +26 -56
- package/contributions/blender-node-editor.document.tsx +30 -181
- package/contributions/blender-node-geometry.ts +44 -230
- package/contributions/blender-node-panels.source.mjs +7 -121
- package/contributions/blender-outliner-authoring.ts +61 -415
- package/contributions/blender-outliner-model.ts +15 -112
- package/contributions/blender-play-area.tsx +99 -0
- package/contributions/blender-playback-movie.ts +409 -0
- package/contributions/blender-pose.ts +18 -368
- package/contributions/blender-properties-context.ts +1 -1
- package/contributions/blender-properties-model.ts +59 -91
- package/contributions/blender-properties-tab.tsx +3 -50
- package/contributions/blender-properties-view.tsx +39 -181
- package/contributions/blender-remedy.ts +14 -0
- package/contributions/blender-runtime-skin.ts +103 -99
- package/contributions/blender-runtime.document.tsx +170 -618
- package/contributions/blender-scene-mixer.ts +51 -0
- package/contributions/blender-screenshot.service.ts +28 -0
- package/contributions/blender-timeline-geometry.ts +16 -136
- package/contributions/blender-timeline.document.tsx +145 -333
- package/contributions/blender-uv-editor.document.tsx +48 -112
- package/contributions/blender-uv-geometry.ts +14 -179
- package/contributions/blender-version.status.tsx +45 -31
- package/contributions/blender.environment.ts +2 -2
- package/contributions/blender.icons.json +0 -9
- package/contributions/blender.keymap.ts +9 -51
- package/contributions/blender.node-panels.json +4 -13
- package/contributions/blender.page.ts +64 -0
- package/contributions/blender.status.tsx +8 -150
- package/contributions/blender.style.ts +25 -152
- package/contributions/blender.verbs.ts +52 -0
- package/contributions/model.layout.ts +2 -34
- package/contributions/models.finder.ts +3 -30
- package/contributions/properties-bone-constraints.inspector.tsx +1 -16
- package/contributions/properties-bone.inspector.tsx +2 -31
- package/contributions/properties-collection.inspector.tsx +1 -34
- package/contributions/properties-constraints.inspector.tsx +1 -21
- package/contributions/properties-data.inspector.tsx +2 -39
- package/contributions/properties-material.inspector.tsx +2 -33
- package/contributions/properties-modifiers.inspector.tsx +2 -30
- package/contributions/properties-object.inspector.tsx +2 -44
- package/contributions/properties-output.inspector.tsx +1 -27
- package/contributions/properties-particles.inspector.tsx +2 -46
- package/contributions/properties-physics.inspector.tsx +2 -41
- package/contributions/properties-render.inspector.tsx +1 -45
- package/contributions/properties-scene.inspector.tsx +1 -35
- package/contributions/properties-texture.inspector.tsx +1 -49
- package/contributions/properties-view-layer.inspector.tsx +1 -29
- package/contributions/properties-world.inspector.tsx +2 -39
- package/contributions/shading.layout.ts +3 -78
- package/contributions/uv-editing.layout.ts +3 -70
- package/dist-node/serving.mjs +13 -32
- package/host/blender-runtime-host.ts +795 -412
- package/host/scene-sound.ts +143 -0
- package/node/blender-mcp.ts +52 -95
- package/package.json +1221 -46
- package/producticons/blender-icons.otf +0 -0
- package/producticons/blender-product-icon-theme.json +129 -0
- package/serving/blender-routes.ts +9 -48
- package/serving/blender-wasm-artifact.ts +29 -166
- package/serving/index.ts +1 -1
- package/src/api.ts +9 -0
- package/src/layouts.tsx +1 -17
- package/src/looks.ts +1 -12
- package/src/model-document-saving.ts +25 -0
- package/src/node-view-state.ts +3 -25
- package/src/play-mode.ts +13 -42
- package/src/play-tool.ts +44 -0
- package/src/presentation.ts +6 -18
- package/src/regions.ts +3 -43
- package/src/start-document.ts +15 -0
- package/src/timeline-view-state.ts +4 -39
- package/src/uv-view-state.ts +4 -20
- package/themes/blender.json +299 -0
- package/view/blender-view-routes.ts +1 -1
- package/contributions/blender-game-panel.tsx +0 -809
- package/contributions/blender-palette.source.mjs +0 -350
- package/contributions/blender-play-skin.ts +0 -508
- package/contributions/blender-screenshot.menu.ts +0 -32
- package/contributions/blender.command.ts +0 -109
- package/contributions/blender.palette.json +0 -102
- package/src/play-live.ts +0 -27
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE SOURCE of `blender.icons.json`, which is PART OURS AND PART BLENDER'S.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `blender.icons.traced.json`); the other 149 are drawn HERE in Blender's
|
|
3
|
+
* Traced glyphs come from Blender's own vector sources and are GPL-2.0-or-later
|
|
4
|
+
* artwork (see `blender-icon-trace.mjs` and the provenance file
|
|
5
|
+
* `blender.icons.traced.json`); the remaining glyphs are drawn HERE in Blender's
|
|
7
6
|
* IDIOM. That mixture is why `@volter/editor-blender` is
|
|
8
|
-
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`).
|
|
7
|
+
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/editor-blender/LICENSE`).
|
|
9
8
|
* The idiom is what a screenshot conveys: a 16-unit grid, monochrome filled
|
|
10
9
|
* silhouettes, ~1.3-unit strokes rendered as filled capsules with round ends,
|
|
11
10
|
* no outline around a fill, optical size ~12 units inside the 16 box.
|
|
@@ -14,7 +13,7 @@
|
|
|
14
13
|
* the JSON. The JSON is the artifact the style bundle imports; THIS is where a
|
|
15
14
|
* glyph is edited, because a path `d` is not a thing a person edits by hand.
|
|
16
15
|
*
|
|
17
|
-
* Keys are
|
|
16
|
+
* Keys are icon NAMES (`icon.iconName`) — the editor's one icon
|
|
18
17
|
* primitive swaps a glyph in per name and falls through to its own for any
|
|
19
18
|
* name absent here, so this set is partial by design.
|
|
20
19
|
*/
|
|
@@ -72,27 +71,7 @@ const TAB_ICON_SOURCES = {
|
|
|
72
71
|
'properties-data-empty': 'empty_data', // no data → ICON_EMPTY_DATA
|
|
73
72
|
};
|
|
74
73
|
|
|
75
|
-
/**
|
|
76
|
-
* THE OUTLINER'S MARKS — every icon `tree_element_get_icon`
|
|
77
|
-
* (`outliner_draw.cc:2619-2945`) and `tree_element_get_icon_from_id`
|
|
78
|
-
* (`:2479-2610`) can pick for a row of Blender's View Layer tree, by FAMILY
|
|
79
|
-
* rather than one line per mark (WORK.md §Blender in the tab is Blender,
|
|
80
|
-
* "Inspection parity", I3).
|
|
81
|
-
*
|
|
82
|
-
* A FAMILY AND NOT A LIST, deliberately. The door reports whatever `ICON_*`
|
|
83
|
-
* Blender picked, and the two places that pick are a `switch` over ID codes and
|
|
84
|
-
* `RNA`'s own enum items (`rna_enum_object_type_items`,
|
|
85
|
-
* `rna_enum_object_modifier_type_items`, `rna_enum_constraint_type_items`) —
|
|
86
|
-
* sixty-three modifier marks and twenty-eight constraint marks among them. A
|
|
87
|
-
* table of names here would be a transcription of those enums maintained in a
|
|
88
|
-
* second place, and the first mark it fell behind on would draw a Font Awesome
|
|
89
|
-
* circle in the middle of Blender's tree. The prefix is what the enums
|
|
90
|
-
* themselves are named by, so tracing the prefix traces the enum.
|
|
91
|
-
*
|
|
92
|
-
* Each traced glyph is named `blender-<file>` with dashes — which is exactly
|
|
93
|
-
* what `blender-outliner-authoring.ts`'s `blenderOutlinerKind` spells from the
|
|
94
|
-
* icon name the door sends, so the two meet with no table at all.
|
|
95
|
-
*/
|
|
74
|
+
/** Trace Outliner icon families from tree_element_get_icon, tree_element_get_icon_from_id and RNA enum prefixes. blenderOutlinerKind derives the same blender-<file> names without a second per-icon lookup table. */
|
|
96
75
|
const OUTLINER_ICON_FAMILIES = [
|
|
97
76
|
// `icon_from_object_type` and `tree_element_get_icon_from_id`'s data marks.
|
|
98
77
|
'outliner_',
|
|
@@ -159,14 +138,9 @@ const OUTLINER_ICON_NAMES = [
|
|
|
159
138
|
const TRACED = new URL('./blender.icons.traced.json', import.meta.url);
|
|
160
139
|
|
|
161
140
|
/**
|
|
162
|
-
* `--trace`
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* tab is Blender, "Inspection parity"), and a generator that silently emits
|
|
166
|
-
* different glyphs depending on whether a sibling directory happens to exist
|
|
167
|
-
* is worse than one that needs a flag. The JSON it writes is checked in and
|
|
168
|
-
* records each glyph's source path and sha256, so a later trace that disagrees
|
|
169
|
-
* is a visible diff rather than a mystery.
|
|
141
|
+
* `--trace` rebuilds the traced paths from a Blender checkout at the engine's
|
|
142
|
+
* pin, selected by BLENDER_SRC or the default checkout path below. Ordinary
|
|
143
|
+
* generation consumes the saved JSON, which records each source path and sha256.
|
|
170
144
|
*/
|
|
171
145
|
if (process.argv.includes('--trace')) {
|
|
172
146
|
const checkout = process.env['BLENDER_SRC'] ?? `${homedir()}/volter/blender-src`;
|
|
@@ -230,8 +204,7 @@ if (process.argv.includes('--trace')) {
|
|
|
230
204
|
} catch {
|
|
231
205
|
throw new Error(
|
|
232
206
|
`${relative} is not in the Blender checkout at ${checkout}. Set BLENDER_SRC, or add ` +
|
|
233
|
-
`the directory to the sparse checkout: \`git sparse-checkout add release/datafiles/icons_svg
|
|
234
|
-
'followed by the LFS-off checkout line in WORK.md.',
|
|
207
|
+
`the directory to the sparse checkout: \`git sparse-checkout add release/datafiles/icons_svg\`.`,
|
|
235
208
|
);
|
|
236
209
|
}
|
|
237
210
|
if (svg.startsWith('version https://git-lfs')) {
|
|
@@ -267,17 +240,7 @@ const f = (n) => {
|
|
|
267
240
|
return Object.is(r, -0) ? 0 : r;
|
|
268
241
|
};
|
|
269
242
|
|
|
270
|
-
/**
|
|
271
|
-
* TWO GLOBAL DIALS, set against the running chrome rather than guessed. The
|
|
272
|
-
* editor paints a glyph at the ACTIVE font size — an 11px hierarchy row, a
|
|
273
|
-
* 10px status bar — where a 1.3-unit stroke in a 16-unit box lands at 0.9
|
|
274
|
-
* device pixels and greys out. `S` grows the optical size to ~14.5 units
|
|
275
|
-
* inside the 16 box — Blender's glyphs FILL their box where Font Awesome's
|
|
276
|
-
* carry padding, and measured against the reference frames that ratio (ink
|
|
277
|
-
* over row height) is what makes a row read as Blender's; `W` fattens every
|
|
278
|
-
* stroke so it still covers a pixel in an 11px row. Geometry below is
|
|
279
|
-
* authored on the plain 16 grid and both dials are applied here, once.
|
|
280
|
-
*/
|
|
243
|
+
/** S sets optical extent within the 16-unit grid; W gives strokes enough coverage at small chrome sizes. Apply both once to authored geometry. */
|
|
281
244
|
const S = 1.22;
|
|
282
245
|
const W = 1.12;
|
|
283
246
|
const p = (x, y) => `${f(8 + (x - 8) * S)} ${f(8 + (y - 8) * S)}`;
|
|
@@ -287,28 +250,10 @@ const rr = (r) => f(r * S);
|
|
|
287
250
|
/**
|
|
288
251
|
* A straight stroke with round ends: the capsule its outline encloses.
|
|
289
252
|
*
|
|
290
|
-
* THE SWEEP FLAG IS 0
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* at a polyline's joint the two bites ate a round notch out of the corner.
|
|
295
|
-
* Nothing reported it because at the 14 px chrome size the bite is one device
|
|
296
|
-
* pixel; it was found at 6x in the editor-type well, whose chevron drew as
|
|
297
|
-
* two arrowheads with a hole where they meet (rendered side by side, sweep 1
|
|
298
|
-
* against sweep 0, before the flag was touched).
|
|
299
|
-
*
|
|
300
|
-
* Closed shapes change too, and toward this docblock's own words: `line(…,
|
|
301
|
-
* closed)` and `rframe` corners were each losing a notch to the same bites,
|
|
302
|
-
* half-covered by the neighbouring capsule.
|
|
303
|
-
*
|
|
304
|
-
* THE BLAST RADIUS, MEASURED rather than reasoned (every glyph rasterized at
|
|
305
|
-
* 64 px both ways and the masks differenced): 97 of the 146 paths move. 96 of
|
|
306
|
-
* them ONLY GAIN ink, at an end or a corner — a knockout that is not there
|
|
307
|
-
* can take nothing. The 97th, `layer-group`, gains 199 px and loses one, on an
|
|
308
|
-
* antialiased boundary. Nothing here was tuned against the bitten rendering:
|
|
309
|
-
* the two glyphs this set fits pixel-by-pixel to a reference frame
|
|
310
|
-
* (`properties-modifiers`, and `outliner`/`properties` below) are built from
|
|
311
|
-
* `poly` and `rrect` and do not move at all.
|
|
253
|
+
* THE SWEEP FLAG IS 0. With sweep 1 each end arc would bulge INWARD instead of
|
|
254
|
+
* outward, so the path would be the rectangle MINUS two half-discs: every open
|
|
255
|
+
* end with a crescent bitten out of it, and at a polyline's joint a round notch
|
|
256
|
+
* out of the corner.
|
|
312
257
|
*/
|
|
313
258
|
function cap(x1, y1, x2, y2, width = 1.3) {
|
|
314
259
|
const w = width * W;
|
|
@@ -342,20 +287,7 @@ function ring(cx, cy, r, width = 1.3) {
|
|
|
342
287
|
return dot(cx, cy, r + w / 2) + dot(cx, cy, Math.max(r - w / 2, 0.01), true);
|
|
343
288
|
}
|
|
344
289
|
|
|
345
|
-
/**
|
|
346
|
-
* A polyline stroked with round ends and round joins.
|
|
347
|
-
*
|
|
348
|
-
* THE JOINS NEED NO EXTRA DISC, and stamping one was actively destructive.
|
|
349
|
-
* Each `cap` ends in a full semicircle of radius w/2 centred on the join, so
|
|
350
|
-
* the union of two capsules already contains the whole join disc — the joins
|
|
351
|
-
* are round for free. The disc this used to add at every interior vertex had
|
|
352
|
-
* the opposite winding to the capsules it landed in, so under
|
|
353
|
-
* `fill-rule: nonzero` it PUNCHED A HOLE: render `line([[3,3],[3,13],[13,13]])`
|
|
354
|
-
* at 24x and the corner is a white ring around a black core, where the same
|
|
355
|
-
* two `cap` calls alone give a solid corner. At 14px that hole is a sub-pixel
|
|
356
|
-
* dark pip on every corner of every stroked glyph in the set — the
|
|
357
|
-
* "greys out at a small size" symptom, dropped here for all 130 at once.
|
|
358
|
-
*/
|
|
290
|
+
/** Round-ended capsules already cover the join disc. An extra disc reverses winding under nonzero fill and cuts a hole in the join. */
|
|
359
291
|
function line(pts, w = 1.3, closed = false) {
|
|
360
292
|
let d = '';
|
|
361
293
|
const n = pts.length;
|
|
@@ -386,20 +318,7 @@ function poly(pts, hole = false) {
|
|
|
386
318
|
return `M${wound.map(([x, y]) => p(x, y)).join('L')}Z`;
|
|
387
319
|
}
|
|
388
320
|
|
|
389
|
-
/**
|
|
390
|
-
* A filled rectangle with equal corner radii.
|
|
391
|
-
*
|
|
392
|
-
* IT WINDS THE WAY `poly` AND `cap` WIND, and that is load-bearing, not a
|
|
393
|
-
* detail: for most of this file's life the rounded branch ran the other way
|
|
394
|
-
* round the corners, so under `fill-rule: nonzero` every `holeBar`/`dot(ccw)`
|
|
395
|
-
* aimed at a rounded rect UNIONED instead of punching, and every ordinary
|
|
396
|
-
* `poly`/`dot` drawn over one punched a hole instead of adding ink. Nothing
|
|
397
|
-
* reported it — a knockout that does nothing just looks like a solid glyph.
|
|
398
|
-
* It silently ate the collection box's pull slot, the wrench's fork and the
|
|
399
|
-
* bulb's screw thread, and it is why the pan hand carried a bite out of its
|
|
400
|
-
* palm where its thumb should be. Measured, not reasoned: render
|
|
401
|
-
* `rrect(r) + holeBar` and read the centre pixel.
|
|
402
|
-
*/
|
|
321
|
+
/** A rounded rectangle winds in the same direction as poly and cap so ordinary geometry unions and reversed geometry cuts holes under nonzero fill. */
|
|
403
322
|
function rrect(x, y, w, h, r = 0) {
|
|
404
323
|
const rad = Math.min(r, w / 2, h / 2);
|
|
405
324
|
if (rad <= 0.01)
|
|
@@ -417,19 +336,8 @@ function rrect(x, y, w, h, r = 0) {
|
|
|
417
336
|
);
|
|
418
337
|
}
|
|
419
338
|
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
*
|
|
423
|
-
* A corner radius SMALLER than the stroke's own round end is not a corner
|
|
424
|
-
* radius, it is a defect: the bevel becomes a segment shorter than the capsule
|
|
425
|
-
* is wide, its capsule overlaps both neighbours almost entirely, and the
|
|
426
|
-
* overlap cancels under `fill-rule: nonzero` — at 12x, `border-all` (r 0.4,
|
|
427
|
-
* t 1) and `object-group` (r 0.3, t 2.1) both showed a hollow pip at every
|
|
428
|
-
* corner. Below `t/2` the caps already round the corner by more than `r`
|
|
429
|
-
* asked for, so the plain four-point path is both cleaner and closer to the
|
|
430
|
-
* request.
|
|
431
|
-
*/
|
|
432
|
-
function rframe(x, y, w, h, t = 1.3, r = 0) {
|
|
339
|
+
/** A rectangular outline uses rounded joins. For r at or below half the stroke width, use a plain four-point path; the caps already provide the requested corner rounding. */
|
|
340
|
+
function rframe(x, y, w, h, t, r = 0) {
|
|
433
341
|
const inset = t / 2;
|
|
434
342
|
if (r <= inset) {
|
|
435
343
|
return line(
|
|
@@ -453,22 +361,18 @@ function rframe(x, y, w, h, t = 1.3, r = 0) {
|
|
|
453
361
|
[x + inset, y + h - inset - r],
|
|
454
362
|
[x + inset, y + inset + r],
|
|
455
363
|
];
|
|
456
|
-
return line(
|
|
364
|
+
return line(pts, t, true);
|
|
457
365
|
}
|
|
458
366
|
|
|
459
367
|
/**
|
|
460
368
|
* A stroked arc, angles in degrees, 0 = +x, growing clockwise on screen.
|
|
461
369
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* capsules overlap almost entirely, and the overlaps cancel under
|
|
465
|
-
* `fill-rule: nonzero` — rendered at 24x, `rotate`'s arc and `lock`'s shackle
|
|
466
|
-
* came out as a row of hollow circles rather than a stroke. A band has no
|
|
467
|
-
* overlaps to cancel: out along the outer radius, back along the inner.
|
|
370
|
+
* One annulus band follows the outer radius and returns along the inner
|
|
371
|
+
* radius, preserving a continuous fill without overlapping capsule windings.
|
|
468
372
|
*/
|
|
469
|
-
function arc(cx, cy, r, a0, a1, w
|
|
373
|
+
function arc(cx, cy, r, a0, a1, w) {
|
|
470
374
|
const t = (w * W) / 2;
|
|
471
|
-
const n =
|
|
375
|
+
const n = Math.max(6, Math.ceil(Math.abs(a1 - a0) / 9));
|
|
472
376
|
const at = (i, rad) => {
|
|
473
377
|
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
474
378
|
return [cx + Math.cos(a) * rad, cy + Math.sin(a) * rad];
|
|
@@ -481,7 +385,7 @@ function arc(cx, cy, r, a0, a1, w = 1.3, steps = 0) {
|
|
|
481
385
|
}
|
|
482
386
|
|
|
483
387
|
/** A stroked elliptical arc — cylinder lips, cloud shoulders, orbits. */
|
|
484
|
-
function ellipseArc(cx, cy, rx, ry, a0, a1, w
|
|
388
|
+
function ellipseArc(cx, cy, rx, ry, a0, a1, w, rot = 0, closed = false) {
|
|
485
389
|
const t = (rot * Math.PI) / 180;
|
|
486
390
|
const n = Math.max(4, Math.ceil(Math.abs(a1 - a0) / 15));
|
|
487
391
|
const pts = [];
|
|
@@ -539,7 +443,7 @@ function holePoly(pts) {
|
|
|
539
443
|
|
|
540
444
|
/** Blender's chevron: a thin V, the outliner's disclosure and every menu arrow. */
|
|
541
445
|
function chevron(cx, cy, s, dir) {
|
|
542
|
-
const v = { right: [1, 0], left: [-1, 0], down: [0, 1]
|
|
446
|
+
const v = { right: [1, 0], left: [-1, 0], down: [0, 1] }[dir];
|
|
543
447
|
const perp = [-v[1], v[0]];
|
|
544
448
|
const tip = [cx + v[0] * s * 0.55, cy + v[1] * s * 0.55];
|
|
545
449
|
const back = -s * 0.55;
|
|
@@ -553,14 +457,7 @@ function chevron(cx, cy, s, dir) {
|
|
|
553
457
|
);
|
|
554
458
|
}
|
|
555
459
|
|
|
556
|
-
/**
|
|
557
|
-
* Blender's isometric cube — a SOLID hexagonal silhouette with its three
|
|
558
|
-
* interior seams knocked out, never a wire box. `outliner.png` at matched
|
|
559
|
-
* scale settles it: every object mark in a row is a filled silhouette with its
|
|
560
|
-
* detail punched through, and the wire version of this glyph collapsed into an
|
|
561
|
-
* undifferentiated grey circle at the 14px row size (its three 1.15-unit
|
|
562
|
-
* spokes merged and the hexagon's corners rounded away).
|
|
563
|
-
*/
|
|
460
|
+
/** Blender’s isometric cube is a filled hexagonal silhouette with three interior seams knocked out. */
|
|
564
461
|
function isoCube(cx = 8, cy = 8, s = 5.4, seam = 1.05) {
|
|
565
462
|
const top = [cx, cy - s];
|
|
566
463
|
const tr = [cx + s * 0.87, cy - s * 0.5];
|
|
@@ -577,16 +474,10 @@ function isoCube(cx = 8, cy = 8, s = 5.4, seam = 1.05) {
|
|
|
577
474
|
}
|
|
578
475
|
|
|
579
476
|
// ------------------------------------------------------------------- glyphs
|
|
580
|
-
// Keyed by
|
|
477
|
+
// Keyed by icon name. Grouped the way the chrome reads them.
|
|
581
478
|
|
|
582
479
|
const glyphs = {};
|
|
583
|
-
/**
|
|
584
|
-
* `meta` is the glyph's COLOUR channel (`IconSetContribution`): `tone` names
|
|
585
|
-
* a category the palette inks, and `tonedPath` splits the drawing so only
|
|
586
|
-
* that second path takes it. Whole-glyph tones are assigned from one table
|
|
587
|
-
* further down, next to the frames they were measured in; `tonedPath` has to
|
|
588
|
-
* be authored HERE, because it is a division of the geometry.
|
|
589
|
-
*/
|
|
480
|
+
/** meta carries category tone and optional tonedPath. A whole-glyph tone belongs to the table below; a tonedPath is part of the geometry. */
|
|
590
481
|
const g = (name, d, meta) => {
|
|
591
482
|
glyphs[name] = { path: d, ...(meta ?? {}) };
|
|
592
483
|
};
|
|
@@ -595,18 +486,7 @@ const g = (name, d, meta) => {
|
|
|
595
486
|
g('chevron-right', chevron(7.4, 8, 3.1, 'right'));
|
|
596
487
|
g('chevron-left', chevron(8.6, 8, 3.1, 'left'));
|
|
597
488
|
g('chevron-down', chevron(8, 7.4, 3.1, 'down'));
|
|
598
|
-
//
|
|
599
|
-
// trigger's disclosure and a tree row's expander — Blender draws an open
|
|
600
|
-
// CHEVRON, measured in two frames: `modeling.png` at its native 2x, the
|
|
601
|
-
// orientation well's mark at x 1304..1316, y 76..83 (13x8 device px = 6.5x4
|
|
602
|
-
// CSS, peak 216); and `outliner.png` (also native 2x), the collapsed Camera
|
|
603
|
-
// row's mark at x 96..105, y 146..163 (10x18 = 5x9 CSS) and the expanded
|
|
604
|
-
// Collection row's at x 52..69, y 110..119 (18x10 = 9x5 CSS), peak 194. Both
|
|
605
|
-
// are a 1 CSS px stroke and both run 1.8:1, which is exactly what `chevron()`
|
|
606
|
-
// draws. The names stay `caret-*` — a set draws the editor's names its own
|
|
607
|
-
// way, and Font Awesome's filled triangle is still what every other skin gets.
|
|
608
|
-
// `s` is 4.4 rather than `chevron-*`'s 3.1 so the mark keeps the EXTENT the
|
|
609
|
-
// filled caret had here (8.8 of 16 units, ~7 CSS px against Blender's 6.5).
|
|
489
|
+
// Caret names draw open chevrons. Their 4.4-unit extent preserves the dropdown caret’s size; chevron-* uses 3.1.
|
|
610
490
|
g('caret-right', chevron(7.4, 8, 4.4, 'right'));
|
|
611
491
|
g('caret-down', chevron(8, 7.4, 4.4, 'down'));
|
|
612
492
|
g(
|
|
@@ -686,24 +566,11 @@ g(
|
|
|
686
566
|
cap(9.3, 7.4, 9.1, 11.5, 1.1),
|
|
687
567
|
);
|
|
688
568
|
g('magnifying-glass', ring(7.1, 7.1, 3.7, 1.3) + cap(9.9, 9.9, 13.2, 13.2, 1.4));
|
|
689
|
-
/**
|
|
690
|
-
|
|
691
|
-
* agree: the Search field's magnifier (`outliner.png`, `properties-object.png`)
|
|
692
|
-
* is a thin RING with a handle, while the viewport navigation cluster's zoom
|
|
693
|
-
* (`modeling-object-none.png`) is a SOLID lens disc with the sign knocked
|
|
694
|
-
* through it and a short fat handle. So `magnifying-glass` keeps its ring and
|
|
695
|
-
* the two signed ones become lenses; drawn as rings, their 1.1-unit sign
|
|
696
|
-
* strokes were the faintest ink on the whole navigation cluster.
|
|
697
|
-
*/
|
|
698
|
-
// The handle STARTS INSIDE the lens (9.3,9.3 is 3.1 from the centre of a
|
|
699
|
-
// 4.25 disc): begun at 10.2 it cleared the disc's edge by a tenth of a unit
|
|
700
|
-
// and the round cap floated off as a separate lozenge at 16px — measured in
|
|
701
|
-
// round 1's frame, where the navigation cluster showed a disc and a diamond.
|
|
569
|
+
/** Search uses a ring magnifier; viewport zoom uses a solid lens with a cut-out sign. */
|
|
570
|
+
// Start the handle inside the lens so its rounded cap joins the disc.
|
|
702
571
|
const zoomLens = (sign) => dot(7.1, 7.1, 4.25) + sign + cap(9.3, 9.3, 13.4, 13.4, 2);
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
// Its arms are Blender's on `gizmo-select-box.png`'s cluster: 14 device px across a 26 px
|
|
706
|
-
// lens, 2.2 px thick.
|
|
572
|
+
// Draw the plus as one cross-shaped hole; crossing two hole bars fills their shared center under nonzero winding.
|
|
573
|
+
// Its arms span 14 of the 26-device-pixel lens and are 2.2 pixels thick.
|
|
707
574
|
const plusHole = (cx, cy, a, t) =>
|
|
708
575
|
poly(
|
|
709
576
|
[
|
|
@@ -730,17 +597,7 @@ g('grip-lines-vertical', cap(6.3, 3, 6.3, 13, 1.2) + cap(9.7, 3, 9.7, 13, 1.2));
|
|
|
730
597
|
g('copy', rframe(2.4, 2.4, 8.4, 8.4, 1.2, 1) + rframe(5.9, 5.9, 8.4, 8.4, 1.2, 1));
|
|
731
598
|
|
|
732
599
|
// -- visibility, the outliner's right column ------------------------------
|
|
733
|
-
/**
|
|
734
|
-
* The eye is the KNOCKOUT construction the header describes, and it is what
|
|
735
|
-
* `outliner.png` shows at matched scale: one solid almond, a dark iris punched
|
|
736
|
-
* out of it, a bright pupil inside the iris — white / dark / white, three
|
|
737
|
-
* bands that survive a 14px row. Drawn instead as two thin lid strokes with a
|
|
738
|
-
* dot between them (what this was through round 0), every band is a
|
|
739
|
-
* partial-coverage stroke: measured against its own row, the glyph peaked at
|
|
740
|
-
* p95 145 where the label beside it peaked at 185, while Blender's peaks
|
|
741
|
-
* ABOVE its label (151 vs 129). A silhouette reaches full ink; a hairline
|
|
742
|
-
* cannot.
|
|
743
|
-
*/
|
|
600
|
+
/** The eye is a solid almond with a dark iris cut out and a bright pupil inside, preserving three visible bands in small rows. */
|
|
744
601
|
const eyeLid = [
|
|
745
602
|
[1.8, 8],
|
|
746
603
|
[3.6, 5.3],
|
|
@@ -767,12 +624,7 @@ g('cubes-stacked', isoCube(8, 4.6, 3.3, 0.8) + isoCube(8, 11.4, 3.3, 0.8));
|
|
|
767
624
|
g('square', rframe(3, 3, 10, 10, 1.3));
|
|
768
625
|
g('circle', ring(8, 8, 4.4, 1.3));
|
|
769
626
|
g('circle-dot', dot(8, 8, 6) + dot(8, 8, 3.9, true) + dot(8, 8, 2));
|
|
770
|
-
// Blender
|
|
771
|
-
// Filled body, filled lid band, and the pull slot KNOCKED OUT of the body —
|
|
772
|
-
// exactly what `outliner.png` shows at matched scale, where Blender's
|
|
773
|
-
// Collection is the brightest mark in the row. Drawn as a 1.25-unit frame
|
|
774
|
-
// under a filled lid, it read as a hairline rectangle no heavier than the
|
|
775
|
-
// label beside it.
|
|
627
|
+
// Blender’s collection mark is a filled, lidded box with a pull slot knocked out of its body.
|
|
776
628
|
const box =
|
|
777
629
|
rrect(2.6, 5.4, 10.8, 8, 0.6) + rrect(2, 2.4, 12, 2.6, 0.5) + holeBar(6.2, 8.6, 9.8, 8.6, 1.6);
|
|
778
630
|
g('box-archive', box);
|
|
@@ -969,15 +821,7 @@ g(
|
|
|
969
821
|
'table-cells-large',
|
|
970
822
|
rframe(2, 3, 12, 10, 1.2, 0.5) + cap(2.6, 8, 13.4, 8, 1.15) + cap(8, 3.4, 8, 12.6, 1.15),
|
|
971
823
|
);
|
|
972
|
-
/**
|
|
973
|
-
* A FLAT 3x3 grid, and it STAYS FLAT. Four sites ask for `border-all` and
|
|
974
|
-
* three of them mean a flat grid — the canvas viewport's grid toggle, the
|
|
975
|
-
* 3D viewport overlay's, and the CSS border editor's "all borders" — so the
|
|
976
|
-
* trapezoid Blender draws on the navigation cluster's projection toggle is
|
|
977
|
-
* NOT this glyph's to become. It gets its own name below, which is the same
|
|
978
|
-
* ruling `editorIcons.modeling`'s docblock already records for the mesh
|
|
979
|
-
* operators: a glyph that IS an operation owns its own name.
|
|
980
|
-
*/
|
|
824
|
+
/** border-all draws a flat 3×3 grid for grid and border controls. The viewport’s perspective projection toggle has its own glyph name. */
|
|
981
825
|
g(
|
|
982
826
|
'border-all',
|
|
983
827
|
rframe(3.2, 3.2, 9.6, 9.6, 1, 0.4) +
|
|
@@ -986,32 +830,7 @@ g(
|
|
|
986
830
|
cap(3.7, 6.4, 12.3, 6.4, 0.85) +
|
|
987
831
|
cap(3.7, 9.6, 12.3, 9.6, 0.85),
|
|
988
832
|
);
|
|
989
|
-
/**
|
|
990
|
-
* THE PROJECTION TOGGLE at the foot of the navigation cluster — Blender's
|
|
991
|
-
* fourth cell, and it is a PERSPECTIVE grid, not a square one. Read off
|
|
992
|
-
* `modeling-edit-none.png` (device x 2774..2805, y 541..572): a symmetric
|
|
993
|
-
* trapezoid 32 device wide by 26 tall, four horizontal rules whose spacing
|
|
994
|
-
* OPENS downward (6, 8, 10 device) and four verticals converging upward, the
|
|
995
|
-
* outer pair running x 6.8 -> 1.5 on the left and 24.2 -> 29.5 on the right.
|
|
996
|
-
* Ours was a flat square 27x26 — the height was already Blender's and the
|
|
997
|
-
* width was 5 device short, because a square cannot be as wide at its foot as
|
|
998
|
-
* a trapezoid.
|
|
999
|
-
*
|
|
1000
|
-
* The prior cut said so itself and stopped there, for the right reason: it
|
|
1001
|
-
* shared `border-all` with three flat-grid sites. The name is what was in the
|
|
1002
|
-
* way, so the name is what moved (`editorIcons.viewport.projection` is now
|
|
1003
|
-
* `viewport-projection`, carrying Font Awesome's `border-all` drawing as its
|
|
1004
|
-
* fallback, so a set without this key paints exactly what it painted before).
|
|
1005
|
-
* THE PRIOR CUT ALSO MIS-COUNTED IT as "four cells across": it is three, in
|
|
1006
|
-
* both axes — four rules bound three columns.
|
|
1007
|
-
*
|
|
1008
|
-
* Every constant below was seeded off that character map and then settled
|
|
1009
|
-
* against the cell's own half-coverage mask, the same instrument the wrench
|
|
1010
|
-
* and the hand use. Seeded 150, settled 7 of 374 reference ink px (the old
|
|
1011
|
-
* square: 316). No constant moved as much as one device pixel from its
|
|
1012
|
-
* reading, and the four rows moved by ONE SHARED half-pixel, which is the
|
|
1013
|
-
* cell's own centring rather than a fit.
|
|
1014
|
-
*/
|
|
833
|
+
/** The viewport projection glyph is a perspective grid with four rules bounding three columns. Horizontal spacing increases downward and vertical lines converge upward. */
|
|
1015
834
|
const perspectiveGrid = () => {
|
|
1016
835
|
const cx = 8.01; // the trapezoid's axis
|
|
1017
836
|
const yTop = 3.71; // its near and far rules
|
|
@@ -1037,11 +856,7 @@ const perspectiveGrid = () => {
|
|
|
1037
856
|
);
|
|
1038
857
|
};
|
|
1039
858
|
g('viewport-projection', perspectiveGrid());
|
|
1040
|
-
/*
|
|
1041
|
-
* VIEW_ORTHO, the same toggle in an orthographic view: a flat square grid of three by three
|
|
1042
|
-
* cells, as wide as the perspective trapezoid (32 device px on `gizmo-select-box.png`'s
|
|
1043
|
-
* navigation capsule at 2x) and centred with it, its rules the trapezoid's weight.
|
|
1044
|
-
*/
|
|
859
|
+
/* VIEW_ORTHO draws a centered 3×3 square grid at the perspective trapezoid’s width and rule weight. */
|
|
1045
860
|
const orthographicGrid = () => {
|
|
1046
861
|
const lo = 2.61;
|
|
1047
862
|
const hi = 13.41;
|
|
@@ -1089,9 +904,7 @@ g(
|
|
|
1089
904
|
[8.8, 11.4],
|
|
1090
905
|
]),
|
|
1091
906
|
);
|
|
1092
|
-
// The Light
|
|
1093
|
-
// a wide screw base, a foot. At the old 3.5 head it rendered ~7px of ink in a
|
|
1094
|
-
// 14px row and read as a dim pebble beside its own label.
|
|
907
|
+
// The Light glyph fills its slot with a bulb head, screw base and foot.
|
|
1095
908
|
g(
|
|
1096
909
|
'lightbulb',
|
|
1097
910
|
dot(8, 5.7, 4.3) +
|
|
@@ -1273,59 +1086,13 @@ g(
|
|
|
1273
1086
|
[12.6, 8.6],
|
|
1274
1087
|
]),
|
|
1275
1088
|
);
|
|
1276
|
-
//
|
|
1277
|
-
// the cluster's 16 px slot. Ours spanned 1.8..14.2, which the scale dial pushes
|
|
1278
|
-
// to ~15 px rendered — a hand a full size larger than the zoom beside it.
|
|
1089
|
+
// Keep the pan hand compact at about 13 pixels of ink within its 16-pixel slot.
|
|
1279
1090
|
g(
|
|
1280
1091
|
'hand-pointer',
|
|
1281
|
-
//
|
|
1282
|
-
// Round 2's shrink left ours 18x20: right width, three pixels too tall,
|
|
1283
|
-
// because the middle finger still reached the top of the box.
|
|
1092
|
+
// Keep the pan hand square within the navigation glyph box.
|
|
1284
1093
|
rrect(6.7, 3.9, 2.8, 5.8, 1.4) + rrect(4.9, 8, 8.2, 5.1, 2) + dot(5.2, 9.3, 1.3),
|
|
1285
1094
|
);
|
|
1286
|
-
|
|
1287
|
-
* THE PAN TOOL'S OWN GLYPH. `editorIcons.viewport.pan` is `faHand`, whose
|
|
1288
|
-
* iconName is `hand` — a name this set did not carry, so the navigation
|
|
1289
|
-
* cluster drew Font Awesome's hand and nothing else on the Model workspace
|
|
1290
|
-
* did (checked by resolving every `fa*` identifier in `packages/editor/src`
|
|
1291
|
-
* and `packages/mesh/contributions` to its `iconName` and differencing
|
|
1292
|
-
* against these keys: `hand` was the only genuine miss; `faExclamationTriangle`
|
|
1293
|
-
* and `faTimesCircle` are FA-5 aliases that resolve to `triangle-exclamation`
|
|
1294
|
-
* and `circle-xmark`, both present).
|
|
1295
|
-
*
|
|
1296
|
-
* RE-MEASURED 2026-09-19 against the capsule's SECOND CELL on
|
|
1297
|
-
* `modeling-edit-none.png` (device x 2774..2805, y 421..452 — the cluster's
|
|
1298
|
-
* cells are 32x32 device and this one's glyph FILLS it, 32x32 of ink). Ours
|
|
1299
|
-
* inked 28x27: "15x15 in a 16 px slot" was a reading of an earlier frame and
|
|
1300
|
-
* of the wrong pixels; Blender's pan hand is 16.0 x 16.0 CSS, the same box its
|
|
1301
|
-
* magnifier fills, which is the whole point — the cluster's glyphs are one
|
|
1302
|
-
* size. Three things were wrong besides the size and each is visible at 8x:
|
|
1303
|
-
* the fingers were PARALLEL and touching where Blender SPLAYS them with a
|
|
1304
|
-
* clear gap (the index leans right going down, the ring leans left, only the
|
|
1305
|
-
* middle is vertical); the thumb was a horizontal block at the palm's waist
|
|
1306
|
-
* where Blender's points LEFT and DOWN out of the palm's lower third; and the
|
|
1307
|
-
* palm was a flat-bottomed box where Blender's bottom is a wide dome. Together
|
|
1308
|
-
* they read as a mitten.
|
|
1309
|
-
*
|
|
1310
|
-
* FITTED THE WAY `modifierWrench` WAS: the structure read off the cell as a
|
|
1311
|
-
* character map first (four splayed finger capsules, a domed palm, a thumb),
|
|
1312
|
-
* then every constant settled by rendering at the reference's own scale and
|
|
1313
|
-
* counting the pixels where the two HALF-COVERAGE masks disagree. Seeded from
|
|
1314
|
-
* the map alone it scored 106 of 534 reference ink px against the old glyph's
|
|
1315
|
-
* 277; settled, 39, of which seven are the dome's last row. Every finger constant stayed within a device pixel of its
|
|
1316
|
-
* own reading. THE PALM'S DID NOT, and that is honest rather than hidden: an
|
|
1317
|
-
* axis-aligned rounded rect cannot be Blender's asymmetric dome (the thumb's
|
|
1318
|
-
* mass makes its bottom-left tighter than its bottom-right), so the fit moved
|
|
1319
|
-
* the palm 2 device px left to trade that error against the thumb's. The one
|
|
1320
|
-
* run of reference ink we do not cover is the dome's last row.
|
|
1321
|
-
*
|
|
1322
|
-
* THE INSTRUMENT, stated because it decides every number here: our render box
|
|
1323
|
-
* is the `2xl` rung, 18 CSS = 36 device, while Blender's cell is 32 — so a
|
|
1324
|
-
* comparison crops our 36 px raster to its central 32, and a glyph that FILLS
|
|
1325
|
-
* Blender's cell occupies only 32/36 of our own viewBox. Reading ours at a
|
|
1326
|
-
* loose coverage cut instead of half makes it measure a pixel wider than it
|
|
1327
|
-
* is, which is the trap the mesh-data glyph's note already records.
|
|
1328
|
-
*/
|
|
1095
|
+
|
|
1329
1096
|
g(
|
|
1330
1097
|
'hand',
|
|
1331
1098
|
// Four fingers, each a capsule from its own cap centre down into the palm.
|
|
@@ -1337,24 +1104,7 @@ g(
|
|
|
1337
1104
|
cap(3.3, 8.99, 5.38, 10.82, 2.0), // the thumb, out of the palm's lower third
|
|
1338
1105
|
);
|
|
1339
1106
|
g(
|
|
1340
|
-
/**
|
|
1341
|
-
* The MOVE tool. Blender's (`modeling-edit-none.png`, tool shelf) is four
|
|
1342
|
-
* SOLID arrowheads standing apart on short stubs around an empty centre —
|
|
1343
|
-
* not a crosshair. Ours was one continuous 1.2-unit cross with 1.15-unit
|
|
1344
|
-
* chevrons on its ends, which at matched scale is a thin plus sign with
|
|
1345
|
-
* ticks: a different mark, and the lightest thing on the rail.
|
|
1346
|
-
*
|
|
1347
|
-
* RE-MEASURED 2026-09-19 off the same frame, this time as an ASCII map in
|
|
1348
|
-
* AUTHORED units (0.5/char) rather than by eye: the previous fit was RIGHT
|
|
1349
|
-
* IN FORM AND WRONG IN SIZE AND PARTS. Ours inked 35x34 device px against
|
|
1350
|
-
* Blender's 46x46 — 74% — because the heads stopped at radius 6.4 where
|
|
1351
|
-
* Blender's tips reach the box edge (radius 8, base at 5.5, half-width
|
|
1352
|
-
* 2.4); the STUBS were missing entirely (a 1.0-unit bar from radius 2.5 to
|
|
1353
|
-
* 4.6 under each head); and a 1.35 centre dot was drawn where Blender's
|
|
1354
|
-
* centre is empty. Size is not a detail on this rail — the same frame's
|
|
1355
|
-
* inset and bevel marks ink 40x40 and ours match them to a pixel, so the
|
|
1356
|
-
* transform group was the odd one out in its own column.
|
|
1357
|
-
*/
|
|
1107
|
+
/** Move: four solid arrowheads on separate stubs around an empty centre. */
|
|
1358
1108
|
'up-down-left-right',
|
|
1359
1109
|
[
|
|
1360
1110
|
[0, -1],
|
|
@@ -1374,20 +1124,7 @@ g(
|
|
|
1374
1124
|
);
|
|
1375
1125
|
g('arrows-up-down-left-right', glyphs['up-down-left-right'].path);
|
|
1376
1126
|
g(
|
|
1377
|
-
/**
|
|
1378
|
-
* The ROTATE tool. Blender draws a heavy arc with a SOLID arrowhead at each
|
|
1379
|
-
* end and a solid diamond at the pivot; ours was a 1.3-unit open circle with
|
|
1380
|
-
* one small tick, which at matched scale is an empty ring — the pivot, the
|
|
1381
|
-
* direction and the weight all missing.
|
|
1382
|
-
*
|
|
1383
|
-
* RE-MEASURED 2026-09-19 (ASCII map in authored units, and the cell read at
|
|
1384
|
-
* 8x): it is TWO arcs, not one — the top arc ends at the LEFT in a
|
|
1385
|
-
* DOWN-pointing triangle and the bottom arc ends at the RIGHT in an
|
|
1386
|
-
* UP-pointing one, the ordinary two-headed circular arrow, where the
|
|
1387
|
-
* previous fit drew one dome with both heads under its ends and no bottom
|
|
1388
|
-
* arc at all. And it was 72% of Blender's size (33x29 device against
|
|
1389
|
-
* 46x38): the ring's radius is 6.5 of the 16 box, not 4.3.
|
|
1390
|
-
*/
|
|
1127
|
+
/** Rotate: two heavy arcs with opposing solid arrowheads and a diamond pivot. */
|
|
1391
1128
|
'rotate',
|
|
1392
1129
|
arc(8, 7.8, 7.0, 200, 336, 0.8) +
|
|
1393
1130
|
arc(8, 7.8, 7.0, 20, 156, 0.8) +
|
|
@@ -1497,25 +1234,12 @@ g(
|
|
|
1497
1234
|
/**
|
|
1498
1235
|
* THE OTHER TWO TOOLS OF BLENDER'S TRANSFORM GROUP, under names of ours.
|
|
1499
1236
|
*
|
|
1500
|
-
* Move and Rotate
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
* the host's Scale tool asked for `maximize` (Font Awesome's window-expand
|
|
1504
|
-
* cross) and its Transform tool for `arrows-to-dot` (a crosshair), and
|
|
1505
|
-
* Blender's marks for the two — a small square growing into a bigger one, and
|
|
1506
|
-
* a square inside a broken ring of arrows — are meaningless under those
|
|
1507
|
-
* generic meanings. Same reasoning as `editorIcons.modeling`'s `mesh-*`:
|
|
1508
|
-
* a glyph that IS an operation needs its own name or it recuts every other
|
|
1509
|
-
* site that asked for the generic one.
|
|
1237
|
+
* Move and Rotate share Font Awesome names (`up-down-left-right`, `rotate`).
|
|
1238
|
+
* Scale and Transform use operation-specific names so their Blender glyphs
|
|
1239
|
+
* do not replace generic window-expand or crosshair icons elsewhere.
|
|
1510
1240
|
*/
|
|
1511
1241
|
g(
|
|
1512
|
-
/**
|
|
1513
|
-
* SCALE. Measured on `modeling-edit-none.png` (button 5) as an ASCII map in
|
|
1514
|
-
* authored units: a 1-unit square OUTLINE from (2.0, 1.5) to (14.0, 13.5),
|
|
1515
|
-
* a SOLID square from (1.5, 7.5) to (8.0, 14.0) overlapping its lower-left
|
|
1516
|
-
* corner, and a diagonal arrow inside the frame running up-and-right from
|
|
1517
|
-
* (8.6, 6.9) to a head whose apex is (12.4, 3.1).
|
|
1518
|
-
*/
|
|
1242
|
+
/** Scale: an outlined square overlapped at lower left by a solid square, with a diagonal arrow inside. */
|
|
1519
1243
|
'tool-scale',
|
|
1520
1244
|
rframe(2, 1.5, 12, 12, 0.8) +
|
|
1521
1245
|
rrect(1.5, 7.6, 6.3, 6.3, 0.2) +
|
|
@@ -1526,13 +1250,7 @@ g(
|
|
|
1526
1250
|
[9.3, 4],
|
|
1527
1251
|
]),
|
|
1528
1252
|
);
|
|
1529
|
-
/*
|
|
1530
|
-
* SELECT BOX. Measured on `gizmo-select-box.png` (the lit first button, native 2x) and mapped onto
|
|
1531
|
-
* the 16-unit grid with the marquee spanning 1..15: a dashed square, 0.7 units thick, dashes about
|
|
1532
|
-
* 2.3 units with 1.1-unit gaps and an L at each corner, in the selection tools' orange (the
|
|
1533
|
-
* `select` tone, `tonedPath`); inside it a pointer from its tip (6.1, 4.5) down to (6.2, 11.5),
|
|
1534
|
-
* notched at (8.2, 9.4), to (11.2, 9.4).
|
|
1535
|
-
*/
|
|
1253
|
+
/* Select Box: an orange dashed marquee with corner marks and a pointer, authored on the 16-unit grid. */
|
|
1536
1254
|
const MARQUEE_T = 0.7;
|
|
1537
1255
|
const marquee =
|
|
1538
1256
|
// corners: an L each
|
|
@@ -1754,20 +1472,13 @@ g(
|
|
|
1754
1472
|
]),
|
|
1755
1473
|
);
|
|
1756
1474
|
g(
|
|
1757
|
-
/**
|
|
1758
|
-
* The Properties TOOL tab. Blender's (`properties-object.png`, top of the
|
|
1759
|
-
* tab rail, 14x14 at matched scale) is two tools standing UPRIGHT side by
|
|
1760
|
-
* side — a screwdriver and an open-end wrench, both solid. Ours was one
|
|
1761
|
-
* diagonal shaft with a ring at one end and a wedge at the other: at 14px it
|
|
1762
|
-
* reads as a single key, and the ring is an outline that greys out.
|
|
1763
|
-
*/
|
|
1475
|
+
/** The Tool tab draws an upright screwdriver and open-end wrench, both solid. */
|
|
1764
1476
|
'screwdriver-wrench',
|
|
1765
1477
|
// screwdriver: blade, collar, handle
|
|
1766
1478
|
rrect(3.5, 2.6, 1.4, 4.6, 0.3) +
|
|
1767
1479
|
rrect(2.9, 7.2, 2.6, 1.2, 0.3) +
|
|
1768
1480
|
rrect(3.1, 8.4, 2.2, 5, 0.8) +
|
|
1769
|
-
//
|
|
1770
|
-
// survive 14px — at 1.7 units it closed up and the head read as a block.
|
|
1481
|
+
// Wrench: a forked head over a shaft, with a 2.2-unit jaw opening for 14px use.
|
|
1771
1482
|
rrect(9.6, 6.6, 2.8, 6.8, 0.9) +
|
|
1772
1483
|
rrect(8.4, 2.6, 5.2, 4.6, 0.5) +
|
|
1773
1484
|
holeBar(11, 2, 11, 4.9, 2.2),
|
|
@@ -1799,19 +1510,9 @@ g(
|
|
|
1799
1510
|
cap(11.6, 2, 14, 4.4, 1.6),
|
|
1800
1511
|
);
|
|
1801
1512
|
g(
|
|
1802
|
-
/**
|
|
1803
|
-
* The MATERIAL section's mark (`inspection/compose.ts` maps /material/ here).
|
|
1804
|
-
* Blender's Material tab is a SOLID disc with quadrants knocked out of it;
|
|
1805
|
-
* ours was a 1.3-unit outline with three dots inside, and at the 14px rail
|
|
1806
|
-
* the outline greyed and the dots merged — it read as a cookie. Same
|
|
1807
|
-
* palette silhouette, drawn Blender's way: one filled blob, wells and thumb
|
|
1808
|
-
* hole punched THROUGH it.
|
|
1809
|
-
*/
|
|
1513
|
+
/** The Material section draws a solid palette with wells and a thumb hole cut through it. */
|
|
1810
1514
|
'palette',
|
|
1811
|
-
//
|
|
1812
|
-
// grew a handle off the bottom to sell the palette's notch and the glyph came
|
|
1813
|
-
// back a mushroom; Blender's Material tab is a compact rounded mass and this
|
|
1814
|
-
// matches that silhouette.
|
|
1515
|
+
// A compact palette silhouette with wells and thumb hole cut through it.
|
|
1815
1516
|
poly(
|
|
1816
1517
|
Array.from({ length: 28 }, (_, i) => {
|
|
1817
1518
|
const a = (i / 28) * Math.PI * 2;
|
|
@@ -1824,10 +1525,7 @@ g(
|
|
|
1824
1525
|
dot(10.4, 10.7, 2, true),
|
|
1825
1526
|
);
|
|
1826
1527
|
g(
|
|
1827
|
-
// A stack of plates:
|
|
1828
|
-
// glyph at a 14px row), the two beneath it strokes that read as its shadow.
|
|
1829
|
-
// Drawn as three outlines it was three grey lozenges with nothing solid in
|
|
1830
|
-
// any of them — the same failure the Collection box had.
|
|
1528
|
+
// A stack of plates: a solid top silhouette with two stroked plates beneath.
|
|
1831
1529
|
'layer-group',
|
|
1832
1530
|
poly([
|
|
1833
1531
|
[8, 2],
|
|
@@ -1853,10 +1551,7 @@ g(
|
|
|
1853
1551
|
),
|
|
1854
1552
|
);
|
|
1855
1553
|
g(
|
|
1856
|
-
// The
|
|
1857
|
-
// matched scale, a 1.1-unit frame with four nubs was indistinguishable from
|
|
1858
|
-
// the letterforms of its own label — same weight, same grey. Blender's is a
|
|
1859
|
-
// solid box: a filled band for the frame, and solid corner handles.
|
|
1554
|
+
// The group mark uses a solid box frame and solid corner handles.
|
|
1860
1555
|
'object-group',
|
|
1861
1556
|
rframe(2.4, 3.8, 11.2, 8.4, 2.1, 0.3) +
|
|
1862
1557
|
rrect(0.6, 2, 3.1, 3.1, 0.3) +
|
|
@@ -1866,63 +1561,35 @@ g(
|
|
|
1866
1561
|
);
|
|
1867
1562
|
|
|
1868
1563
|
// -- the mesh operators: THE OPERATION, ON A CUBE ---------------------------
|
|
1869
|
-
/**
|
|
1870
|
-
*
|
|
1871
|
-
* six are free to be what Blender's toolbar draws: not an abstraction of the
|
|
1872
|
-
* verb (an up-arrow for Extrude, scissors for Knife) but THE OPERATION PERFORMED
|
|
1873
|
-
* ON A CUBE — a cube with one face pulled out, a cube with an inset face, a cube
|
|
1874
|
-
* with a chamfered edge, a cube with a cut running round it. Measured in
|
|
1875
|
-
* `modeling-edit-none.png` (2x, halved): each glyph inks ~20x20 CSS px in a
|
|
1876
|
-
* 38x34 button (Extrude 20x23 — its lifted slab is taller), the cube is a
|
|
1877
|
-
* ~2 px wire with the operated element FILLED, and the cube sits in three-quarter
|
|
1878
|
-
* view: a front square with a top and a right face receding up-and-right.
|
|
1879
|
-
*
|
|
1880
|
-
* MONOCHROME, which is this set's contract (`blender.style.ts`) and a real
|
|
1881
|
-
* difference from the reference: Blender tints the operated element (green for
|
|
1882
|
-
* these six, purple for Smooth) and reads the operation by COLOUR as much as by
|
|
1883
|
-
* shape. Here the same job is done by FILL — the operated face is solid against
|
|
1884
|
-
* the wire — and whether the set gains an accent channel is its own decision,
|
|
1885
|
-
* not one to smuggle in through six glyphs.
|
|
1886
|
-
*/
|
|
1887
|
-
|
|
1888
|
-
/**
|
|
1889
|
-
* THE OPERATOR CUBE, once. `P(u, v, k)` is a point in the cube's own axes: `u`
|
|
1890
|
-
* across the front face, `v` up it, `k` back along the depth the top and right
|
|
1891
|
-
* faces recede along. Every one of the six is authored in those three, so the
|
|
1892
|
-
* cube can be re-measured in one place and all six follow.
|
|
1893
|
-
*
|
|
1894
|
-
* It fills the 16 grid corner to corner (~14 units of extent), because the
|
|
1895
|
-
* toolbar's glyph is the biggest in the chrome: that is the ~20 CSS px of ink
|
|
1896
|
-
* Blender's 38x34 button carries, once the file's `S` dial is applied.
|
|
1897
|
-
*/
|
|
1564
|
+
/** Shared three-quarter cube geometry for mesh operators. Fill marks the operated element;
|
|
1565
|
+
* the rest stays wire geometry. tonedPath tints only the operated element. */
|
|
1898
1566
|
const CUBE = { x0: 1.1, x1: 10.9, yb: 14.9, yt: 5.1, dx: 3.9, dy: -3.9 };
|
|
1567
|
+
/** P(u, v, k) maps the operator cube’s front axes and receding depth. All six operators share this geometry and its optical scale. */
|
|
1899
1568
|
const P = (u, v, k = 0) => [
|
|
1900
1569
|
CUBE.x0 + (CUBE.x1 - CUBE.x0) * u + CUBE.dx * k,
|
|
1901
1570
|
CUBE.yb + (CUBE.yt - CUBE.yb) * v + CUBE.dy * k,
|
|
1902
1571
|
];
|
|
1903
1572
|
|
|
1904
1573
|
/**
|
|
1905
|
-
* The cube
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
* v the box stops at, so Extrude's body can be shorter than the others' and
|
|
1909
|
-
* still leave the glyph the same total ink.
|
|
1574
|
+
* The cube's nine visible edges are named so each operator can omit the edges
|
|
1575
|
+
* it replaces. `top` sets the body's height while the operator's added geometry
|
|
1576
|
+
* completes the glyph.
|
|
1910
1577
|
*/
|
|
1911
|
-
function cubeWire({ top = 1,
|
|
1578
|
+
function cubeWire({ top = 1, omit = [] } = {}) {
|
|
1912
1579
|
const edges = {
|
|
1913
|
-
frontBottom: [P(
|
|
1914
|
-
frontRight: [P(
|
|
1915
|
-
frontTop: [P(
|
|
1916
|
-
frontLeft: [P(
|
|
1917
|
-
topLeft: [P(
|
|
1918
|
-
topBack: [P(
|
|
1919
|
-
rightBack: [P(
|
|
1920
|
-
rightBottom: [P(
|
|
1921
|
-
seam: [P(
|
|
1580
|
+
frontBottom: [P(0, 0), P(1, 0)],
|
|
1581
|
+
frontRight: [P(1, 0), P(1, top)],
|
|
1582
|
+
frontTop: [P(0, top), P(1, top)],
|
|
1583
|
+
frontLeft: [P(0, 0), P(0, top)],
|
|
1584
|
+
topLeft: [P(0, top), P(0, top, 1)],
|
|
1585
|
+
topBack: [P(0, top, 1), P(1, top, 1)],
|
|
1586
|
+
rightBack: [P(1, top, 1), P(1, 0, 1)],
|
|
1587
|
+
rightBottom: [P(1, 0), P(1, 0, 1)],
|
|
1588
|
+
seam: [P(1, top), P(1, top, 1)],
|
|
1922
1589
|
};
|
|
1923
1590
|
return Object.entries(edges)
|
|
1924
1591
|
.filter(([name]) => !omit.includes(name))
|
|
1925
|
-
.map(([, [a, b]]) => cap(a[0], a[1], b[0], b[1],
|
|
1592
|
+
.map(([, [a, b]]) => cap(a[0], a[1], b[0], b[1], 1.15))
|
|
1926
1593
|
.join('');
|
|
1927
1594
|
}
|
|
1928
1595
|
|
|
@@ -1940,20 +1607,12 @@ function slab(vLow, vHigh) {
|
|
|
1940
1607
|
* face floating above it as a solid slab with its own three faces. */
|
|
1941
1608
|
g(
|
|
1942
1609
|
'mesh-extrude',
|
|
1943
|
-
//
|
|
1944
|
-
// so by letting the verticals run up and vanish behind the slab rather than
|
|
1945
|
-
// closing a rim under it (round 2 closed the rim and the pair read as a
|
|
1946
|
-
// crate with a lid; round 1 stopped the verticals short and read as a
|
|
1947
|
-
// bracket). So: full-height verticals to the slab's underside, no top rim.
|
|
1610
|
+
// Full-height cube verticals end behind the lifted slab; omit the top rim of the extruded body.
|
|
1948
1611
|
cubeWire({ top: 0.74, omit: ['frontTop', 'topLeft', 'topBack', 'seam'] }),
|
|
1949
1612
|
{ tone: 'operator', tonedPath: slab(0.74, 1) },
|
|
1950
1613
|
);
|
|
1951
1614
|
|
|
1952
|
-
/**
|
|
1953
|
-
* inside it is knocked back out, and the four WALLS between them are knocked
|
|
1954
|
-
* apart by their own seams. Blender's inset glyph is that funnel — four
|
|
1955
|
-
* trapezoids converging on a small dark square — and round 4's flat frame
|
|
1956
|
-
* (no seams) read as a picture frame instead of a recess. */
|
|
1615
|
+
/** Inset: four solid rim trapezoids separated by seams around the recessed face. */
|
|
1957
1616
|
g('mesh-inset', cubeWire(), {
|
|
1958
1617
|
tone: 'operator',
|
|
1959
1618
|
tonedPath:
|
|
@@ -1965,12 +1624,8 @@ g('mesh-inset', cubeWire(), {
|
|
|
1965
1624
|
holeBar(...P(0, 1), ...P(0.3, 0.7), 0.7),
|
|
1966
1625
|
});
|
|
1967
1626
|
|
|
1968
|
-
/**
|
|
1969
|
-
*
|
|
1970
|
-
* right face), and the glyph shows the top face solid with a second facet
|
|
1971
|
-
* flapping off it along that seam, a hairline of background between the two.
|
|
1972
|
-
* Rounds 2-4 chamfered the top-FRONT edge and the result read as a solid top,
|
|
1973
|
-
* which is Extrude's answer, not Bevel's. */
|
|
1627
|
+
/** Bevel chamfers the top-right seam between the top and right faces. A solid
|
|
1628
|
+
* top face and chamfer facet meet along a narrow background separation. */
|
|
1974
1629
|
g('mesh-bevel', cubeWire({ omit: ['seam'] }), {
|
|
1975
1630
|
tone: 'operator',
|
|
1976
1631
|
tonedPath:
|
|
@@ -1979,10 +1634,7 @@ g('mesh-bevel', cubeWire({ omit: ['seam'] }), {
|
|
|
1979
1634
|
holeBar(...P(0.56, 1), ...P(0.56, 1, 1), 0.7),
|
|
1980
1635
|
});
|
|
1981
1636
|
|
|
1982
|
-
/**
|
|
1983
|
-
* it is two half-boxes standing apart with the cut itself, a solid bar, in the
|
|
1984
|
-
* gap between them, so the new edge loop is the subject. Round 4 drew a line
|
|
1985
|
-
* across an intact cube and read as decoration on the face. */
|
|
1637
|
+
/** Loop Cut: two separated half-boxes with a solid cut bar in the gap. */
|
|
1986
1638
|
g('mesh-loop-cut', cubeWire(), {
|
|
1987
1639
|
tone: 'operator',
|
|
1988
1640
|
tonedPath:
|
|
@@ -1994,8 +1646,7 @@ g('mesh-loop-cut', cubeWire(), {
|
|
|
1994
1646
|
* turns at a vertex, its ends pinned by the points a knife sets. */
|
|
1995
1647
|
g('mesh-knife', cubeWire(), {
|
|
1996
1648
|
tone: 'operator',
|
|
1997
|
-
//
|
|
1998
|
-
// top face. Round 1 fell to the right and read as a droop.
|
|
1649
|
+
// The cut rises across the front and ends on the top face.
|
|
1999
1650
|
tonedPath:
|
|
2000
1651
|
line([P(0.16, 0.06), P(0.52, 0.72), P(0.78, 1, 0.5)], 1.5) +
|
|
2001
1652
|
dot(...P(0.16, 0.06), 1.45) +
|
|
@@ -2020,19 +1671,11 @@ g('mesh-subdivide', cubeWire(), {
|
|
|
2020
1671
|
cap(...P(1, 0, 0.5), ...P(1, 1, 0.5), 1.05),
|
|
2021
1672
|
});
|
|
2022
1673
|
|
|
2023
|
-
/**
|
|
2024
|
-
* only this one of the shelf's marks) on a BALL, because smoothing is what
|
|
2025
|
-
* turns the cage into one. Measured on `modeling-edit-none.png` (button 17)
|
|
2026
|
-
* as an ASCII map in authored units: a faceted sphere ~6.3 units of radius
|
|
2027
|
-
* centred at (8.2, 6.6), its quads separated by seams KNOCKED OUT of the
|
|
2028
|
-
* fill — an equator across the full width, two meridians, and a latitude
|
|
2029
|
-
* seam near the top. The seams are holes, not strokes: drawn as strokes over
|
|
2030
|
-
* the ball they would be the same ink as the ball and vanish. */
|
|
1674
|
+
/** Smooth draws a faceted ball with seam knockouts for the equator, meridians and upper latitude. */
|
|
2031
1675
|
g(
|
|
2032
1676
|
'mesh-smooth',
|
|
2033
1677
|
poly(
|
|
2034
|
-
//
|
|
2035
|
-
// and that is half of what says "quads" before the seams are read.
|
|
1678
|
+
// Nine vertices give the ball a faceted silhouette.
|
|
2036
1679
|
Array.from({ length: 9 }, (_, i) => {
|
|
2037
1680
|
const a = (i / 9) * Math.PI * 2 + 0.34;
|
|
2038
1681
|
return [7.8 + Math.cos(a) * 7.1, 6.4 + Math.sin(a) * 7.1];
|
|
@@ -2040,9 +1683,7 @@ g(
|
|
|
2040
1683
|
) +
|
|
2041
1684
|
// equator, meridians, and the top latitude — each a bar-shaped hole.
|
|
2042
1685
|
holeBar(0.8, 7.6, 15, 7.6, 0.62) +
|
|
2043
|
-
// The meridians and
|
|
2044
|
-
// seams are not straight, and drawn straight the ball read as a beach
|
|
2045
|
-
// ball with a grid printed on it rather than as faceted geometry.
|
|
1686
|
+
// The meridians and top latitude bow through two segments each.
|
|
2046
1687
|
holeBar(6.9, -0.7, 5.9, 6.4, 0.52) +
|
|
2047
1688
|
holeBar(5.9, 6.4, 6.7, 13.5, 0.52) +
|
|
2048
1689
|
holeBar(12.7, 1.1, 12.2, 6.4, 0.48) +
|
|
@@ -2052,13 +1693,7 @@ g(
|
|
|
2052
1693
|
{ tone: 'operator' },
|
|
2053
1694
|
);
|
|
2054
1695
|
|
|
2055
|
-
/**
|
|
2056
|
-
* which is why Blender's mark (button 19) carries eight arrowheads: four
|
|
2057
|
-
* outside the cube pointing out and four inside pointing in. The cube here is
|
|
2058
|
-
* a pair of offset squares rather than the operator CUBE's three-quarter box
|
|
2059
|
-
* — measured, the front square is (3.5, 3.5)-(12, 11) and the back one is
|
|
2060
|
-
* offset up-and-right by 1.2 — because the arrows need the box read from the
|
|
2061
|
-
* front to be legible through it. */
|
|
1696
|
+
/** Shrink/Fatten draws offset front-facing squares with four inward and four outward arrowheads for motion along normals. */
|
|
2062
1697
|
g(
|
|
2063
1698
|
'mesh-shrink-fatten',
|
|
2064
1699
|
// The BOX, front square plus the two BACK edges the front does not hide
|
|
@@ -2219,13 +1854,6 @@ g(
|
|
|
2219
1854
|
'mobile-screen-button',
|
|
2220
1855
|
rframe(4.2, 1.6, 7.6, 12.8, 1.2, 1) + dot(8, 12.4, 0.8) + cap(6.4, 3.6, 9.6, 3.6, 1),
|
|
2221
1856
|
);
|
|
2222
|
-
g(
|
|
2223
|
-
'users',
|
|
2224
|
-
dot(5.4, 5, 2.5) +
|
|
2225
|
-
arc(5.4, 12.6, 4.4, 180, 360, 1.25) +
|
|
2226
|
-
arc(11.4, 5.4, 2.1, 250, 470, 1.15) +
|
|
2227
|
-
arc(11.6, 12, 3.6, 200, 340, 1.2),
|
|
2228
|
-
);
|
|
2229
1857
|
g(
|
|
2230
1858
|
'circle-user',
|
|
2231
1859
|
dot(8, 8, 6) +
|
|
@@ -2394,8 +2022,6 @@ g(
|
|
|
2394
2022
|
true,
|
|
2395
2023
|
) + cap(8, 2.6, 8, 13.4, 1.15),
|
|
2396
2024
|
);
|
|
2397
|
-
g('boxes-placeholder', '');
|
|
2398
|
-
delete glyphs['boxes-placeholder'];
|
|
2399
2025
|
g(
|
|
2400
2026
|
'download',
|
|
2401
2027
|
cap(8, 2.2, 8, 9.8, 1.3) +
|
|
@@ -2457,72 +2083,17 @@ g(
|
|
|
2457
2083
|
);
|
|
2458
2084
|
|
|
2459
2085
|
// ------------------------------------------------------- category tones
|
|
2460
|
-
/**
|
|
2461
|
-
* COLOUR AS A GLYPH'S OWN CHANNEL. Blender's Properties-tab rail groups by
|
|
2462
|
-
* HUE — in `properties-object.png` at 14 px the tab shapes are near
|
|
2463
|
-
* indistinguishable and the ink is what says which group a tab belongs to —
|
|
2464
|
-
* and the outliner tints a row's type glyph the same way. Each name below is
|
|
2465
|
-
* a category the palette inks (`EditorTheme.color.category`, where every
|
|
2466
|
-
* value's source coordinates are recorded); a glyph paints
|
|
2467
|
-
* `var(--volter-category-<tone>, currentColor)`, so under a palette that names
|
|
2468
|
-
* no category group it paints exactly as it did monochrome.
|
|
2469
|
-
*
|
|
2470
|
-
* A NAME, NOT A SITE. A tone here tints EVERY site that asks for this icon
|
|
2471
|
-
* name, so a name is toned only when its meaning is the category at every
|
|
2472
|
-
* one of them. Measured live, that is not a hypothetical: the first cut
|
|
2473
|
-
* toned `cube`, `camera`, `lightbulb` and `globe`, and the running editor
|
|
2474
|
-
* painted the Perspective/Orthographic dropdown orange and the world-space
|
|
2475
|
-
* toggle red — both are viewport CONTROLS that happen to reuse the drawing.
|
|
2476
|
-
* The fix is `editorIcons.modeling`'s: a meaning that needs its own ink gets
|
|
2477
|
-
* its own name. `hierarchy-kind-icon.ts` now asks for `outliner-object`,
|
|
2478
|
-
* `outliner-light` and `outliner-camera`, which no other site asks for, and
|
|
2479
|
-
* they are aliased to the same drawings below.
|
|
2480
|
-
*
|
|
2481
|
-
* - `outliner-object`, `outliner-light`, `outliner-camera` → `object`.
|
|
2482
|
-
* `outliner.png`: the Camera, Cube and Light rows' type glyphs all
|
|
2483
|
-
* measure #bb7f4d over the row's #272727 — ONE orange for all three,
|
|
2484
|
-
* because Blender's outliner discriminates OBJECT from DATA, not camera
|
|
2485
|
-
* from light. The same ink is the Object tab's in `properties-object.png`.
|
|
2486
|
-
* - `atom` → `modifier`. Its only site is the inspector's physics/rigid-
|
|
2487
|
-
* body/collider group (`inspection/compose.ts`'s `GROUP_ICON_RULES`);
|
|
2488
|
-
* `properties-object.png` paints Physics — with Modifier, Particles and
|
|
2489
|
-
* Constraints — one blue.
|
|
2490
|
-
* - `palette` → `material`. Its product sites are all material sections
|
|
2491
|
-
* (the same `GROUP_ICON_RULES`, the model-asset and react inspectors);
|
|
2492
|
-
* the Material tab of the same rail.
|
|
2493
|
-
* - `screwdriver-wrench` → `tool`. Its only site is a project tool's own
|
|
2494
|
-
* glyph (`tool-loader.ts`); the Tool tab of the same rail.
|
|
2495
|
-
*
|
|
2496
|
-
* The six `mesh-*` operator marks carry their tone at the glyph instead,
|
|
2497
|
-
* with a `tonedPath`, because an operator mark tints only the element it
|
|
2498
|
-
* operates on and leaves the cube neutral.
|
|
2499
|
-
*
|
|
2500
|
-
* NOT toned, each for a measured reason rather than an oversight:
|
|
2501
|
-
* - `globe` — Blender's World tab is red, but no site in this editor draws
|
|
2502
|
-
* a globe to mean a World datablock: the two are the transform-SPACE
|
|
2503
|
-
* toggle and a build profile. A red globe there reads as an error state.
|
|
2504
|
-
* - `table` — its one product site is the mesh inspector's Vertex Groups
|
|
2505
|
-
* panel, which is Blender's green `DATA_PT_vertex_groups`, but it renders
|
|
2506
|
-
* through `SectionHeader`, which passes `tone="dim"` to every section
|
|
2507
|
-
* glyph — and an explicit site tone wins by contract. A tone there would
|
|
2508
|
-
* never paint.
|
|
2509
|
-
*/
|
|
2510
2086
|
/** The union `IconCategoryTone` declares (`@volter/sdk/looks`), and the
|
|
2511
2087
|
* reason `blender.style.ts` may narrow this JSON with an assertion: a tone
|
|
2512
|
-
* outside it never reaches the artifact.
|
|
2088
|
+
* outside it never reaches the artifact. Assign tones only to icon names whose
|
|
2089
|
+
* meaning has that category at every site. Outliner aliases own their tint;
|
|
2090
|
+
* generic viewport controls stay neutral. Mesh operators tint only tonedPath. */
|
|
2513
2091
|
const TONES = ['object', 'modifier', 'material', 'tool', 'operator', 'data', 'scene', 'collection', 'select'];
|
|
2514
2092
|
|
|
2515
2093
|
// The outliner's three names, aliased onto the drawings they share with the
|
|
2516
2094
|
// generic glyphs. Same picture, its own name, so only the outliner is tinted.
|
|
2517
2095
|
g('outliner-object', glyphs['cube'].path);
|
|
2518
|
-
//
|
|
2519
|
-
// `outliner.png` (the Cube row, src x 127-154, y 179-206): Blender draws an
|
|
2520
|
-
// APEX-DOWN triangle, heavily stroked, with a triangular knockout in its upper
|
|
2521
|
-
// half — the object-type sibling of the datablock's `properties-data` mark, in
|
|
2522
|
-
// the object orange rather than the data green. On the set's 16-unit grid the
|
|
2523
|
-
// outer triangle is (1.25,1.25)-(14.75,1.25)-(8,14.75) and the knockout
|
|
2524
|
-
// (4.75,4.25)-(11.25,4.25)-(8,9.75). `outliner-object` keeps the cube for the
|
|
2525
|
-
// generic three-object row, which has no mesh to name.
|
|
2096
|
+
// The mesh-object mark is an apex-down triangle with an upper triangular knockout. outliner-object remains the generic cube mark.
|
|
2526
2097
|
g(
|
|
2527
2098
|
'outliner-mesh',
|
|
2528
2099
|
poly([
|
|
@@ -2539,174 +2110,10 @@ g(
|
|
|
2539
2110
|
g('outliner-light', glyphs.lightbulb.path);
|
|
2540
2111
|
g('outliner-camera', glyphs.camera.path);
|
|
2541
2112
|
|
|
2542
|
-
//
|
|
2543
|
-
|
|
2544
|
-
// Modifiers tab, in the modifier blue. `properties-data` is Blender's
|
|
2545
|
-
// MESH_DATA mark — a triangle with a vertex dot at each corner — drawn on
|
|
2546
|
-
// the set's own grid at its own stroke, measured from `properties-object.png`'s
|
|
2547
|
-
// Object Data tab (src x 10-56, y 802-830).
|
|
2548
|
-
/**
|
|
2549
|
-
* BLENDER'S MODIFIER TAB IS ONE OPEN-END WRENCH, and this glyph used to be
|
|
2550
|
-
* `screwdriver-wrench` — the TOOL tab's two-tool silhouette — reused under a
|
|
2551
|
-
* second name. At the 14px rail the two read as different marks, and the rail
|
|
2552
|
-
* paints them side by side, so the reuse was the loudest thing left in it.
|
|
2553
|
-
*
|
|
2554
|
-
* MEASURED off `modeling-edit-none.png` at its native 2x (the rail's OPEN
|
|
2555
|
-
* Modifier tab, ink x 2861..2888, y 1004..1031 — 28x28 device px, exactly the
|
|
2556
|
-
* box the Data tab's mark fills two cells below, which is what calibrates the
|
|
2557
|
-
* scale: `properties-data` spans 16.6 viewBox units, so this does too).
|
|
2558
|
-
* Row-by-row ink runs, thresholded at half coverage and mapped onto the set's
|
|
2559
|
-
* plain 16 grid, give a shape with four parts:
|
|
2560
|
-
*
|
|
2561
|
-
* - a HEAD. Not a stroke of constant width: the outer boundary is a circle
|
|
2562
|
-
* (5.08 left, 14.81 right at the head's own centre line) and the bore is a
|
|
2563
|
-
* SMALLER, concentric circle, so the ring is ~2.9 thick where the handle
|
|
2564
|
-
* leaves it and the jaw's flats eat into it from the other side.
|
|
2565
|
-
* - a JAW cut as a SLOT WITH PARALLEL FLATS, not a wedge and not a wider
|
|
2566
|
-
* bore. Both inner faces measure dx/dy = -1.00 over their whole length
|
|
2567
|
-
* (left face x+y = 12.84, right face x+y = 19.41) - parallel, ~4.3 apart,
|
|
2568
|
-
* on an axis at -45 degrees, opposite the handle. That parallelism is the
|
|
2569
|
-
* one fact that makes the mark read as a WRENCH rather than a C: a
|
|
2570
|
-
* radial-ended annulus puts the jaw's inner corner 3.8 device px off.
|
|
2571
|
-
* - the slot STOPS about a unit short of the head's centre, which is what
|
|
2572
|
-
* keeps the ring closed across the bottom (the frame's row y 1018 is one
|
|
2573
|
-
* unbroken run, 5.08..14.57).
|
|
2574
|
-
* - a HANDLE at exactly 45 degrees - its right boundary moves one device
|
|
2575
|
-
* pixel per row for nine rows - ~3.7 wide, ending in a round cap whose
|
|
2576
|
-
* extremes ARE the glyph's left and bottom edges.
|
|
2577
|
-
*
|
|
2578
|
-
* THE CONSTANTS BELOW ARE THE FIT, NOT THE RAW READING. Every feature above
|
|
2579
|
-
* is a boundary read at a 50% threshold on a 28px glyph, so each carries about
|
|
2580
|
-
* half a device pixel of slack, and the rim's apparent radius swings 4.5..5.0
|
|
2581
|
-
* depending on which row it is read from (near the top the circle is almost
|
|
2582
|
-
* horizontal, so half a pixel of radius moves x by two). The reading fixed the
|
|
2583
|
-
* STRUCTURE; the numbers were then settled by rendering the path at the
|
|
2584
|
-
* reference's own scale and counting the pixels where the two half-coverage
|
|
2585
|
-
* masks disagree. Every constant stayed inside its own reading's error bar and
|
|
2586
|
-
* the count came down 18 -> 5 of 298 ref ink pixels, which is the floor this
|
|
2587
|
-
* instrument has: the five are single pixels on antialiased diagonals.
|
|
2588
|
-
*
|
|
2589
|
-
* DRAWN AS ONE CLOSED BOUNDARY, NO KNOCKOUT AT ALL — and that is the whole
|
|
2590
|
-
* reason this glyph is a traced outline rather than the file's usual pile of
|
|
2591
|
-
* primitives. Two knockout constructions were tried and both are impossible
|
|
2592
|
-
* here, which is worth stating so the third attempt does not repeat them:
|
|
2593
|
-
* - disc + handle capsule, then bore and slot punched. The handle has to
|
|
2594
|
-
* reach PAST the bore to cover the head (its cap would need to sit at
|
|
2595
|
-
* radius >4.04 to clear the bore and <3.11 to stay inside the outer
|
|
2596
|
-
* circle — no such radius), so the head is DOUBLY covered exactly where
|
|
2597
|
-
* the punches land: winding 2 - 1 = 1, and every knockout fills back in.
|
|
2598
|
-
* - one outer polygon with the bore and the slot as holes. The slot has to
|
|
2599
|
-
* run PAST the rim to cut the jaw open, and a hole outside its fill is not
|
|
2600
|
-
* a hole — it is ink (winding +1), which is what it drew: a blue bar
|
|
2601
|
-
* across the top-right corner. The bore and the slot also overlap, and two
|
|
2602
|
-
* overlapping holes cancel to ink in their intersection.
|
|
2603
|
-
* The material is SIMPLY CONNECTED — the bore opens into the jaw, which opens
|
|
2604
|
-
* to the outside — so its boundary is one walk: the rim from the upper arm's
|
|
2605
|
-
* tip round to the handle, the handle's two sides and its cap, the rim again
|
|
2606
|
-
* to the lower jaw's tip, in along one flat, across the jaw's stop, round the
|
|
2607
|
-
* BACK of the bore, and out along the other flat.
|
|
2608
|
-
*/
|
|
2609
|
-
const modifierWrench = () => {
|
|
2610
|
-
const cx = 10.28; // the head's centre
|
|
2611
|
-
const cy = 5.84;
|
|
2612
|
-
const rOut = 4.78; // its outer rim
|
|
2613
|
-
const rIn = 1.85; // its bore
|
|
2614
|
-
const hw = 2.15; // half the jaw's opening
|
|
2615
|
-
const off = -0.12; // the jaw's midline, off the head's centre
|
|
2616
|
-
const stop = -1.1; // where the jaw stops, along its own axis
|
|
2617
|
-
const hx = 3.2; // the handle's end cap
|
|
2618
|
-
const hy = 12.75;
|
|
2619
|
-
const hr = 1.87;
|
|
2620
|
-
const A = [Math.SQRT1_2, -Math.SQRT1_2]; // jaw/handle axis, up-right
|
|
2621
|
-
const N = [Math.SQRT1_2, Math.SQRT1_2]; // its perpendicular, down-right
|
|
2622
|
-
/** A point in the jaw's own frame: `s` along the axis, `f` across it. */
|
|
2623
|
-
const at = (s, f) => [cx + A[0] * s + N[0] * f, cy + A[1] * s + N[1] * f];
|
|
2624
|
-
const ang = ([x, y]) => (Math.atan2(y - cy, x - cx) * 180) / Math.PI;
|
|
2625
|
-
const sweep = (r, a0, a1, ox = cx, oy = cy) => {
|
|
2626
|
-
const n = Math.max(2, Math.ceil(Math.abs(a1 - a0) / 6));
|
|
2627
|
-
return Array.from({ length: n + 1 }, (_, i) => {
|
|
2628
|
-
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
2629
|
-
return [ox + Math.cos(a) * r, oy + Math.sin(a) * r];
|
|
2630
|
-
});
|
|
2631
|
-
};
|
|
2632
|
-
const rim = (f) => Math.sqrt(Math.max(rOut * rOut - f * f, 0));
|
|
2633
|
-
const fUp = off - hw; // the jaw's two flats, across its axis
|
|
2634
|
-
const fLo = off + hw;
|
|
2635
|
-
const t1 = at(rim(fUp), fUp); // the upper arm's tip
|
|
2636
|
-
const t2 = at(rim(fLo), fLo); // the lower jaw's tip
|
|
2637
|
-
// Where each side of the handle runs into the rim, in the same frame.
|
|
2638
|
-
const offAxis = (cx - hx) * N[0] + (cy - hy) * N[1];
|
|
2639
|
-
const pUp = at(-rim(-hr - offAxis), -hr - offAxis);
|
|
2640
|
-
const pLo = at(-rim(hr - offAxis), hr - offAxis);
|
|
2641
|
-
// BOTH FLATS CLEAR THE BORE (2.32 out against a 2.07 radius) and the jaw's
|
|
2642
|
-
// stop crosses it, so the bore meets the jaw at the STOP, never at a flat.
|
|
2643
|
-
const b = Math.sqrt(Math.max(rIn * rIn - stop * stop, 0));
|
|
2644
|
-
const v1 = at(stop, b);
|
|
2645
|
-
const v2 = at(stop, -b);
|
|
2646
|
-
return poly([
|
|
2647
|
-
...sweep(rOut, ang(t1), ang(pUp) - 360), // the rim, over the top, to the handle
|
|
2648
|
-
...sweep(hr, 225, 45, hx, hy), // the handle: its two sides and its cap
|
|
2649
|
-
...sweep(rOut, ang(pLo) - 360, ang(t2) - 360), // the rim again, to the jaw
|
|
2650
|
-
at(stop, fLo), // in along the jaw's lower flat, then across its stop
|
|
2651
|
-
...sweep(rIn, ang(v1), ang(v2) + 360), // round the BACK of the bore
|
|
2652
|
-
at(stop, fUp), // across the stop again, and out along the upper flat
|
|
2653
|
-
]);
|
|
2654
|
-
};
|
|
2655
|
-
g('properties-modifiers', modifierWrench());
|
|
2656
|
-
/**
|
|
2657
|
-
* RE-MEASURED 2026-09-19 EDGE BY EDGE, and the mark is not a triangle drawn
|
|
2658
|
-
* THROUGH three vertex marks. The 2026-09-18 cut got the orientation and the
|
|
2659
|
-
* square vertices right and then drew the triangle as one closed `line`
|
|
2660
|
-
* running vertex-centre to vertex-centre, which fills each square's hollow
|
|
2661
|
-
* from the inside: at the 14 px the Outliner paints it the three squares
|
|
2662
|
-
* closed to blobs and the whole glyph read as a fat stem with two legs — a
|
|
2663
|
-
* Y — where Blender's reads as a clean triangle with three open boxes on it.
|
|
2664
|
-
*
|
|
2665
|
-
* The frames are NOT ANTIALIASED here (every ink pixel is the full
|
|
2666
|
-
* `#07b189`/`#05ae87` and every background pixel the full row), so these are
|
|
2667
|
-
* EXACT EDGES, not fits. Read down `modeling-object-none.png`'s Cube row (src
|
|
2668
|
-
* x 269..296, y 179..206 — the Outliner crop `[x2843..3452, y53..419]`'s
|
|
2669
|
-
* x 269..296, y 179..206), confirmed identical in `modeling-object-selected.png`
|
|
2670
|
-
* and `outliner.png`, and the same mark at the same size in
|
|
2671
|
-
* `properties-object.png`'s Data tab:
|
|
2672
|
-
*
|
|
2673
|
-
* three squares, OUTER 8 device px, WALL 2, HOLLOW 4 — at [0,8), [20,28)
|
|
2674
|
-
* and [10,18) of the mark's own 28 px box
|
|
2675
|
-
* a top BAR x [6,22) y [2,4) — it runs wall to wall between the two top
|
|
2676
|
-
* squares and stops dead at each hollow; it does NOT cross them, and it
|
|
2677
|
-
* sits in the UPPER half of the hollow band, not on the squares' centre
|
|
2678
|
-
* two DIAGONALS, 2 px wide, living only in y [8,20) — the gap between the
|
|
2679
|
-
* top squares' bottom edge and the bottom square's top edge. The left
|
|
2680
|
-
* one's centre runs x 273.04 → 279.44 down that span (a subpixel fit over
|
|
2681
|
-
* twelve rows, residual under 0.1 px), which is the top square's own
|
|
2682
|
-
* centre-x at the top and the bottom square's LEFT edge at the bottom;
|
|
2683
|
-
* the right one is its mirror about the box's centre.
|
|
2684
|
-
*
|
|
2685
|
-
* So the three edges are drawn BETWEEN the squares and the squares are
|
|
2686
|
-
* FILLED-WITH-A-KNOCKOUT rather than stroked outlines — which is also why
|
|
2687
|
-
* their corners are square where `rframe`'s round joins gave ours a dot.
|
|
2688
|
-
* `poly`/`rrect` take no `W` fattening, which is what this mark wants: it is
|
|
2689
|
-
* fitted to pixels, like `properties-modifiers` and the editor-type wells.
|
|
2690
|
-
*
|
|
2691
|
-
* PROVED, the way the wrench was: the path rasterized at the reference's own
|
|
2692
|
-
* scale (28x28, 4x4 supersampled, inked at half coverage) and differenced
|
|
2693
|
-
* against the frame's mask — 1 of 222 reference ink pixels disagrees, a
|
|
2694
|
-
* single antialiased pixel on the right diagonal's edge.
|
|
2695
|
-
*
|
|
2696
|
-
* A NOTE ON THE THRESHOLD, because it nearly cost this glyph its geometry:
|
|
2697
|
-
* the mark is crisp everywhere EXCEPT the diagonals' edges, and read at a
|
|
2698
|
-
* 0.44-coverage cut those two bars mask one pixel wider than they are. Fitted
|
|
2699
|
-
* to that mask the diagonals want 2.35 device px; fitted to the frame's own
|
|
2700
|
-
* subpixel MASS (2.26 px per row across twelve rows, times cos 28.06 deg)
|
|
2701
|
-
* they measure 1.99, and at the honest half-coverage cut the measured width
|
|
2702
|
-
* is the one that scores 1/222 where the fitted width scores 3. A shape
|
|
2703
|
-
* tuned to a rasterizer's threshold is tuned to the rasterizer.
|
|
2704
|
-
*/
|
|
2705
|
-
/** One device px of the reference, in this file's plain 16 units: the mark
|
|
2706
|
-
* inks exactly 28x28 device px, which is 13.5 units once `S` has scaled it
|
|
2707
|
-
* (1.25..14.75) — the same calibration the wells below use. */
|
|
2113
|
+
// Properties tabs use distinct names so their category tones affect only the rail.
|
|
2114
|
+
/** The reference's 28-device-pixel mark spans 13.5 glyph units (1.25..14.75). */
|
|
2708
2115
|
const DATA_PX = 13.5 / 28;
|
|
2709
|
-
/**
|
|
2116
|
+
/** An edge offset from the glyph’s first ink pixel. */
|
|
2710
2117
|
const dpx = (px) => 1.25 + px * DATA_PX;
|
|
2711
2118
|
/** A bar with SQUARE ends. `cap`'s round ends would push ink past the square
|
|
2712
2119
|
* each diagonal dies against, into its hollow, where Blender draws none. */
|
|
@@ -2740,6 +2147,7 @@ const dataVertex = (px, py) => {
|
|
|
2740
2147
|
])
|
|
2741
2148
|
);
|
|
2742
2149
|
};
|
|
2150
|
+
/** The mesh-data mark has three hollow square vertices and three edges drawn only between their walls. Filled shapes with knockouts preserve square corners; diagonal widths use half-coverage edges without optical W expansion. */
|
|
2743
2151
|
g(
|
|
2744
2152
|
'properties-data',
|
|
2745
2153
|
// the bar between the two top squares
|
|
@@ -2752,185 +2160,15 @@ g(
|
|
|
2752
2160
|
dataVertex(10, 20),
|
|
2753
2161
|
);
|
|
2754
2162
|
|
|
2755
|
-
// The Outliner
|
|
2756
|
-
// the same MESH_DATA mark the Properties rail's Data tab draws, under its own
|
|
2757
|
-
// name because the two sites composite differently: `outliner.png`'s data
|
|
2758
|
-
// glyph is #07b189 over the row's #272727 and `properties-object.png`'s tab is
|
|
2759
|
-
// #05ae87 over the rail's #1c1c1c, one ink at α 0.791 in both — while our rail
|
|
2760
|
-
// paints its tab at full alpha, so the two will not stay one value.
|
|
2163
|
+
// The Outliner datablock uses the mesh-data shape under its own name because its row and the Properties rail apply different alpha and category colours.
|
|
2761
2164
|
g('outliner-data', glyphs['properties-data'].path);
|
|
2762
2165
|
|
|
2763
|
-
/**
|
|
2764
|
-
|
|
2765
|
-
*
|
|
2766
|
-
* `ED_buttons_tabs_list` (`space_buttons.cc:201-255`) draws up to sixteen tabs
|
|
2767
|
-
* and each one's mark is named in `buttons_context_items`
|
|
2768
|
-
* (`makesrna/intern/rna_space.cc:579-611`) — `ICON_SCENE` for Render,
|
|
2769
|
-
* `ICON_SCENE_DATA` for Scene, `ICON_OBJECT_DATA` for Object, and so on. Each
|
|
2770
|
-
* of those icons IS A FILE in the Blender checkout at the engine's pin:
|
|
2771
|
-
* `release/datafiles/icons_svg/<lowercase name>.svg`, authored on a 16-unit
|
|
2772
|
-
* grid inside a 1600 viewBox. The compiled `.dat` sheets under
|
|
2773
|
-
* `release/datafiles/icons/` are that file's BUILD PRODUCT, not its source,
|
|
2774
|
-
* and Blender 5.x has no combined `blender_icons.svg` master.
|
|
2775
|
-
*
|
|
2776
|
-
* WHAT I1 LEFT AND THIS REPLACES: thirteen of the sixteen were a distinct NAME
|
|
2777
|
-
* over a neighbouring silhouette this set already drew — a globe for World, a
|
|
2778
|
-
* magnet for Constraints, a palette for Material — and the remaining two
|
|
2779
|
-
* (`properties-modifiers`, `properties-data`) were measured pixel-by-pixel off
|
|
2780
|
-
* a screenshot. All sixteen are now traced from the vector source, which is
|
|
2781
|
-
* both more exact than a screenshot reading and far less code: the two
|
|
2782
|
-
* hand-measured marks above this line took two paragraphs of prose each to
|
|
2783
|
-
* justify one shape.
|
|
2784
|
-
*
|
|
2785
|
-
* GPL PATHS, DELIBERATELY — AND THE PACKAGE'S SPDX SAYS SO. Blender's icon
|
|
2786
|
-
* sources are GPL-2.0-or-later, and `@volter/editor-blender` is
|
|
2787
|
-
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`, whose
|
|
2788
|
-
* notice block names this set, its provenance file and the §13 combination):
|
|
2789
|
-
* our code is AGPL, these 197 paths are Blender's artwork conveyed as GPL-3.0,
|
|
2790
|
-
* and both licences' §13 permit the combination. Nothing traced may be copied
|
|
2791
|
-
* into an Apache/MIT part of this repo; `packages/editor`'s own icon set in
|
|
2792
|
-
* particular stays free of it.
|
|
2793
|
-
*
|
|
2794
|
-
* NO `S` AND NO `W`. Those two dials exist to bring OUR drawings up to
|
|
2795
|
-
* Blender's ink ratio (see their note at the top of this file); a traced mark
|
|
2796
|
-
* already has it. Measured over the traced set, Blender's marks ink 14 of the
|
|
2797
|
-
* 16 units (x and y both 1..15 in almost every file), against the ~14.5 the
|
|
2798
|
-
* dialled geometry above produces — within 3.5%, so the rail reads as one set
|
|
2799
|
-
* while every traced glyph keeps Blender's own proportions exactly.
|
|
2800
|
-
*
|
|
2801
|
-
* Re-derive `blender.icons.traced.json` with
|
|
2802
|
-
* `node blender-icons.source.mjs --trace`, which reads the checkout at
|
|
2803
|
-
* `$BLENDER_SRC` (default `~/volter/blender-src`; the recipe is in WORK.md
|
|
2804
|
-
* §Blender in the tab is Blender, "Inspection parity") and records each
|
|
2805
|
-
* glyph's source file and sha256 beside its path. The ordinary run consumes
|
|
2806
|
-
* that JSON and needs no checkout.
|
|
2807
|
-
*/
|
|
2808
|
-
// The TONE rides with the traced glyph, read from that icon's own
|
|
2809
|
-
// `DEF_ICON_<GROUP>` macro at trace time (see the `--trace` block). The
|
|
2810
|
-
// Properties rail's sixteen keep their assignments in `CATEGORY_TONE` below,
|
|
2811
|
-
// which is where they were measured; a traced entry that carries `tone` is
|
|
2812
|
-
// answering for itself and that table names none of them.
|
|
2166
|
+
/** Trace Properties-rail icons from release/datafiles/icons_svg at the engine pin. Blender artwork is conveyed under GPL-3.0-or-later with the package’s AGPL-3.0-only code; preserve LICENSE and per-glyph source hashes in blender.icons.traced.json. Apply neither S nor W to traced paths. Rebuild the trace with node blender-icons.source.mjs --trace using BLENDER_SRC; ordinary generation consumes the saved trace JSON. */
|
|
2167
|
+
// Traced entries carry their source DEF_ICON_<GROUP> tone. CATEGORY_TONE supplies the Properties-rail assignments.
|
|
2813
2168
|
for (const [name, entry] of Object.entries(traced))
|
|
2814
2169
|
g(name, entry.path, entry.tone ? { tone: entry.tone } : undefined);
|
|
2815
2170
|
|
|
2816
|
-
|
|
2817
|
-
/**
|
|
2818
|
-
* THE TWO MARKS BLENDER PUTS IN AN AREA'S EDITOR-TYPE WELL, in the two areas
|
|
2819
|
-
* the reference frames photograph. Every Blender area begins with that well —
|
|
2820
|
-
* a 32x20 CSS rounded box, 1 px `#3c3c3c` over a constant `#272727` inner —
|
|
2821
|
-
* and the glyph inside it is what says which editor the area is
|
|
2822
|
-
* (`workspace-regions.ts`'s `groupTabs`).
|
|
2823
|
-
*
|
|
2824
|
-
* BOTH ARE READ AT NATIVE 2x AND BOTH INK EXACTLY 28x28 DEVICE PX, which is
|
|
2825
|
-
* the same box `properties-data` and the modifier wrench fill two paragraphs
|
|
2826
|
-
* up — so the same calibration carries over unchanged: 28 device px of ink is
|
|
2827
|
-
* 13.5 units on this file's plain 16 grid (1 unit = 2.074 device px), and the
|
|
2828
|
-
* ink lands at 1.25..14.75 in both axes once `S` has scaled it. Every constant
|
|
2829
|
-
* below is a measured pixel edge divided by that one number; none is a taste
|
|
2830
|
-
* call.
|
|
2831
|
-
*
|
|
2832
|
-
* Neither is antialiased in the frame — the runs are the same at a 60 and a
|
|
2833
|
-
* 120 threshold — so every edge quoted is exact rather than a fit.
|
|
2834
|
-
*
|
|
2835
|
-
* THE OTHER FIVE PANELS OF THIS EDITOR GET NO GLYPH HERE, and the reason is
|
|
2836
|
-
* that there is nothing to measure: the reference set photographs the Layout,
|
|
2837
|
-
* Modeling, Sculpting and Texture Paint workspaces, whose areas are the 3D
|
|
2838
|
-
* View, the Outliner, the Properties editor, the Timeline, the Image Editor
|
|
2839
|
-
* and the asset shelf. No frame contains a File Browser or an Asset Browser,
|
|
2840
|
-
* and Blender's editor-type dropdown is closed in all of them, so the Content
|
|
2841
|
-
* browser's and the Asset Library's wells have NO Blender reading at all.
|
|
2842
|
-
* They reuse names this set already draws (`workspace-static-panels.ts` says
|
|
2843
|
-
* which, and says that the choice is ours).
|
|
2844
|
-
*/
|
|
2845
|
-
|
|
2846
|
-
// `outliner.png`, the Outliner area's own well: ink x 27..54, y 11..38 device.
|
|
2847
|
-
// Six parts, each a pixel-exact rectangle with SQUARE corners (no corner in
|
|
2848
|
-
// this glyph loses a pixel at either threshold):
|
|
2849
|
-
// the collection box x [27,37) y [11,17)
|
|
2850
|
-
// the tree's stem x [31,33) y [19,37)
|
|
2851
|
-
// its upper arm x [31,39) y [25,27)
|
|
2852
|
-
// its lower arm x [31,39) y [35,37)
|
|
2853
|
-
// the upper child box x [41,55) y [23,29)
|
|
2854
|
-
// the lower child box x [41,55) y [33,39)
|
|
2855
|
-
// The two GAPS are the mark's own: 2 device px between the collection box and
|
|
2856
|
-
// the stem's top, and 2 more between each arm's end and its child box. Closing
|
|
2857
|
-
// either — which is what a tree icon drawn from memory does — turns three
|
|
2858
|
-
// separated rows into one connected bracket.
|
|
2859
|
-
g(
|
|
2860
|
-
'outliner',
|
|
2861
|
-
rrect(1.25, 1.25, 4.82, 2.89) +
|
|
2862
|
-
rrect(3.18, 5.11, 0.96, 8.68) +
|
|
2863
|
-
rrect(3.18, 8.0, 3.86, 0.96) +
|
|
2864
|
-
rrect(3.18, 12.82, 3.86, 0.96) +
|
|
2865
|
-
rrect(8.0, 7.04, 6.75, 2.89) +
|
|
2866
|
-
rrect(8.0, 11.86, 6.75, 2.89),
|
|
2867
|
-
);
|
|
2868
|
-
|
|
2869
|
-
/**
|
|
2870
|
-
* `modeling-edit-none.png`, the Properties area's own well: ink x 2870..2897,
|
|
2871
|
-
* y 437..464 device. TWO SOLID STADIUMS, each 28x12 device (radius = half the
|
|
2872
|
-
* height), stacked with 4 device px between them — and each carries a
|
|
2873
|
-
* KNOCKOUT, which is what makes the pair read as two sliders at different
|
|
2874
|
-
* settings rather than as two blank pills:
|
|
2875
|
-
* stadium rows y [437,449) and y [453,465), both x [2870,2898)
|
|
2876
|
-
* upper knockout x [2882,2896) y [439,447)
|
|
2877
|
-
* lower knockout x [2888,2896) y [455,463)
|
|
2878
|
-
* Each knockout is FLAT ON ITS LEFT and semicircular on its right: the right
|
|
2879
|
-
* boundary reads 2893/2894/2895/2895 down its four half-rows, which fits a
|
|
2880
|
-
* radius of exactly 4 device px (= half the knockout's own height) and nothing
|
|
2881
|
-
* else; the left boundary is one x for all eight rows. The two differ only in
|
|
2882
|
-
* where that flat left edge sits, and that difference is the whole picture.
|
|
2883
|
-
*
|
|
2884
|
-
* The knockouts are strictly INSIDE their stadiums (2 device px of ink on
|
|
2885
|
-
* every side), so the nonzero winding is well posed — unlike the wrench's two
|
|
2886
|
-
* failed constructions recorded above, nothing here is punched outside its own
|
|
2887
|
-
* fill or punched twice.
|
|
2888
|
-
*/
|
|
2889
|
-
const sliderSlot = (xL, y, xR, h) => {
|
|
2890
|
-
const r = h / 2;
|
|
2891
|
-
const cy = y + r;
|
|
2892
|
-
const cx = xR - r;
|
|
2893
|
-
const cap = Array.from({ length: 13 }, (_, i) => {
|
|
2894
|
-
const a = ((-90 + 15 * i) * Math.PI) / 180;
|
|
2895
|
-
return [cx + Math.cos(a) * r, cy + Math.sin(a) * r];
|
|
2896
|
-
});
|
|
2897
|
-
return holePoly([[xL, y], ...cap, [xL, y + h]]);
|
|
2898
|
-
};
|
|
2899
|
-
g(
|
|
2900
|
-
'properties',
|
|
2901
|
-
rrect(1.25, 1.25, 13.5, 5.79, 2.41) +
|
|
2902
|
-
sliderSlot(7.04, 2.21, 13.79, 3.86) +
|
|
2903
|
-
rrect(1.25, 8.96, 13.5, 5.79, 2.41) +
|
|
2904
|
-
sliderSlot(9.93, 9.92, 13.79, 3.86),
|
|
2905
|
-
);
|
|
2906
|
-
|
|
2907
|
-
/**
|
|
2908
|
-
* BLENDER HUE-GROUPS ITS RAIL, AND THE SOURCE SAYS WHICH GROUP EACH MARK IS
|
|
2909
|
-
* IN. Every icon in `UI_icons.hh` is declared through a `DEF_ICON_<GROUP>`
|
|
2910
|
-
* macro, and `interface_icons.cc:126-132` maps those macros onto the theme
|
|
2911
|
-
* members `interface/resources.cc:1059-1078` reads:
|
|
2912
|
-
*
|
|
2913
|
-
* DEF_ICON_SCENE → TH_ICON_SCENE `.tui.icon_scene` #cccccc
|
|
2914
|
-
* DEF_ICON_COLLECTION → TH_ICON_COLLECTION `.tui.icon_collection` #ffffff
|
|
2915
|
-
* DEF_ICON_OBJECT → TH_ICON_OBJECT `.tui.icon_object` #e19658
|
|
2916
|
-
* DEF_ICON_OBJECT_DATA → TH_ICON_OBJECT_DATA `.tui.icon_object_data` #00d4a3
|
|
2917
|
-
* DEF_ICON_MODIFIER → TH_ICON_MODIFIER `.tui.icon_modifier` #74a2ff
|
|
2918
|
-
* DEF_ICON_SHADING → TH_ICON_SHADING `.tui.icon_shading` #cc6670
|
|
2919
|
-
*
|
|
2920
|
-
* (`userdef_default_theme.c:272-277`.) So the assignments below are READ, not
|
|
2921
|
-
* matched by eye — and reading them corrected two guesses I1 recorded as open:
|
|
2922
|
-
* WORLD is `DEF_ICON_SHADING(WORLD)` (`UI_icons.hh:193`), the same red as
|
|
2923
|
-
* Material and Texture, NOT the scene grey; and GROUP — the Collection tab —
|
|
2924
|
-
* is `DEF_ICON_COLLECTION(GROUP)` (`:248`), a white of its own rather than
|
|
2925
|
-
* that grey either. The scene group is exactly four tabs: Render
|
|
2926
|
-
* (`DEF_ICON_SCENE(SCENE)`, `:188`), Output (`:187`), View Layer
|
|
2927
|
-
* (`DEF_ICON_SCENE(RENDER_RESULT)`, `:265`) and Scene
|
|
2928
|
-
* (`DEF_ICON_SCENE(SCENE_DATA)`, `:267`).
|
|
2929
|
-
*
|
|
2930
|
-
* Every object-data mark — MESH_DATA, ARMATURE_DATA, BONE_DATA and the rest of
|
|
2931
|
-
* the per-type set the Data tab draws — is `DEF_ICON_OBJECT_DATA`
|
|
2932
|
-
* (`:251-263`), which is why they share one tone rather than list one each.
|
|
2933
|
-
*/
|
|
2171
|
+
/** Map UI_icons.hh DEF_ICON groups to theme categories: scene, collection, object, object-data, modifier and shading. World shares shading with Material; Render, Output, View Layer and Scene share scene; per-type Data icons share object-data. */
|
|
2934
2172
|
const CATEGORY_TONE = {
|
|
2935
2173
|
'properties-render': 'scene',
|
|
2936
2174
|
'properties-output': 'scene',
|
|
@@ -2965,9 +2203,7 @@ const CATEGORY_TONE = {
|
|
|
2965
2203
|
),
|
|
2966
2204
|
};
|
|
2967
2205
|
for (const [name, tone] of Object.entries(CATEGORY_TONE)) {
|
|
2968
|
-
//
|
|
2969
|
-
// and a tone assignment that silently stops applying is exactly the kind
|
|
2970
|
-
// of thing nobody notices until the rail goes grey.
|
|
2206
|
+
// Every tone assignment must name a glyph in this set.
|
|
2971
2207
|
if (!glyphs[name]) throw new Error(`CATEGORY_TONE names a glyph this set does not draw: ${name}`);
|
|
2972
2208
|
glyphs[name] = { ...glyphs[name], tone };
|
|
2973
2209
|
}
|