pixalynx/nrf9151-gps-tracker
A compact 44×32 mm, 4-layer nRF9151 LTE-M/NB-IoT/GNSS tracker PCB with nPM1300 LiPo/USB-C power, nano-SIM/eSIM switching, nRF7002 Wi‑Fi scanning, accelerometer, antennas, buzzer, LEDs, SOS button, and SWD/debug connectors.
- Version
- 0.1.4
- License
- unset
- Stars
- 0
lib/geometry.ts
/** Small PCB geometry helpers shared by the three boards.
*
* tscircuit `<trace pcbPath>` points are interpreted as offsets from the centre
* of the component that owns the trace's anchor pad (the `from` pad unless
* `pcbPathRelativeTo` names another port), rotated by that component's
* `pcbRotation`. Board-frame coordinates therefore have to be converted before
* they are handed to a trace. `toLocal` does that conversion.
*/
export type Pt = { x: number; y: number }
export function toLocal(p: Pt, centre: Pt, rotationDeg = 0): Pt {
const dx = p.x - centre.x, dy = p.y - centre.y
const a = (-rotationDeg * Math.PI) / 180
const c = Math.cos(a), s = Math.sin(a)
return { x: +(dx * c - dy * s).toFixed(4), y: +(dx * s + dy * c).toFixed(4) }
}
type Layer = "top" | "bottom" | "inner1" | "inner2"
type PathPt = Pt & { via?: boolean; fromLayer?: Layer; toLayer?: Layer }
/** Two tscircuit copper-pour traps, both of which leave a pour flooding over foreign copper (a short):
* 1. a pcbPath via only gets the layers between its from and to layer, and the pour solver cuts antipads on those
* layers alone, yet every via here is drilled through: a bottom->inner2 signal via would sit bare in the top and
* inner1 GND pours. So each via is authored as a full top->bottom via, followed (same spot) by a bottom->target
* step when the trace continues on another layer.
* 2. the pour solver only treats runs of consecutive *wire* points as trace obstacles; a via point ends the run, so a
* pad->via or via->next-point segment is invisible to it. A wire point is therefore added on the via, on both
* sides (zero-length hops, same copper). */
export function throughVia<T extends PathPt>(p: T): PathPt[] {
if (!p.via) return [p]
const out: PathPt[] = [{ x: p.x, y: p.y }, { x: p.x, y: p.y, via: true, fromLayer: "top", toLayer: "bottom" }]
if (p.toLayer && p.toLayer !== "bottom") out.push({ x: p.x, y: p.y, via: true, fromLayer: "bottom", toLayer: p.toLayer })
out.push({ x: p.x, y: p.y })
return out
}
export const throughVias = (points: PathPt[]) => points.flatMap(throughVia)
/** Convert a board-frame polyline (optionally with layer changes) into pcbPath entries (vias drilled through). */
export function localPath(points: (Pt & { via?: boolean; toLayer?: Layer })[], centre: Pt, rotationDeg = 0) {
return points.flatMap((p) => throughVia({ ...toLocal(p, centre, rotationDeg), ...(p.via ? { via: true, toLayer: p.toLayer } : {}) }))
}
export const round = (v: number, d = 3) => +v.toFixed(d)