Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/tools/generate_lucide_data.mjs Mon Aug 17 22:16:14 2026 -0700 @@ -0,0 +1,462 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { pathToFileURL } from "node:url"; + +const VERSION = "1.31.0"; +const output = path.resolve( + process.argv[2] ?? "infinite_canvas/generated/lucide_data.h", +); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "lucide-data-")); +const archive = path.join(temporary, "lucide.tgz"); +const packageUrl = + `https://registry.npmjs.org/lucide/-/lucide-${VERSION}.tgz`; + +function run(command, args) { + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function point(x, y) { + return { x, y }; +} + +function addLine(segments, from, to) { + if (Math.hypot(to.x - from.x, to.y - from.y) < 0.0001) return; + segments.push([from.x, from.y, to.x, to.y]); +} + +function addCurve(segments, evaluate, steps = 8) { + let previous = evaluate(0); + for (let step = 1; step <= steps; step++) { + const next = evaluate(step / steps); + addLine(segments, previous, next); + previous = next; + } +} + +function addArc(segments, from, rxValue, ryValue, rotation, large, sweep, to) { + let rx = Math.abs(rxValue); + let ry = Math.abs(ryValue); + if (rx === 0 || ry === 0 || (from.x === to.x && from.y === to.y)) { + addLine(segments, from, to); + return; + } + + const phi = rotation * Math.PI / 180; + const cosPhi = Math.cos(phi); + const sinPhi = Math.sin(phi); + const halfX = (from.x - to.x) * 0.5; + const halfY = (from.y - to.y) * 0.5; + const xPrime = cosPhi * halfX + sinPhi * halfY; + const yPrime = -sinPhi * halfX + cosPhi * halfY; + const scale = xPrime * xPrime / (rx * rx) + + yPrime * yPrime / (ry * ry); + if (scale > 1) { + const root = Math.sqrt(scale); + rx *= root; + ry *= root; + } + + const numerator = Math.max( + 0, + rx * rx * ry * ry - + rx * rx * yPrime * yPrime - + ry * ry * xPrime * xPrime, + ); + const denominator = + rx * rx * yPrime * yPrime + ry * ry * xPrime * xPrime; + const sign = large === sweep ? -1 : 1; + const factor = denominator === 0 ? + 0 : + sign * Math.sqrt(numerator / denominator); + const centerPrimeX = factor * rx * yPrime / ry; + const centerPrimeY = factor * -ry * xPrime / rx; + const centerX = + cosPhi * centerPrimeX - sinPhi * centerPrimeY + + (from.x + to.x) * 0.5; + const centerY = + sinPhi * centerPrimeX + cosPhi * centerPrimeY + + (from.y + to.y) * 0.5; + + const angle = (ux, uy, vx, vy) => { + const dot = ux * vx + uy * vy; + const cross = ux * vy - uy * vx; + return Math.atan2(cross, dot); + }; + const startX = (xPrime - centerPrimeX) / rx; + const startY = (yPrime - centerPrimeY) / ry; + const endX = (-xPrime - centerPrimeX) / rx; + const endY = (-yPrime - centerPrimeY) / ry; + const startAngle = angle(1, 0, startX, startY); + let deltaAngle = angle(startX, startY, endX, endY); + if (!sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2; + if (sweep && deltaAngle < 0) deltaAngle += Math.PI * 2; + const steps = Math.max(4, Math.ceil(Math.abs(deltaAngle) / (Math.PI / 8))); + + addCurve(segments, (amount) => { + const theta = startAngle + deltaAngle * amount; + return point( + centerX + cosPhi * rx * Math.cos(theta) - + sinPhi * ry * Math.sin(theta), + centerY + sinPhi * rx * Math.cos(theta) + + cosPhi * ry * Math.sin(theta), + ); + }, steps); +} + +function pathSegments(data) { + const tokens = data.match(/[a-zA-Z]|[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?/gi) ?? []; + const arity = { + M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0, + }; + const segments = []; + let cursor = point(0, 0); + let start = point(0, 0); + let control = null; + let command = null; + let index = 0; + + const coordinate = (value, axis, relative) => + Number(value) + (relative ? cursor[axis] : 0); + + while (index < tokens.length) { + if (/^[a-zA-Z]$/.test(tokens[index])) command = tokens[index++]; + if (!command) throw new Error(`Invalid path: ${data}`); + const upper = command.toUpperCase(); + const relative = command !== upper; + if (upper === "Z") { + addLine(segments, cursor, start); + cursor = point(start.x, start.y); + control = null; + command = null; + continue; + } + const count = arity[upper]; + const values = []; + for (let valueIndex = 0; valueIndex < count; valueIndex++) { + if (index >= tokens.length || /^[a-zA-Z]$/.test(tokens[index])) break; + if (upper === "A" && + (valueIndex === 3 || valueIndex === 4) && + /^[01]\d+$/.test(tokens[index])) { + values.push(Number(tokens[index][0])); + tokens[index] = tokens[index].slice(1); + } else { + values.push(Number(tokens[index++])); + } + } + if (values.length !== count) break; + const from = point(cursor.x, cursor.y); + + if (upper === "M" || upper === "L" || upper === "T") { + const to = point( + coordinate(values[0], "x", relative), + coordinate(values[1], "y", relative), + ); + if (upper === "M") { + start = point(to.x, to.y); + command = relative ? "l" : "L"; + } else if (upper === "T") { + const reflected = control ? + point(2 * from.x - control.x, 2 * from.y - control.y) : + from; + addCurve(segments, (amount) => { + const inverse = 1 - amount; + return point( + inverse * inverse * from.x + + 2 * inverse * amount * reflected.x + + amount * amount * to.x, + inverse * inverse * from.y + + 2 * inverse * amount * reflected.y + + amount * amount * to.y, + ); + }); + control = reflected; + } else { + addLine(segments, from, to); + control = null; + } + cursor = to; + continue; + } + + if (upper === "H") { + cursor = point(coordinate(values[0], "x", relative), cursor.y); + addLine(segments, from, cursor); + control = null; + } else if (upper === "V") { + cursor = point(cursor.x, coordinate(values[0], "y", relative)); + addLine(segments, from, cursor); + control = null; + } else if (upper === "C") { + const first = point( + coordinate(values[0], "x", relative), + coordinate(values[1], "y", relative), + ); + const second = point( + coordinate(values[2], "x", relative), + coordinate(values[3], "y", relative), + ); + const to = point( + coordinate(values[4], "x", relative), + coordinate(values[5], "y", relative), + ); + addCurve(segments, (amount) => { + const inverse = 1 - amount; + return point( + inverse ** 3 * from.x + + 3 * inverse * inverse * amount * first.x + + 3 * inverse * amount * amount * second.x + + amount ** 3 * to.x, + inverse ** 3 * from.y + + 3 * inverse * inverse * amount * first.y + + 3 * inverse * amount * amount * second.y + + amount ** 3 * to.y, + ); + }); + cursor = to; + control = second; + } else if (upper === "S") { + const first = control ? + point(2 * from.x - control.x, 2 * from.y - control.y) : + from; + const second = point( + coordinate(values[0], "x", relative), + coordinate(values[1], "y", relative), + ); + const to = point( + coordinate(values[2], "x", relative), + coordinate(values[3], "y", relative), + ); + addCurve(segments, (amount) => { + const inverse = 1 - amount; + return point( + inverse ** 3 * from.x + + 3 * inverse * inverse * amount * first.x + + 3 * inverse * amount * amount * second.x + + amount ** 3 * to.x, + inverse ** 3 * from.y + + 3 * inverse * inverse * amount * first.y + + 3 * inverse * amount * amount * second.y + + amount ** 3 * to.y, + ); + }); + cursor = to; + control = second; + } else if (upper === "Q") { + const nextControl = point( + coordinate(values[0], "x", relative), + coordinate(values[1], "y", relative), + ); + const to = point( + coordinate(values[2], "x", relative), + coordinate(values[3], "y", relative), + ); + addCurve(segments, (amount) => { + const inverse = 1 - amount; + return point( + inverse * inverse * from.x + + 2 * inverse * amount * nextControl.x + + amount * amount * to.x, + inverse * inverse * from.y + + 2 * inverse * amount * nextControl.y + + amount * amount * to.y, + ); + }); + cursor = to; + control = nextControl; + } else if (upper === "A") { + const to = point( + coordinate(values[5], "x", relative), + coordinate(values[6], "y", relative), + ); + addArc( + segments, + from, + values[0], + values[1], + values[2], + values[3] !== 0, + values[4] !== 0, + to, + ); + cursor = to; + control = null; + } + } + return segments; +} + +function nodeSegments([tag, attributes]) { + const number = (name, fallback = 0) => + Number(attributes[name] ?? fallback); + const segments = []; + if (tag === "path") return pathSegments(attributes.d); + if (tag === "line") { + addLine( + segments, + point(number("x1"), number("y1")), + point(number("x2"), number("y2")), + ); + } else if (tag === "polyline" || tag === "polygon") { + const values = attributes.points.trim().split(/[ ,]+/).map(Number); + const points = []; + for (let index = 0; index < values.length; index += 2) { + points.push(point(values[index], values[index + 1])); + } + for (let index = 1; index < points.length; index++) { + addLine(segments, points[index - 1], points[index]); + } + if (tag === "polygon" && points.length > 1) { + addLine(segments, points.at(-1), points[0]); + } + } else if (tag === "circle" || tag === "ellipse") { + const center = point(number("cx"), number("cy")); + const rx = tag === "circle" ? number("r") : number("rx"); + const ry = tag === "circle" ? number("r") : number("ry"); + addCurve(segments, (amount) => { + const angle = amount * Math.PI * 2; + return point( + center.x + Math.cos(angle) * rx, + center.y + Math.sin(angle) * ry, + ); + }, 24); + } else if (tag === "rect") { + const x = number("x"); + const y = number("y"); + const width = number("width"); + const height = number("height"); + const radius = Math.min(number("rx"), width * 0.5, height * 0.5); + if (radius <= 0) { + const corners = [ + point(x, y), + point(x + width, y), + point(x + width, y + height), + point(x, y + height), + ]; + for (let index = 0; index < 4; index++) { + addLine(segments, corners[index], corners[(index + 1) % 4]); + } + } else { + const centers = [ + point(x + width - radius, y + radius), + point(x + width - radius, y + height - radius), + point(x + radius, y + height - radius), + point(x + radius, y + radius), + ]; + const starts = [-Math.PI / 2, 0, Math.PI / 2, Math.PI]; + let previous = point(x + radius, y); + for (let corner = 0; corner < 4; corner++) { + const lineEnd = corner === 0 ? + point(x + width - radius, y) : + corner === 1 ? + point(x + width, y + height - radius) : + corner === 2 ? + point(x + radius, y + height) : + point(x, y + radius); + addLine(segments, previous, lineEnd); + addCurve(segments, (amount) => { + const angle = starts[corner] + amount * Math.PI / 2; + return point( + centers[corner].x + Math.cos(angle) * radius, + centers[corner].y + Math.sin(angle) * radius, + ); + }, 4); + previous = corner === 0 ? + point(x + width, y + radius) : + corner === 1 ? + point(x + width - radius, y + height) : + corner === 2 ? + point(x, y + height - radius) : + point(x + radius, y); + } + } + } + return segments; +} + +try { + run("curl", ["-fsSL", packageUrl, "-o", archive]); + run("tar", ["-xzf", archive, "-C", temporary]); + const iconsDirectory = path.join(temporary, "package/dist/esm/icons"); + const files = fs.readdirSync(iconsDirectory) + .filter((name) => name.endsWith(".mjs")) + .sort(); + const icons = []; + const allSegments = []; + for (const file of files) { + const icon = (await import( + `${pathToFileURL(path.join(iconsDirectory, file)).href}?generated` + )).default; + const firstSegment = allSegments.length; + for (const node of icon) { + const generated = nodeSegments(node); + if (generated.some((segment) => segment.some((value) => !Number.isFinite(value)))) { + throw new Error(`Invalid geometry in ${file}: ${JSON.stringify(node)}`); + } + allSegments.push(...generated); + } + icons.push({ + name: file.slice(0, -4), + firstSegment, + segmentCount: allSegments.length - firstSegment, + }); + } + + const format = (value) => { + const rounded = Math.abs(value) < 0.0005 ? 0 : value; + const text = String(Number(rounded.toFixed(3))); + return `${text.includes(".") ? text : `${text}.0`}f`; + }; + const lines = [ + "/* Generated from Lucide 1.31.0. See assets/LUCIDE_LICENSE.txt. */", + "#ifndef INFINITE_CANVAS_GENERATED_LUCIDE_DATA_H", + "#define INFINITE_CANVAS_GENERATED_LUCIDE_DATA_H", + "", + "typedef struct {", + " float x1;", + " float y1;", + " float x2;", + " float y2;", + "} Canvas_Lucide_Segment;", + "", + "typedef struct {", + " const char *p_name;", + " uint32 first_segment;", + " uint16 segment_count;", + "} Canvas_Lucide_Icon;", + "", + "static const Canvas_Lucide_Segment CANVAS_LUCIDE_SEGMENTS[] = {", + ]; + for (const segment of allSegments) { + lines.push(` {${segment.map(format).join(", ")}},`); + } + lines.push( + "};", + "", + "static const Canvas_Lucide_Icon CANVAS_LUCIDE_ICONS[] = {", + ); + for (const icon of icons) { + lines.push( + ` {"${icon.name}", ${icon.firstSegment}, ${icon.segmentCount}},`, + ); + } + lines.push( + "};", + "", + `#define CANVAS_LUCIDE_ICON_COUNT ${icons.length}`, + "", + "#endif", + "", + ); + fs.mkdirSync(path.dirname(output), { recursive: true }); + fs.writeFileSync(output, lines.join("\n")); + console.log( + `Generated ${icons.length} icons and ${allSegments.length} segments in ${output}`, + ); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +}