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 });
+}