glauber/sois-controller-rev-c

Source code for the SOIS Controller PCB, featuring an Adafruit Feather microcontroller, two EC11 rotary encoders, multiple push buttons and a slide switch. View more at glauber.org

Version
0.1.3
License
UNLICENSED
Stars
0

src/silkArt.tsx

import silk from "./silk.json"
import { bx, by } from "./geometry"

/**
 * The silkscreen, from the same `silk.json` the KiCad generator uses — Simple
 * LL glyph outlines and the SOIS mark, so neither board needs the font
 * installed. `src/silk.json` is a symlink; it has to live inside the project
 * directory or `tsci dev` cannot resolve it.
 *
 * tscircuit has no filled polygon on the silkscreen layer — `silkscreenpath`
 * carries a stroke width and no fill — so each glyph is filled here: the
 * outline is stroked, and the interior hatched with scanlines clipped by the
 * even-odd rule at half the stroke width. Prints solid, at the cost of some
 * 3600 paths standing in for what KiCad writes as one filled polygon.
 */

const STROKE = 0.15
/** Half the stroke, so neighbouring lines overlap into a solid area. */
const PITCH = STROKE / 2

type Poly = Array<[number, number]>
type Entry = { size: number; bbox: [number, number, number, number]; polys: Poly[] }
const SILK = silk as unknown as Record<string, Entry>

/** Design-space points (KiCad frame, +y down) for one silk.json entry. */
const place = (
  name: string,
  cx: number,
  cy: number,
  rot = 0,
  anchor: "c" | "tl" = "c",
): Poly[] => {
  const e = SILK[name]
  if (!e) throw new Error(`silk.json has no entry ${JSON.stringify(name)}`)
  const [x0, y0, x1, y1] = e.bbox
  const [mx, my] = anchor === "tl" ? [x0, y0] : [(x0 + x1) / 2, (y0 + y1) / 2]
  const th = (rot * Math.PI) / 180
  return e.polys.map((poly) =>
    poly.map(([px, py]): [number, number] => {
      const dx = (px - mx) * e.size
      const dy = (py - my) * e.size
      return [
        cx + dx * Math.cos(th) + dy * Math.sin(th),
        cy + -dx * Math.sin(th) + dy * Math.cos(th),
      ]
    }),
  )
}

/** Horizontal spans of a polygon at height y, by the even-odd rule. */
const spansAt = (poly: Poly, y: number): Array<[number, number]> => {
  const xs: number[] = []
  for (let i = 0; i < poly.length; i++) {
    const [x1, y1] = poly[i]
    const [x2, y2] = poly[(i + 1) % poly.length]
    if (y1 === y2) continue
    if (y >= Math.min(y1, y2) && y < Math.max(y1, y2)) {
      xs.push(x1 + ((y - y1) / (y2 - y1)) * (x2 - x1))
    }
  }
  xs.sort((a, b) => a - b)
  const out: Array<[number, number]> = []
  for (let i = 0; i + 1 < xs.length; i += 2) out.push([xs[i], xs[i + 1]])
  return out
}

/** Outline + scanline fill for one polygon, in design coordinates. */
const fillPoly = (poly: Poly, key: string) => {
  const els = [
    <silkscreenpath
      key={`${key}o`}
      strokeWidth={`${STROKE}mm`}
      route={[...poly, poly[0]].map(([x, y]) => ({ x: bx(x), y: by(y) }))}
    />,
  ]
  const ys = poly.map((p) => p[1])
  const top = Math.min(...ys) + PITCH / 2
  const bottom = Math.max(...ys)
  let n = 0
  for (let y = top; y < bottom; y += PITCH) {
    for (const [a, b] of spansAt(poly, y)) {
      if (b - a < STROKE / 4) continue
      els.push(
        <silkscreenpath
          key={`${key}f${n++}`}
          strokeWidth={`${STROKE}mm`}
          route={[
            { x: bx(a), y: by(y) },
            { x: bx(b), y: by(y) },
          ]}
        />,
      )
    }
  }
  return els
}

export const silkArt = (
  name: string,
  cx: number,
  cy: number,
  rot = 0,
  anchor: "c" | "tl" = "c",
) => place(name, cx, cy, rot, anchor).flatMap((poly, i) => fillPoly(poly, `${name}${cx}${cy}${i}`))