@volter/editor-blender 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1409 -0
- package/README.md +17 -0
- package/contributions/blender-header-menus.tsx +483 -0
- package/contributions/blender-icon-trace.mjs +403 -0
- package/contributions/blender-icons.source.mjs +2925 -0
- package/contributions/blender-node-editor.document.tsx +1402 -0
- package/contributions/blender-node-geometry.ts +1138 -0
- package/contributions/blender-node-panels.source.mjs +485 -0
- package/contributions/blender-outliner-authoring.ts +1729 -0
- package/contributions/blender-outliner-model.ts +389 -0
- package/contributions/blender-palette.source.mjs +319 -0
- package/contributions/blender-properties-model.ts +351 -0
- package/contributions/blender-properties-tab.tsx +100 -0
- package/contributions/blender-properties-view.tsx +1191 -0
- package/contributions/blender-runtime-skin.ts +619 -0
- package/contributions/blender-runtime.document.tsx +232 -0
- package/contributions/blender-timeline-geometry.ts +323 -0
- package/contributions/blender-timeline.document.tsx +1056 -0
- package/contributions/blender-uv-editor.document.tsx +483 -0
- package/contributions/blender-uv-geometry.ts +305 -0
- package/contributions/blender-version.status.tsx +93 -0
- package/contributions/blender.command.ts +102 -0
- package/contributions/blender.icons.json +1247 -0
- package/contributions/blender.icons.traced.json +1561 -0
- package/contributions/blender.keymap.ts +39 -0
- package/contributions/blender.node-panels.json +2436 -0
- package/contributions/blender.palette.json +93 -0
- package/contributions/blender.status.tsx +263 -0
- package/contributions/blender.style.ts +271 -0
- package/contributions/model.layout.ts +53 -0
- package/contributions/models.finder.ts +59 -0
- package/contributions/properties-bone-constraints.inspector.tsx +50 -0
- package/contributions/properties-bone.inspector.tsx +184 -0
- package/contributions/properties-collection.inspector.tsx +96 -0
- package/contributions/properties-constraints.inspector.tsx +69 -0
- package/contributions/properties-data.inspector.tsx +229 -0
- package/contributions/properties-material.inspector.tsx +121 -0
- package/contributions/properties-modifiers.inspector.tsx +74 -0
- package/contributions/properties-object.inspector.tsx +215 -0
- package/contributions/properties-output.inspector.tsx +210 -0
- package/contributions/properties-particles.inspector.tsx +494 -0
- package/contributions/properties-physics.inspector.tsx +614 -0
- package/contributions/properties-render.inspector.tsx +446 -0
- package/contributions/properties-scene.inspector.tsx +174 -0
- package/contributions/properties-texture.inspector.tsx +300 -0
- package/contributions/properties-view-layer.inspector.tsx +145 -0
- package/contributions/properties-world.inspector.tsx +130 -0
- package/contributions/sculpt.layout.ts +25 -0
- package/contributions/shading.layout.ts +99 -0
- package/contributions/texture.layout.ts +16 -0
- package/contributions/uv-editing.layout.ts +93 -0
- package/host/blender-runtime-host.ts +1256 -0
- package/package.json +77 -0
- package/src/layouts.tsx +48 -0
- package/src/looks.ts +14 -0
- package/src/node-view-state.ts +125 -0
- package/src/timeline-view-state.ts +154 -0
- package/src/uv-view-state.ts +125 -0
|
@@ -0,0 +1,2925 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SOURCE of `blender.icons.json`, which is PART OURS AND PART BLENDER'S.
|
|
3
|
+
* 197 of the 346 glyphs it emits are machine-TRACED from Blender's own vector
|
|
4
|
+
* sources and are GPL-2.0-or-later artwork (see "GPL PATHS, DELIBERATELY"
|
|
5
|
+
* below, `blender-icon-trace.mjs`, and the provenance file
|
|
6
|
+
* `blender.icons.traced.json`); the other 149 are drawn HERE in Blender's
|
|
7
|
+
* IDIOM. That mixture is why `@vgai/blender` is
|
|
8
|
+
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`).
|
|
9
|
+
* The idiom is what a screenshot conveys: a 16-unit grid, monochrome filled
|
|
10
|
+
* silhouettes, ~1.3-unit strokes rendered as filled capsules with round ends,
|
|
11
|
+
* no outline around a fill, optical size ~12 units inside the 16 box.
|
|
12
|
+
*
|
|
13
|
+
* Run `node packages/blender/contributions/blender-icons.source.mjs` to rewrite
|
|
14
|
+
* the JSON. The JSON is the artifact the style bundle imports; THIS is where a
|
|
15
|
+
* glyph is edited, because a path `d` is not a thing a person edits by hand.
|
|
16
|
+
*
|
|
17
|
+
* Keys are Font Awesome icon NAMES (`icon.iconName`) — the editor's one icon
|
|
18
|
+
* primitive swaps a glyph in per name and falls through to its own for any
|
|
19
|
+
* name absent here, so this set is partial by design.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { traceBlenderIcon } from './blender-icon-trace.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* WHICH OF BLENDER'S ICONS EACH PROPERTIES TAB DRAWS, and the file each one
|
|
29
|
+
* is. The `ICON_*` per tab is `buttons_context_items`
|
|
30
|
+
* (`makesrna/intern/rna_space.cc:579-611`); the Data tab's is dynamic
|
|
31
|
+
* (`buttons_context_compute`, `space_buttons/buttons_context.cc:795-810`:
|
|
32
|
+
* `RNA_struct_ui_icon(ptr->type)`, with Light special-cased to
|
|
33
|
+
* `ICON_OUTLINER_DATA_LIGHT`), so it has one entry per object data type,
|
|
34
|
+
* each icon read from that type's own `RNA_def_struct_ui_icon` call.
|
|
35
|
+
* An icon's file is its name lowercased.
|
|
36
|
+
*/
|
|
37
|
+
const TAB_ICON_SOURCES = {
|
|
38
|
+
// The scene group, `ED_buttons_tabs_list`'s first block after the tool tab.
|
|
39
|
+
'properties-render': 'scene', // ICON_SCENE
|
|
40
|
+
'properties-output': 'output', // ICON_OUTPUT
|
|
41
|
+
'properties-view-layer': 'render_result', // ICON_RENDER_RESULT
|
|
42
|
+
'properties-scene': 'scene_data', // ICON_SCENE_DATA
|
|
43
|
+
'properties-world': 'world', // ICON_WORLD
|
|
44
|
+
// Its own block.
|
|
45
|
+
'properties-collection': 'group', // ICON_GROUP
|
|
46
|
+
// The object group.
|
|
47
|
+
'properties-object': 'object_data', // ICON_OBJECT_DATA
|
|
48
|
+
'properties-modifiers': 'modifier', // ICON_MODIFIER
|
|
49
|
+
'properties-particles': 'particles', // ICON_PARTICLES
|
|
50
|
+
'properties-physics': 'physics', // ICON_PHYSICS
|
|
51
|
+
'properties-constraints': 'constraint', // ICON_CONSTRAINT
|
|
52
|
+
'properties-bone': 'bone_data', // ICON_BONE_DATA
|
|
53
|
+
'properties-bone-constraints': 'constraint_bone', // ICON_CONSTRAINT_BONE
|
|
54
|
+
'properties-material': 'material', // ICON_MATERIAL
|
|
55
|
+
// Its own block.
|
|
56
|
+
'properties-texture': 'texture', // ICON_TEXTURE
|
|
57
|
+
// The Data tab, per object type. `properties-data` is the fallback and is
|
|
58
|
+
// MESH_DATA, which is what Blender shows for the starter cube.
|
|
59
|
+
'properties-data': 'mesh_data',
|
|
60
|
+
'properties-data-mesh': 'mesh_data', // Mesh → ICON_MESH_DATA
|
|
61
|
+
'properties-data-armature': 'armature_data', // Armature → ICON_ARMATURE_DATA
|
|
62
|
+
'properties-data-curve': 'curve_data', // Curve → ICON_CURVE_DATA
|
|
63
|
+
'properties-data-surface': 'surface_data', // SurfaceCurve→ ICON_SURFACE_DATA
|
|
64
|
+
'properties-data-meta': 'meta_data', // MetaBall → ICON_META_DATA
|
|
65
|
+
'properties-data-font': 'font_data', // TextCurve → ICON_FONT_DATA
|
|
66
|
+
'properties-data-lattice': 'lattice_data', // Lattice → ICON_LATTICE_DATA
|
|
67
|
+
'properties-data-camera': 'camera_data', // Camera → ICON_CAMERA_DATA
|
|
68
|
+
'properties-data-light': 'outliner_data_light', // Light, special-cased
|
|
69
|
+
'properties-data-speaker': 'speaker', // Speaker → ICON_SPEAKER
|
|
70
|
+
'properties-data-pointcloud': 'pointcloud_data', // PointCloud → ICON_POINTCLOUD_DATA
|
|
71
|
+
'properties-data-volume': 'volume_data', // Volume → ICON_VOLUME_DATA
|
|
72
|
+
'properties-data-empty': 'empty_data', // no data → ICON_EMPTY_DATA
|
|
73
|
+
};
|
|
74
|
+
|
|
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
|
+
*/
|
|
96
|
+
const OUTLINER_ICON_FAMILIES = [
|
|
97
|
+
// `icon_from_object_type` and `tree_element_get_icon_from_id`'s data marks.
|
|
98
|
+
'outliner_',
|
|
99
|
+
// A light's lamp type and a probe's — the two sub-switches.
|
|
100
|
+
'light_',
|
|
101
|
+
'lightprobe_',
|
|
102
|
+
// `ModifierTypeInfo::icon` and the constraint switch.
|
|
103
|
+
'mod_',
|
|
104
|
+
'con_',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** The marks the Outliner draws that belong to no family above — the base
|
|
108
|
+
* rows' labels ("Modifiers", "Vertex Groups", "Pose"), the datablock marks a
|
|
109
|
+
* non-object ID answers, and the two restriction-column glyphs. */
|
|
110
|
+
const OUTLINER_ICON_NAMES = [
|
|
111
|
+
'modifier_data',
|
|
112
|
+
'constraint',
|
|
113
|
+
'constraint_bone',
|
|
114
|
+
'group_vertex',
|
|
115
|
+
'group_bone',
|
|
116
|
+
'bone_data',
|
|
117
|
+
'armature_data',
|
|
118
|
+
'shapekey_data',
|
|
119
|
+
'anim_data',
|
|
120
|
+
'particles',
|
|
121
|
+
'material_data',
|
|
122
|
+
'texture_data',
|
|
123
|
+
'image_data',
|
|
124
|
+
'world_data',
|
|
125
|
+
'scene_data',
|
|
126
|
+
'object_data',
|
|
127
|
+
'nodetree',
|
|
128
|
+
'action',
|
|
129
|
+
'line_data',
|
|
130
|
+
'brush_data',
|
|
131
|
+
'mod_mask',
|
|
132
|
+
'file_text',
|
|
133
|
+
'file_font',
|
|
134
|
+
'sequence',
|
|
135
|
+
'color',
|
|
136
|
+
'workspace',
|
|
137
|
+
'library_data_direct',
|
|
138
|
+
'dot',
|
|
139
|
+
// The Disable in Renders column (`outliner_draw.cc:1363-1384`), under names
|
|
140
|
+
// of OURS so the bare `camera` glyph — the viewport header's camera menu —
|
|
141
|
+
// is not repainted. `hierarchy-kind-icon.ts` mints the same two names.
|
|
142
|
+
['outliner-render-on', 'restrict_render_off'],
|
|
143
|
+
['outliner-render-off', 'restrict_render_on'],
|
|
144
|
+
// The EXCLUDE column (`outliner_draw.cc:1634-1653`). Blender asks for
|
|
145
|
+
// `ICON_NONE` there, which makes the icon toggle draw the widget's own
|
|
146
|
+
// checkbox — `checkbox_hlt` / `checkbox_dehlt` — so those are the two marks,
|
|
147
|
+
// under names of OURS for the same reason the render pair has them.
|
|
148
|
+
['outliner-exclude-on', 'checkbox_hlt'],
|
|
149
|
+
['outliner-exclude-off', 'checkbox_dehlt'],
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const TRACED = new URL('./blender.icons.traced.json', import.meta.url);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `--trace` — RE-DERIVE the traced paths from a Blender checkout. Separate
|
|
156
|
+
* from the ordinary run on purpose: the checkout is a developer's local clone
|
|
157
|
+
* of Blender at the engine's pin (the recipe is in WORK.md §Blender in the
|
|
158
|
+
* tab is Blender, "Inspection parity"), and a generator that silently emits
|
|
159
|
+
* different glyphs depending on whether a sibling directory happens to exist
|
|
160
|
+
* is worse than one that needs a flag. The JSON it writes is checked in and
|
|
161
|
+
* records each glyph's source path and sha256, so a later trace that disagrees
|
|
162
|
+
* is a visible diff rather than a mystery.
|
|
163
|
+
*/
|
|
164
|
+
if (process.argv.includes('--trace')) {
|
|
165
|
+
const checkout = process.env['BLENDER_SRC'] ?? `${homedir()}/volter/blender-src`;
|
|
166
|
+
const out = {};
|
|
167
|
+
// WHICH THEME COLOUR EACH MARK TAKES, read rather than assigned by eye. An
|
|
168
|
+
// icon's `DEF_ICON_<GROUP>` macro in `editors/include/UI_icons.hh` is what
|
|
169
|
+
// `interface_icons.cc:120-134` turns into a `TH_ICON_*`, which
|
|
170
|
+
// `resources.cc:1059-1078` resolves against `.tui` — so the macro IS the
|
|
171
|
+
// tone, and this reads it per icon at trace time instead of keeping a second
|
|
172
|
+
// table of sixty-odd assignments in sync by hand. A plain `DEF_ICON` (and
|
|
173
|
+
// `DEF_ICON_COLOR`, which is an SVG drawn in its own colours) takes none.
|
|
174
|
+
const MACRO_TONE = {
|
|
175
|
+
DEF_ICON_SCENE: 'scene',
|
|
176
|
+
DEF_ICON_COLLECTION: 'collection',
|
|
177
|
+
DEF_ICON_OBJECT: 'object',
|
|
178
|
+
DEF_ICON_OBJECT_DATA: 'data',
|
|
179
|
+
DEF_ICON_MODIFIER: 'modifier',
|
|
180
|
+
DEF_ICON_SHADING: 'material',
|
|
181
|
+
};
|
|
182
|
+
const iconsHeader = readFileSync(
|
|
183
|
+
`${checkout}/source/blender/editors/include/UI_icons.hh`,
|
|
184
|
+
'utf8',
|
|
185
|
+
);
|
|
186
|
+
const toneByIcon = new Map();
|
|
187
|
+
for (const match of iconsHeader.matchAll(/^DEF_ICON(?:_([A-Z_]+))?\(([A-Z0-9_]+)\)/gm)) {
|
|
188
|
+
const tone = MACRO_TONE[`DEF_ICON_${match[1] ?? ''}`];
|
|
189
|
+
if (tone) toneByIcon.set(match[2], tone);
|
|
190
|
+
}
|
|
191
|
+
if (toneByIcon.size === 0)
|
|
192
|
+
throw new Error(
|
|
193
|
+
'UI_icons.hh yielded no DEF_ICON_<GROUP> lines — the tone read is the source of every ' +
|
|
194
|
+
"glyph's colour, so an empty read is a silent grey rail, not a missing nicety.",
|
|
195
|
+
);
|
|
196
|
+
// The Outliner's families, resolved against the checkout's own icon
|
|
197
|
+
// directory so a family is traced whole (see OUTLINER_ICON_FAMILIES).
|
|
198
|
+
const available = readdirSync(`${checkout}/release/datafiles/icons_svg`)
|
|
199
|
+
.filter((file) => file.endsWith('.svg'))
|
|
200
|
+
.map((file) => file.slice(0, -4))
|
|
201
|
+
.sort();
|
|
202
|
+
const outlinerSources = {};
|
|
203
|
+
for (const family of OUTLINER_ICON_FAMILIES) {
|
|
204
|
+
const members = available.filter((icon) => icon.startsWith(family));
|
|
205
|
+
if (members.length === 0)
|
|
206
|
+
throw new Error(
|
|
207
|
+
`No icon in the checkout starts with "${family}". A family that traces to nothing is ` +
|
|
208
|
+
'how the Outliner silently falls back to Font Awesome circles.',
|
|
209
|
+
);
|
|
210
|
+
for (const icon of members) outlinerSources[`blender-${icon.replace(/_/g, '-')}`] = icon;
|
|
211
|
+
}
|
|
212
|
+
for (const entry of OUTLINER_ICON_NAMES) {
|
|
213
|
+
const [name, icon] = Array.isArray(entry)
|
|
214
|
+
? entry
|
|
215
|
+
: [`blender-${entry.replace(/_/g, '-')}`, entry];
|
|
216
|
+
outlinerSources[name] = icon;
|
|
217
|
+
}
|
|
218
|
+
for (const [name, icon] of Object.entries({ ...TAB_ICON_SOURCES, ...outlinerSources })) {
|
|
219
|
+
const relative = `release/datafiles/icons_svg/${icon}.svg`;
|
|
220
|
+
let svg;
|
|
221
|
+
try {
|
|
222
|
+
svg = readFileSync(`${checkout}/${relative}`, 'utf8');
|
|
223
|
+
} catch {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`${relative} is not in the Blender checkout at ${checkout}. Set BLENDER_SRC, or add ` +
|
|
226
|
+
`the directory to the sparse checkout: \`git sparse-checkout add release/datafiles/icons_svg\` ` +
|
|
227
|
+
'followed by the LFS-off checkout line in WORK.md.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (svg.startsWith('version https://git-lfs')) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${relative} is an LFS POINTER, not an SVG. Fetch it: ` +
|
|
233
|
+
`\`git lfs pull -I ${relative}\` in ${checkout}.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const { path, box } = traceBlenderIcon(svg);
|
|
237
|
+
const tone = toneByIcon.get(icon.toUpperCase());
|
|
238
|
+
out[name] = {
|
|
239
|
+
icon: `ICON_${icon.toUpperCase()}`,
|
|
240
|
+
source: relative,
|
|
241
|
+
sha256: createHash('sha256').update(svg).digest('hex'),
|
|
242
|
+
box,
|
|
243
|
+
...(tone ? { tone } : {}),
|
|
244
|
+
path,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
writeFileSync(TRACED, `${JSON.stringify(out, null, 2)}\n`);
|
|
248
|
+
console.log(`blender.icons.traced.json — ${Object.keys(out).length} glyphs traced`);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The traced marks, keyed by this set's glyph name. See the Properties-rail
|
|
253
|
+
* block near the bottom of this file for what they are and why. */
|
|
254
|
+
const traced = JSON.parse(readFileSync(TRACED, 'utf8'));
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------- primitives
|
|
257
|
+
|
|
258
|
+
const f = (n) => {
|
|
259
|
+
const r = Math.round(n * 100) / 100;
|
|
260
|
+
return Object.is(r, -0) ? 0 : r;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* TWO GLOBAL DIALS, set against the running chrome rather than guessed. The
|
|
265
|
+
* editor paints a glyph at the ACTIVE font size — an 11px hierarchy row, a
|
|
266
|
+
* 10px status bar — where a 1.3-unit stroke in a 16-unit box lands at 0.9
|
|
267
|
+
* device pixels and greys out. `S` grows the optical size to ~14.5 units
|
|
268
|
+
* inside the 16 box — Blender's glyphs FILL their box where Font Awesome's
|
|
269
|
+
* carry padding, and measured against the reference frames that ratio (ink
|
|
270
|
+
* over row height) is what makes a row read as Blender's; `W` fattens every
|
|
271
|
+
* stroke so it still covers a pixel in an 11px row. Geometry below is
|
|
272
|
+
* authored on the plain 16 grid and both dials are applied here, once.
|
|
273
|
+
*/
|
|
274
|
+
const S = 1.22;
|
|
275
|
+
const W = 1.12;
|
|
276
|
+
const p = (x, y) => `${f(8 + (x - 8) * S)} ${f(8 + (y - 8) * S)}`;
|
|
277
|
+
/** A radius, in the same scaled space `p` emits into. */
|
|
278
|
+
const rr = (r) => f(r * S);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A straight stroke with round ends: the capsule its outline encloses.
|
|
282
|
+
*
|
|
283
|
+
* THE SWEEP FLAG IS 0, AND IT WAS 1 UNTIL 2026-09-19 — a one-character defect
|
|
284
|
+
* under every open stroke end in this set. With sweep 1 each end arc bulges
|
|
285
|
+
* INWARD instead of outward, so the path is the rectangle MINUS two
|
|
286
|
+
* half-discs: every open end came out with a crescent bitten out of it, and
|
|
287
|
+
* at a polyline's joint the two bites ate a round notch out of the corner.
|
|
288
|
+
* Nothing reported it because at the 14 px chrome size the bite is one device
|
|
289
|
+
* pixel; it was found at 6x in the editor-type well, whose chevron drew as
|
|
290
|
+
* two arrowheads with a hole where they meet (rendered side by side, sweep 1
|
|
291
|
+
* against sweep 0, before the flag was touched).
|
|
292
|
+
*
|
|
293
|
+
* Closed shapes change too, and toward this docblock's own words: `line(…,
|
|
294
|
+
* closed)` and `rframe` corners were each losing a notch to the same bites,
|
|
295
|
+
* half-covered by the neighbouring capsule.
|
|
296
|
+
*
|
|
297
|
+
* THE BLAST RADIUS, MEASURED rather than reasoned (every glyph rasterized at
|
|
298
|
+
* 64 px both ways and the masks differenced): 97 of the 146 paths move. 96 of
|
|
299
|
+
* them ONLY GAIN ink, at an end or a corner — a knockout that is not there
|
|
300
|
+
* can take nothing. The 97th, `layer-group`, gains 199 px and loses one, on an
|
|
301
|
+
* antialiased boundary. Nothing here was tuned against the bitten rendering:
|
|
302
|
+
* the two glyphs this set fits pixel-by-pixel to a reference frame
|
|
303
|
+
* (`properties-modifiers`, and `outliner`/`properties` below) are built from
|
|
304
|
+
* `poly` and `rrect` and do not move at all.
|
|
305
|
+
*/
|
|
306
|
+
function cap(x1, y1, x2, y2, width = 1.3) {
|
|
307
|
+
const w = width * W;
|
|
308
|
+
const r = w / 2;
|
|
309
|
+
const dx = x2 - x1;
|
|
310
|
+
const dy = y2 - y1;
|
|
311
|
+
const L = Math.hypot(dx, dy);
|
|
312
|
+
if (L < 1e-6) return dot(x1, y1, r);
|
|
313
|
+
const nx = -dy / L;
|
|
314
|
+
const ny = dx / L;
|
|
315
|
+
return (
|
|
316
|
+
`M${p(x1 + nx * r, y1 + ny * r)}L${p(x2 + nx * r, y2 + ny * r)}` +
|
|
317
|
+
`A${rr(r)} ${rr(r)} 0 0 0 ${p(x2 - nx * r, y2 - ny * r)}` +
|
|
318
|
+
`L${p(x1 - nx * r, y1 - ny * r)}` +
|
|
319
|
+
`A${rr(r)} ${rr(r)} 0 0 0 ${p(x1 + nx * r, y1 + ny * r)}Z`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** A filled disc. `ccw` winds it backwards so it punches a hole (nonzero). */
|
|
324
|
+
function dot(cx, cy, r, ccw = false) {
|
|
325
|
+
const s = ccw ? 1 : 0;
|
|
326
|
+
return (
|
|
327
|
+
`M${p(cx - r, cy)}A${rr(r)} ${rr(r)} 0 1 ${s} ${p(cx + r, cy)}` +
|
|
328
|
+
`A${rr(r)} ${rr(r)} 0 1 ${s} ${p(cx - r, cy)}Z`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A circular outline of thickness `w` — a disc with a disc punched out. */
|
|
333
|
+
function ring(cx, cy, r, width = 1.3) {
|
|
334
|
+
const w = width * W;
|
|
335
|
+
return dot(cx, cy, r + w / 2) + dot(cx, cy, Math.max(r - w / 2, 0.01), true);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A polyline stroked with round ends and round joins.
|
|
340
|
+
*
|
|
341
|
+
* THE JOINS NEED NO EXTRA DISC, and stamping one was actively destructive.
|
|
342
|
+
* Each `cap` ends in a full semicircle of radius w/2 centred on the join, so
|
|
343
|
+
* the union of two capsules already contains the whole join disc — the joins
|
|
344
|
+
* are round for free. The disc this used to add at every interior vertex had
|
|
345
|
+
* the opposite winding to the capsules it landed in, so under
|
|
346
|
+
* `fill-rule: nonzero` it PUNCHED A HOLE: render `line([[3,3],[3,13],[13,13]])`
|
|
347
|
+
* at 24x and the corner is a white ring around a black core, where the same
|
|
348
|
+
* two `cap` calls alone give a solid corner. At 14px that hole is a sub-pixel
|
|
349
|
+
* dark pip on every corner of every stroked glyph in the set — the
|
|
350
|
+
* "greys out at a small size" symptom, dropped here for all 130 at once.
|
|
351
|
+
*/
|
|
352
|
+
function line(pts, w = 1.3, closed = false) {
|
|
353
|
+
let d = '';
|
|
354
|
+
const n = pts.length;
|
|
355
|
+
const last = closed ? n : n - 1;
|
|
356
|
+
for (let i = 0; i < last; i++) {
|
|
357
|
+
const a = pts[i];
|
|
358
|
+
const b = pts[(i + 1) % n];
|
|
359
|
+
d += cap(a[0], a[1], b[0], b[1], w);
|
|
360
|
+
}
|
|
361
|
+
return d;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Twice the signed area — the sign IS the winding, and under `fill-rule:
|
|
365
|
+
* nonzero` the winding is the whole difference between ink and a hole. */
|
|
366
|
+
function area2(pts) {
|
|
367
|
+
let a = 0;
|
|
368
|
+
for (let i = 0; i < pts.length; i++) {
|
|
369
|
+
const [x1, y1] = pts[i];
|
|
370
|
+
const [x2, y2] = pts[(i + 1) % pts.length];
|
|
371
|
+
a += x1 * y2 - x2 * y1;
|
|
372
|
+
}
|
|
373
|
+
return a;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** A filled polygon, wound the way `dot` winds a disc so the two union. */
|
|
377
|
+
function poly(pts, hole = false) {
|
|
378
|
+
const wound = area2(pts) > 0 === hole ? pts : [...pts].reverse();
|
|
379
|
+
return `M${wound.map(([x, y]) => p(x, y)).join('L')}Z`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A filled rectangle with equal corner radii.
|
|
384
|
+
*
|
|
385
|
+
* IT WINDS THE WAY `poly` AND `cap` WIND, and that is load-bearing, not a
|
|
386
|
+
* detail: for most of this file's life the rounded branch ran the other way
|
|
387
|
+
* round the corners, so under `fill-rule: nonzero` every `holeBar`/`dot(ccw)`
|
|
388
|
+
* aimed at a rounded rect UNIONED instead of punching, and every ordinary
|
|
389
|
+
* `poly`/`dot` drawn over one punched a hole instead of adding ink. Nothing
|
|
390
|
+
* reported it — a knockout that does nothing just looks like a solid glyph.
|
|
391
|
+
* It silently ate the collection box's pull slot, the wrench's fork and the
|
|
392
|
+
* bulb's screw thread, and it is why the pan hand carried a bite out of its
|
|
393
|
+
* palm where its thumb should be. Measured, not reasoned: render
|
|
394
|
+
* `rrect(r) + holeBar` and read the centre pixel.
|
|
395
|
+
*/
|
|
396
|
+
function rrect(x, y, w, h, r = 0) {
|
|
397
|
+
const rad = Math.min(r, w / 2, h / 2);
|
|
398
|
+
if (rad <= 0.01)
|
|
399
|
+
return poly([
|
|
400
|
+
[x, y],
|
|
401
|
+
[x + w, y],
|
|
402
|
+
[x + w, y + h],
|
|
403
|
+
[x, y + h],
|
|
404
|
+
]);
|
|
405
|
+
const a = (px, py) => `A${rr(rad)} ${rr(rad)} 0 0 0 ${p(px, py)}`;
|
|
406
|
+
return (
|
|
407
|
+
`M${p(x + rad, y)}${a(x, y + rad)}L${p(x, y + h - rad)}${a(x + rad, y + h)}` +
|
|
408
|
+
`L${p(x + w - rad, y + h)}${a(x + w, y + h - rad)}` +
|
|
409
|
+
`L${p(x + w, y + rad)}${a(x + w - rad, y)}Z`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A rectangular outline of thickness `w`, corners rounded by the joins.
|
|
415
|
+
*
|
|
416
|
+
* A corner radius SMALLER than the stroke's own round end is not a corner
|
|
417
|
+
* radius, it is a defect: the bevel becomes a segment shorter than the capsule
|
|
418
|
+
* is wide, its capsule overlaps both neighbours almost entirely, and the
|
|
419
|
+
* overlap cancels under `fill-rule: nonzero` — at 12x, `border-all` (r 0.4,
|
|
420
|
+
* t 1) and `object-group` (r 0.3, t 2.1) both showed a hollow pip at every
|
|
421
|
+
* corner. Below `t/2` the caps already round the corner by more than `r`
|
|
422
|
+
* asked for, so the plain four-point path is both cleaner and closer to the
|
|
423
|
+
* request.
|
|
424
|
+
*/
|
|
425
|
+
function rframe(x, y, w, h, t = 1.3, r = 0) {
|
|
426
|
+
const inset = t / 2;
|
|
427
|
+
if (r <= inset) {
|
|
428
|
+
return line(
|
|
429
|
+
[
|
|
430
|
+
[x + inset, y + inset],
|
|
431
|
+
[x + w - inset, y + inset],
|
|
432
|
+
[x + w - inset, y + h - inset],
|
|
433
|
+
[x + inset, y + h - inset],
|
|
434
|
+
],
|
|
435
|
+
t,
|
|
436
|
+
true,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const pts = [
|
|
440
|
+
[x + inset + r, y + inset],
|
|
441
|
+
[x + w - inset - r, y + inset],
|
|
442
|
+
[x + w - inset, y + inset + r],
|
|
443
|
+
[x + w - inset, y + h - inset - r],
|
|
444
|
+
[x + w - inset - r, y + h - inset],
|
|
445
|
+
[x + inset + r, y + h - inset],
|
|
446
|
+
[x + inset, y + h - inset - r],
|
|
447
|
+
[x + inset, y + inset + r],
|
|
448
|
+
];
|
|
449
|
+
return line(r > 0 ? pts : [pts[0], pts[1], pts[3], pts[5]], t, true);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A stroked arc, angles in degrees, 0 = +x, growing clockwise on screen.
|
|
454
|
+
*
|
|
455
|
+
* ONE annulus band, not a chain of capsules. Built as `line()` over sampled
|
|
456
|
+
* points, each segment's capsule is shorter than it is wide, consecutive
|
|
457
|
+
* capsules overlap almost entirely, and the overlaps cancel under
|
|
458
|
+
* `fill-rule: nonzero` — rendered at 24x, `rotate`'s arc and `lock`'s shackle
|
|
459
|
+
* came out as a row of hollow circles rather than a stroke. A band has no
|
|
460
|
+
* overlaps to cancel: out along the outer radius, back along the inner.
|
|
461
|
+
*/
|
|
462
|
+
function arc(cx, cy, r, a0, a1, w = 1.3, steps = 0) {
|
|
463
|
+
const t = (w * W) / 2;
|
|
464
|
+
const n = steps || Math.max(6, Math.ceil(Math.abs(a1 - a0) / 9));
|
|
465
|
+
const at = (i, rad) => {
|
|
466
|
+
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
467
|
+
return [cx + Math.cos(a) * rad, cy + Math.sin(a) * rad];
|
|
468
|
+
};
|
|
469
|
+
const pts = [];
|
|
470
|
+
for (let i = 0; i <= n; i++) pts.push(at(i, r + t));
|
|
471
|
+
for (let i = n; i >= 0; i--) pts.push(at(i, Math.max(r - t, 0.01)));
|
|
472
|
+
// Round the two ends so the band still reads as a stroke, not a ribbon.
|
|
473
|
+
return poly(pts) + dot(...at(0, r), t) + dot(...at(n, r), t);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** A stroked elliptical arc — cylinder lips, cloud shoulders, orbits. */
|
|
477
|
+
function ellipseArc(cx, cy, rx, ry, a0, a1, w = 1.15, rot = 0, closed = false) {
|
|
478
|
+
const t = (rot * Math.PI) / 180;
|
|
479
|
+
const n = Math.max(4, Math.ceil(Math.abs(a1 - a0) / 15));
|
|
480
|
+
const pts = [];
|
|
481
|
+
for (let i = 0; i <= (closed ? n - 1 : n); i++) {
|
|
482
|
+
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
483
|
+
const x = Math.cos(a) * rx;
|
|
484
|
+
const y = Math.sin(a) * ry;
|
|
485
|
+
pts.push([cx + x * Math.cos(t) - y * Math.sin(t), cy + x * Math.sin(t) + y * Math.cos(t)]);
|
|
486
|
+
}
|
|
487
|
+
return line(pts, w, closed);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** A filled half-disc, `a0`..`a1` swept — the lit half of a shading toggle. */
|
|
491
|
+
function wedge(cx, cy, r, a0, a1) {
|
|
492
|
+
const n = Math.max(6, Math.ceil(Math.abs(a1 - a0) / 12));
|
|
493
|
+
const pts = [[cx, cy]];
|
|
494
|
+
for (let i = 0; i <= n; i++) {
|
|
495
|
+
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
496
|
+
pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]);
|
|
497
|
+
}
|
|
498
|
+
return poly(pts);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* A HOLE — the same bar or wedge, wound backwards so `fill-rule: nonzero`
|
|
503
|
+
* knocks it out of a solid silhouette underneath. This is how the status
|
|
504
|
+
* family survives the 10px status bar: at that size a thin ring with a mark
|
|
505
|
+
* inside it fills in and every glyph becomes a disc, where a SOLID disc with
|
|
506
|
+
* its mark knocked out keeps the mark's contrast at one device pixel. It is
|
|
507
|
+
* also Blender's own construction — the collection box's slot and the
|
|
508
|
+
* camera's lens are knocked out of their silhouettes, not drawn on them.
|
|
509
|
+
*/
|
|
510
|
+
function holeBar(x1, y1, x2, y2, w) {
|
|
511
|
+
const r = w / 2;
|
|
512
|
+
const dx = x2 - x1;
|
|
513
|
+
const dy = y2 - y1;
|
|
514
|
+
const L = Math.hypot(dx, dy) || 1;
|
|
515
|
+
const nx = (-dy / L) * r;
|
|
516
|
+
const ny = (dx / L) * r;
|
|
517
|
+
return poly(
|
|
518
|
+
[
|
|
519
|
+
[x1 + nx, y1 + ny],
|
|
520
|
+
[x1 - nx, y1 - ny],
|
|
521
|
+
[x2 - nx, y2 - ny],
|
|
522
|
+
[x2 + nx, y2 + ny],
|
|
523
|
+
],
|
|
524
|
+
true,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** A polygon wound backwards — a knocked-out shape. */
|
|
529
|
+
function holePoly(pts) {
|
|
530
|
+
return poly(pts, true);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Blender's chevron: a thin V, the outliner's disclosure and every menu arrow. */
|
|
534
|
+
function chevron(cx, cy, s, dir) {
|
|
535
|
+
const v = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[dir];
|
|
536
|
+
const perp = [-v[1], v[0]];
|
|
537
|
+
const tip = [cx + v[0] * s * 0.55, cy + v[1] * s * 0.55];
|
|
538
|
+
const back = -s * 0.55;
|
|
539
|
+
return line(
|
|
540
|
+
[
|
|
541
|
+
[cx + v[0] * back + perp[0] * s, cy + v[1] * back + perp[1] * s],
|
|
542
|
+
tip,
|
|
543
|
+
[cx + v[0] * back - perp[0] * s, cy + v[1] * back - perp[1] * s],
|
|
544
|
+
],
|
|
545
|
+
1.3,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Blender's isometric cube — a SOLID hexagonal silhouette with its three
|
|
551
|
+
* interior seams knocked out, never a wire box. `outliner.png` at matched
|
|
552
|
+
* scale settles it: every object mark in a row is a filled silhouette with its
|
|
553
|
+
* detail punched through, and the wire version of this glyph collapsed into an
|
|
554
|
+
* undifferentiated grey circle at the 14px row size (its three 1.15-unit
|
|
555
|
+
* spokes merged and the hexagon's corners rounded away).
|
|
556
|
+
*/
|
|
557
|
+
function isoCube(cx = 8, cy = 8, s = 5.4, seam = 1.05) {
|
|
558
|
+
const top = [cx, cy - s];
|
|
559
|
+
const tr = [cx + s * 0.87, cy - s * 0.5];
|
|
560
|
+
const br = [cx + s * 0.87, cy + s * 0.5];
|
|
561
|
+
const bot = [cx, cy + s];
|
|
562
|
+
const bl = [cx - s * 0.87, cy + s * 0.5];
|
|
563
|
+
const tl = [cx - s * 0.87, cy - s * 0.5];
|
|
564
|
+
return (
|
|
565
|
+
poly([tl, top, tr, br, bot, bl]) +
|
|
566
|
+
holeBar(tl[0], tl[1], cx, cy, seam) +
|
|
567
|
+
holeBar(tr[0], tr[1], cx, cy, seam) +
|
|
568
|
+
holeBar(bot[0], bot[1], cx, cy, seam)
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ------------------------------------------------------------------- glyphs
|
|
573
|
+
// Keyed by Font Awesome icon name. Grouped the way the chrome reads them.
|
|
574
|
+
|
|
575
|
+
const glyphs = {};
|
|
576
|
+
/**
|
|
577
|
+
* `meta` is the glyph's COLOUR channel (`IconSetContribution`): `tone` names
|
|
578
|
+
* a category the palette inks, and `tonedPath` splits the drawing so only
|
|
579
|
+
* that second path takes it. Whole-glyph tones are assigned from one table
|
|
580
|
+
* further down, next to the frames they were measured in; `tonedPath` has to
|
|
581
|
+
* be authored HERE, because it is a division of the geometry.
|
|
582
|
+
*/
|
|
583
|
+
const g = (name, d, meta) => {
|
|
584
|
+
glyphs[name] = { path: d, ...(meta ?? {}) };
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// -- disclosure, carets, arrows ------------------------------------------
|
|
588
|
+
g('chevron-right', chevron(7.4, 8, 3.1, 'right'));
|
|
589
|
+
g('chevron-left', chevron(8.6, 8, 3.1, 'left'));
|
|
590
|
+
g('chevron-down', chevron(8, 7.4, 3.1, 'down'));
|
|
591
|
+
// BLENDER HAS NO SOLID CARET. Both places the editor spends one — a dropdown
|
|
592
|
+
// trigger's disclosure and a tree row's expander — Blender draws an open
|
|
593
|
+
// CHEVRON, measured in two frames: `modeling.png` at its native 2x, the
|
|
594
|
+
// orientation well's mark at x 1304..1316, y 76..83 (13x8 device px = 6.5x4
|
|
595
|
+
// CSS, peak 216); and `outliner.png` (also native 2x), the collapsed Camera
|
|
596
|
+
// row's mark at x 96..105, y 146..163 (10x18 = 5x9 CSS) and the expanded
|
|
597
|
+
// Collection row's at x 52..69, y 110..119 (18x10 = 9x5 CSS), peak 194. Both
|
|
598
|
+
// are a 1 CSS px stroke and both run 1.8:1, which is exactly what `chevron()`
|
|
599
|
+
// draws. The names stay `caret-*` — a set draws the editor's names its own
|
|
600
|
+
// way, and Font Awesome's filled triangle is still what every other skin gets.
|
|
601
|
+
// `s` is 4.4 rather than `chevron-*`'s 3.1 so the mark keeps the EXTENT the
|
|
602
|
+
// filled caret had here (8.8 of 16 units, ~7 CSS px against Blender's 6.5).
|
|
603
|
+
g('caret-right', chevron(7.4, 8, 4.4, 'right'));
|
|
604
|
+
g('caret-down', chevron(8, 7.4, 4.4, 'down'));
|
|
605
|
+
g(
|
|
606
|
+
'arrow-right',
|
|
607
|
+
cap(2.6, 8, 12.4, 8) +
|
|
608
|
+
line([
|
|
609
|
+
[9.2, 4.8],
|
|
610
|
+
[12.6, 8],
|
|
611
|
+
[9.2, 11.2],
|
|
612
|
+
]),
|
|
613
|
+
);
|
|
614
|
+
g(
|
|
615
|
+
'arrow-left',
|
|
616
|
+
cap(3.6, 8, 13.4, 8) +
|
|
617
|
+
line([
|
|
618
|
+
[6.8, 4.8],
|
|
619
|
+
[3.4, 8],
|
|
620
|
+
[6.8, 11.2],
|
|
621
|
+
]),
|
|
622
|
+
);
|
|
623
|
+
g(
|
|
624
|
+
'arrow-up',
|
|
625
|
+
cap(8, 3.6, 8, 13.4) +
|
|
626
|
+
line([
|
|
627
|
+
[4.8, 6.8],
|
|
628
|
+
[8, 3.4],
|
|
629
|
+
[11.2, 6.8],
|
|
630
|
+
]),
|
|
631
|
+
);
|
|
632
|
+
g(
|
|
633
|
+
'arrow-down',
|
|
634
|
+
cap(8, 2.6, 8, 12.4) +
|
|
635
|
+
line([
|
|
636
|
+
[4.8, 9.2],
|
|
637
|
+
[8, 12.6],
|
|
638
|
+
[11.2, 9.2],
|
|
639
|
+
]),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// -- the primary actions --------------------------------------------------
|
|
643
|
+
g('plus', cap(8, 3, 8, 13) + cap(3, 8, 13, 8));
|
|
644
|
+
g('minus', cap(3, 8, 13, 8));
|
|
645
|
+
g('xmark', cap(4.2, 4.2, 11.8, 11.8) + cap(11.8, 4.2, 4.2, 11.8));
|
|
646
|
+
g(
|
|
647
|
+
'check',
|
|
648
|
+
line(
|
|
649
|
+
[
|
|
650
|
+
[3.4, 8.4],
|
|
651
|
+
[6.6, 11.6],
|
|
652
|
+
[12.8, 4.6],
|
|
653
|
+
],
|
|
654
|
+
1.5,
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
g(
|
|
658
|
+
'trash',
|
|
659
|
+
rrect(3.3, 4.2, 9.4, 1.5, 0.5) +
|
|
660
|
+
line(
|
|
661
|
+
[
|
|
662
|
+
[6.2, 4.2],
|
|
663
|
+
[6.2, 3.1],
|
|
664
|
+
[9.8, 3.1],
|
|
665
|
+
[9.8, 4.2],
|
|
666
|
+
],
|
|
667
|
+
1.2,
|
|
668
|
+
) +
|
|
669
|
+
line(
|
|
670
|
+
[
|
|
671
|
+
[4.5, 6.1],
|
|
672
|
+
[5.1, 13.1],
|
|
673
|
+
[10.9, 13.1],
|
|
674
|
+
[11.5, 6.1],
|
|
675
|
+
],
|
|
676
|
+
1.25,
|
|
677
|
+
) +
|
|
678
|
+
cap(6.7, 7.4, 6.9, 11.5, 1.1) +
|
|
679
|
+
cap(9.3, 7.4, 9.1, 11.5, 1.1),
|
|
680
|
+
);
|
|
681
|
+
g('magnifying-glass', ring(7.1, 7.1, 3.7, 1.3) + cap(9.9, 9.9, 13.2, 13.2, 1.4));
|
|
682
|
+
/**
|
|
683
|
+
* ZOOM is not SEARCH, and Blender draws the two differently — both frames
|
|
684
|
+
* agree: the Search field's magnifier (`outliner.png`, `properties-object.png`)
|
|
685
|
+
* is a thin RING with a handle, while the viewport navigation cluster's zoom
|
|
686
|
+
* (`modeling-object-none.png`) is a SOLID lens disc with the sign knocked
|
|
687
|
+
* through it and a short fat handle. So `magnifying-glass` keeps its ring and
|
|
688
|
+
* the two signed ones become lenses; drawn as rings, their 1.1-unit sign
|
|
689
|
+
* strokes were the faintest ink on the whole navigation cluster.
|
|
690
|
+
*/
|
|
691
|
+
// The handle STARTS INSIDE the lens (9.3,9.3 is 3.1 from the centre of a
|
|
692
|
+
// 4.25 disc): begun at 10.2 it cleared the disc's edge by a tenth of a unit
|
|
693
|
+
// and the round cap floated off as a separate lozenge at 16px — measured in
|
|
694
|
+
// round 1's frame, where the navigation cluster showed a disc and a diamond.
|
|
695
|
+
const zoomLens = (sign) => dot(7.1, 7.1, 4.25) + sign + cap(9.3, 9.3, 13.4, 13.4, 2);
|
|
696
|
+
g(
|
|
697
|
+
'magnifying-glass-plus',
|
|
698
|
+
zoomLens(holeBar(7.1, 5.2, 7.1, 9, 1.1) + holeBar(5.2, 7.1, 9, 7.1, 1.1)),
|
|
699
|
+
);
|
|
700
|
+
g('magnifying-glass-minus', zoomLens(holeBar(5.2, 7.1, 9, 7.1, 1.1)));
|
|
701
|
+
g('ellipsis', dot(3.6, 8, 1.15) + dot(8, 8, 1.15) + dot(12.4, 8, 1.15));
|
|
702
|
+
g('grip-lines', cap(3, 6.3, 13, 6.3, 1.2) + cap(3, 9.7, 13, 9.7, 1.2));
|
|
703
|
+
g('grip-lines-vertical', cap(6.3, 3, 6.3, 13, 1.2) + cap(9.7, 3, 9.7, 13, 1.2));
|
|
704
|
+
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));
|
|
705
|
+
|
|
706
|
+
// -- visibility, the outliner's right column ------------------------------
|
|
707
|
+
/**
|
|
708
|
+
* The eye is the KNOCKOUT construction the header describes, and it is what
|
|
709
|
+
* `outliner.png` shows at matched scale: one solid almond, a dark iris punched
|
|
710
|
+
* out of it, a bright pupil inside the iris — white / dark / white, three
|
|
711
|
+
* bands that survive a 14px row. Drawn instead as two thin lid strokes with a
|
|
712
|
+
* dot between them (what this was through round 0), every band is a
|
|
713
|
+
* partial-coverage stroke: measured against its own row, the glyph peaked at
|
|
714
|
+
* p95 145 where the label beside it peaked at 185, while Blender's peaks
|
|
715
|
+
* ABOVE its label (151 vs 129). A silhouette reaches full ink; a hairline
|
|
716
|
+
* cannot.
|
|
717
|
+
*/
|
|
718
|
+
const eyeLid = [
|
|
719
|
+
[1.8, 8],
|
|
720
|
+
[3.6, 5.3],
|
|
721
|
+
[5.9, 4],
|
|
722
|
+
[8, 3.7],
|
|
723
|
+
[10.1, 4],
|
|
724
|
+
[12.4, 5.3],
|
|
725
|
+
[14.2, 8],
|
|
726
|
+
];
|
|
727
|
+
const eyeAlmond = [...eyeLid, ...eyeLid.map(([x, y]) => [x, 16 - y]).reverse()];
|
|
728
|
+
const eye = poly(eyeAlmond) + dot(8, 8, 2.9, true) + dot(8, 8, 1.5);
|
|
729
|
+
g('eye', eye);
|
|
730
|
+
g('eye-slash', eye + cap(3.4, 12.6, 12.6, 3.4, 1.35));
|
|
731
|
+
g('lock', rrect(3.4, 7.2, 9.2, 6.4, 1.1) + arc(8, 7.1, 2.75, 180, 360, 1.25) + dot(8, 10.4, 1.1));
|
|
732
|
+
g(
|
|
733
|
+
'lock-open',
|
|
734
|
+
rrect(3.4, 7.2, 9.2, 6.4, 1.1) + arc(11.1, 7.1, 2.75, 180, 330, 1.25) + dot(8, 10.4, 1.1),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// -- content kinds --------------------------------------------------------
|
|
738
|
+
g('cube', isoCube());
|
|
739
|
+
g('cubes', isoCube(5.1, 5.6, 3.5, 0.8) + isoCube(10.9, 10.4, 3.5, 0.8));
|
|
740
|
+
g('cubes-stacked', isoCube(8, 4.6, 3.3, 0.8) + isoCube(8, 11.4, 3.3, 0.8));
|
|
741
|
+
g('square', rframe(3, 3, 10, 10, 1.3));
|
|
742
|
+
g('circle', ring(8, 8, 4.4, 1.3));
|
|
743
|
+
g('circle-dot', dot(8, 8, 6) + dot(8, 8, 3.9, true) + dot(8, 8, 2));
|
|
744
|
+
// Blender's collection: a lidded box with a pull slot.
|
|
745
|
+
// Filled body, filled lid band, and the pull slot KNOCKED OUT of the body —
|
|
746
|
+
// exactly what `outliner.png` shows at matched scale, where Blender's
|
|
747
|
+
// Collection is the brightest mark in the row. Drawn as a 1.25-unit frame
|
|
748
|
+
// under a filled lid, it read as a hairline rectangle no heavier than the
|
|
749
|
+
// label beside it.
|
|
750
|
+
const box =
|
|
751
|
+
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);
|
|
752
|
+
g('box-archive', box);
|
|
753
|
+
g(
|
|
754
|
+
'folder',
|
|
755
|
+
line(
|
|
756
|
+
[
|
|
757
|
+
[2.2, 12.9],
|
|
758
|
+
[2.2, 4.1],
|
|
759
|
+
[6.4, 4.1],
|
|
760
|
+
[7.6, 5.7],
|
|
761
|
+
[13.8, 5.7],
|
|
762
|
+
[13.8, 12.9],
|
|
763
|
+
],
|
|
764
|
+
1.3,
|
|
765
|
+
true,
|
|
766
|
+
),
|
|
767
|
+
);
|
|
768
|
+
g(
|
|
769
|
+
'folder-open',
|
|
770
|
+
line(
|
|
771
|
+
[
|
|
772
|
+
[2.2, 12.9],
|
|
773
|
+
[2.2, 4.1],
|
|
774
|
+
[6.4, 4.1],
|
|
775
|
+
[7.6, 5.7],
|
|
776
|
+
[12.4, 5.7],
|
|
777
|
+
[12.4, 7.6],
|
|
778
|
+
],
|
|
779
|
+
1.3,
|
|
780
|
+
) +
|
|
781
|
+
line(
|
|
782
|
+
[
|
|
783
|
+
[2.2, 12.9],
|
|
784
|
+
[4.4, 7.6],
|
|
785
|
+
[15, 7.6],
|
|
786
|
+
[12.8, 12.9],
|
|
787
|
+
],
|
|
788
|
+
1.3,
|
|
789
|
+
true,
|
|
790
|
+
),
|
|
791
|
+
);
|
|
792
|
+
g(
|
|
793
|
+
'folder-tree',
|
|
794
|
+
rframe(2, 2.6, 5.6, 4.2, 1.2, 0.5) +
|
|
795
|
+
rframe(8.4, 9.2, 5.6, 4.2, 1.2, 0.5) +
|
|
796
|
+
line(
|
|
797
|
+
[
|
|
798
|
+
[4.8, 6.8],
|
|
799
|
+
[4.8, 11.3],
|
|
800
|
+
[8.4, 11.3],
|
|
801
|
+
],
|
|
802
|
+
1.15,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
const page =
|
|
806
|
+
line(
|
|
807
|
+
[
|
|
808
|
+
[3.4, 1.9],
|
|
809
|
+
[9.2, 1.9],
|
|
810
|
+
[12.6, 5.3],
|
|
811
|
+
[12.6, 14.1],
|
|
812
|
+
[3.4, 14.1],
|
|
813
|
+
],
|
|
814
|
+
1.25,
|
|
815
|
+
true,
|
|
816
|
+
) +
|
|
817
|
+
line(
|
|
818
|
+
[
|
|
819
|
+
[9, 2.1],
|
|
820
|
+
[9, 5.5],
|
|
821
|
+
[12.4, 5.5],
|
|
822
|
+
],
|
|
823
|
+
1.15,
|
|
824
|
+
);
|
|
825
|
+
g('file', page);
|
|
826
|
+
g('file-lines', page + cap(5.6, 8, 10.4, 8, 1.1) + cap(5.6, 10.6, 10.4, 10.6, 1.1));
|
|
827
|
+
g(
|
|
828
|
+
'file-code',
|
|
829
|
+
page +
|
|
830
|
+
line(
|
|
831
|
+
[
|
|
832
|
+
[7.2, 8],
|
|
833
|
+
[5.6, 9.8],
|
|
834
|
+
[7.2, 11.6],
|
|
835
|
+
],
|
|
836
|
+
1.1,
|
|
837
|
+
) +
|
|
838
|
+
line(
|
|
839
|
+
[
|
|
840
|
+
[9.4, 8],
|
|
841
|
+
[11, 9.8],
|
|
842
|
+
[9.4, 11.6],
|
|
843
|
+
],
|
|
844
|
+
1.1,
|
|
845
|
+
),
|
|
846
|
+
);
|
|
847
|
+
g(
|
|
848
|
+
'file-image',
|
|
849
|
+
page +
|
|
850
|
+
dot(6.6, 8.2, 1) +
|
|
851
|
+
line(
|
|
852
|
+
[
|
|
853
|
+
[5.2, 12.1],
|
|
854
|
+
[7.6, 9.4],
|
|
855
|
+
[9.2, 11.2],
|
|
856
|
+
[10.4, 10.1],
|
|
857
|
+
[12, 12.1],
|
|
858
|
+
],
|
|
859
|
+
1.1,
|
|
860
|
+
),
|
|
861
|
+
);
|
|
862
|
+
g(
|
|
863
|
+
'file-export',
|
|
864
|
+
page +
|
|
865
|
+
cap(8.4, 10.4, 14.4, 10.4, 1.2) +
|
|
866
|
+
line(
|
|
867
|
+
[
|
|
868
|
+
[12.2, 8.4],
|
|
869
|
+
[14.6, 10.4],
|
|
870
|
+
[12.2, 12.4],
|
|
871
|
+
],
|
|
872
|
+
1.2,
|
|
873
|
+
),
|
|
874
|
+
);
|
|
875
|
+
g(
|
|
876
|
+
'image',
|
|
877
|
+
rframe(2, 3, 12, 10, 1.25, 0.8) +
|
|
878
|
+
dot(5.8, 6.6, 1.15) +
|
|
879
|
+
line(
|
|
880
|
+
[
|
|
881
|
+
[3.4, 11.6],
|
|
882
|
+
[6.8, 8.1],
|
|
883
|
+
[8.8, 10.2],
|
|
884
|
+
[10.6, 8.4],
|
|
885
|
+
[12.9, 11.2],
|
|
886
|
+
],
|
|
887
|
+
1.15,
|
|
888
|
+
),
|
|
889
|
+
);
|
|
890
|
+
g(
|
|
891
|
+
'film',
|
|
892
|
+
rframe(1.8, 3, 12.4, 10, 1.25, 0.6) +
|
|
893
|
+
cap(4.6, 3.6, 4.6, 12.4, 1.15) +
|
|
894
|
+
cap(11.4, 3.6, 11.4, 12.4, 1.15) +
|
|
895
|
+
cap(4.6, 8, 11.4, 8, 1.15),
|
|
896
|
+
);
|
|
897
|
+
g(
|
|
898
|
+
'code',
|
|
899
|
+
line(
|
|
900
|
+
[
|
|
901
|
+
[6, 3.4],
|
|
902
|
+
[2.2, 8],
|
|
903
|
+
[6, 12.6],
|
|
904
|
+
],
|
|
905
|
+
1.3,
|
|
906
|
+
) +
|
|
907
|
+
line(
|
|
908
|
+
[
|
|
909
|
+
[10, 3.4],
|
|
910
|
+
[13.8, 8],
|
|
911
|
+
[10, 12.6],
|
|
912
|
+
],
|
|
913
|
+
1.3,
|
|
914
|
+
),
|
|
915
|
+
);
|
|
916
|
+
g(
|
|
917
|
+
'terminal',
|
|
918
|
+
line(
|
|
919
|
+
[
|
|
920
|
+
[3, 4.2],
|
|
921
|
+
[7, 8],
|
|
922
|
+
[3, 11.8],
|
|
923
|
+
],
|
|
924
|
+
1.3,
|
|
925
|
+
) + cap(8.4, 12.2, 13.4, 12.2, 1.25),
|
|
926
|
+
);
|
|
927
|
+
g(
|
|
928
|
+
'table',
|
|
929
|
+
rframe(2, 3, 12, 10, 1.2, 0.5) +
|
|
930
|
+
cap(2.6, 6.4, 13.4, 6.4, 1.15) +
|
|
931
|
+
cap(7.2, 6.4, 7.2, 12.4, 1.15) +
|
|
932
|
+
cap(2.6, 9.6, 13.4, 9.6, 1.15),
|
|
933
|
+
);
|
|
934
|
+
g(
|
|
935
|
+
'table-cells',
|
|
936
|
+
rframe(2, 3, 12, 10, 1.2, 0.5) +
|
|
937
|
+
cap(2.6, 6.4, 13.4, 6.4, 1.1) +
|
|
938
|
+
cap(2.6, 9.6, 13.4, 9.6, 1.1) +
|
|
939
|
+
cap(6.2, 3.4, 6.2, 12.6, 1.1) +
|
|
940
|
+
cap(9.8, 3.4, 9.8, 12.6, 1.1),
|
|
941
|
+
);
|
|
942
|
+
g(
|
|
943
|
+
'table-cells-large',
|
|
944
|
+
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),
|
|
945
|
+
);
|
|
946
|
+
/**
|
|
947
|
+
* A FLAT 3x3 grid, and it STAYS FLAT. Four sites ask for `border-all` and
|
|
948
|
+
* three of them mean a flat grid — the canvas viewport's grid toggle, the
|
|
949
|
+
* 3D viewport overlay's, and the CSS border editor's "all borders" — so the
|
|
950
|
+
* trapezoid Blender draws on the navigation cluster's projection toggle is
|
|
951
|
+
* NOT this glyph's to become. It gets its own name below, which is the same
|
|
952
|
+
* ruling `editorIcons.modeling`'s docblock already records for the mesh
|
|
953
|
+
* operators: a glyph that IS an operation owns its own name.
|
|
954
|
+
*/
|
|
955
|
+
g(
|
|
956
|
+
'border-all',
|
|
957
|
+
rframe(3.2, 3.2, 9.6, 9.6, 1, 0.4) +
|
|
958
|
+
cap(6.4, 3.7, 6.4, 12.3, 0.85) +
|
|
959
|
+
cap(9.6, 3.7, 9.6, 12.3, 0.85) +
|
|
960
|
+
cap(3.7, 6.4, 12.3, 6.4, 0.85) +
|
|
961
|
+
cap(3.7, 9.6, 12.3, 9.6, 0.85),
|
|
962
|
+
);
|
|
963
|
+
/**
|
|
964
|
+
* THE PROJECTION TOGGLE at the foot of the navigation cluster — Blender's
|
|
965
|
+
* fourth cell, and it is a PERSPECTIVE grid, not a square one. Read off
|
|
966
|
+
* `modeling-edit-none.png` (device x 2774..2805, y 541..572): a symmetric
|
|
967
|
+
* trapezoid 32 device wide by 26 tall, four horizontal rules whose spacing
|
|
968
|
+
* OPENS downward (6, 8, 10 device) and four verticals converging upward, the
|
|
969
|
+
* outer pair running x 6.8 -> 1.5 on the left and 24.2 -> 29.5 on the right.
|
|
970
|
+
* Ours was a flat square 27x26 — the height was already Blender's and the
|
|
971
|
+
* width was 5 device short, because a square cannot be as wide at its foot as
|
|
972
|
+
* a trapezoid.
|
|
973
|
+
*
|
|
974
|
+
* The prior cut said so itself and stopped there, for the right reason: it
|
|
975
|
+
* shared `border-all` with three flat-grid sites. The name is what was in the
|
|
976
|
+
* way, so the name is what moved (`editorIcons.viewport.projection` is now
|
|
977
|
+
* `viewport-projection`, carrying Font Awesome's `border-all` drawing as its
|
|
978
|
+
* fallback, so a set without this key paints exactly what it painted before).
|
|
979
|
+
* THE PRIOR CUT ALSO MIS-COUNTED IT as "four cells across": it is three, in
|
|
980
|
+
* both axes — four rules bound three columns.
|
|
981
|
+
*
|
|
982
|
+
* Every constant below was seeded off that character map and then settled
|
|
983
|
+
* against the cell's own half-coverage mask, the same instrument the wrench
|
|
984
|
+
* and the hand use. Seeded 150, settled 7 of 374 reference ink px (the old
|
|
985
|
+
* square: 316). No constant moved as much as one device pixel from its
|
|
986
|
+
* reading, and the four rows moved by ONE SHARED half-pixel, which is the
|
|
987
|
+
* cell's own centring rather than a fit.
|
|
988
|
+
*/
|
|
989
|
+
const perspectiveGrid = () => {
|
|
990
|
+
const cx = 8.01; // the trapezoid's axis
|
|
991
|
+
const yTop = 3.71; // its near and far rules
|
|
992
|
+
const yBot = 12.38;
|
|
993
|
+
const rows = [yTop, 5.78, 8.73, yBot]; // spacing opens toward the viewer
|
|
994
|
+
const hOuterTop = 3.17; // half-widths at each end, outer pair and inner
|
|
995
|
+
const hOuterBot = 5.4;
|
|
996
|
+
const hInnerTop = 0.89;
|
|
997
|
+
const hInnerBot = 1.86;
|
|
998
|
+
const w = 0.74;
|
|
999
|
+
/** A vertical's half-width at depth `y`, linear between the two ends. */
|
|
1000
|
+
const at = (h0, h1, y) => h0 + ((h1 - h0) * (y - yTop)) / (yBot - yTop);
|
|
1001
|
+
return (
|
|
1002
|
+
rows
|
|
1003
|
+
.map((y) => cap(cx - at(hOuterTop, hOuterBot, y), y, cx + at(hOuterTop, hOuterBot, y), y, w))
|
|
1004
|
+
.join('') +
|
|
1005
|
+
[
|
|
1006
|
+
[hOuterTop, hOuterBot],
|
|
1007
|
+
[hInnerTop, hInnerBot],
|
|
1008
|
+
]
|
|
1009
|
+
.flatMap(([h0, h1]) => [-1, 1].map((s) => cap(cx + s * h0, yTop, cx + s * h1, yBot, w)))
|
|
1010
|
+
.join('')
|
|
1011
|
+
);
|
|
1012
|
+
};
|
|
1013
|
+
g('viewport-projection', perspectiveGrid());
|
|
1014
|
+
g(
|
|
1015
|
+
'list',
|
|
1016
|
+
dot(3.2, 4.4, 1) +
|
|
1017
|
+
dot(3.2, 8, 1) +
|
|
1018
|
+
dot(3.2, 11.6, 1) +
|
|
1019
|
+
cap(6.2, 4.4, 13.2, 4.4, 1.15) +
|
|
1020
|
+
cap(6.2, 8, 13.2, 8, 1.15) +
|
|
1021
|
+
cap(6.2, 11.6, 13.2, 11.6, 1.15),
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
// -- scene objects: Blender's outliner glyphs ------------------------------
|
|
1025
|
+
g(
|
|
1026
|
+
'camera',
|
|
1027
|
+
rrect(1.8, 5.2, 8.2, 6.6, 0.9) +
|
|
1028
|
+
poly([
|
|
1029
|
+
[10.4, 8.5],
|
|
1030
|
+
[14.2, 5.4],
|
|
1031
|
+
[14.2, 11.6],
|
|
1032
|
+
]) +
|
|
1033
|
+
dot(3.9, 3.2, 1.25) +
|
|
1034
|
+
dot(7.3, 3.2, 1.25),
|
|
1035
|
+
);
|
|
1036
|
+
g(
|
|
1037
|
+
'camera-rotate',
|
|
1038
|
+
rrect(1.6, 4.4, 7.4, 6, 0.9) +
|
|
1039
|
+
poly([
|
|
1040
|
+
[9.4, 7.4],
|
|
1041
|
+
[12.8, 4.6],
|
|
1042
|
+
[12.8, 10.2],
|
|
1043
|
+
]) +
|
|
1044
|
+
dot(3.5, 2.6, 1.15) +
|
|
1045
|
+
dot(6.6, 2.6, 1.15) +
|
|
1046
|
+
arc(10.4, 11.6, 2.7, 110, 400, 1.2) +
|
|
1047
|
+
poly([
|
|
1048
|
+
[8.4, 9.2],
|
|
1049
|
+
[10.4, 9.4],
|
|
1050
|
+
[8.8, 11.4],
|
|
1051
|
+
]),
|
|
1052
|
+
);
|
|
1053
|
+
// The Light row in `outliner.png`: the bulb FILLS its 14px slot — a big head,
|
|
1054
|
+
// a wide screw base, a foot. At the old 3.5 head it rendered ~7px of ink in a
|
|
1055
|
+
// 14px row and read as a dim pebble beside its own label.
|
|
1056
|
+
g(
|
|
1057
|
+
'lightbulb',
|
|
1058
|
+
dot(8, 5.7, 4.3) +
|
|
1059
|
+
rrect(5.7, 8.6, 4.6, 3.1, 0.5) +
|
|
1060
|
+
holeBar(5.9, 9.6, 10.1, 9.6, 0.7) +
|
|
1061
|
+
cap(6.2, 13.1, 9.8, 13.1, 1.5),
|
|
1062
|
+
);
|
|
1063
|
+
g(
|
|
1064
|
+
'sun',
|
|
1065
|
+
dot(8, 8, 2.9) +
|
|
1066
|
+
[0, 45, 90, 135, 180, 225, 270, 315]
|
|
1067
|
+
.map((a) => {
|
|
1068
|
+
const r = (a * Math.PI) / 180;
|
|
1069
|
+
return cap(
|
|
1070
|
+
8 + Math.cos(r) * 3,
|
|
1071
|
+
8 + Math.sin(r) * 3,
|
|
1072
|
+
8 + Math.cos(r) * 6,
|
|
1073
|
+
8 + Math.sin(r) * 6,
|
|
1074
|
+
1.5,
|
|
1075
|
+
);
|
|
1076
|
+
})
|
|
1077
|
+
.join(''),
|
|
1078
|
+
);
|
|
1079
|
+
g(
|
|
1080
|
+
'cloud-sun',
|
|
1081
|
+
dot(10.6, 4.4, 2.1) +
|
|
1082
|
+
[215, 270, 325, 20]
|
|
1083
|
+
.map((a) => {
|
|
1084
|
+
const r = (a * Math.PI) / 180;
|
|
1085
|
+
return cap(
|
|
1086
|
+
10.6 + Math.cos(r) * 3,
|
|
1087
|
+
4.4 + Math.sin(r) * 3,
|
|
1088
|
+
10.6 + Math.cos(r) * 4.4,
|
|
1089
|
+
4.4 + Math.sin(r) * 4.4,
|
|
1090
|
+
1.15,
|
|
1091
|
+
);
|
|
1092
|
+
})
|
|
1093
|
+
.join('') +
|
|
1094
|
+
dot(4.8, 10.2, 2.4) +
|
|
1095
|
+
dot(7.6, 9.4, 2.9) +
|
|
1096
|
+
dot(10, 10.8, 2) +
|
|
1097
|
+
rrect(3.8, 10.2, 7.6, 2.6, 1.2),
|
|
1098
|
+
);
|
|
1099
|
+
g(
|
|
1100
|
+
'globe',
|
|
1101
|
+
ring(8, 8, 5.3, 1.25) +
|
|
1102
|
+
cap(2.7, 8, 13.3, 8, 1.1) +
|
|
1103
|
+
ellipseArc(8, 8, 2.6, 5.3, 0, 360, 1.1, 0, true),
|
|
1104
|
+
);
|
|
1105
|
+
g(
|
|
1106
|
+
'person',
|
|
1107
|
+
dot(8, 3.4, 1.9) +
|
|
1108
|
+
cap(8, 5.6, 8, 9.6, 1.4) +
|
|
1109
|
+
cap(4.4, 6.8, 11.6, 6.8, 1.25) +
|
|
1110
|
+
cap(8, 9.4, 5.4, 13.4, 1.3) +
|
|
1111
|
+
cap(8, 9.4, 10.6, 13.4, 1.3),
|
|
1112
|
+
);
|
|
1113
|
+
g(
|
|
1114
|
+
'person-running',
|
|
1115
|
+
dot(10, 3.2, 1.8) +
|
|
1116
|
+
cap(10.4, 5.4, 7.2, 8.4, 1.5) +
|
|
1117
|
+
cap(7.2, 8.4, 8.8, 10.8, 1.4) +
|
|
1118
|
+
cap(8.8, 10.8, 8.4, 14, 1.4) +
|
|
1119
|
+
cap(7.2, 8.4, 3.4, 9.6, 1.35) +
|
|
1120
|
+
cap(3.4, 9.6, 2.8, 12.8, 1.3) +
|
|
1121
|
+
cap(10.6, 5.8, 13.6, 7.8, 1.3),
|
|
1122
|
+
);
|
|
1123
|
+
g(
|
|
1124
|
+
'person-falling',
|
|
1125
|
+
dot(11.8, 3.4, 1.8) +
|
|
1126
|
+
cap(10.8, 5.6, 6, 7.4, 1.5) +
|
|
1127
|
+
cap(6, 7.4, 2.4, 5.4, 1.35) +
|
|
1128
|
+
cap(6, 7.4, 7.6, 10.8, 1.4) +
|
|
1129
|
+
cap(7.6, 10.8, 4.6, 13.2, 1.35) +
|
|
1130
|
+
cap(7.6, 10.8, 12.4, 12, 1.35),
|
|
1131
|
+
);
|
|
1132
|
+
g(
|
|
1133
|
+
'bone',
|
|
1134
|
+
poly([
|
|
1135
|
+
[8, 1.6],
|
|
1136
|
+
[10.6, 5],
|
|
1137
|
+
[8, 8],
|
|
1138
|
+
[5.4, 5],
|
|
1139
|
+
]) +
|
|
1140
|
+
line(
|
|
1141
|
+
[
|
|
1142
|
+
[8, 8],
|
|
1143
|
+
[10.6, 5],
|
|
1144
|
+
[8, 1.6],
|
|
1145
|
+
[5.4, 5],
|
|
1146
|
+
[8, 8],
|
|
1147
|
+
[8, 14.4],
|
|
1148
|
+
],
|
|
1149
|
+
1.2,
|
|
1150
|
+
) +
|
|
1151
|
+
line(
|
|
1152
|
+
[
|
|
1153
|
+
[10.6, 5],
|
|
1154
|
+
[8, 14.4],
|
|
1155
|
+
[5.4, 5],
|
|
1156
|
+
],
|
|
1157
|
+
1.2,
|
|
1158
|
+
) +
|
|
1159
|
+
dot(8, 14.4, 1.1),
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
// -- status ----------------------------------------------------------------
|
|
1163
|
+
g('circle-info', dot(8, 8, 6) + dot(8, 4.4, 1.05, true) + holeBar(8, 6.6, 8, 11.6, 1.9));
|
|
1164
|
+
g(
|
|
1165
|
+
'circle-check',
|
|
1166
|
+
dot(8, 8, 6) + holeBar(4.8, 8.1, 7, 10.4, 1.9) + holeBar(6.6, 10.4, 11.2, 5.5, 1.9),
|
|
1167
|
+
);
|
|
1168
|
+
g('circle-exclamation', dot(8, 8, 6) + holeBar(8, 4.2, 8, 9.4, 1.9) + dot(8, 11.5, 1.05, true));
|
|
1169
|
+
g(
|
|
1170
|
+
'circle-xmark',
|
|
1171
|
+
dot(8, 8, 6) + holeBar(5.4, 5.4, 10.6, 10.6, 1.9) + holeBar(10.6, 5.4, 5.4, 10.6, 1.9),
|
|
1172
|
+
);
|
|
1173
|
+
g('circle-minus', dot(8, 8, 6) + holeBar(4.8, 8, 11.2, 8, 1.9));
|
|
1174
|
+
g(
|
|
1175
|
+
'circle-play',
|
|
1176
|
+
dot(8, 8, 6) +
|
|
1177
|
+
holePoly([
|
|
1178
|
+
[6.2, 4.8],
|
|
1179
|
+
[11.4, 8],
|
|
1180
|
+
[6.2, 11.2],
|
|
1181
|
+
]),
|
|
1182
|
+
);
|
|
1183
|
+
g('circle-half-stroke', ring(8, 8, 5.4, 1.2) + wedge(8, 8, 4.9, 90, 270));
|
|
1184
|
+
g(
|
|
1185
|
+
'triangle-exclamation',
|
|
1186
|
+
poly([
|
|
1187
|
+
[8, 1.9],
|
|
1188
|
+
[15, 13.8],
|
|
1189
|
+
[1, 13.8],
|
|
1190
|
+
]) +
|
|
1191
|
+
holeBar(8, 6.2, 8, 10, 1.9) +
|
|
1192
|
+
dot(8, 11.9, 1.05, true),
|
|
1193
|
+
);
|
|
1194
|
+
g(
|
|
1195
|
+
'spinner',
|
|
1196
|
+
[0, 45, 90, 135, 180, 225, 270, 315]
|
|
1197
|
+
.map((a, i) => {
|
|
1198
|
+
const r = (a * Math.PI) / 180;
|
|
1199
|
+
return dot(8 + Math.cos(r) * 5.1, 8 + Math.sin(r) * 5.1, 0.85 + i * 0.11);
|
|
1200
|
+
})
|
|
1201
|
+
.join(''),
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
// -- transport -------------------------------------------------------------
|
|
1205
|
+
g(
|
|
1206
|
+
'play',
|
|
1207
|
+
poly([
|
|
1208
|
+
[4.4, 2.8],
|
|
1209
|
+
[13.2, 8],
|
|
1210
|
+
[4.4, 13.2],
|
|
1211
|
+
]),
|
|
1212
|
+
);
|
|
1213
|
+
g('pause', rrect(4, 3, 3, 10, 0.4) + rrect(9, 3, 3, 10, 0.4));
|
|
1214
|
+
g('stop', rrect(3.4, 3.4, 9.2, 9.2, 0.5));
|
|
1215
|
+
g(
|
|
1216
|
+
'forward-step',
|
|
1217
|
+
poly([
|
|
1218
|
+
[3.6, 3.2],
|
|
1219
|
+
[11, 8],
|
|
1220
|
+
[3.6, 12.8],
|
|
1221
|
+
]) + rrect(11.4, 3.2, 2.2, 9.6, 0.4),
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
// -- tools -----------------------------------------------------------------
|
|
1225
|
+
g(
|
|
1226
|
+
'arrow-pointer',
|
|
1227
|
+
poly([
|
|
1228
|
+
[4.2, 1.9],
|
|
1229
|
+
[4.2, 12.4],
|
|
1230
|
+
[6.9, 9.9],
|
|
1231
|
+
[8.7, 14.1],
|
|
1232
|
+
[10.9, 13.1],
|
|
1233
|
+
[9.1, 9],
|
|
1234
|
+
[12.6, 8.6],
|
|
1235
|
+
]),
|
|
1236
|
+
);
|
|
1237
|
+
// Blender's pan hand (`modeling-object-none.png`) is COMPACT: ~13 px of ink in
|
|
1238
|
+
// the cluster's 16 px slot. Ours spanned 1.8..14.2, which the scale dial pushes
|
|
1239
|
+
// to ~15 px rendered — a hand a full size larger than the zoom beside it.
|
|
1240
|
+
g(
|
|
1241
|
+
'hand-pointer',
|
|
1242
|
+
// Blender's pan hand measures 15x15 in the cluster at matched scale — SQUARE.
|
|
1243
|
+
// Round 2's shrink left ours 18x20: right width, three pixels too tall,
|
|
1244
|
+
// because the middle finger still reached the top of the box.
|
|
1245
|
+
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),
|
|
1246
|
+
);
|
|
1247
|
+
/**
|
|
1248
|
+
* THE PAN TOOL'S OWN GLYPH. `editorIcons.viewport.pan` is `faHand`, whose
|
|
1249
|
+
* iconName is `hand` — a name this set did not carry, so the navigation
|
|
1250
|
+
* cluster drew Font Awesome's hand and nothing else on the Model workspace
|
|
1251
|
+
* did (checked by resolving every `fa*` identifier in `packages/editor/src`
|
|
1252
|
+
* and `packages/mesh/contributions` to its `iconName` and differencing
|
|
1253
|
+
* against these keys: `hand` was the only genuine miss; `faExclamationTriangle`
|
|
1254
|
+
* and `faTimesCircle` are FA-5 aliases that resolve to `triangle-exclamation`
|
|
1255
|
+
* and `circle-xmark`, both present).
|
|
1256
|
+
*
|
|
1257
|
+
* RE-MEASURED 2026-09-19 against the capsule's SECOND CELL on
|
|
1258
|
+
* `modeling-edit-none.png` (device x 2774..2805, y 421..452 — the cluster's
|
|
1259
|
+
* cells are 32x32 device and this one's glyph FILLS it, 32x32 of ink). Ours
|
|
1260
|
+
* inked 28x27: "15x15 in a 16 px slot" was a reading of an earlier frame and
|
|
1261
|
+
* of the wrong pixels; Blender's pan hand is 16.0 x 16.0 CSS, the same box its
|
|
1262
|
+
* magnifier fills, which is the whole point — the cluster's glyphs are one
|
|
1263
|
+
* size. Three things were wrong besides the size and each is visible at 8x:
|
|
1264
|
+
* the fingers were PARALLEL and touching where Blender SPLAYS them with a
|
|
1265
|
+
* clear gap (the index leans right going down, the ring leans left, only the
|
|
1266
|
+
* middle is vertical); the thumb was a horizontal block at the palm's waist
|
|
1267
|
+
* where Blender's points LEFT and DOWN out of the palm's lower third; and the
|
|
1268
|
+
* palm was a flat-bottomed box where Blender's bottom is a wide dome. Together
|
|
1269
|
+
* they read as a mitten.
|
|
1270
|
+
*
|
|
1271
|
+
* FITTED THE WAY `modifierWrench` WAS: the structure read off the cell as a
|
|
1272
|
+
* character map first (four splayed finger capsules, a domed palm, a thumb),
|
|
1273
|
+
* then every constant settled by rendering at the reference's own scale and
|
|
1274
|
+
* counting the pixels where the two HALF-COVERAGE masks disagree. Seeded from
|
|
1275
|
+
* the map alone it scored 106 of 534 reference ink px against the old glyph's
|
|
1276
|
+
* 277; settled, 39, of which seven are the dome's last row. Every finger constant stayed within a device pixel of its
|
|
1277
|
+
* own reading. THE PALM'S DID NOT, and that is honest rather than hidden: an
|
|
1278
|
+
* axis-aligned rounded rect cannot be Blender's asymmetric dome (the thumb's
|
|
1279
|
+
* mass makes its bottom-left tighter than its bottom-right), so the fit moved
|
|
1280
|
+
* the palm 2 device px left to trade that error against the thumb's. The one
|
|
1281
|
+
* run of reference ink we do not cover is the dome's last row.
|
|
1282
|
+
*
|
|
1283
|
+
* THE INSTRUMENT, stated because it decides every number here: our render box
|
|
1284
|
+
* is the `2xl` rung, 18 CSS = 36 device, while Blender's cell is 32 — so a
|
|
1285
|
+
* comparison crops our 36 px raster to its central 32, and a glyph that FILLS
|
|
1286
|
+
* Blender's cell occupies only 32/36 of our own viewBox. Reading ours at a
|
|
1287
|
+
* loose coverage cut instead of half makes it measure a pixel wider than it
|
|
1288
|
+
* is, which is the trap the mesh-data glyph's note already records.
|
|
1289
|
+
*/
|
|
1290
|
+
g(
|
|
1291
|
+
'hand',
|
|
1292
|
+
// Four fingers, each a capsule from its own cap centre down into the palm.
|
|
1293
|
+
cap(5.81, 3.63, 6.4, 8.76, 1.33) + // index, leaning right going down
|
|
1294
|
+
cap(8.73, 2.9, 8.73, 8.36, 1.33) + // middle — the tallest, and the only vertical one
|
|
1295
|
+
cap(11.64, 3.63, 10.81, 7.96, 1.33) + // ring, leaning the other way
|
|
1296
|
+
cap(13.28, 6.34, 12.34, 8.76, 1.33) + // pinky, starting well below the rest
|
|
1297
|
+
rrect(4.65, 7.6, 7.62, 5.83, 2.95) + // the palm
|
|
1298
|
+
cap(3.3, 8.99, 5.38, 10.82, 2.0), // the thumb, out of the palm's lower third
|
|
1299
|
+
);
|
|
1300
|
+
g(
|
|
1301
|
+
/**
|
|
1302
|
+
* The MOVE tool. Blender's (`modeling-edit-none.png`, tool shelf) is four
|
|
1303
|
+
* SOLID arrowheads standing apart on short stubs around an empty centre —
|
|
1304
|
+
* not a crosshair. Ours was one continuous 1.2-unit cross with 1.15-unit
|
|
1305
|
+
* chevrons on its ends, which at matched scale is a thin plus sign with
|
|
1306
|
+
* ticks: a different mark, and the lightest thing on the rail.
|
|
1307
|
+
*
|
|
1308
|
+
* RE-MEASURED 2026-09-19 off the same frame, this time as an ASCII map in
|
|
1309
|
+
* AUTHORED units (0.5/char) rather than by eye: the previous fit was RIGHT
|
|
1310
|
+
* IN FORM AND WRONG IN SIZE AND PARTS. Ours inked 35x34 device px against
|
|
1311
|
+
* Blender's 46x46 — 74% — because the heads stopped at radius 6.4 where
|
|
1312
|
+
* Blender's tips reach the box edge (radius 8, base at 5.5, half-width
|
|
1313
|
+
* 2.4); the STUBS were missing entirely (a 1.0-unit bar from radius 2.5 to
|
|
1314
|
+
* 4.6 under each head); and a 1.35 centre dot was drawn where Blender's
|
|
1315
|
+
* centre is empty. Size is not a detail on this rail — the same frame's
|
|
1316
|
+
* inset and bevel marks ink 40x40 and ours match them to a pixel, so the
|
|
1317
|
+
* transform group was the odd one out in its own column.
|
|
1318
|
+
*/
|
|
1319
|
+
'up-down-left-right',
|
|
1320
|
+
[
|
|
1321
|
+
[0, -1],
|
|
1322
|
+
[0, 1],
|
|
1323
|
+
[-1, 0],
|
|
1324
|
+
[1, 0],
|
|
1325
|
+
]
|
|
1326
|
+
.map(
|
|
1327
|
+
([dx, dy]) =>
|
|
1328
|
+
poly([
|
|
1329
|
+
[8 + dx * 5.4 - dy * 2.45, 8 + dy * 5.4 + dx * 2.45],
|
|
1330
|
+
[8 + dx * 5.4 + dy * 2.45, 8 + dy * 5.4 - dx * 2.45],
|
|
1331
|
+
[8 + dx * 8.4, 8 + dy * 8.4],
|
|
1332
|
+
]) + cap(8 + dx * 2.6, 8 + dy * 2.6, 8 + dx * 5.05, 8 + dy * 5.05, 0.96),
|
|
1333
|
+
)
|
|
1334
|
+
.join(''),
|
|
1335
|
+
);
|
|
1336
|
+
g('arrows-up-down-left-right', glyphs['up-down-left-right'].path);
|
|
1337
|
+
g(
|
|
1338
|
+
/**
|
|
1339
|
+
* The ROTATE tool. Blender draws a heavy arc with a SOLID arrowhead at each
|
|
1340
|
+
* end and a solid diamond at the pivot; ours was a 1.3-unit open circle with
|
|
1341
|
+
* one small tick, which at matched scale is an empty ring — the pivot, the
|
|
1342
|
+
* direction and the weight all missing.
|
|
1343
|
+
*
|
|
1344
|
+
* RE-MEASURED 2026-09-19 (ASCII map in authored units, and the cell read at
|
|
1345
|
+
* 8x): it is TWO arcs, not one — the top arc ends at the LEFT in a
|
|
1346
|
+
* DOWN-pointing triangle and the bottom arc ends at the RIGHT in an
|
|
1347
|
+
* UP-pointing one, the ordinary two-headed circular arrow, where the
|
|
1348
|
+
* previous fit drew one dome with both heads under its ends and no bottom
|
|
1349
|
+
* arc at all. And it was 72% of Blender's size (33x29 device against
|
|
1350
|
+
* 46x38): the ring's radius is 6.5 of the 16 box, not 4.3.
|
|
1351
|
+
*/
|
|
1352
|
+
'rotate',
|
|
1353
|
+
arc(8, 7.8, 7.0, 200, 336, 0.8) +
|
|
1354
|
+
arc(8, 7.8, 7.0, 20, 156, 0.8) +
|
|
1355
|
+
// The head SITS ON the arc's end and points across it, so the two read as
|
|
1356
|
+
// one stroke that turns into a point rather than as an arc with a dart
|
|
1357
|
+
// beside it.
|
|
1358
|
+
poly([
|
|
1359
|
+
[0, 6.7],
|
|
1360
|
+
[3.3, 6.7],
|
|
1361
|
+
[1.65, 9.9],
|
|
1362
|
+
]) +
|
|
1363
|
+
poly([
|
|
1364
|
+
[12.6, 9.3],
|
|
1365
|
+
[15.9, 9.3],
|
|
1366
|
+
[14.25, 6.1],
|
|
1367
|
+
]) +
|
|
1368
|
+
poly([
|
|
1369
|
+
[7.75, 4.9],
|
|
1370
|
+
[10.05, 7.65],
|
|
1371
|
+
[7.75, 10.4],
|
|
1372
|
+
[5.45, 7.65],
|
|
1373
|
+
]),
|
|
1374
|
+
);
|
|
1375
|
+
g(
|
|
1376
|
+
'rotate-left',
|
|
1377
|
+
arc(8, 8, 5, 240, -60, 1.3) +
|
|
1378
|
+
poly([
|
|
1379
|
+
[5.4, 1.6],
|
|
1380
|
+
[2.4, 4.8],
|
|
1381
|
+
[6.6, 5.8],
|
|
1382
|
+
]),
|
|
1383
|
+
);
|
|
1384
|
+
g(
|
|
1385
|
+
'rotate-right',
|
|
1386
|
+
arc(8, 8, 5, -60, 240, 1.3) +
|
|
1387
|
+
poly([
|
|
1388
|
+
[10.6, 1.6],
|
|
1389
|
+
[13.6, 4.8],
|
|
1390
|
+
[9.4, 5.8],
|
|
1391
|
+
]),
|
|
1392
|
+
);
|
|
1393
|
+
g(
|
|
1394
|
+
'arrows-rotate',
|
|
1395
|
+
arc(8, 8, 5, 150, 330, 1.25) +
|
|
1396
|
+
arc(8, 8, 5, -30, 150, 1.25) +
|
|
1397
|
+
poly([
|
|
1398
|
+
[11.2, 1.8],
|
|
1399
|
+
[13.8, 5.4],
|
|
1400
|
+
[9.6, 5.6],
|
|
1401
|
+
]) +
|
|
1402
|
+
poly([
|
|
1403
|
+
[4.8, 14.2],
|
|
1404
|
+
[2.2, 10.6],
|
|
1405
|
+
[6.4, 10.4],
|
|
1406
|
+
]),
|
|
1407
|
+
);
|
|
1408
|
+
g(
|
|
1409
|
+
'crosshairs',
|
|
1410
|
+
ring(8, 8, 4.2, 1.2) +
|
|
1411
|
+
cap(8, 1.6, 8, 5, 1.2) +
|
|
1412
|
+
cap(8, 11, 8, 14.4, 1.2) +
|
|
1413
|
+
cap(1.6, 8, 5, 8, 1.2) +
|
|
1414
|
+
cap(11, 8, 14.4, 8, 1.2),
|
|
1415
|
+
);
|
|
1416
|
+
g('bullseye', ring(8, 8, 5.4, 1.2) + ring(8, 8, 2.9, 1.2) + dot(8, 8, 1.2));
|
|
1417
|
+
g(
|
|
1418
|
+
'arrows-to-dot',
|
|
1419
|
+
dot(8, 8, 1.4) +
|
|
1420
|
+
cap(8, 1.8, 8, 4.4, 1.2) +
|
|
1421
|
+
cap(8, 11.6, 8, 14.2, 1.2) +
|
|
1422
|
+
cap(1.8, 8, 4.4, 8, 1.2) +
|
|
1423
|
+
cap(11.6, 8, 14.2, 8, 1.2) +
|
|
1424
|
+
line(
|
|
1425
|
+
[
|
|
1426
|
+
[6.6, 3.2],
|
|
1427
|
+
[8, 4.6],
|
|
1428
|
+
[9.4, 3.2],
|
|
1429
|
+
],
|
|
1430
|
+
1.1,
|
|
1431
|
+
) +
|
|
1432
|
+
line(
|
|
1433
|
+
[
|
|
1434
|
+
[6.6, 12.8],
|
|
1435
|
+
[8, 11.4],
|
|
1436
|
+
[9.4, 12.8],
|
|
1437
|
+
],
|
|
1438
|
+
1.1,
|
|
1439
|
+
) +
|
|
1440
|
+
line(
|
|
1441
|
+
[
|
|
1442
|
+
[3.2, 6.6],
|
|
1443
|
+
[4.6, 8],
|
|
1444
|
+
[3.2, 9.4],
|
|
1445
|
+
],
|
|
1446
|
+
1.1,
|
|
1447
|
+
) +
|
|
1448
|
+
line(
|
|
1449
|
+
[
|
|
1450
|
+
[12.8, 6.6],
|
|
1451
|
+
[11.4, 8],
|
|
1452
|
+
[12.8, 9.4],
|
|
1453
|
+
],
|
|
1454
|
+
1.1,
|
|
1455
|
+
),
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* THE OTHER TWO TOOLS OF BLENDER'S TRANSFORM GROUP, under names of ours.
|
|
1460
|
+
*
|
|
1461
|
+
* Move and Rotate above are shared Font Awesome names (`up-down-left-right`,
|
|
1462
|
+
* `rotate`) and their Blender drawings say the same thing everywhere they
|
|
1463
|
+
* appear, so those stay keyed to the shared name. Scale and Transform do not:
|
|
1464
|
+
* the host's Scale tool asked for `maximize` (Font Awesome's window-expand
|
|
1465
|
+
* cross) and its Transform tool for `arrows-to-dot` (a crosshair), and
|
|
1466
|
+
* Blender's marks for the two — a small square growing into a bigger one, and
|
|
1467
|
+
* a square inside a broken ring of arrows — are meaningless under those
|
|
1468
|
+
* generic meanings. Same reasoning as `editorIcons.modeling`'s `mesh-*`:
|
|
1469
|
+
* a glyph that IS an operation needs its own name or it recuts every other
|
|
1470
|
+
* site that asked for the generic one.
|
|
1471
|
+
*/
|
|
1472
|
+
g(
|
|
1473
|
+
/**
|
|
1474
|
+
* SCALE. Measured on `modeling-edit-none.png` (button 5) as an ASCII map in
|
|
1475
|
+
* authored units: a 1-unit square OUTLINE from (2.0, 1.5) to (14.0, 13.5),
|
|
1476
|
+
* a SOLID square from (1.5, 7.5) to (8.0, 14.0) overlapping its lower-left
|
|
1477
|
+
* corner, and a diagonal arrow inside the frame running up-and-right from
|
|
1478
|
+
* (8.6, 6.9) to a head whose apex is (12.4, 3.1).
|
|
1479
|
+
*/
|
|
1480
|
+
'tool-scale',
|
|
1481
|
+
rframe(2, 1.5, 12, 12, 0.8) +
|
|
1482
|
+
rrect(1.5, 7.6, 6.3, 6.3, 0.2) +
|
|
1483
|
+
cap(8.6, 6.9, 10.7, 4.8, 0.95) +
|
|
1484
|
+
poly([
|
|
1485
|
+
[12.4, 3.1],
|
|
1486
|
+
[11.5, 6.2],
|
|
1487
|
+
[9.3, 4],
|
|
1488
|
+
]),
|
|
1489
|
+
);
|
|
1490
|
+
g(
|
|
1491
|
+
/**
|
|
1492
|
+
* TRANSFORM (all handles). Blender's (button 6): a solid square from
|
|
1493
|
+
* (5.0, 5.0) to (10.5, 10.5), four solid arrowheads whose tips touch the
|
|
1494
|
+
* box edge with bases 3.0 units in and half-width 1.6, and FOUR ARCS on a
|
|
1495
|
+
* radius-7.0 circle filling the 50° between one head and the next — the
|
|
1496
|
+
* ring is what says "all of them at once", and it is why this is not the
|
|
1497
|
+
* Move mark with a box in it.
|
|
1498
|
+
*/
|
|
1499
|
+
'tool-transform',
|
|
1500
|
+
rrect(5.15, 5.15, 5.7, 5.7, 0.25) +
|
|
1501
|
+
[
|
|
1502
|
+
[0, -1],
|
|
1503
|
+
[0, 1],
|
|
1504
|
+
[-1, 0],
|
|
1505
|
+
[1, 0],
|
|
1506
|
+
]
|
|
1507
|
+
.map(([dx, dy]) =>
|
|
1508
|
+
poly([
|
|
1509
|
+
[8 + dx * 5.4 - dy * 1.7, 8 + dy * 5.4 + dx * 1.7],
|
|
1510
|
+
[8 + dx * 5.4 + dy * 1.7, 8 + dy * 5.4 - dx * 1.7],
|
|
1511
|
+
[8 + dx * 7.9, 8 + dy * 7.9],
|
|
1512
|
+
]),
|
|
1513
|
+
)
|
|
1514
|
+
.join('') +
|
|
1515
|
+
[20, 110, 200, 290].map((a) => arc(8, 8, 7.1, a, a + 48, 0.92)).join(''),
|
|
1516
|
+
);
|
|
1517
|
+
g(
|
|
1518
|
+
'magnet',
|
|
1519
|
+
arc(8, 7.6, 4.7, 180, 360, 1.5) +
|
|
1520
|
+
cap(3.3, 7.6, 3.3, 11.4, 1.5) +
|
|
1521
|
+
cap(12.7, 7.6, 12.7, 11.4, 1.5) +
|
|
1522
|
+
cap(1.9, 12.6, 4.7, 12.6, 1.7) +
|
|
1523
|
+
cap(11.3, 12.6, 14.1, 12.6, 1.7),
|
|
1524
|
+
);
|
|
1525
|
+
g(
|
|
1526
|
+
'expand',
|
|
1527
|
+
line(
|
|
1528
|
+
[
|
|
1529
|
+
[6.2, 2.4],
|
|
1530
|
+
[2.4, 2.4],
|
|
1531
|
+
[2.4, 6.2],
|
|
1532
|
+
],
|
|
1533
|
+
1.3,
|
|
1534
|
+
) +
|
|
1535
|
+
line(
|
|
1536
|
+
[
|
|
1537
|
+
[9.8, 2.4],
|
|
1538
|
+
[13.6, 2.4],
|
|
1539
|
+
[13.6, 6.2],
|
|
1540
|
+
],
|
|
1541
|
+
1.3,
|
|
1542
|
+
) +
|
|
1543
|
+
line(
|
|
1544
|
+
[
|
|
1545
|
+
[6.2, 13.6],
|
|
1546
|
+
[2.4, 13.6],
|
|
1547
|
+
[2.4, 9.8],
|
|
1548
|
+
],
|
|
1549
|
+
1.3,
|
|
1550
|
+
) +
|
|
1551
|
+
line(
|
|
1552
|
+
[
|
|
1553
|
+
[9.8, 13.6],
|
|
1554
|
+
[13.6, 13.6],
|
|
1555
|
+
[13.6, 9.8],
|
|
1556
|
+
],
|
|
1557
|
+
1.3,
|
|
1558
|
+
),
|
|
1559
|
+
);
|
|
1560
|
+
g(
|
|
1561
|
+
'compress',
|
|
1562
|
+
line(
|
|
1563
|
+
[
|
|
1564
|
+
[2.4, 6.2],
|
|
1565
|
+
[6.2, 6.2],
|
|
1566
|
+
[6.2, 2.4],
|
|
1567
|
+
],
|
|
1568
|
+
1.3,
|
|
1569
|
+
) +
|
|
1570
|
+
line(
|
|
1571
|
+
[
|
|
1572
|
+
[13.6, 6.2],
|
|
1573
|
+
[9.8, 6.2],
|
|
1574
|
+
[9.8, 2.4],
|
|
1575
|
+
],
|
|
1576
|
+
1.3,
|
|
1577
|
+
) +
|
|
1578
|
+
line(
|
|
1579
|
+
[
|
|
1580
|
+
[2.4, 9.8],
|
|
1581
|
+
[6.2, 9.8],
|
|
1582
|
+
[6.2, 13.6],
|
|
1583
|
+
],
|
|
1584
|
+
1.3,
|
|
1585
|
+
) +
|
|
1586
|
+
line(
|
|
1587
|
+
[
|
|
1588
|
+
[13.6, 9.8],
|
|
1589
|
+
[9.8, 9.8],
|
|
1590
|
+
[9.8, 13.6],
|
|
1591
|
+
],
|
|
1592
|
+
1.3,
|
|
1593
|
+
),
|
|
1594
|
+
);
|
|
1595
|
+
g('maximize', glyphs['expand'].path);
|
|
1596
|
+
g(
|
|
1597
|
+
'up-right-and-down-left-from-center',
|
|
1598
|
+
cap(3.2, 12.8, 12.8, 3.2, 1.25) +
|
|
1599
|
+
line(
|
|
1600
|
+
[
|
|
1601
|
+
[9, 2.6],
|
|
1602
|
+
[13.4, 2.6],
|
|
1603
|
+
[13.4, 7],
|
|
1604
|
+
],
|
|
1605
|
+
1.25,
|
|
1606
|
+
) +
|
|
1607
|
+
line(
|
|
1608
|
+
[
|
|
1609
|
+
[7, 13.4],
|
|
1610
|
+
[2.6, 13.4],
|
|
1611
|
+
[2.6, 9],
|
|
1612
|
+
],
|
|
1613
|
+
1.25,
|
|
1614
|
+
),
|
|
1615
|
+
);
|
|
1616
|
+
g('window-minimize', cap(3, 11.4, 13, 11.4, 1.4));
|
|
1617
|
+
g(
|
|
1618
|
+
'window-restore',
|
|
1619
|
+
rframe(2.2, 5.4, 8.4, 8.4, 1.2, 0.5) +
|
|
1620
|
+
line(
|
|
1621
|
+
[
|
|
1622
|
+
[5.8, 5],
|
|
1623
|
+
[5.8, 2.4],
|
|
1624
|
+
[13.8, 2.4],
|
|
1625
|
+
[13.8, 10.2],
|
|
1626
|
+
[11.2, 10.2],
|
|
1627
|
+
],
|
|
1628
|
+
1.2,
|
|
1629
|
+
),
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
// -- modeling operators ----------------------------------------------------
|
|
1633
|
+
g(
|
|
1634
|
+
'arrow-up-from-bracket',
|
|
1635
|
+
cap(8, 2.4, 8, 10, 1.35) +
|
|
1636
|
+
line(
|
|
1637
|
+
[
|
|
1638
|
+
[4.8, 5.4],
|
|
1639
|
+
[8, 2.2],
|
|
1640
|
+
[11.2, 5.4],
|
|
1641
|
+
],
|
|
1642
|
+
1.3,
|
|
1643
|
+
) +
|
|
1644
|
+
line(
|
|
1645
|
+
[
|
|
1646
|
+
[3, 9.6],
|
|
1647
|
+
[3, 13.6],
|
|
1648
|
+
[13, 13.6],
|
|
1649
|
+
[13, 9.6],
|
|
1650
|
+
],
|
|
1651
|
+
1.25,
|
|
1652
|
+
),
|
|
1653
|
+
);
|
|
1654
|
+
g(
|
|
1655
|
+
'border-top-left',
|
|
1656
|
+
line(
|
|
1657
|
+
[
|
|
1658
|
+
[2.4, 13.6],
|
|
1659
|
+
[2.4, 2.4],
|
|
1660
|
+
[13.6, 2.4],
|
|
1661
|
+
],
|
|
1662
|
+
1.5,
|
|
1663
|
+
) +
|
|
1664
|
+
dot(6.6, 9.4, 0.95) +
|
|
1665
|
+
dot(9.4, 6.6, 0.95) +
|
|
1666
|
+
dot(9.4, 9.4, 0.95) +
|
|
1667
|
+
dot(12.9, 9.4, 0.95) +
|
|
1668
|
+
dot(9.4, 12.9, 0.95) +
|
|
1669
|
+
dot(12.9, 12.9, 0.95),
|
|
1670
|
+
);
|
|
1671
|
+
g(
|
|
1672
|
+
'scissors',
|
|
1673
|
+
ring(4.1, 12, 1.9, 1.15) +
|
|
1674
|
+
ring(11.9, 12, 1.9, 1.15) +
|
|
1675
|
+
cap(3.4, 2.2, 10.9, 10.4, 1.25) +
|
|
1676
|
+
cap(12.6, 2.2, 5.1, 10.4, 1.25),
|
|
1677
|
+
);
|
|
1678
|
+
g(
|
|
1679
|
+
'hammer',
|
|
1680
|
+
cap(3, 13.8, 9, 7.2, 1.9) +
|
|
1681
|
+
poly([
|
|
1682
|
+
[7.6, 5.8],
|
|
1683
|
+
[10.4, 2.2],
|
|
1684
|
+
[14.2, 5.2],
|
|
1685
|
+
[11.4, 8.8],
|
|
1686
|
+
]),
|
|
1687
|
+
);
|
|
1688
|
+
g(
|
|
1689
|
+
/**
|
|
1690
|
+
* The Properties TOOL tab. Blender's (`properties-object.png`, top of the
|
|
1691
|
+
* tab rail, 14x14 at matched scale) is two tools standing UPRIGHT side by
|
|
1692
|
+
* side — a screwdriver and an open-end wrench, both solid. Ours was one
|
|
1693
|
+
* diagonal shaft with a ring at one end and a wedge at the other: at 14px it
|
|
1694
|
+
* reads as a single key, and the ring is an outline that greys out.
|
|
1695
|
+
*/
|
|
1696
|
+
'screwdriver-wrench',
|
|
1697
|
+
// screwdriver: blade, collar, handle
|
|
1698
|
+
rrect(3.5, 2.6, 1.4, 4.6, 0.3) +
|
|
1699
|
+
rrect(2.9, 7.2, 2.6, 1.2, 0.3) +
|
|
1700
|
+
rrect(3.1, 8.4, 2.2, 5, 0.8) +
|
|
1701
|
+
// wrench: a forked head over a shaft. The fork must be WIDE enough to
|
|
1702
|
+
// survive 14px — at 1.7 units it closed up and the head read as a block.
|
|
1703
|
+
rrect(9.6, 6.6, 2.8, 6.8, 0.9) +
|
|
1704
|
+
rrect(8.4, 2.6, 5.2, 4.6, 0.5) +
|
|
1705
|
+
holeBar(11, 2, 11, 4.9, 2.2),
|
|
1706
|
+
);
|
|
1707
|
+
g('puzzle-piece', rrect(2.6, 2.6, 10.8, 10.8, 1) + dot(13.6, 6.5, 1.9) + dot(8, 2.6, 1.9, true));
|
|
1708
|
+
g(
|
|
1709
|
+
'sliders',
|
|
1710
|
+
cap(2.4, 4.4, 13.6, 4.4, 1.2) +
|
|
1711
|
+
cap(2.4, 8, 13.6, 8, 1.2) +
|
|
1712
|
+
cap(2.4, 11.6, 13.6, 11.6, 1.2) +
|
|
1713
|
+
dot(5.4, 4.4, 1.6) +
|
|
1714
|
+
dot(10.4, 8, 1.6) +
|
|
1715
|
+
dot(6.6, 11.6, 1.6),
|
|
1716
|
+
);
|
|
1717
|
+
g(
|
|
1718
|
+
'pen-ruler',
|
|
1719
|
+
cap(3, 13, 10.6, 5.4, 2.1) +
|
|
1720
|
+
poly([
|
|
1721
|
+
[1.2, 14.8],
|
|
1722
|
+
[2.9, 10.8],
|
|
1723
|
+
[5.2, 13.1],
|
|
1724
|
+
]) +
|
|
1725
|
+
poly([
|
|
1726
|
+
[10.2, 4.6],
|
|
1727
|
+
[12.2, 2.6],
|
|
1728
|
+
[13.4, 3.8],
|
|
1729
|
+
[11.4, 5.8],
|
|
1730
|
+
]) +
|
|
1731
|
+
cap(11.6, 2, 14, 4.4, 1.6),
|
|
1732
|
+
);
|
|
1733
|
+
g(
|
|
1734
|
+
/**
|
|
1735
|
+
* The MATERIAL section's mark (`inspection/compose.ts` maps /material/ here).
|
|
1736
|
+
* Blender's Material tab is a SOLID disc with quadrants knocked out of it;
|
|
1737
|
+
* ours was a 1.3-unit outline with three dots inside, and at the 14px rail
|
|
1738
|
+
* the outline greyed and the dots merged — it read as a cookie. Same
|
|
1739
|
+
* palette silhouette, drawn Blender's way: one filled blob, wells and thumb
|
|
1740
|
+
* hole punched THROUGH it.
|
|
1741
|
+
*/
|
|
1742
|
+
'palette',
|
|
1743
|
+
// One blob with the wells and the thumb hole punched THROUGH it. Round 3
|
|
1744
|
+
// grew a handle off the bottom to sell the palette's notch and the glyph came
|
|
1745
|
+
// back a mushroom; Blender's Material tab is a compact rounded mass and this
|
|
1746
|
+
// matches that silhouette.
|
|
1747
|
+
poly(
|
|
1748
|
+
Array.from({ length: 28 }, (_, i) => {
|
|
1749
|
+
const a = (i / 28) * Math.PI * 2;
|
|
1750
|
+
return [8 + Math.cos(a) * 6, 8.2 + Math.sin(a) * 5.4];
|
|
1751
|
+
}),
|
|
1752
|
+
) +
|
|
1753
|
+
dot(5.2, 6, 1.3, true) +
|
|
1754
|
+
dot(8.4, 4.9, 1.3, true) +
|
|
1755
|
+
dot(11.3, 6.6, 1.3, true) +
|
|
1756
|
+
dot(10.4, 10.7, 2, true),
|
|
1757
|
+
);
|
|
1758
|
+
g(
|
|
1759
|
+
// A stack of plates: the top one SOLID (the silhouette that carries the
|
|
1760
|
+
// glyph at a 14px row), the two beneath it strokes that read as its shadow.
|
|
1761
|
+
// Drawn as three outlines it was three grey lozenges with nothing solid in
|
|
1762
|
+
// any of them — the same failure the Collection box had.
|
|
1763
|
+
'layer-group',
|
|
1764
|
+
poly([
|
|
1765
|
+
[8, 2],
|
|
1766
|
+
[14.2, 5.3],
|
|
1767
|
+
[8, 8.6],
|
|
1768
|
+
[1.8, 5.3],
|
|
1769
|
+
]) +
|
|
1770
|
+
line(
|
|
1771
|
+
[
|
|
1772
|
+
[2.6, 8.1],
|
|
1773
|
+
[8, 10.9],
|
|
1774
|
+
[13.4, 8.1],
|
|
1775
|
+
],
|
|
1776
|
+
1.35,
|
|
1777
|
+
) +
|
|
1778
|
+
line(
|
|
1779
|
+
[
|
|
1780
|
+
[2.6, 11],
|
|
1781
|
+
[8, 13.8],
|
|
1782
|
+
[13.4, 11],
|
|
1783
|
+
],
|
|
1784
|
+
1.35,
|
|
1785
|
+
),
|
|
1786
|
+
);
|
|
1787
|
+
g(
|
|
1788
|
+
// The GROUP row's mark. Beside Blender's Collection box in `outliner.png` at
|
|
1789
|
+
// matched scale, a 1.1-unit frame with four nubs was indistinguishable from
|
|
1790
|
+
// the letterforms of its own label — same weight, same grey. Blender's is a
|
|
1791
|
+
// solid box: a filled band for the frame, and solid corner handles.
|
|
1792
|
+
'object-group',
|
|
1793
|
+
rframe(2.4, 3.8, 11.2, 8.4, 2.1, 0.3) +
|
|
1794
|
+
rrect(0.6, 2, 3.1, 3.1, 0.3) +
|
|
1795
|
+
rrect(12.3, 2, 3.1, 3.1, 0.3) +
|
|
1796
|
+
rrect(0.6, 10.9, 3.1, 3.1, 0.3) +
|
|
1797
|
+
rrect(12.3, 10.9, 3.1, 3.1, 0.3),
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
// -- the mesh operators: THE OPERATION, ON A CUBE ---------------------------
|
|
1801
|
+
/**
|
|
1802
|
+
* `editorIcons.modeling.*` asks for `mesh-*` (see that file's header), so these
|
|
1803
|
+
* six are free to be what Blender's toolbar draws: not an abstraction of the
|
|
1804
|
+
* verb (an up-arrow for Extrude, scissors for Knife) but THE OPERATION PERFORMED
|
|
1805
|
+
* ON A CUBE — a cube with one face pulled out, a cube with an inset face, a cube
|
|
1806
|
+
* with a chamfered edge, a cube with a cut running round it. Measured in
|
|
1807
|
+
* `modeling-edit-none.png` (2x, halved): each glyph inks ~20x20 CSS px in a
|
|
1808
|
+
* 38x34 button (Extrude 20x23 — its lifted slab is taller), the cube is a
|
|
1809
|
+
* ~2 px wire with the operated element FILLED, and the cube sits in three-quarter
|
|
1810
|
+
* view: a front square with a top and a right face receding up-and-right.
|
|
1811
|
+
*
|
|
1812
|
+
* MONOCHROME, which is this set's contract (`blender.style.ts`) and a real
|
|
1813
|
+
* difference from the reference: Blender tints the operated element (green for
|
|
1814
|
+
* these six, purple for Smooth) and reads the operation by COLOUR as much as by
|
|
1815
|
+
* shape. Here the same job is done by FILL — the operated face is solid against
|
|
1816
|
+
* the wire — and whether the set gains an accent channel is its own decision,
|
|
1817
|
+
* not one to smuggle in through six glyphs.
|
|
1818
|
+
*/
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* THE OPERATOR CUBE, once. `P(u, v, k)` is a point in the cube's own axes: `u`
|
|
1822
|
+
* across the front face, `v` up it, `k` back along the depth the top and right
|
|
1823
|
+
* faces recede along. Every one of the six is authored in those three, so the
|
|
1824
|
+
* cube can be re-measured in one place and all six follow.
|
|
1825
|
+
*
|
|
1826
|
+
* It fills the 16 grid corner to corner (~14 units of extent), because the
|
|
1827
|
+
* toolbar's glyph is the biggest in the chrome: that is the ~20 CSS px of ink
|
|
1828
|
+
* Blender's 38x34 button carries, once the file's `S` dial is applied.
|
|
1829
|
+
*/
|
|
1830
|
+
const CUBE = { x0: 1.1, x1: 10.9, yb: 14.9, yt: 5.1, dx: 3.9, dy: -3.9 };
|
|
1831
|
+
const P = (u, v, k = 0) => [
|
|
1832
|
+
CUBE.x0 + (CUBE.x1 - CUBE.x0) * u + CUBE.dx * k,
|
|
1833
|
+
CUBE.yb + (CUBE.yt - CUBE.yb) * v + CUBE.dy * k,
|
|
1834
|
+
];
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* The cube as WIRE — the nine edges a cube shows from here, named so an
|
|
1838
|
+
* operator can drop the one it replaces (Bevel replaces the top-front edge,
|
|
1839
|
+
* Extrude takes the whole top away with the face it pulled out). `top` is the
|
|
1840
|
+
* v the box stops at, so Extrude's body can be shorter than the others' and
|
|
1841
|
+
* still leave the glyph the same total ink.
|
|
1842
|
+
*/
|
|
1843
|
+
function cubeWire({ top = 1, w = 1.15, omit = [], u0 = 0, u1 = 1 } = {}) {
|
|
1844
|
+
const edges = {
|
|
1845
|
+
frontBottom: [P(u0, 0), P(u1, 0)],
|
|
1846
|
+
frontRight: [P(u1, 0), P(u1, top)],
|
|
1847
|
+
frontTop: [P(u0, top), P(u1, top)],
|
|
1848
|
+
frontLeft: [P(u0, 0), P(u0, top)],
|
|
1849
|
+
topLeft: [P(u0, top), P(u0, top, 1)],
|
|
1850
|
+
topBack: [P(u0, top, 1), P(u1, top, 1)],
|
|
1851
|
+
rightBack: [P(u1, top, 1), P(u1, 0, 1)],
|
|
1852
|
+
rightBottom: [P(u1, 0), P(u1, 0, 1)],
|
|
1853
|
+
seam: [P(u1, top), P(u1, top, 1)],
|
|
1854
|
+
};
|
|
1855
|
+
return Object.entries(edges)
|
|
1856
|
+
.filter(([name]) => !omit.includes(name))
|
|
1857
|
+
.map(([, [a, b]]) => cap(a[0], a[1], b[0], b[1], w))
|
|
1858
|
+
.join('');
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/** A SOLID slab between two v levels — the piece an operation adds: its top
|
|
1862
|
+
* face, the band down its front, the band down its right. */
|
|
1863
|
+
function slab(vLow, vHigh) {
|
|
1864
|
+
return (
|
|
1865
|
+
poly([P(0, vHigh), P(1, vHigh), P(1, vHigh, 1), P(0, vHigh, 1)]) +
|
|
1866
|
+
poly([P(0, vLow), P(1, vLow), P(1, vHigh), P(0, vHigh)]) +
|
|
1867
|
+
poly([P(1, vLow), P(1, vHigh), P(1, vHigh, 1), P(1, vLow, 1)])
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/** EXTRUDE — the top face PULLED OUT: the cube left open at the top, and the
|
|
1872
|
+
* face floating above it as a solid slab with its own three faces. */
|
|
1873
|
+
g(
|
|
1874
|
+
'mesh-extrude',
|
|
1875
|
+
// The body is OPEN at the top — it lost that face, and Blender's glyph says
|
|
1876
|
+
// so by letting the verticals run up and vanish behind the slab rather than
|
|
1877
|
+
// closing a rim under it (round 2 closed the rim and the pair read as a
|
|
1878
|
+
// crate with a lid; round 1 stopped the verticals short and read as a
|
|
1879
|
+
// bracket). So: full-height verticals to the slab's underside, no top rim.
|
|
1880
|
+
cubeWire({ top: 0.74, omit: ['frontTop', 'topLeft', 'topBack', 'seam'] }),
|
|
1881
|
+
{ tone: 'operator', tonedPath: slab(0.74, 1) },
|
|
1882
|
+
);
|
|
1883
|
+
|
|
1884
|
+
/** INSET — the front face INSET: the rim it leaves is solid, the smaller face
|
|
1885
|
+
* inside it is knocked back out, and the four WALLS between them are knocked
|
|
1886
|
+
* apart by their own seams. Blender's inset glyph is that funnel — four
|
|
1887
|
+
* trapezoids converging on a small dark square — and round 4's flat frame
|
|
1888
|
+
* (no seams) read as a picture frame instead of a recess. */
|
|
1889
|
+
g('mesh-inset', cubeWire(), {
|
|
1890
|
+
tone: 'operator',
|
|
1891
|
+
tonedPath:
|
|
1892
|
+
poly([P(0, 0), P(1, 0), P(1, 1), P(0, 1)]) +
|
|
1893
|
+
holePoly([P(0.3, 0.3), P(0.7, 0.3), P(0.7, 0.7), P(0.3, 0.7)]) +
|
|
1894
|
+
holeBar(...P(0, 0), ...P(0.3, 0.3), 0.7) +
|
|
1895
|
+
holeBar(...P(1, 0), ...P(0.7, 0.3), 0.7) +
|
|
1896
|
+
holeBar(...P(1, 1), ...P(0.7, 0.7), 0.7) +
|
|
1897
|
+
holeBar(...P(0, 1), ...P(0.3, 0.7), 0.7),
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
/** BEVEL — one edge CHAMFERED. RE-READ at 8x against the reference: the edge
|
|
1901
|
+
* Blender cuts is the TOP-RIGHT one (the seam between the top face and the
|
|
1902
|
+
* right face), and the glyph shows the top face solid with a second facet
|
|
1903
|
+
* flapping off it along that seam, a hairline of background between the two.
|
|
1904
|
+
* Rounds 2-4 chamfered the top-FRONT edge and the result read as a solid top,
|
|
1905
|
+
* which is Extrude's answer, not Bevel's. */
|
|
1906
|
+
g('mesh-bevel', cubeWire({ omit: ['seam'] }), {
|
|
1907
|
+
tone: 'operator',
|
|
1908
|
+
tonedPath:
|
|
1909
|
+
poly([P(0, 1), P(0.56, 1), P(0.56, 1, 1), P(0, 1, 1)]) +
|
|
1910
|
+
poly([P(0.56, 1), P(1, 0.56), P(1, 0.56, 1), P(0.56, 1, 1)]) +
|
|
1911
|
+
holeBar(...P(0.56, 1), ...P(0.56, 1, 1), 0.7),
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
/** LOOP CUT — the cube CUT IN TWO. Blender's glyph is not a stripe on a cube:
|
|
1915
|
+
* it is two half-boxes standing apart with the cut itself, a solid bar, in the
|
|
1916
|
+
* gap between them, so the new edge loop is the subject. Round 4 drew a line
|
|
1917
|
+
* across an intact cube and read as decoration on the face. */
|
|
1918
|
+
g('mesh-loop-cut', cubeWire(), {
|
|
1919
|
+
tone: 'operator',
|
|
1920
|
+
tonedPath:
|
|
1921
|
+
poly([P(0.44, 0), P(0.56, 0), P(0.56, 1), P(0.44, 1)]) +
|
|
1922
|
+
poly([P(0.44, 1), P(0.56, 1), P(0.56, 1, 1), P(0.44, 1, 1)]),
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
/** KNIFE — a cut drawn ACROSS a face rather than round the cube: a stroke that
|
|
1926
|
+
* turns at a vertex, its ends pinned by the points a knife sets. */
|
|
1927
|
+
g('mesh-knife', cubeWire(), {
|
|
1928
|
+
tone: 'operator',
|
|
1929
|
+
// A RISING cut with its turn on the front face and its far end up on the
|
|
1930
|
+
// top face. Round 1 fell to the right and read as a droop.
|
|
1931
|
+
tonedPath:
|
|
1932
|
+
line([P(0.16, 0.06), P(0.52, 0.72), P(0.78, 1, 0.5)], 1.5) +
|
|
1933
|
+
dot(...P(0.16, 0.06), 1.45) +
|
|
1934
|
+
dot(...P(0.52, 0.72), 1.15) +
|
|
1935
|
+
dot(...P(0.78, 1, 0.5), 1.45),
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
/** SUBDIVIDE — the cube GRIDDED: every visible face cut once each way, in a
|
|
1939
|
+
* lighter stroke than the cube's own edges so the cut reads as new geometry
|
|
1940
|
+
* rather than as the silhouette. */
|
|
1941
|
+
g('mesh-subdivide', cubeWire(), {
|
|
1942
|
+
tone: 'operator',
|
|
1943
|
+
tonedPath:
|
|
1944
|
+
// front face
|
|
1945
|
+
cap(...P(0.5, 0), ...P(0.5, 1), 1.05) +
|
|
1946
|
+
cap(...P(0, 0.5), ...P(1, 0.5), 1.05) +
|
|
1947
|
+
// top face
|
|
1948
|
+
cap(...P(0.5, 1), ...P(0.5, 1, 1), 1.05) +
|
|
1949
|
+
cap(...P(0, 1, 0.5), ...P(1, 1, 0.5), 1.05) +
|
|
1950
|
+
// right face
|
|
1951
|
+
cap(...P(1, 0.5), ...P(1, 0.5, 1), 1.05) +
|
|
1952
|
+
cap(...P(1, 0, 0.5), ...P(1, 1, 0.5), 1.05),
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
/** SMOOTH — not an operation on the operator CUBE: Blender draws this one (and
|
|
1956
|
+
* only this one of the shelf's marks) on a BALL, because smoothing is what
|
|
1957
|
+
* turns the cage into one. Measured on `modeling-edit-none.png` (button 17)
|
|
1958
|
+
* as an ASCII map in authored units: a faceted sphere ~6.3 units of radius
|
|
1959
|
+
* centred at (8.2, 6.6), its quads separated by seams KNOCKED OUT of the
|
|
1960
|
+
* fill — an equator across the full width, two meridians, and a latitude
|
|
1961
|
+
* seam near the top. The seams are holes, not strokes: drawn as strokes over
|
|
1962
|
+
* the ball they would be the same ink as the ball and vanish. */
|
|
1963
|
+
g(
|
|
1964
|
+
'mesh-smooth',
|
|
1965
|
+
poly(
|
|
1966
|
+
// ELEVEN vertices, not a smooth circle: Blender's silhouette is faceted
|
|
1967
|
+
// and that is half of what says "quads" before the seams are read.
|
|
1968
|
+
Array.from({ length: 9 }, (_, i) => {
|
|
1969
|
+
const a = (i / 9) * Math.PI * 2 + 0.34;
|
|
1970
|
+
return [7.8 + Math.cos(a) * 7.1, 6.4 + Math.sin(a) * 7.1];
|
|
1971
|
+
}),
|
|
1972
|
+
) +
|
|
1973
|
+
// equator, meridians, and the top latitude — each a bar-shaped hole.
|
|
1974
|
+
holeBar(0.8, 7.6, 15, 7.6, 0.62) +
|
|
1975
|
+
// The meridians and the top latitude BOW, two segments each — a sphere's
|
|
1976
|
+
// seams are not straight, and drawn straight the ball read as a beach
|
|
1977
|
+
// ball with a grid printed on it rather than as faceted geometry.
|
|
1978
|
+
holeBar(6.9, -0.7, 5.9, 6.4, 0.52) +
|
|
1979
|
+
holeBar(5.9, 6.4, 6.7, 13.5, 0.52) +
|
|
1980
|
+
holeBar(12.7, 1.1, 12.2, 6.4, 0.48) +
|
|
1981
|
+
holeBar(12.2, 6.4, 11.5, 11.7, 0.48) +
|
|
1982
|
+
holeBar(2.2, 3.5, 8, 1.8, 0.48) +
|
|
1983
|
+
holeBar(8, 1.8, 13.8, 3.3, 0.48),
|
|
1984
|
+
{ tone: 'operator' },
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
/** SHRINK/FATTEN — the selection moved ALONG ITS NORMALS, in either direction,
|
|
1988
|
+
* which is why Blender's mark (button 19) carries eight arrowheads: four
|
|
1989
|
+
* outside the cube pointing out and four inside pointing in. The cube here is
|
|
1990
|
+
* a pair of offset squares rather than the operator CUBE's three-quarter box
|
|
1991
|
+
* — measured, the front square is (3.5, 3.5)-(12, 11) and the back one is
|
|
1992
|
+
* offset up-and-right by 1.2 — because the arrows need the box read from the
|
|
1993
|
+
* front to be legible through it. */
|
|
1994
|
+
g(
|
|
1995
|
+
'mesh-shrink-fatten',
|
|
1996
|
+
// The BOX, front square plus the two BACK edges the front does not hide
|
|
1997
|
+
// (drawing the whole back frame adds two interior lines Blender has not
|
|
1998
|
+
// got) and the three short connectors between them.
|
|
1999
|
+
rframe(3.5, 3.5, 8.5, 7.5, 0.8) +
|
|
2000
|
+
line(
|
|
2001
|
+
[
|
|
2002
|
+
[5, 2.5],
|
|
2003
|
+
[13.5, 2.5],
|
|
2004
|
+
[13.5, 10.5],
|
|
2005
|
+
],
|
|
2006
|
+
0.7,
|
|
2007
|
+
) +
|
|
2008
|
+
cap(3.5, 3.5, 5, 2.5, 0.7) +
|
|
2009
|
+
cap(12, 3.5, 13.5, 2.5, 0.7) +
|
|
2010
|
+
cap(12, 11, 13.5, 10.5, 0.7),
|
|
2011
|
+
{
|
|
2012
|
+
tone: 'operator',
|
|
2013
|
+
// FOUR OUT and FOUR IN, about the mark's own centre (8, 7) — the box sits
|
|
2014
|
+
// a little high in the box because the outer heads need the room.
|
|
2015
|
+
tonedPath: [
|
|
2016
|
+
[0, -1],
|
|
2017
|
+
[0, 1],
|
|
2018
|
+
[-1, 0],
|
|
2019
|
+
[1, 0],
|
|
2020
|
+
]
|
|
2021
|
+
.map(
|
|
2022
|
+
([dx, dy]) =>
|
|
2023
|
+
poly([
|
|
2024
|
+
[8 + dx * 6.3 - dy * 1.85, 7 + dy * 6.3 + dx * 1.85],
|
|
2025
|
+
[8 + dx * 6.3 + dy * 1.85, 7 + dy * 6.3 - dx * 1.85],
|
|
2026
|
+
[8 + dx * 8.3, 7 + dy * 8.3],
|
|
2027
|
+
]) +
|
|
2028
|
+
poly([
|
|
2029
|
+
[8 + dx * 3 - dy * 1.5, 7 + dy * 3 + dx * 1.5],
|
|
2030
|
+
[8 + dx * 3 + dy * 1.5, 7 + dy * 3 - dx * 1.5],
|
|
2031
|
+
[8 + dx * 0.9, 7 + dy * 0.9],
|
|
2032
|
+
]),
|
|
2033
|
+
)
|
|
2034
|
+
.join(''),
|
|
2035
|
+
},
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
// -- panels / apps ---------------------------------------------------------
|
|
2039
|
+
g(
|
|
2040
|
+
'book-open',
|
|
2041
|
+
cap(8, 4.2, 8, 13.2, 1.2) +
|
|
2042
|
+
line(
|
|
2043
|
+
[
|
|
2044
|
+
[8, 4.2],
|
|
2045
|
+
[5.2, 2.6],
|
|
2046
|
+
[1.8, 2.6],
|
|
2047
|
+
[1.8, 11.6],
|
|
2048
|
+
[5.2, 11.6],
|
|
2049
|
+
[8, 13.2],
|
|
2050
|
+
],
|
|
2051
|
+
1.2,
|
|
2052
|
+
) +
|
|
2053
|
+
line(
|
|
2054
|
+
[
|
|
2055
|
+
[8, 4.2],
|
|
2056
|
+
[10.8, 2.6],
|
|
2057
|
+
[14.2, 2.6],
|
|
2058
|
+
[14.2, 11.6],
|
|
2059
|
+
[10.8, 11.6],
|
|
2060
|
+
[8, 13.2],
|
|
2061
|
+
],
|
|
2062
|
+
1.2,
|
|
2063
|
+
),
|
|
2064
|
+
);
|
|
2065
|
+
g(
|
|
2066
|
+
'book-bookmark',
|
|
2067
|
+
line(
|
|
2068
|
+
[
|
|
2069
|
+
[3.6, 2.2],
|
|
2070
|
+
[12.6, 2.2],
|
|
2071
|
+
[12.6, 13.8],
|
|
2072
|
+
[3.6, 13.8],
|
|
2073
|
+
],
|
|
2074
|
+
1.2,
|
|
2075
|
+
true,
|
|
2076
|
+
) +
|
|
2077
|
+
cap(5.8, 2.8, 5.8, 13.2, 1.1) +
|
|
2078
|
+
poly([
|
|
2079
|
+
[8.4, 2.8],
|
|
2080
|
+
[10.8, 2.8],
|
|
2081
|
+
[10.8, 7.8],
|
|
2082
|
+
[9.6, 6.6],
|
|
2083
|
+
[8.4, 7.8],
|
|
2084
|
+
]),
|
|
2085
|
+
);
|
|
2086
|
+
g(
|
|
2087
|
+
'graduation-cap',
|
|
2088
|
+
poly([
|
|
2089
|
+
[8, 2.4],
|
|
2090
|
+
[15, 5.8],
|
|
2091
|
+
[8, 9.2],
|
|
2092
|
+
[1, 5.8],
|
|
2093
|
+
]) +
|
|
2094
|
+
line(
|
|
2095
|
+
[
|
|
2096
|
+
[4.2, 7.4],
|
|
2097
|
+
[4.2, 11.6],
|
|
2098
|
+
],
|
|
2099
|
+
1.2,
|
|
2100
|
+
) +
|
|
2101
|
+
line(
|
|
2102
|
+
[
|
|
2103
|
+
[11.8, 7.4],
|
|
2104
|
+
[11.8, 11.6],
|
|
2105
|
+
],
|
|
2106
|
+
1.2,
|
|
2107
|
+
) +
|
|
2108
|
+
arc(8, 9.6, 3.9, 10, 170, 1.2),
|
|
2109
|
+
);
|
|
2110
|
+
g(
|
|
2111
|
+
'chart-simple',
|
|
2112
|
+
rrect(2.4, 9, 2.6, 4.8, 0.4) + rrect(6.7, 5.4, 2.6, 8.4, 0.4) + rrect(11, 2.4, 2.6, 11.4, 0.4),
|
|
2113
|
+
);
|
|
2114
|
+
g(
|
|
2115
|
+
'diagram-project',
|
|
2116
|
+
rframe(1.8, 2.2, 5.2, 3.8, 1.15, 0.3) +
|
|
2117
|
+
rframe(9, 10, 5.2, 3.8, 1.15, 0.3) +
|
|
2118
|
+
line(
|
|
2119
|
+
[
|
|
2120
|
+
[4.4, 6.4],
|
|
2121
|
+
[4.4, 11.9],
|
|
2122
|
+
[8.6, 11.9],
|
|
2123
|
+
],
|
|
2124
|
+
1.15,
|
|
2125
|
+
),
|
|
2126
|
+
);
|
|
2127
|
+
g(
|
|
2128
|
+
'circle-nodes',
|
|
2129
|
+
ring(3.6, 11.6, 1.9, 1.15) +
|
|
2130
|
+
ring(12.4, 12, 1.9, 1.15) +
|
|
2131
|
+
ring(9.2, 3.8, 1.9, 1.15) +
|
|
2132
|
+
cap(4.9, 10.3, 8.2, 5.4, 1) +
|
|
2133
|
+
cap(5.5, 11.8, 10.5, 11.9, 1) +
|
|
2134
|
+
cap(10.6, 5.4, 11.8, 10.2, 1),
|
|
2135
|
+
);
|
|
2136
|
+
g(
|
|
2137
|
+
'database',
|
|
2138
|
+
ellipseArc(8, 4.4, 5.2, 2.2, 0, 360, 1.15, 0, true) +
|
|
2139
|
+
cap(2.8, 4.4, 2.8, 11.6, 1.15) +
|
|
2140
|
+
cap(13.2, 4.4, 13.2, 11.6, 1.15) +
|
|
2141
|
+
ellipseArc(8, 11.6, 5.2, 2.2, 0, 180, 1.15) +
|
|
2142
|
+
ellipseArc(8, 8, 5.2, 2.2, 10, 170, 1.05),
|
|
2143
|
+
);
|
|
2144
|
+
g(
|
|
2145
|
+
'desktop',
|
|
2146
|
+
rframe(1.8, 2.6, 12.4, 8.6, 1.2, 0.5) +
|
|
2147
|
+
cap(5.4, 13.6, 10.6, 13.6, 1.2) +
|
|
2148
|
+
cap(8, 11.4, 8, 13.4, 1.2),
|
|
2149
|
+
);
|
|
2150
|
+
g(
|
|
2151
|
+
'mobile-screen-button',
|
|
2152
|
+
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),
|
|
2153
|
+
);
|
|
2154
|
+
g(
|
|
2155
|
+
'users',
|
|
2156
|
+
dot(5.4, 5, 2.5) +
|
|
2157
|
+
arc(5.4, 12.6, 4.4, 180, 360, 1.25) +
|
|
2158
|
+
arc(11.4, 5.4, 2.1, 250, 470, 1.15) +
|
|
2159
|
+
arc(11.6, 12, 3.6, 200, 340, 1.2),
|
|
2160
|
+
);
|
|
2161
|
+
g(
|
|
2162
|
+
'circle-user',
|
|
2163
|
+
dot(8, 8, 6) +
|
|
2164
|
+
dot(8, 6, 2.1, true) +
|
|
2165
|
+
holePoly([
|
|
2166
|
+
[4.2, 13.3],
|
|
2167
|
+
[5, 11.2],
|
|
2168
|
+
[11, 11.2],
|
|
2169
|
+
[11.8, 13.3],
|
|
2170
|
+
]),
|
|
2171
|
+
);
|
|
2172
|
+
g(
|
|
2173
|
+
'thumbtack',
|
|
2174
|
+
cap(8, 9.4, 8, 14, 1.2) +
|
|
2175
|
+
poly([
|
|
2176
|
+
[5, 2.2],
|
|
2177
|
+
[11, 2.2],
|
|
2178
|
+
[10, 4],
|
|
2179
|
+
[10.6, 7.2],
|
|
2180
|
+
[12.2, 9],
|
|
2181
|
+
[3.8, 9],
|
|
2182
|
+
[5.4, 7.2],
|
|
2183
|
+
[6, 4],
|
|
2184
|
+
]),
|
|
2185
|
+
);
|
|
2186
|
+
g(
|
|
2187
|
+
'link',
|
|
2188
|
+
arc(5.6, 10.4, 3, 45, 315, 1.35) +
|
|
2189
|
+
arc(10.4, 5.6, 3, 225, 495, 1.35) +
|
|
2190
|
+
cap(5.8, 10.2, 10.2, 5.8, 1.35),
|
|
2191
|
+
);
|
|
2192
|
+
g(
|
|
2193
|
+
'code-branch',
|
|
2194
|
+
ring(4.4, 3.6, 1.8, 1.2) +
|
|
2195
|
+
ring(4.4, 12.4, 1.8, 1.2) +
|
|
2196
|
+
ring(11.6, 5.6, 1.8, 1.2) +
|
|
2197
|
+
cap(4.4, 5.4, 4.4, 10.6, 1.15) +
|
|
2198
|
+
line(
|
|
2199
|
+
[
|
|
2200
|
+
[11.6, 7.4],
|
|
2201
|
+
[11.6, 9],
|
|
2202
|
+
[4.4, 9],
|
|
2203
|
+
],
|
|
2204
|
+
1.15,
|
|
2205
|
+
),
|
|
2206
|
+
);
|
|
2207
|
+
g(
|
|
2208
|
+
'hashtag',
|
|
2209
|
+
cap(5.8, 2.4, 4.4, 13.6, 1.2) +
|
|
2210
|
+
cap(11.2, 2.4, 9.8, 13.6, 1.2) +
|
|
2211
|
+
cap(2.6, 5.6, 13.4, 5.6, 1.2) +
|
|
2212
|
+
cap(2.2, 10.4, 13, 10.4, 1.2),
|
|
2213
|
+
);
|
|
2214
|
+
g(
|
|
2215
|
+
'font',
|
|
2216
|
+
line(
|
|
2217
|
+
[
|
|
2218
|
+
[3, 13.4],
|
|
2219
|
+
[8, 2.4],
|
|
2220
|
+
[13, 13.4],
|
|
2221
|
+
],
|
|
2222
|
+
1.3,
|
|
2223
|
+
) + cap(5.2, 9.4, 10.8, 9.4, 1.2),
|
|
2224
|
+
);
|
|
2225
|
+
g(
|
|
2226
|
+
'tags',
|
|
2227
|
+
poly([
|
|
2228
|
+
[1.6, 8.2],
|
|
2229
|
+
[7.4, 2.4],
|
|
2230
|
+
[13.2, 2.4],
|
|
2231
|
+
[13.2, 8.2],
|
|
2232
|
+
[7.4, 14],
|
|
2233
|
+
]),
|
|
2234
|
+
);
|
|
2235
|
+
g(
|
|
2236
|
+
'align-left',
|
|
2237
|
+
cap(2.4, 3.6, 13.6, 3.6, 1.2) + cap(2.4, 8, 9.4, 8, 1.2) + cap(2.4, 12.4, 12, 12.4, 1.2),
|
|
2238
|
+
);
|
|
2239
|
+
g(
|
|
2240
|
+
'align-center',
|
|
2241
|
+
cap(2.4, 3.6, 13.6, 3.6, 1.2) + cap(4.6, 8, 11.4, 8, 1.2) + cap(3.2, 12.4, 12.8, 12.4, 1.2),
|
|
2242
|
+
);
|
|
2243
|
+
g(
|
|
2244
|
+
'align-right',
|
|
2245
|
+
cap(2.4, 3.6, 13.6, 3.6, 1.2) + cap(6.6, 8, 13.6, 8, 1.2) + cap(4, 12.4, 13.6, 12.4, 1.2),
|
|
2246
|
+
);
|
|
2247
|
+
g(
|
|
2248
|
+
'volume-high',
|
|
2249
|
+
poly([
|
|
2250
|
+
[1.8, 6],
|
|
2251
|
+
[4.6, 6],
|
|
2252
|
+
[8, 2.8],
|
|
2253
|
+
[8, 13.2],
|
|
2254
|
+
[4.6, 10],
|
|
2255
|
+
[1.8, 10],
|
|
2256
|
+
]) +
|
|
2257
|
+
arc(8.4, 8, 2.8, -55, 55, 1.15) +
|
|
2258
|
+
arc(8.4, 8, 5.2, -50, 50, 1.15),
|
|
2259
|
+
);
|
|
2260
|
+
g(
|
|
2261
|
+
'volume-xmark',
|
|
2262
|
+
poly([
|
|
2263
|
+
[1.8, 6],
|
|
2264
|
+
[4.6, 6],
|
|
2265
|
+
[8, 2.8],
|
|
2266
|
+
[8, 13.2],
|
|
2267
|
+
[4.6, 10],
|
|
2268
|
+
[1.8, 10],
|
|
2269
|
+
]) +
|
|
2270
|
+
cap(10, 6, 14.2, 10, 1.25) +
|
|
2271
|
+
cap(14.2, 6, 10, 10, 1.25),
|
|
2272
|
+
);
|
|
2273
|
+
g(
|
|
2274
|
+
'music',
|
|
2275
|
+
cap(6.2, 4.2, 6.2, 11.6, 1.2) +
|
|
2276
|
+
cap(13, 2.6, 13, 10, 1.2) +
|
|
2277
|
+
poly([
|
|
2278
|
+
[5.6, 2.4],
|
|
2279
|
+
[13.6, 1.4],
|
|
2280
|
+
[13.6, 4],
|
|
2281
|
+
[5.6, 5],
|
|
2282
|
+
]) +
|
|
2283
|
+
ring(4.2, 11.6, 2, 1.2) +
|
|
2284
|
+
ring(11, 10, 2, 1.2),
|
|
2285
|
+
);
|
|
2286
|
+
g(
|
|
2287
|
+
'atom',
|
|
2288
|
+
dot(8, 8, 1.4) +
|
|
2289
|
+
ellipseArc(8, 8, 6.2, 2.1, 0, 360, 0.9, 30, true) +
|
|
2290
|
+
ellipseArc(8, 8, 6.2, 2.1, 0, 360, 0.9, -30, true) +
|
|
2291
|
+
ellipseArc(8, 8, 6.2, 2.1, 0, 360, 0.9, 90, true),
|
|
2292
|
+
);
|
|
2293
|
+
g(
|
|
2294
|
+
'brain',
|
|
2295
|
+
line(
|
|
2296
|
+
[
|
|
2297
|
+
[8, 2.4],
|
|
2298
|
+
[11.6, 3],
|
|
2299
|
+
[13.5, 6],
|
|
2300
|
+
[12.6, 9.8],
|
|
2301
|
+
[9.8, 13.4],
|
|
2302
|
+
[6, 13.5],
|
|
2303
|
+
[3, 11.2],
|
|
2304
|
+
[2.5, 7.4],
|
|
2305
|
+
[4.4, 4],
|
|
2306
|
+
],
|
|
2307
|
+
1.3,
|
|
2308
|
+
true,
|
|
2309
|
+
) +
|
|
2310
|
+
cap(8, 3.2, 8, 13, 1.15) +
|
|
2311
|
+
cap(5.2, 6, 8, 7.4, 1.05) +
|
|
2312
|
+
cap(10.8, 9.6, 8, 8.6, 1.05),
|
|
2313
|
+
);
|
|
2314
|
+
g(
|
|
2315
|
+
'shield-halved',
|
|
2316
|
+
line(
|
|
2317
|
+
[
|
|
2318
|
+
[8, 1.8],
|
|
2319
|
+
[13.6, 4.2],
|
|
2320
|
+
[13.6, 8.4],
|
|
2321
|
+
[8, 14.2],
|
|
2322
|
+
[2.4, 8.4],
|
|
2323
|
+
[2.4, 4.2],
|
|
2324
|
+
],
|
|
2325
|
+
1.3,
|
|
2326
|
+
true,
|
|
2327
|
+
) + cap(8, 2.6, 8, 13.4, 1.15),
|
|
2328
|
+
);
|
|
2329
|
+
g('boxes-placeholder', '');
|
|
2330
|
+
delete glyphs['boxes-placeholder'];
|
|
2331
|
+
g(
|
|
2332
|
+
'download',
|
|
2333
|
+
cap(8, 2.2, 8, 9.8, 1.3) +
|
|
2334
|
+
line(
|
|
2335
|
+
[
|
|
2336
|
+
[4.8, 6.8],
|
|
2337
|
+
[8, 10.2],
|
|
2338
|
+
[11.2, 6.8],
|
|
2339
|
+
],
|
|
2340
|
+
1.25,
|
|
2341
|
+
) +
|
|
2342
|
+
line(
|
|
2343
|
+
[
|
|
2344
|
+
[2.8, 11],
|
|
2345
|
+
[2.8, 13.8],
|
|
2346
|
+
[13.2, 13.8],
|
|
2347
|
+
[13.2, 11],
|
|
2348
|
+
],
|
|
2349
|
+
1.25,
|
|
2350
|
+
),
|
|
2351
|
+
);
|
|
2352
|
+
g(
|
|
2353
|
+
'cloud-arrow-down',
|
|
2354
|
+
dot(5.5, 6.6, 2.5) +
|
|
2355
|
+
dot(9, 5.6, 3.1) +
|
|
2356
|
+
dot(11.6, 7.3, 2.2) +
|
|
2357
|
+
rrect(4.4, 6.6, 8.4, 2.6, 1.3) +
|
|
2358
|
+
cap(8, 9, 8, 13.4, 1.25) +
|
|
2359
|
+
line(
|
|
2360
|
+
[
|
|
2361
|
+
[5.9, 11.2],
|
|
2362
|
+
[8, 13.6],
|
|
2363
|
+
[10.1, 11.2],
|
|
2364
|
+
],
|
|
2365
|
+
1.2,
|
|
2366
|
+
),
|
|
2367
|
+
);
|
|
2368
|
+
g(
|
|
2369
|
+
'arrow-up-right-from-square',
|
|
2370
|
+
line(
|
|
2371
|
+
[
|
|
2372
|
+
[7.4, 3],
|
|
2373
|
+
[3, 3],
|
|
2374
|
+
[3, 13],
|
|
2375
|
+
[13, 13],
|
|
2376
|
+
[13, 8.6],
|
|
2377
|
+
],
|
|
2378
|
+
1.25,
|
|
2379
|
+
) +
|
|
2380
|
+
cap(7.6, 8.4, 13.2, 2.8, 1.25) +
|
|
2381
|
+
line(
|
|
2382
|
+
[
|
|
2383
|
+
[9.4, 2.6],
|
|
2384
|
+
[13.4, 2.6],
|
|
2385
|
+
[13.4, 6.6],
|
|
2386
|
+
],
|
|
2387
|
+
1.25,
|
|
2388
|
+
),
|
|
2389
|
+
);
|
|
2390
|
+
|
|
2391
|
+
// ------------------------------------------------------- category tones
|
|
2392
|
+
/**
|
|
2393
|
+
* COLOUR AS A GLYPH'S OWN CHANNEL. Blender's Properties-tab rail groups by
|
|
2394
|
+
* HUE — in `properties-object.png` at 14 px the tab shapes are near
|
|
2395
|
+
* indistinguishable and the ink is what says which group a tab belongs to —
|
|
2396
|
+
* and the outliner tints a row's type glyph the same way. Each name below is
|
|
2397
|
+
* a category the palette inks (`EditorTheme.color.category`, where every
|
|
2398
|
+
* value's source coordinates are recorded); a glyph paints
|
|
2399
|
+
* `var(--vgai-category-<tone>, currentColor)`, so under a palette that names
|
|
2400
|
+
* no category group it paints exactly as it did monochrome.
|
|
2401
|
+
*
|
|
2402
|
+
* A NAME, NOT A SITE. A tone here tints EVERY site that asks for this icon
|
|
2403
|
+
* name, so a name is toned only when its meaning is the category at every
|
|
2404
|
+
* one of them. Measured live, that is not a hypothetical: the first cut
|
|
2405
|
+
* toned `cube`, `camera`, `lightbulb` and `globe`, and the running editor
|
|
2406
|
+
* painted the Perspective/Orthographic dropdown orange and the world-space
|
|
2407
|
+
* toggle red — both are viewport CONTROLS that happen to reuse the drawing.
|
|
2408
|
+
* The fix is `editorIcons.modeling`'s: a meaning that needs its own ink gets
|
|
2409
|
+
* its own name. `hierarchy-kind-icon.ts` now asks for `outliner-object`,
|
|
2410
|
+
* `outliner-light` and `outliner-camera`, which no other site asks for, and
|
|
2411
|
+
* they are aliased to the same drawings below.
|
|
2412
|
+
*
|
|
2413
|
+
* - `outliner-object`, `outliner-light`, `outliner-camera` → `object`.
|
|
2414
|
+
* `outliner.png`: the Camera, Cube and Light rows' type glyphs all
|
|
2415
|
+
* measure #bb7f4d over the row's #272727 — ONE orange for all three,
|
|
2416
|
+
* because Blender's outliner discriminates OBJECT from DATA, not camera
|
|
2417
|
+
* from light. The same ink is the Object tab's in `properties-object.png`.
|
|
2418
|
+
* - `atom` → `modifier`. Its only site is the inspector's physics/rigid-
|
|
2419
|
+
* body/collider group (`inspection/compose.ts`'s `GROUP_ICON_RULES`);
|
|
2420
|
+
* `properties-object.png` paints Physics — with Modifier, Particles and
|
|
2421
|
+
* Constraints — one blue.
|
|
2422
|
+
* - `palette` → `material`. Its product sites are all material sections
|
|
2423
|
+
* (the same `GROUP_ICON_RULES`, the model-asset and react inspectors);
|
|
2424
|
+
* the Material tab of the same rail.
|
|
2425
|
+
* - `screwdriver-wrench` → `tool`. Its only site is a project tool's own
|
|
2426
|
+
* glyph (`tool-loader.ts`); the Tool tab of the same rail.
|
|
2427
|
+
*
|
|
2428
|
+
* The six `mesh-*` operator marks carry their tone at the glyph instead,
|
|
2429
|
+
* with a `tonedPath`, because an operator mark tints only the element it
|
|
2430
|
+
* operates on and leaves the cube neutral.
|
|
2431
|
+
*
|
|
2432
|
+
* NOT toned, each for a measured reason rather than an oversight:
|
|
2433
|
+
* - `globe` — Blender's World tab is red, but no site in this editor draws
|
|
2434
|
+
* a globe to mean a World datablock: the two are the transform-SPACE
|
|
2435
|
+
* toggle and a build profile. A red globe there reads as an error state.
|
|
2436
|
+
* - `table` — its one product site is the mesh inspector's Vertex Groups
|
|
2437
|
+
* panel, which is Blender's green `DATA_PT_vertex_groups`, but it renders
|
|
2438
|
+
* through `SectionHeader`, which passes `tone="dim"` to every section
|
|
2439
|
+
* glyph — and an explicit site tone wins by contract. A tone there would
|
|
2440
|
+
* never paint.
|
|
2441
|
+
*/
|
|
2442
|
+
/** The union `IconCategoryTone` declares (`@vgai/editor-sdk/looks`), and the
|
|
2443
|
+
* reason `blender.style.ts` may narrow this JSON with an assertion: a tone
|
|
2444
|
+
* outside it never reaches the artifact. */
|
|
2445
|
+
const TONES = ['object', 'modifier', 'material', 'tool', 'operator', 'data', 'scene', 'collection'];
|
|
2446
|
+
|
|
2447
|
+
// The outliner's three names, aliased onto the drawings they share with the
|
|
2448
|
+
// generic glyphs. Same picture, its own name, so only the outliner is tinted.
|
|
2449
|
+
g('outliner-object', glyphs['cube'].path);
|
|
2450
|
+
// A MESH OBJECT's row mark is not a cube. Measured on the native 2x
|
|
2451
|
+
// `outliner.png` (the Cube row, src x 127-154, y 179-206): Blender draws an
|
|
2452
|
+
// APEX-DOWN triangle, heavily stroked, with a triangular knockout in its upper
|
|
2453
|
+
// half — the object-type sibling of the datablock's `properties-data` mark, in
|
|
2454
|
+
// the object orange rather than the data green. On the set's 16-unit grid the
|
|
2455
|
+
// outer triangle is (1.25,1.25)-(14.75,1.25)-(8,14.75) and the knockout
|
|
2456
|
+
// (4.75,4.25)-(11.25,4.25)-(8,9.75). `outliner-object` keeps the cube for the
|
|
2457
|
+
// generic three-object row, which has no mesh to name.
|
|
2458
|
+
g(
|
|
2459
|
+
'outliner-mesh',
|
|
2460
|
+
poly([
|
|
2461
|
+
[1.25, 1.25],
|
|
2462
|
+
[14.75, 1.25],
|
|
2463
|
+
[8, 14.75],
|
|
2464
|
+
]) +
|
|
2465
|
+
holePoly([
|
|
2466
|
+
[4.75, 4.25],
|
|
2467
|
+
[11.25, 4.25],
|
|
2468
|
+
[8, 9.75],
|
|
2469
|
+
]),
|
|
2470
|
+
);
|
|
2471
|
+
g('outliner-light', glyphs.lightbulb.path);
|
|
2472
|
+
g('outliner-camera', glyphs.camera.path);
|
|
2473
|
+
|
|
2474
|
+
// The Properties editor's two package tabs, each its own name so only the
|
|
2475
|
+
// rail is tinted. `properties-modifiers` is the wrench Blender draws for the
|
|
2476
|
+
// Modifiers tab, in the modifier blue. `properties-data` is Blender's
|
|
2477
|
+
// MESH_DATA mark — a triangle with a vertex dot at each corner — drawn on
|
|
2478
|
+
// the set's own grid at its own stroke, measured from `properties-object.png`'s
|
|
2479
|
+
// Object Data tab (src x 10-56, y 802-830).
|
|
2480
|
+
/**
|
|
2481
|
+
* BLENDER'S MODIFIER TAB IS ONE OPEN-END WRENCH, and this glyph used to be
|
|
2482
|
+
* `screwdriver-wrench` — the TOOL tab's two-tool silhouette — reused under a
|
|
2483
|
+
* second name. At the 14px rail the two read as different marks, and the rail
|
|
2484
|
+
* paints them side by side, so the reuse was the loudest thing left in it.
|
|
2485
|
+
*
|
|
2486
|
+
* MEASURED off `modeling-edit-none.png` at its native 2x (the rail's OPEN
|
|
2487
|
+
* Modifier tab, ink x 2861..2888, y 1004..1031 — 28x28 device px, exactly the
|
|
2488
|
+
* box the Data tab's mark fills two cells below, which is what calibrates the
|
|
2489
|
+
* scale: `properties-data` spans 16.6 viewBox units, so this does too).
|
|
2490
|
+
* Row-by-row ink runs, thresholded at half coverage and mapped onto the set's
|
|
2491
|
+
* plain 16 grid, give a shape with four parts:
|
|
2492
|
+
*
|
|
2493
|
+
* - a HEAD. Not a stroke of constant width: the outer boundary is a circle
|
|
2494
|
+
* (5.08 left, 14.81 right at the head's own centre line) and the bore is a
|
|
2495
|
+
* SMALLER, concentric circle, so the ring is ~2.9 thick where the handle
|
|
2496
|
+
* leaves it and the jaw's flats eat into it from the other side.
|
|
2497
|
+
* - a JAW cut as a SLOT WITH PARALLEL FLATS, not a wedge and not a wider
|
|
2498
|
+
* bore. Both inner faces measure dx/dy = -1.00 over their whole length
|
|
2499
|
+
* (left face x+y = 12.84, right face x+y = 19.41) - parallel, ~4.3 apart,
|
|
2500
|
+
* on an axis at -45 degrees, opposite the handle. That parallelism is the
|
|
2501
|
+
* one fact that makes the mark read as a WRENCH rather than a C: a
|
|
2502
|
+
* radial-ended annulus puts the jaw's inner corner 3.8 device px off.
|
|
2503
|
+
* - the slot STOPS about a unit short of the head's centre, which is what
|
|
2504
|
+
* keeps the ring closed across the bottom (the frame's row y 1018 is one
|
|
2505
|
+
* unbroken run, 5.08..14.57).
|
|
2506
|
+
* - a HANDLE at exactly 45 degrees - its right boundary moves one device
|
|
2507
|
+
* pixel per row for nine rows - ~3.7 wide, ending in a round cap whose
|
|
2508
|
+
* extremes ARE the glyph's left and bottom edges.
|
|
2509
|
+
*
|
|
2510
|
+
* THE CONSTANTS BELOW ARE THE FIT, NOT THE RAW READING. Every feature above
|
|
2511
|
+
* is a boundary read at a 50% threshold on a 28px glyph, so each carries about
|
|
2512
|
+
* half a device pixel of slack, and the rim's apparent radius swings 4.5..5.0
|
|
2513
|
+
* depending on which row it is read from (near the top the circle is almost
|
|
2514
|
+
* horizontal, so half a pixel of radius moves x by two). The reading fixed the
|
|
2515
|
+
* STRUCTURE; the numbers were then settled by rendering the path at the
|
|
2516
|
+
* reference's own scale and counting the pixels where the two half-coverage
|
|
2517
|
+
* masks disagree. Every constant stayed inside its own reading's error bar and
|
|
2518
|
+
* the count came down 18 -> 5 of 298 ref ink pixels, which is the floor this
|
|
2519
|
+
* instrument has: the five are single pixels on antialiased diagonals.
|
|
2520
|
+
*
|
|
2521
|
+
* DRAWN AS ONE CLOSED BOUNDARY, NO KNOCKOUT AT ALL — and that is the whole
|
|
2522
|
+
* reason this glyph is a traced outline rather than the file's usual pile of
|
|
2523
|
+
* primitives. Two knockout constructions were tried and both are impossible
|
|
2524
|
+
* here, which is worth stating so the third attempt does not repeat them:
|
|
2525
|
+
* - disc + handle capsule, then bore and slot punched. The handle has to
|
|
2526
|
+
* reach PAST the bore to cover the head (its cap would need to sit at
|
|
2527
|
+
* radius >4.04 to clear the bore and <3.11 to stay inside the outer
|
|
2528
|
+
* circle — no such radius), so the head is DOUBLY covered exactly where
|
|
2529
|
+
* the punches land: winding 2 - 1 = 1, and every knockout fills back in.
|
|
2530
|
+
* - one outer polygon with the bore and the slot as holes. The slot has to
|
|
2531
|
+
* run PAST the rim to cut the jaw open, and a hole outside its fill is not
|
|
2532
|
+
* a hole — it is ink (winding +1), which is what it drew: a blue bar
|
|
2533
|
+
* across the top-right corner. The bore and the slot also overlap, and two
|
|
2534
|
+
* overlapping holes cancel to ink in their intersection.
|
|
2535
|
+
* The material is SIMPLY CONNECTED — the bore opens into the jaw, which opens
|
|
2536
|
+
* to the outside — so its boundary is one walk: the rim from the upper arm's
|
|
2537
|
+
* tip round to the handle, the handle's two sides and its cap, the rim again
|
|
2538
|
+
* to the lower jaw's tip, in along one flat, across the jaw's stop, round the
|
|
2539
|
+
* BACK of the bore, and out along the other flat.
|
|
2540
|
+
*/
|
|
2541
|
+
const modifierWrench = () => {
|
|
2542
|
+
const cx = 10.28; // the head's centre
|
|
2543
|
+
const cy = 5.84;
|
|
2544
|
+
const rOut = 4.78; // its outer rim
|
|
2545
|
+
const rIn = 1.85; // its bore
|
|
2546
|
+
const hw = 2.15; // half the jaw's opening
|
|
2547
|
+
const off = -0.12; // the jaw's midline, off the head's centre
|
|
2548
|
+
const stop = -1.1; // where the jaw stops, along its own axis
|
|
2549
|
+
const hx = 3.2; // the handle's end cap
|
|
2550
|
+
const hy = 12.75;
|
|
2551
|
+
const hr = 1.87;
|
|
2552
|
+
const A = [Math.SQRT1_2, -Math.SQRT1_2]; // jaw/handle axis, up-right
|
|
2553
|
+
const N = [Math.SQRT1_2, Math.SQRT1_2]; // its perpendicular, down-right
|
|
2554
|
+
/** A point in the jaw's own frame: `s` along the axis, `f` across it. */
|
|
2555
|
+
const at = (s, f) => [cx + A[0] * s + N[0] * f, cy + A[1] * s + N[1] * f];
|
|
2556
|
+
const ang = ([x, y]) => (Math.atan2(y - cy, x - cx) * 180) / Math.PI;
|
|
2557
|
+
const sweep = (r, a0, a1, ox = cx, oy = cy) => {
|
|
2558
|
+
const n = Math.max(2, Math.ceil(Math.abs(a1 - a0) / 6));
|
|
2559
|
+
return Array.from({ length: n + 1 }, (_, i) => {
|
|
2560
|
+
const a = ((a0 + ((a1 - a0) * i) / n) * Math.PI) / 180;
|
|
2561
|
+
return [ox + Math.cos(a) * r, oy + Math.sin(a) * r];
|
|
2562
|
+
});
|
|
2563
|
+
};
|
|
2564
|
+
const rim = (f) => Math.sqrt(Math.max(rOut * rOut - f * f, 0));
|
|
2565
|
+
const fUp = off - hw; // the jaw's two flats, across its axis
|
|
2566
|
+
const fLo = off + hw;
|
|
2567
|
+
const t1 = at(rim(fUp), fUp); // the upper arm's tip
|
|
2568
|
+
const t2 = at(rim(fLo), fLo); // the lower jaw's tip
|
|
2569
|
+
// Where each side of the handle runs into the rim, in the same frame.
|
|
2570
|
+
const offAxis = (cx - hx) * N[0] + (cy - hy) * N[1];
|
|
2571
|
+
const pUp = at(-rim(-hr - offAxis), -hr - offAxis);
|
|
2572
|
+
const pLo = at(-rim(hr - offAxis), hr - offAxis);
|
|
2573
|
+
// BOTH FLATS CLEAR THE BORE (2.32 out against a 2.07 radius) and the jaw's
|
|
2574
|
+
// stop crosses it, so the bore meets the jaw at the STOP, never at a flat.
|
|
2575
|
+
const b = Math.sqrt(Math.max(rIn * rIn - stop * stop, 0));
|
|
2576
|
+
const v1 = at(stop, b);
|
|
2577
|
+
const v2 = at(stop, -b);
|
|
2578
|
+
return poly([
|
|
2579
|
+
...sweep(rOut, ang(t1), ang(pUp) - 360), // the rim, over the top, to the handle
|
|
2580
|
+
...sweep(hr, 225, 45, hx, hy), // the handle: its two sides and its cap
|
|
2581
|
+
...sweep(rOut, ang(pLo) - 360, ang(t2) - 360), // the rim again, to the jaw
|
|
2582
|
+
at(stop, fLo), // in along the jaw's lower flat, then across its stop
|
|
2583
|
+
...sweep(rIn, ang(v1), ang(v2) + 360), // round the BACK of the bore
|
|
2584
|
+
at(stop, fUp), // across the stop again, and out along the upper flat
|
|
2585
|
+
]);
|
|
2586
|
+
};
|
|
2587
|
+
g('properties-modifiers', modifierWrench());
|
|
2588
|
+
/**
|
|
2589
|
+
* RE-MEASURED 2026-09-19 EDGE BY EDGE, and the mark is not a triangle drawn
|
|
2590
|
+
* THROUGH three vertex marks. The 2026-09-18 cut got the orientation and the
|
|
2591
|
+
* square vertices right and then drew the triangle as one closed `line`
|
|
2592
|
+
* running vertex-centre to vertex-centre, which fills each square's hollow
|
|
2593
|
+
* from the inside: at the 14 px the Outliner paints it the three squares
|
|
2594
|
+
* closed to blobs and the whole glyph read as a fat stem with two legs — a
|
|
2595
|
+
* Y — where Blender's reads as a clean triangle with three open boxes on it.
|
|
2596
|
+
*
|
|
2597
|
+
* The frames are NOT ANTIALIASED here (every ink pixel is the full
|
|
2598
|
+
* `#07b189`/`#05ae87` and every background pixel the full row), so these are
|
|
2599
|
+
* EXACT EDGES, not fits. Read down `modeling-object-none.png`'s Cube row (src
|
|
2600
|
+
* x 269..296, y 179..206 — the Outliner crop `[x2843..3452, y53..419]`'s
|
|
2601
|
+
* x 269..296, y 179..206), confirmed identical in `modeling-object-selected.png`
|
|
2602
|
+
* and `outliner.png`, and the same mark at the same size in
|
|
2603
|
+
* `properties-object.png`'s Data tab:
|
|
2604
|
+
*
|
|
2605
|
+
* three squares, OUTER 8 device px, WALL 2, HOLLOW 4 — at [0,8), [20,28)
|
|
2606
|
+
* and [10,18) of the mark's own 28 px box
|
|
2607
|
+
* a top BAR x [6,22) y [2,4) — it runs wall to wall between the two top
|
|
2608
|
+
* squares and stops dead at each hollow; it does NOT cross them, and it
|
|
2609
|
+
* sits in the UPPER half of the hollow band, not on the squares' centre
|
|
2610
|
+
* two DIAGONALS, 2 px wide, living only in y [8,20) — the gap between the
|
|
2611
|
+
* top squares' bottom edge and the bottom square's top edge. The left
|
|
2612
|
+
* one's centre runs x 273.04 → 279.44 down that span (a subpixel fit over
|
|
2613
|
+
* twelve rows, residual under 0.1 px), which is the top square's own
|
|
2614
|
+
* centre-x at the top and the bottom square's LEFT edge at the bottom;
|
|
2615
|
+
* the right one is its mirror about the box's centre.
|
|
2616
|
+
*
|
|
2617
|
+
* So the three edges are drawn BETWEEN the squares and the squares are
|
|
2618
|
+
* FILLED-WITH-A-KNOCKOUT rather than stroked outlines — which is also why
|
|
2619
|
+
* their corners are square where `rframe`'s round joins gave ours a dot.
|
|
2620
|
+
* `poly`/`rrect` take no `W` fattening, which is what this mark wants: it is
|
|
2621
|
+
* fitted to pixels, like `properties-modifiers` and the editor-type wells.
|
|
2622
|
+
*
|
|
2623
|
+
* PROVED, the way the wrench was: the path rasterized at the reference's own
|
|
2624
|
+
* scale (28x28, 4x4 supersampled, inked at half coverage) and differenced
|
|
2625
|
+
* against the frame's mask — 1 of 222 reference ink pixels disagrees, a
|
|
2626
|
+
* single antialiased pixel on the right diagonal's edge.
|
|
2627
|
+
*
|
|
2628
|
+
* A NOTE ON THE THRESHOLD, because it nearly cost this glyph its geometry:
|
|
2629
|
+
* the mark is crisp everywhere EXCEPT the diagonals' edges, and read at a
|
|
2630
|
+
* 0.44-coverage cut those two bars mask one pixel wider than they are. Fitted
|
|
2631
|
+
* to that mask the diagonals want 2.35 device px; fitted to the frame's own
|
|
2632
|
+
* subpixel MASS (2.26 px per row across twelve rows, times cos 28.06 deg)
|
|
2633
|
+
* they measure 1.99, and at the honest half-coverage cut the measured width
|
|
2634
|
+
* is the one that scores 1/222 where the fitted width scores 3. A shape
|
|
2635
|
+
* tuned to a rasterizer's threshold is tuned to the rasterizer.
|
|
2636
|
+
*/
|
|
2637
|
+
/** One device px of the reference, in this file's plain 16 units: the mark
|
|
2638
|
+
* inks exactly 28x28 device px, which is 13.5 units once `S` has scaled it
|
|
2639
|
+
* (1.25..14.75) — the same calibration the wells below use. */
|
|
2640
|
+
const DATA_PX = 13.5 / 28;
|
|
2641
|
+
/** A measured edge, counted from the mark's own first ink pixel. */
|
|
2642
|
+
const dpx = (px) => 1.25 + px * DATA_PX;
|
|
2643
|
+
/** A bar with SQUARE ends. `cap`'s round ends would push ink past the square
|
|
2644
|
+
* each diagonal dies against, into its hollow, where Blender draws none. */
|
|
2645
|
+
const dataBar = (x1, y1, x2, y2, w) => {
|
|
2646
|
+
const dx = x2 - x1;
|
|
2647
|
+
const dy = y2 - y1;
|
|
2648
|
+
const L = Math.hypot(dx, dy);
|
|
2649
|
+
const nx = (-dy / L) * (w / 2);
|
|
2650
|
+
const ny = (dx / L) * (w / 2);
|
|
2651
|
+
return poly([
|
|
2652
|
+
[x1 + nx, y1 + ny],
|
|
2653
|
+
[x2 + nx, y2 + ny],
|
|
2654
|
+
[x2 - nx, y2 - ny],
|
|
2655
|
+
[x1 - nx, y1 - ny],
|
|
2656
|
+
]);
|
|
2657
|
+
};
|
|
2658
|
+
/** A vertex mark: an 8-px square with its 4-px middle punched out. */
|
|
2659
|
+
const dataVertex = (px, py) => {
|
|
2660
|
+
const side = 8 * DATA_PX;
|
|
2661
|
+
const wall = 2 * DATA_PX;
|
|
2662
|
+
const hole = 4 * DATA_PX;
|
|
2663
|
+
const x = dpx(px);
|
|
2664
|
+
const y = dpx(py);
|
|
2665
|
+
return (
|
|
2666
|
+
rrect(x, y, side, side) +
|
|
2667
|
+
holePoly([
|
|
2668
|
+
[x + wall, y + wall],
|
|
2669
|
+
[x + wall + hole, y + wall],
|
|
2670
|
+
[x + wall + hole, y + wall + hole],
|
|
2671
|
+
[x + wall, y + wall + hole],
|
|
2672
|
+
])
|
|
2673
|
+
);
|
|
2674
|
+
};
|
|
2675
|
+
g(
|
|
2676
|
+
'properties-data',
|
|
2677
|
+
// the bar between the two top squares
|
|
2678
|
+
rrect(dpx(6), dpx(2), 16 * DATA_PX, 2 * DATA_PX) +
|
|
2679
|
+
// the two diagonals, square-ended, spanning only the gap between squares
|
|
2680
|
+
dataBar(dpx(4.04), dpx(8), dpx(10.44), dpx(20), 2 * DATA_PX) +
|
|
2681
|
+
dataBar(dpx(23.96), dpx(8), dpx(17.56), dpx(20), 2 * DATA_PX) +
|
|
2682
|
+
dataVertex(0, 0) +
|
|
2683
|
+
dataVertex(20, 0) +
|
|
2684
|
+
dataVertex(10, 20),
|
|
2685
|
+
);
|
|
2686
|
+
|
|
2687
|
+
// The Outliner's DATABLOCK row (`@vgai/blender`'s datablock hierarchy) —
|
|
2688
|
+
// the same MESH_DATA mark the Properties rail's Data tab draws, under its own
|
|
2689
|
+
// name because the two sites composite differently: `outliner.png`'s data
|
|
2690
|
+
// glyph is #07b189 over the row's #272727 and `properties-object.png`'s tab is
|
|
2691
|
+
// #05ae87 over the rail's #1c1c1c, one ink at α 0.791 in both — while our rail
|
|
2692
|
+
// paints its tab at full alpha, so the two will not stay one value.
|
|
2693
|
+
g('outliner-data', glyphs['properties-data'].path);
|
|
2694
|
+
|
|
2695
|
+
/**
|
|
2696
|
+
* BLENDER'S PROPERTIES RAIL, TRACED FROM BLENDER'S OWN ICON SOURCES.
|
|
2697
|
+
*
|
|
2698
|
+
* `ED_buttons_tabs_list` (`space_buttons.cc:201-255`) draws up to sixteen tabs
|
|
2699
|
+
* and each one's mark is named in `buttons_context_items`
|
|
2700
|
+
* (`makesrna/intern/rna_space.cc:579-611`) — `ICON_SCENE` for Render,
|
|
2701
|
+
* `ICON_SCENE_DATA` for Scene, `ICON_OBJECT_DATA` for Object, and so on. Each
|
|
2702
|
+
* of those icons IS A FILE in the Blender checkout at the engine's pin:
|
|
2703
|
+
* `release/datafiles/icons_svg/<lowercase name>.svg`, authored on a 16-unit
|
|
2704
|
+
* grid inside a 1600 viewBox. The compiled `.dat` sheets under
|
|
2705
|
+
* `release/datafiles/icons/` are that file's BUILD PRODUCT, not its source,
|
|
2706
|
+
* and Blender 5.x has no combined `blender_icons.svg` master.
|
|
2707
|
+
*
|
|
2708
|
+
* WHAT I1 LEFT AND THIS REPLACES: thirteen of the sixteen were a distinct NAME
|
|
2709
|
+
* over a neighbouring silhouette this set already drew — a globe for World, a
|
|
2710
|
+
* magnet for Constraints, a palette for Material — and the remaining two
|
|
2711
|
+
* (`properties-modifiers`, `properties-data`) were measured pixel-by-pixel off
|
|
2712
|
+
* a screenshot. All sixteen are now traced from the vector source, which is
|
|
2713
|
+
* both more exact than a screenshot reading and far less code: the two
|
|
2714
|
+
* hand-measured marks above this line took two paragraphs of prose each to
|
|
2715
|
+
* justify one shape.
|
|
2716
|
+
*
|
|
2717
|
+
* GPL PATHS, DELIBERATELY — AND THE PACKAGE'S SPDX SAYS SO. Blender's icon
|
|
2718
|
+
* sources are GPL-2.0-or-later, and `@vgai/blender` is
|
|
2719
|
+
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`, whose
|
|
2720
|
+
* notice block names this set, its provenance file and the §13 combination):
|
|
2721
|
+
* our code is AGPL, these 197 paths are Blender's artwork conveyed as GPL-3.0,
|
|
2722
|
+
* and both licences' §13 permit the combination. Nothing traced may be copied
|
|
2723
|
+
* into an Apache/MIT part of this repo; `packages/editor`'s own icon set in
|
|
2724
|
+
* particular stays free of it.
|
|
2725
|
+
*
|
|
2726
|
+
* NO `S` AND NO `W`. Those two dials exist to bring OUR drawings up to
|
|
2727
|
+
* Blender's ink ratio (see their note at the top of this file); a traced mark
|
|
2728
|
+
* already has it. Measured over the traced set, Blender's marks ink 14 of the
|
|
2729
|
+
* 16 units (x and y both 1..15 in almost every file), against the ~14.5 the
|
|
2730
|
+
* dialled geometry above produces — within 3.5%, so the rail reads as one set
|
|
2731
|
+
* while every traced glyph keeps Blender's own proportions exactly.
|
|
2732
|
+
*
|
|
2733
|
+
* Re-derive `blender.icons.traced.json` with
|
|
2734
|
+
* `node blender-icons.source.mjs --trace`, which reads the checkout at
|
|
2735
|
+
* `$BLENDER_SRC` (default `~/volter/blender-src`; the recipe is in WORK.md
|
|
2736
|
+
* §Blender in the tab is Blender, "Inspection parity") and records each
|
|
2737
|
+
* glyph's source file and sha256 beside its path. The ordinary run consumes
|
|
2738
|
+
* that JSON and needs no checkout.
|
|
2739
|
+
*/
|
|
2740
|
+
// The TONE rides with the traced glyph, read from that icon's own
|
|
2741
|
+
// `DEF_ICON_<GROUP>` macro at trace time (see the `--trace` block). The
|
|
2742
|
+
// Properties rail's sixteen keep their assignments in `CATEGORY_TONE` below,
|
|
2743
|
+
// which is where they were measured; a traced entry that carries `tone` is
|
|
2744
|
+
// answering for itself and that table names none of them.
|
|
2745
|
+
for (const [name, entry] of Object.entries(traced))
|
|
2746
|
+
g(name, entry.path, entry.tone ? { tone: entry.tone } : undefined);
|
|
2747
|
+
|
|
2748
|
+
// -- the EDITOR-TYPE WELLS -------------------------------------------------
|
|
2749
|
+
/**
|
|
2750
|
+
* THE TWO MARKS BLENDER PUTS IN AN AREA'S EDITOR-TYPE WELL, in the two areas
|
|
2751
|
+
* the reference frames photograph. Every Blender area begins with that well —
|
|
2752
|
+
* a 32x20 CSS rounded box, 1 px `#3c3c3c` over a constant `#272727` inner —
|
|
2753
|
+
* and the glyph inside it is what says which editor the area is
|
|
2754
|
+
* (`workspace-regions.ts`'s `groupTabs`).
|
|
2755
|
+
*
|
|
2756
|
+
* BOTH ARE READ AT NATIVE 2x AND BOTH INK EXACTLY 28x28 DEVICE PX, which is
|
|
2757
|
+
* the same box `properties-data` and the modifier wrench fill two paragraphs
|
|
2758
|
+
* up — so the same calibration carries over unchanged: 28 device px of ink is
|
|
2759
|
+
* 13.5 units on this file's plain 16 grid (1 unit = 2.074 device px), and the
|
|
2760
|
+
* ink lands at 1.25..14.75 in both axes once `S` has scaled it. Every constant
|
|
2761
|
+
* below is a measured pixel edge divided by that one number; none is a taste
|
|
2762
|
+
* call.
|
|
2763
|
+
*
|
|
2764
|
+
* Neither is antialiased in the frame — the runs are the same at a 60 and a
|
|
2765
|
+
* 120 threshold — so every edge quoted is exact rather than a fit.
|
|
2766
|
+
*
|
|
2767
|
+
* THE OTHER FIVE PANELS OF THIS EDITOR GET NO GLYPH HERE, and the reason is
|
|
2768
|
+
* that there is nothing to measure: the reference set photographs the Layout,
|
|
2769
|
+
* Modeling, Sculpting and Texture Paint workspaces, whose areas are the 3D
|
|
2770
|
+
* View, the Outliner, the Properties editor, the Timeline, the Image Editor
|
|
2771
|
+
* and the asset shelf. No frame contains a File Browser or an Asset Browser,
|
|
2772
|
+
* and Blender's editor-type dropdown is closed in all of them, so the Content
|
|
2773
|
+
* browser's and the Asset Library's wells have NO Blender reading at all.
|
|
2774
|
+
* They reuse names this set already draws (`workspace-static-panels.ts` says
|
|
2775
|
+
* which, and says that the choice is ours).
|
|
2776
|
+
*/
|
|
2777
|
+
|
|
2778
|
+
// `outliner.png`, the Outliner area's own well: ink x 27..54, y 11..38 device.
|
|
2779
|
+
// Six parts, each a pixel-exact rectangle with SQUARE corners (no corner in
|
|
2780
|
+
// this glyph loses a pixel at either threshold):
|
|
2781
|
+
// the collection box x [27,37) y [11,17)
|
|
2782
|
+
// the tree's stem x [31,33) y [19,37)
|
|
2783
|
+
// its upper arm x [31,39) y [25,27)
|
|
2784
|
+
// its lower arm x [31,39) y [35,37)
|
|
2785
|
+
// the upper child box x [41,55) y [23,29)
|
|
2786
|
+
// the lower child box x [41,55) y [33,39)
|
|
2787
|
+
// The two GAPS are the mark's own: 2 device px between the collection box and
|
|
2788
|
+
// the stem's top, and 2 more between each arm's end and its child box. Closing
|
|
2789
|
+
// either — which is what a tree icon drawn from memory does — turns three
|
|
2790
|
+
// separated rows into one connected bracket.
|
|
2791
|
+
g(
|
|
2792
|
+
'outliner',
|
|
2793
|
+
rrect(1.25, 1.25, 4.82, 2.89) +
|
|
2794
|
+
rrect(3.18, 5.11, 0.96, 8.68) +
|
|
2795
|
+
rrect(3.18, 8.0, 3.86, 0.96) +
|
|
2796
|
+
rrect(3.18, 12.82, 3.86, 0.96) +
|
|
2797
|
+
rrect(8.0, 7.04, 6.75, 2.89) +
|
|
2798
|
+
rrect(8.0, 11.86, 6.75, 2.89),
|
|
2799
|
+
);
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* `modeling-edit-none.png`, the Properties area's own well: ink x 2870..2897,
|
|
2803
|
+
* y 437..464 device. TWO SOLID STADIUMS, each 28x12 device (radius = half the
|
|
2804
|
+
* height), stacked with 4 device px between them — and each carries a
|
|
2805
|
+
* KNOCKOUT, which is what makes the pair read as two sliders at different
|
|
2806
|
+
* settings rather than as two blank pills:
|
|
2807
|
+
* stadium rows y [437,449) and y [453,465), both x [2870,2898)
|
|
2808
|
+
* upper knockout x [2882,2896) y [439,447)
|
|
2809
|
+
* lower knockout x [2888,2896) y [455,463)
|
|
2810
|
+
* Each knockout is FLAT ON ITS LEFT and semicircular on its right: the right
|
|
2811
|
+
* boundary reads 2893/2894/2895/2895 down its four half-rows, which fits a
|
|
2812
|
+
* radius of exactly 4 device px (= half the knockout's own height) and nothing
|
|
2813
|
+
* else; the left boundary is one x for all eight rows. The two differ only in
|
|
2814
|
+
* where that flat left edge sits, and that difference is the whole picture.
|
|
2815
|
+
*
|
|
2816
|
+
* The knockouts are strictly INSIDE their stadiums (2 device px of ink on
|
|
2817
|
+
* every side), so the nonzero winding is well posed — unlike the wrench's two
|
|
2818
|
+
* failed constructions recorded above, nothing here is punched outside its own
|
|
2819
|
+
* fill or punched twice.
|
|
2820
|
+
*/
|
|
2821
|
+
const sliderSlot = (xL, y, xR, h) => {
|
|
2822
|
+
const r = h / 2;
|
|
2823
|
+
const cy = y + r;
|
|
2824
|
+
const cx = xR - r;
|
|
2825
|
+
const cap = Array.from({ length: 13 }, (_, i) => {
|
|
2826
|
+
const a = ((-90 + 15 * i) * Math.PI) / 180;
|
|
2827
|
+
return [cx + Math.cos(a) * r, cy + Math.sin(a) * r];
|
|
2828
|
+
});
|
|
2829
|
+
return holePoly([[xL, y], ...cap, [xL, y + h]]);
|
|
2830
|
+
};
|
|
2831
|
+
g(
|
|
2832
|
+
'properties',
|
|
2833
|
+
rrect(1.25, 1.25, 13.5, 5.79, 2.41) +
|
|
2834
|
+
sliderSlot(7.04, 2.21, 13.79, 3.86) +
|
|
2835
|
+
rrect(1.25, 8.96, 13.5, 5.79, 2.41) +
|
|
2836
|
+
sliderSlot(9.93, 9.92, 13.79, 3.86),
|
|
2837
|
+
);
|
|
2838
|
+
|
|
2839
|
+
/**
|
|
2840
|
+
* BLENDER HUE-GROUPS ITS RAIL, AND THE SOURCE SAYS WHICH GROUP EACH MARK IS
|
|
2841
|
+
* IN. Every icon in `UI_icons.hh` is declared through a `DEF_ICON_<GROUP>`
|
|
2842
|
+
* macro, and `interface_icons.cc:126-132` maps those macros onto the theme
|
|
2843
|
+
* members `interface/resources.cc:1059-1078` reads:
|
|
2844
|
+
*
|
|
2845
|
+
* DEF_ICON_SCENE → TH_ICON_SCENE `.tui.icon_scene` #cccccc
|
|
2846
|
+
* DEF_ICON_COLLECTION → TH_ICON_COLLECTION `.tui.icon_collection` #ffffff
|
|
2847
|
+
* DEF_ICON_OBJECT → TH_ICON_OBJECT `.tui.icon_object` #e19658
|
|
2848
|
+
* DEF_ICON_OBJECT_DATA → TH_ICON_OBJECT_DATA `.tui.icon_object_data` #00d4a3
|
|
2849
|
+
* DEF_ICON_MODIFIER → TH_ICON_MODIFIER `.tui.icon_modifier` #74a2ff
|
|
2850
|
+
* DEF_ICON_SHADING → TH_ICON_SHADING `.tui.icon_shading` #cc6670
|
|
2851
|
+
*
|
|
2852
|
+
* (`userdef_default_theme.c:272-277`.) So the assignments below are READ, not
|
|
2853
|
+
* matched by eye — and reading them corrected two guesses I1 recorded as open:
|
|
2854
|
+
* WORLD is `DEF_ICON_SHADING(WORLD)` (`UI_icons.hh:193`), the same red as
|
|
2855
|
+
* Material and Texture, NOT the scene grey; and GROUP — the Collection tab —
|
|
2856
|
+
* is `DEF_ICON_COLLECTION(GROUP)` (`:248`), a white of its own rather than
|
|
2857
|
+
* that grey either. The scene group is exactly four tabs: Render
|
|
2858
|
+
* (`DEF_ICON_SCENE(SCENE)`, `:188`), Output (`:187`), View Layer
|
|
2859
|
+
* (`DEF_ICON_SCENE(RENDER_RESULT)`, `:265`) and Scene
|
|
2860
|
+
* (`DEF_ICON_SCENE(SCENE_DATA)`, `:267`).
|
|
2861
|
+
*
|
|
2862
|
+
* Every object-data mark — MESH_DATA, ARMATURE_DATA, BONE_DATA and the rest of
|
|
2863
|
+
* the per-type set the Data tab draws — is `DEF_ICON_OBJECT_DATA`
|
|
2864
|
+
* (`:251-263`), which is why they share one tone rather than list one each.
|
|
2865
|
+
*/
|
|
2866
|
+
const CATEGORY_TONE = {
|
|
2867
|
+
'properties-render': 'scene',
|
|
2868
|
+
'properties-output': 'scene',
|
|
2869
|
+
'properties-view-layer': 'scene',
|
|
2870
|
+
'properties-scene': 'scene',
|
|
2871
|
+
'properties-collection': 'collection',
|
|
2872
|
+
'properties-world': 'material',
|
|
2873
|
+
'properties-object': 'object',
|
|
2874
|
+
'properties-physics': 'modifier',
|
|
2875
|
+
'properties-particles': 'modifier',
|
|
2876
|
+
'properties-constraints': 'modifier',
|
|
2877
|
+
'properties-bone-constraints': 'modifier',
|
|
2878
|
+
'properties-bone': 'data',
|
|
2879
|
+
'properties-material': 'material',
|
|
2880
|
+
'properties-texture': 'material',
|
|
2881
|
+
'outliner-object': 'object',
|
|
2882
|
+
'outliner-mesh': 'object',
|
|
2883
|
+
'outliner-light': 'object',
|
|
2884
|
+
'outliner-camera': 'object',
|
|
2885
|
+
'outliner-data': 'data',
|
|
2886
|
+
atom: 'modifier',
|
|
2887
|
+
palette: 'material',
|
|
2888
|
+
'screwdriver-wrench': 'tool',
|
|
2889
|
+
'properties-modifiers': 'modifier',
|
|
2890
|
+
'properties-data': 'data',
|
|
2891
|
+
// The Data tab's per-object-type marks (`buttons_context_compute` sets
|
|
2892
|
+
// `sbuts->dataicon` from `RNA_struct_ui_icon`) — all one group.
|
|
2893
|
+
...Object.fromEntries(
|
|
2894
|
+
Object.keys(traced)
|
|
2895
|
+
.filter((name) => name.startsWith('properties-data-'))
|
|
2896
|
+
.map((name) => [name, 'data']),
|
|
2897
|
+
),
|
|
2898
|
+
};
|
|
2899
|
+
for (const [name, tone] of Object.entries(CATEGORY_TONE)) {
|
|
2900
|
+
// A table that names a glyph this set no longer draws is a silent no-op,
|
|
2901
|
+
// and a tone assignment that silently stops applying is exactly the kind
|
|
2902
|
+
// of thing nobody notices until the rail goes grey.
|
|
2903
|
+
if (!glyphs[name]) throw new Error(`CATEGORY_TONE names a glyph this set does not draw: ${name}`);
|
|
2904
|
+
glyphs[name] = { ...glyphs[name], tone };
|
|
2905
|
+
}
|
|
2906
|
+
for (const [name, glyph] of Object.entries(glyphs)) {
|
|
2907
|
+
if (glyph.tone !== undefined && !TONES.includes(glyph.tone))
|
|
2908
|
+
throw new Error(
|
|
2909
|
+
`glyph "${name}" carries tone "${glyph.tone}", which is not an IconCategoryTone`,
|
|
2910
|
+
);
|
|
2911
|
+
if (glyph.tonedPath !== undefined && glyph.tone === undefined)
|
|
2912
|
+
throw new Error(
|
|
2913
|
+
`glyph "${name}" has a tonedPath and no tone — the second path would paint currentColor`,
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// ---------------------------------------------------------------- emit
|
|
2918
|
+
|
|
2919
|
+
const ordered = {};
|
|
2920
|
+
for (const key of Object.keys(glyphs).sort()) ordered[key] = glyphs[key];
|
|
2921
|
+
|
|
2922
|
+
const out = { id: 'blender', title: 'Blender', glyphs: ordered };
|
|
2923
|
+
const target = new URL('./blender.icons.json', import.meta.url);
|
|
2924
|
+
writeFileSync(target, `${JSON.stringify(out, null, 2)}\n`);
|
|
2925
|
+
console.log(`blender.icons.json — ${Object.keys(ordered).length} glyphs`);
|