Ctrl K
ComunidadGuías…

scripts/mascot-bodies/geometry.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Flattening, bounds and the fit transform into the mascot's face box. Loaded
 * by `node --experimental-strip-types`, so only erasable type syntax.
 */

import { FACE_BOX } from "../../src/components/cursor-face-data.ts"

export interface Bounds {
  minX: number
  minY: number
  maxX: number
  maxY: number
}

type Point = [number, number]


/** Scans numbers exactly like the iOS parser: an optional `-`, digits, an
 * optional `.` and digits. No exponent: a stray `e` would read as a command. */
function tokenizeNumbers(s: string): number[] {
  const re = /-?\d+(?:\.\d+)?/g
  const out: number[] = []
  let m: RegExpExecArray | null
  while ((m = re.exec(s)) !== null) out.push(Number(m[0]))
  return out
}

/** Splits `d` into a sequence of `{ command, args }` runs on M / C / Z. */
function tokenizeCommands(d: string): { command: string; args: number[] }[] {
  const commands: { command: string; args: number[] }[] = []
  const re = /([MCZ])([^MCZ]*)/g
  let m: RegExpExecArray | null
  while ((m = re.exec(d)) !== null) {
    const command = m[1]
    const args = tokenizeNumbers(m[2])
    commands.push({ command, args })
  }
  return commands
}


/** Perpendicular distance from `p` to the line through `a`/`b`. */
function pointLineDistance(p: Point, a: Point, b: Point): number {
  const dx = b[0] - a[0]
  const dy = b[1] - a[1]
  const len = Math.hypot(dx, dy)
  if (len < 1e-12) return Math.hypot(p[0] - a[0], p[1] - a[1])
  // |cross product| / |base|
  return Math.abs(dx * (a[1] - p[1]) - dy * (a[0] - p[0])) / len
}

/**
 * Adaptively subdivides a cubic until the interior points are all within
 * `tolerance` of the chord from p0 to p3, appending sampled points
 * (excluding p0, which the caller already has) to `out`.
 */
function subdivideCubic(
  p0: Point, p1: Point, p2: Point, p3: Point, tolerance: number, out: Point[], depth = 0
): void {
  // Flatness test: distance of both control points from the chord.
  const d1 = pointLineDistance(p1, p0, p3)
  const d2 = pointLineDistance(p2, p0, p3)
  if ((d1 + d2) < tolerance || depth > 24) {
    out.push(p3)
    return
  }
  // De Casteljau split at t = 0.5.
  const p01: Point = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2]
  const p12: Point = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]
  const p23: Point = [(p2[0] + p3[0]) / 2, (p2[1] + p3[1]) / 2]
  const p012: Point = [(p01[0] + p12[0]) / 2, (p01[1] + p12[1]) / 2]
  const p123: Point = [(p12[0] + p23[0]) / 2, (p12[1] + p23[1]) / 2]
  const mid: Point = [(p012[0] + p123[0]) / 2, (p012[1] + p123[1]) / 2]
  subdivideCubic(p0, p01, p012, mid, tolerance, out, depth + 1)
  subdivideCubic(mid, p123, p23, p3, tolerance, out, depth + 1)
}

/**
 * Flattens absolute-cubic path data (`M`, `C`, `Z` only) into one polyline
 * per subpath, sampling each cubic adaptively until the chord deviates from
 * the true curve by less than `tolerance` artwork units.
 */
export function flatten(d: string, tolerance = 0.05): Point[][] {
  const commands = tokenizeCommands(d)
  const polylines: Point[][] = []
  let current: Point[] = []
  let at: Point = [0, 0]
  let start: Point = [0, 0]

  for (const { command, args } of commands) {
    if (command === "M") {
      if (current.length > 0) polylines.push(current)
      const x = args[0]
      const y = args[1]
      at = [x, y]
      start = [x, y]
      current = [at]
    } else if (command === "C") {
      for (let i = 0; i + 5 < args.length; i += 6) {
        const p1: Point = [args[i], args[i + 1]]
        const p2: Point = [args[i + 2], args[i + 3]]
        const p3: Point = [args[i + 4], args[i + 5]]
        subdivideCubic(at, p1, p2, p3, tolerance, current)
        at = p3
      }
    } else if (command === "Z") {
      // Close back to the subpath's start if not already there.
      if (Math.abs(at[0] - start[0]) > 1e-9 || Math.abs(at[1] - start[1]) > 1e-9) {
        current.push(start)
        at = start
      }
    }
  }
  if (current.length > 0) polylines.push(current)
  return polylines
}


export function boundsOf(polylines: Point[][]): Bounds {
  let minX = Infinity
  let minY = Infinity
  let maxX = -Infinity
  let maxY = -Infinity
  for (const line of polylines) {
    for (const [x, y] of line) {
      if (x < minX) minX = x
      if (y < minY) minY = y
      if (x > maxX) maxX = x
      if (y > maxY) maxY = y
    }
  }
  return { minX, minY, maxX, maxY }
}


/** Rounds to `digits` decimal places (not fixed-point string formatting). */
function round(n: number, digits: number): number {
  const f = 10 ** digits
  return Math.round(n * f) / f
}

/**
 * Fills FACE_BOX on the tighter axis and centres the other, as Blob Studio's
 * `measureFit`. Returns the transform string the renderer parses and its rounded
 * components.
 */
export function fitTransform(bounds: Bounds): { transform: string; scale: number; tx: number; ty: number } {
  const width = bounds.maxX - bounds.minX
  const height = bounds.maxY - bounds.minY
  const rawScale = FACE_BOX / Math.max(width, height)
  const scale = round(rawScale, 6)
  const tx = round(-bounds.minX * scale + (FACE_BOX - width * scale) / 2, 4)
  const ty = round(-bounds.minY * scale + (FACE_BOX - height * scale) / 2, 4)
  const transform = `translate(${tx} ${ty}) scale(${scale})`
  return { transform, scale, tx, ty }
}

/** Maps every point through `[x * scale + tx, y * scale + ty]`. */
export function applyFit(
  polylines: Point[][], fit: { scale: number; tx: number; ty: number }
): Point[][] {
  return polylines.map(line => line.map(([x, y]): Point => [x * fit.scale + fit.tx, y * fit.scale + fit.ty]))
}