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}`))