view infinite_canvas/tools/generate_lucide_data.mjs @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -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 });
}