comparison infinite_canvas/tools/generate_lucide_data.mjs @ 278:8d560f50ed4c

Improve infinite canvas interactions and browser chrome Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents. Co-authored-by: Copilot <[email protected]> Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:16:14 -0700
parents
children
comparison
equal deleted inserted replaced
277:1d99147f520c 278:8d560f50ed4c
1 #!/usr/bin/env node
2
3 import fs from "node:fs";
4 import os from "node:os";
5 import path from "node:path";
6 import { spawnSync } from "node:child_process";
7 import { pathToFileURL } from "node:url";
8
9 const VERSION = "1.31.0";
10 const output = path.resolve(
11 process.argv[2] ?? "infinite_canvas/generated/lucide_data.h",
12 );
13 const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "lucide-data-"));
14 const archive = path.join(temporary, "lucide.tgz");
15 const packageUrl =
16 `https://registry.npmjs.org/lucide/-/lucide-${VERSION}.tgz`;
17
18 function run(command, args) {
19 const result = spawnSync(command, args, { stdio: "inherit" });
20 if (result.status !== 0) process.exit(result.status ?? 1);
21 }
22
23 function point(x, y) {
24 return { x, y };
25 }
26
27 function addLine(segments, from, to) {
28 if (Math.hypot(to.x - from.x, to.y - from.y) < 0.0001) return;
29 segments.push([from.x, from.y, to.x, to.y]);
30 }
31
32 function addCurve(segments, evaluate, steps = 8) {
33 let previous = evaluate(0);
34 for (let step = 1; step <= steps; step++) {
35 const next = evaluate(step / steps);
36 addLine(segments, previous, next);
37 previous = next;
38 }
39 }
40
41 function addArc(segments, from, rxValue, ryValue, rotation, large, sweep, to) {
42 let rx = Math.abs(rxValue);
43 let ry = Math.abs(ryValue);
44 if (rx === 0 || ry === 0 || (from.x === to.x && from.y === to.y)) {
45 addLine(segments, from, to);
46 return;
47 }
48
49 const phi = rotation * Math.PI / 180;
50 const cosPhi = Math.cos(phi);
51 const sinPhi = Math.sin(phi);
52 const halfX = (from.x - to.x) * 0.5;
53 const halfY = (from.y - to.y) * 0.5;
54 const xPrime = cosPhi * halfX + sinPhi * halfY;
55 const yPrime = -sinPhi * halfX + cosPhi * halfY;
56 const scale = xPrime * xPrime / (rx * rx) +
57 yPrime * yPrime / (ry * ry);
58 if (scale > 1) {
59 const root = Math.sqrt(scale);
60 rx *= root;
61 ry *= root;
62 }
63
64 const numerator = Math.max(
65 0,
66 rx * rx * ry * ry -
67 rx * rx * yPrime * yPrime -
68 ry * ry * xPrime * xPrime,
69 );
70 const denominator =
71 rx * rx * yPrime * yPrime + ry * ry * xPrime * xPrime;
72 const sign = large === sweep ? -1 : 1;
73 const factor = denominator === 0 ?
74 0 :
75 sign * Math.sqrt(numerator / denominator);
76 const centerPrimeX = factor * rx * yPrime / ry;
77 const centerPrimeY = factor * -ry * xPrime / rx;
78 const centerX =
79 cosPhi * centerPrimeX - sinPhi * centerPrimeY +
80 (from.x + to.x) * 0.5;
81 const centerY =
82 sinPhi * centerPrimeX + cosPhi * centerPrimeY +
83 (from.y + to.y) * 0.5;
84
85 const angle = (ux, uy, vx, vy) => {
86 const dot = ux * vx + uy * vy;
87 const cross = ux * vy - uy * vx;
88 return Math.atan2(cross, dot);
89 };
90 const startX = (xPrime - centerPrimeX) / rx;
91 const startY = (yPrime - centerPrimeY) / ry;
92 const endX = (-xPrime - centerPrimeX) / rx;
93 const endY = (-yPrime - centerPrimeY) / ry;
94 const startAngle = angle(1, 0, startX, startY);
95 let deltaAngle = angle(startX, startY, endX, endY);
96 if (!sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2;
97 if (sweep && deltaAngle < 0) deltaAngle += Math.PI * 2;
98 const steps = Math.max(4, Math.ceil(Math.abs(deltaAngle) / (Math.PI / 8)));
99
100 addCurve(segments, (amount) => {
101 const theta = startAngle + deltaAngle * amount;
102 return point(
103 centerX + cosPhi * rx * Math.cos(theta) -
104 sinPhi * ry * Math.sin(theta),
105 centerY + sinPhi * rx * Math.cos(theta) +
106 cosPhi * ry * Math.sin(theta),
107 );
108 }, steps);
109 }
110
111 function pathSegments(data) {
112 const tokens = data.match(/[a-zA-Z]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/gi) ?? [];
113 const arity = {
114 M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0,
115 };
116 const segments = [];
117 let cursor = point(0, 0);
118 let start = point(0, 0);
119 let control = null;
120 let command = null;
121 let index = 0;
122
123 const coordinate = (value, axis, relative) =>
124 Number(value) + (relative ? cursor[axis] : 0);
125
126 while (index < tokens.length) {
127 if (/^[a-zA-Z]$/.test(tokens[index])) command = tokens[index++];
128 if (!command) throw new Error(`Invalid path: ${data}`);
129 const upper = command.toUpperCase();
130 const relative = command !== upper;
131 if (upper === "Z") {
132 addLine(segments, cursor, start);
133 cursor = point(start.x, start.y);
134 control = null;
135 command = null;
136 continue;
137 }
138 const count = arity[upper];
139 const values = [];
140 for (let valueIndex = 0; valueIndex < count; valueIndex++) {
141 if (index >= tokens.length || /^[a-zA-Z]$/.test(tokens[index])) break;
142 if (upper === "A" &&
143 (valueIndex === 3 || valueIndex === 4) &&
144 /^[01]\d+$/.test(tokens[index])) {
145 values.push(Number(tokens[index][0]));
146 tokens[index] = tokens[index].slice(1);
147 } else {
148 values.push(Number(tokens[index++]));
149 }
150 }
151 if (values.length !== count) break;
152 const from = point(cursor.x, cursor.y);
153
154 if (upper === "M" || upper === "L" || upper === "T") {
155 const to = point(
156 coordinate(values[0], "x", relative),
157 coordinate(values[1], "y", relative),
158 );
159 if (upper === "M") {
160 start = point(to.x, to.y);
161 command = relative ? "l" : "L";
162 } else if (upper === "T") {
163 const reflected = control ?
164 point(2 * from.x - control.x, 2 * from.y - control.y) :
165 from;
166 addCurve(segments, (amount) => {
167 const inverse = 1 - amount;
168 return point(
169 inverse * inverse * from.x +
170 2 * inverse * amount * reflected.x +
171 amount * amount * to.x,
172 inverse * inverse * from.y +
173 2 * inverse * amount * reflected.y +
174 amount * amount * to.y,
175 );
176 });
177 control = reflected;
178 } else {
179 addLine(segments, from, to);
180 control = null;
181 }
182 cursor = to;
183 continue;
184 }
185
186 if (upper === "H") {
187 cursor = point(coordinate(values[0], "x", relative), cursor.y);
188 addLine(segments, from, cursor);
189 control = null;
190 } else if (upper === "V") {
191 cursor = point(cursor.x, coordinate(values[0], "y", relative));
192 addLine(segments, from, cursor);
193 control = null;
194 } else if (upper === "C") {
195 const first = point(
196 coordinate(values[0], "x", relative),
197 coordinate(values[1], "y", relative),
198 );
199 const second = point(
200 coordinate(values[2], "x", relative),
201 coordinate(values[3], "y", relative),
202 );
203 const to = point(
204 coordinate(values[4], "x", relative),
205 coordinate(values[5], "y", relative),
206 );
207 addCurve(segments, (amount) => {
208 const inverse = 1 - amount;
209 return point(
210 inverse ** 3 * from.x +
211 3 * inverse * inverse * amount * first.x +
212 3 * inverse * amount * amount * second.x +
213 amount ** 3 * to.x,
214 inverse ** 3 * from.y +
215 3 * inverse * inverse * amount * first.y +
216 3 * inverse * amount * amount * second.y +
217 amount ** 3 * to.y,
218 );
219 });
220 cursor = to;
221 control = second;
222 } else if (upper === "S") {
223 const first = control ?
224 point(2 * from.x - control.x, 2 * from.y - control.y) :
225 from;
226 const second = point(
227 coordinate(values[0], "x", relative),
228 coordinate(values[1], "y", relative),
229 );
230 const to = point(
231 coordinate(values[2], "x", relative),
232 coordinate(values[3], "y", relative),
233 );
234 addCurve(segments, (amount) => {
235 const inverse = 1 - amount;
236 return point(
237 inverse ** 3 * from.x +
238 3 * inverse * inverse * amount * first.x +
239 3 * inverse * amount * amount * second.x +
240 amount ** 3 * to.x,
241 inverse ** 3 * from.y +
242 3 * inverse * inverse * amount * first.y +
243 3 * inverse * amount * amount * second.y +
244 amount ** 3 * to.y,
245 );
246 });
247 cursor = to;
248 control = second;
249 } else if (upper === "Q") {
250 const nextControl = point(
251 coordinate(values[0], "x", relative),
252 coordinate(values[1], "y", relative),
253 );
254 const to = point(
255 coordinate(values[2], "x", relative),
256 coordinate(values[3], "y", relative),
257 );
258 addCurve(segments, (amount) => {
259 const inverse = 1 - amount;
260 return point(
261 inverse * inverse * from.x +
262 2 * inverse * amount * nextControl.x +
263 amount * amount * to.x,
264 inverse * inverse * from.y +
265 2 * inverse * amount * nextControl.y +
266 amount * amount * to.y,
267 );
268 });
269 cursor = to;
270 control = nextControl;
271 } else if (upper === "A") {
272 const to = point(
273 coordinate(values[5], "x", relative),
274 coordinate(values[6], "y", relative),
275 );
276 addArc(
277 segments,
278 from,
279 values[0],
280 values[1],
281 values[2],
282 values[3] !== 0,
283 values[4] !== 0,
284 to,
285 );
286 cursor = to;
287 control = null;
288 }
289 }
290 return segments;
291 }
292
293 function nodeSegments([tag, attributes]) {
294 const number = (name, fallback = 0) =>
295 Number(attributes[name] ?? fallback);
296 const segments = [];
297 if (tag === "path") return pathSegments(attributes.d);
298 if (tag === "line") {
299 addLine(
300 segments,
301 point(number("x1"), number("y1")),
302 point(number("x2"), number("y2")),
303 );
304 } else if (tag === "polyline" || tag === "polygon") {
305 const values = attributes.points.trim().split(/[ ,]+/).map(Number);
306 const points = [];
307 for (let index = 0; index < values.length; index += 2) {
308 points.push(point(values[index], values[index + 1]));
309 }
310 for (let index = 1; index < points.length; index++) {
311 addLine(segments, points[index - 1], points[index]);
312 }
313 if (tag === "polygon" && points.length > 1) {
314 addLine(segments, points.at(-1), points[0]);
315 }
316 } else if (tag === "circle" || tag === "ellipse") {
317 const center = point(number("cx"), number("cy"));
318 const rx = tag === "circle" ? number("r") : number("rx");
319 const ry = tag === "circle" ? number("r") : number("ry");
320 addCurve(segments, (amount) => {
321 const angle = amount * Math.PI * 2;
322 return point(
323 center.x + Math.cos(angle) * rx,
324 center.y + Math.sin(angle) * ry,
325 );
326 }, 24);
327 } else if (tag === "rect") {
328 const x = number("x");
329 const y = number("y");
330 const width = number("width");
331 const height = number("height");
332 const radius = Math.min(number("rx"), width * 0.5, height * 0.5);
333 if (radius <= 0) {
334 const corners = [
335 point(x, y),
336 point(x + width, y),
337 point(x + width, y + height),
338 point(x, y + height),
339 ];
340 for (let index = 0; index < 4; index++) {
341 addLine(segments, corners[index], corners[(index + 1) % 4]);
342 }
343 } else {
344 const centers = [
345 point(x + width - radius, y + radius),
346 point(x + width - radius, y + height - radius),
347 point(x + radius, y + height - radius),
348 point(x + radius, y + radius),
349 ];
350 const starts = [-Math.PI / 2, 0, Math.PI / 2, Math.PI];
351 let previous = point(x + radius, y);
352 for (let corner = 0; corner < 4; corner++) {
353 const lineEnd = corner === 0 ?
354 point(x + width - radius, y) :
355 corner === 1 ?
356 point(x + width, y + height - radius) :
357 corner === 2 ?
358 point(x + radius, y + height) :
359 point(x, y + radius);
360 addLine(segments, previous, lineEnd);
361 addCurve(segments, (amount) => {
362 const angle = starts[corner] + amount * Math.PI / 2;
363 return point(
364 centers[corner].x + Math.cos(angle) * radius,
365 centers[corner].y + Math.sin(angle) * radius,
366 );
367 }, 4);
368 previous = corner === 0 ?
369 point(x + width, y + radius) :
370 corner === 1 ?
371 point(x + width - radius, y + height) :
372 corner === 2 ?
373 point(x, y + height - radius) :
374 point(x + radius, y);
375 }
376 }
377 }
378 return segments;
379 }
380
381 try {
382 run("curl", ["-fsSL", packageUrl, "-o", archive]);
383 run("tar", ["-xzf", archive, "-C", temporary]);
384 const iconsDirectory = path.join(temporary, "package/dist/esm/icons");
385 const files = fs.readdirSync(iconsDirectory)
386 .filter((name) => name.endsWith(".mjs"))
387 .sort();
388 const icons = [];
389 const allSegments = [];
390 for (const file of files) {
391 const icon = (await import(
392 `${pathToFileURL(path.join(iconsDirectory, file)).href}?generated`
393 )).default;
394 const firstSegment = allSegments.length;
395 for (const node of icon) {
396 const generated = nodeSegments(node);
397 if (generated.some((segment) => segment.some((value) => !Number.isFinite(value)))) {
398 throw new Error(`Invalid geometry in ${file}: ${JSON.stringify(node)}`);
399 }
400 allSegments.push(...generated);
401 }
402 icons.push({
403 name: file.slice(0, -4),
404 firstSegment,
405 segmentCount: allSegments.length - firstSegment,
406 });
407 }
408
409 const format = (value) => {
410 const rounded = Math.abs(value) < 0.0005 ? 0 : value;
411 const text = String(Number(rounded.toFixed(3)));
412 return `${text.includes(".") ? text : `${text}.0`}f`;
413 };
414 const lines = [
415 "/* Generated from Lucide 1.31.0. See assets/LUCIDE_LICENSE.txt. */",
416 "#ifndef INFINITE_CANVAS_GENERATED_LUCIDE_DATA_H",
417 "#define INFINITE_CANVAS_GENERATED_LUCIDE_DATA_H",
418 "",
419 "typedef struct {",
420 " float x1;",
421 " float y1;",
422 " float x2;",
423 " float y2;",
424 "} Canvas_Lucide_Segment;",
425 "",
426 "typedef struct {",
427 " const char *p_name;",
428 " uint32 first_segment;",
429 " uint16 segment_count;",
430 "} Canvas_Lucide_Icon;",
431 "",
432 "static const Canvas_Lucide_Segment CANVAS_LUCIDE_SEGMENTS[] = {",
433 ];
434 for (const segment of allSegments) {
435 lines.push(` {${segment.map(format).join(", ")}},`);
436 }
437 lines.push(
438 "};",
439 "",
440 "static const Canvas_Lucide_Icon CANVAS_LUCIDE_ICONS[] = {",
441 );
442 for (const icon of icons) {
443 lines.push(
444 ` {"${icon.name}", ${icon.firstSegment}, ${icon.segmentCount}},`,
445 );
446 }
447 lines.push(
448 "};",
449 "",
450 `#define CANVAS_LUCIDE_ICON_COUNT ${icons.length}`,
451 "",
452 "#endif",
453 "",
454 );
455 fs.mkdirSync(path.dirname(output), { recursive: true });
456 fs.writeFileSync(output, lines.join("\n"));
457 console.log(
458 `Generated ${icons.length} icons and ${allSegments.length} segments in ${output}`,
459 );
460 } finally {
461 fs.rmSync(temporary, { recursive: true, force: true });
462 }