Mercurial
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 } |