tscircuit/sparkfun-boards
This code defines a detailed PCB schematic and physical footprint layout for various embedded hardware modules, mapping electrical pins, mechanical mounting holes, and silk-screen annotations for manufacturing and assembly.
- Version
- 0.0.32
- License
- unset
- Stars
- 3
util/OutlineBuilder.ts
export interface Point {
x: number
y: number
}
type TurnDirection = "cw" | "ccw"
interface CornerOptions {
radius: number
turn: TurnDirection
}
interface ArcOptions {
radius: number
sweep?: boolean // true = ccw, false = cw
segments?: number // number of interpolation points
}
export class OutlineBuilder {
private points: Point[] = []
private currentX: number
private currentY: number
private lastDirection: "right" | "left" | "up" | "down" | null = null
constructor(startX: number, startY: number) {
this.currentX = startX
this.currentY = startY
this.points.push({ x: startX, y: startY })
}
private updateDirection(x: number, y: number): void {
if (x > this.currentX) this.lastDirection = "right"
else if (x < this.currentX) this.lastDirection = "left"
else if (y > this.currentY) this.lastDirection = "down"
else if (y < this.currentY) this.lastDirection = "up"
}
lineTo(x: number, y: number): this {
if (x !== this.currentX || y !== this.currentY) {
this.updateDirection(x, y)
this.currentX = x
this.currentY = y
this.points.push({ x, y })
}
return this
}
// helper: convert cardinal direction to unit vector
private dirToUnit(dir: "right" | "left" | "up" | "down"): Point {
switch (dir) {
case "right":
return { x: 1, y: 0 }
case "left":
return { x: -1, y: 0 }
case "down":
return { x: 0, y: 1 }
case "up":
return { x: 0, y: -1 }
}
}
// helper: compute outgoing unit & direction given lastDirection and turn
private outgoingFor(
last: "right" | "left" | "up" | "down",
turn: TurnDirection,
): { unit: Point; dir: "right" | "left" | "up" | "down" } {
switch (last) {
case "right":
return turn === "cw"
? { unit: { x: 0, y: 1 }, dir: "down" }
: { unit: { x: 0, y: -1 }, dir: "up" }
case "left":
return turn === "cw"
? { unit: { x: 0, y: -1 }, dir: "up" }
: { unit: { x: 0, y: 1 }, dir: "down" }
case "down":
return turn === "cw"
? { unit: { x: -1, y: 0 }, dir: "left" }
: { unit: { x: 1, y: 0 }, dir: "right" }
case "up":
return turn === "cw"
? { unit: { x: 1, y: 0 }, dir: "right" }
: { unit: { x: -1, y: 0 }, dir: "left" }
}
}
corner({ radius, turn }: CornerOptions): this {
if (this.points.length < 2) {
throw new Error("At least two points are required to create a corner")
}
if (!this.lastDirection) {
throw new Error(
"Cannot create a corner without a known incoming direction",
)
}
const ix = this.currentX
const iy = this.currentY
const prevIndex = this.points.length - 2
const prevPoint = this.points[prevIndex]
if (!prevPoint) {
throw new Error("At least two points are required to create a corner")
}
this.points.pop()
const incomingUnit = this.dirToUnit(this.lastDirection)
const { unit: outgoingUnit, dir: outgoingDir } = this.outgoingFor(
this.lastDirection,
turn,
)
const dxPrev = ix - prevPoint.x
const dyPrev = iy - prevPoint.y
const distPrev = Math.hypot(dxPrev, dyPrev)
const r = Math.min(radius, distPrev)
const startX = ix - incomingUnit.x * r
const startY = iy - incomingUnit.y * r
const endX = ix + outgoingUnit.x * r
const endY = iy + outgoingUnit.y * r
const inVec = { x: -incomingUnit.x, y: -incomingUnit.y }
const outVec = { x: outgoingUnit.x, y: outgoingUnit.y }
const bisector = { x: inVec.x + outVec.x, y: inVec.y + outVec.y }
const bisLen = Math.hypot(bisector.x, bisector.y)
let dot = inVec.x * outVec.x + inVec.y * outVec.y
dot = Math.max(-1, Math.min(1, dot))
const angle = Math.acos(dot)
if (bisLen < 1e-9 || Math.abs(Math.sin(angle / 2)) < 1e-9) {
this.points.push({ x: ix, y: iy })
this.currentX = ix
this.currentY = iy
return this
}
const offset = r / Math.sin(angle / 2)
const bisUnit = { x: bisector.x / bisLen, y: bisector.y / bisLen }
const centerX = ix + bisUnit.x * offset
const centerY = iy + bisUnit.y * offset
const startAngle = Math.atan2(startY - centerY, startX - centerX)
const endAngle = Math.atan2(endY - centerY, endX - centerX)
let angleDelta = endAngle - startAngle
while (angleDelta <= -Math.PI) angleDelta += 2 * Math.PI
while (angleDelta > Math.PI) angleDelta -= 2 * Math.PI
const lastAfterPop = this.points[this.points.length - 1]
const almostEqual = (a: number, b: number) => Math.abs(a - b) < 1e-9
if (
!lastAfterPop ||
!almostEqual(lastAfterPop.x, startX) ||
!almostEqual(lastAfterPop.y, startY)
) {
this.points.push({ x: startX, y: startY })
}
const segments = 8
for (let i = 1; i <= segments; i++) {
const t = i / segments
const angle = startAngle + angleDelta * t
const x = centerX + r * Math.cos(angle)
const y = centerY + r * Math.sin(angle)
this.points.push({ x, y })
}
const last = this.points[this.points.length - 1]
if (!last) {
throw new Error("At least two points are required to create a corner")
}
this.currentX = last.x
this.currentY = last.y
this.lastDirection = outgoingDir
return this
}
arcTo(
x: number,
y: number,
{ radius, sweep = true, segments = 16 }: ArcOptions,
): this {
const start = { x: this.currentX, y: this.currentY }
const end = { x, y }
const mx = (start.x + end.x) / 2
const my = (start.y + end.y) / 2
const dx = end.x - start.x
const dy = end.y - start.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist === 0) return this
const h = Math.sqrt(Math.max(0, radius * radius - (dist / 2) ** 2))
const perpX = -dy / dist
const perpY = dx / dist
const cx = mx + (sweep ? 1 : -1) * perpX * h
const cy = my + (sweep ? 1 : -1) * perpY * h
const startAngle = Math.atan2(start.y - cy, start.x - cx)
const endAngle = Math.atan2(end.y - cy, end.x - cx)
let sweepAngle = endAngle - startAngle
if (sweep && sweepAngle < 0) sweepAngle += 2 * Math.PI
if (!sweep && sweepAngle > 0) sweepAngle -= 2 * Math.PI
for (let i = 1; i <= segments; i++) {
const t = i / segments
const angle = startAngle + sweepAngle * t
const px = cx + radius * Math.cos(angle)
const py = cy + radius * Math.sin(angle)
this.points.push({ x: px, y: py })
}
this.currentX = end.x
this.currentY = end.y
return this
}
toArray(): Point[] {
return [...this.points]
}
}
export const outlineBuilder = (x: number, y: number): OutlineBuilder => {
return new OutlineBuilder(x, y)
}