Mercurial
view infinite_canvas/tools/generate_lucide_data.mjs @ 280:49e9e591c9bb
Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 19:14:53 -0700 |
| parents | 8d560f50ed4c |
| children |
line wrap: on
line source
#!/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 }); }