@rulecad/pcb 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/circuit.mjs +59 -21
- package/package.json +6 -4
- package/pcb.mjs +59 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rulecad/pcb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Circuit boards from rules, for `@rulecad/cad` designs with `"engine": "pcb"`. Footprints come from rule sets
|
|
4
4
|
(datasheets, KiCad's library) rather than numbers in code. The keyboard router draws every matrix track by rule, with
|
|
5
5
|
one diode per key on the back. The output is what a fab needs: Gerbers, drill files, a JLCPCB BOM and placement file,
|
|
6
6
|
and KiCad files.
|
|
@@ -11,6 +11,39 @@ the design checks, and the Gerber, KiCad and drawing exporters. React, which tsc
|
|
|
11
11
|
is never loaded. Two of those libraries list tscircuit among their dependencies, so React may be installed alongside
|
|
12
12
|
them, unused; rulecad's tests fail if it's ever loaded.
|
|
13
13
|
|
|
14
|
+
**Any board.** Place parts from rule sets or any standard footprint, turn them in quarter turns, join their pins with
|
|
15
|
+
nets, and let the autorouter draw the tracks:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// projects/node/objects/carrier.js: a radio module, two Qwiic sensor sockets, a battery socket and its diode
|
|
19
|
+
export function main(v) {
|
|
20
|
+
const { xiao, qwiic, jstph, part } = v.pcb;
|
|
21
|
+
return {
|
|
22
|
+
center: { x: 7.5, y: -2.2 }, width: 35.5, height: 25.4, thickness: v.thickness, track: v.track, autoroute: true,
|
|
23
|
+
parts: [
|
|
24
|
+
xiao('U1', 0, 0, v.xiao), // pins labelled D0..D10, 3V3, GND, 5V from the xiao rules
|
|
25
|
+
qwiic('J1', v.qwiic_x, v.qwiic_y, v.qwiic), // plug in from the top edge
|
|
26
|
+
qwiic('J2', v.j2_x, 0, v.qwiic, { rotation: v.j2_turn }), // turned to face the right edge
|
|
27
|
+
jstph('J3', v.qwiic_x, v.bat_y, v.jstph, { labels: { pin1: 'BAT', pin2: 'BATGND' } }),
|
|
28
|
+
part('D1', 'sod123', v.qwiic_x, v.diode_y, { mpn: 'B5819W' })
|
|
29
|
+
],
|
|
30
|
+
nets: { GND: ['U1.GND', 'J1.GND', 'J2.GND', 'J3.BATGND'], SDA: ['U1.D4', 'J1.SDA', 'J2.SDA'], /* ... */ },
|
|
31
|
+
traces: [], silk: [], holes: []
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **Parts:** `xiao`, `qwiic`, `jstph` and `header` take their sizes and pinouts from rule sets in @rulecad/rules (each
|
|
37
|
+
value with its source); `part(name, footprint, x, y)` takes any footprinter name: `soic8`, `0603`, `sot23`,
|
|
38
|
+
`jst4_sh`, `pinrow4` (SMD or through-hole). Options: `rotation` (0, 90, 180, 270), `layer`, `mpn`, `labels`.
|
|
39
|
+
- **Nets:** each net's pins are joined by the shortest set of links between them, at `board.track` (or the net's own
|
|
40
|
+
`{ pins, width }`); pins are named `part.pin` by label, name or number. The schematic labels them by net.
|
|
41
|
+
- **What a board tells the parts around it:** `board_x`, `board_y`, `board_w`, `board_d` and its mounting holes, so
|
|
42
|
+
a case can read it (`"uses": ["node/carrier"]` in @rulecad/cad) and a holes fit can check the screws find their inserts.
|
|
43
|
+
- **Keyboards** are one recipe built on the same parts: every matrix track drawn by rule, one diode per key.
|
|
44
|
+
|
|
45
|
+
A keyboard:
|
|
46
|
+
|
|
14
47
|
```js
|
|
15
48
|
// projects/macropad/objects/board.js: main(v) returns a board, built with v.pcb
|
|
16
49
|
export function main(v) {
|
package/circuit.mjs
CHANGED
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
// read the result. Only two tscircuit libraries are used, and neither needs a framework: footprinter (standard
|
|
4
4
|
// footprints such as SOD-123) and the capacity autorouter (connections a board leaves to it).
|
|
5
5
|
//
|
|
6
|
-
// board: { center: { x, y }, width, height, thickness, parts, traces, silk, holes, autoroute }
|
|
6
|
+
// board: { center: { x, y }, width, height, thickness, parts, traces, nets, track, silk, holes, autoroute }
|
|
7
7
|
// part { name, ftype: 'simple_switch' | 'simple_chip' | 'simple_diode', mpn, x, y, layer: 'top' | 'bottom',
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// rotation: 0 | 90 | 180 | 270 (counter-clockwise, seen from the side the part is on; default 0),
|
|
9
|
+
// pads: [{ pin, x, y, hole, outer }] (plated, relative to the part) or [{ pin, smd: true, hint, x, y, w, h }],
|
|
10
|
+
// holes: [{ x, y, d }], courtyard: { w, h } | null,
|
|
11
|
+
// footprint: 'sod123' | 'jst4_sh' | 'pinrow4' ... (a footprinter string, SMD or through-hole, instead of pads),
|
|
10
12
|
// sch: { x, y, symbol?, pinLabels? } }
|
|
11
13
|
// trace { from: [part, port], to: [part, port], width, path: [{ x, y }] (absolute, between the two ports) }; a trace
|
|
12
14
|
// without a path is left to the autorouter when board.autoroute is set
|
|
15
|
+
// nets { GND: ['U1.GND', 'J1.1', 'J2.1'] } or { GND: { pins: [...], width } }: each net's pins are joined by the
|
|
16
|
+
// shortest set of connections between them (a minimum spanning tree), left to the autorouter, at the net's
|
|
17
|
+
// width or board.track; pins are named part.pin, by the pin's label, its name or its number
|
|
13
18
|
// silk { rect: { x, y, w, h }, layer } | { text, x, y, size, layer }
|
|
14
19
|
// holes [{ x, y, d }] in the board (mounting holes)
|
|
15
20
|
// All positions are in mm, in the board's own frame. A part on the bottom is seen from above: mirrored in x.
|
|
@@ -17,7 +22,14 @@ import { fp } from '@tscircuit/footprinter';
|
|
|
17
22
|
import { symbols } from 'schematic-symbols';
|
|
18
23
|
|
|
19
24
|
const SUB = 'subcircuit_source_group_0';
|
|
20
|
-
const PASTE = 0.7;
|
|
25
|
+
const PASTE = 0.7;
|
|
26
|
+
// Quarter turns, counter-clockwise, as exact cosines and sines (no rounding error in pad positions)
|
|
27
|
+
const QUARTER = { 0: [1, 0], 90: [0, 1], 180: [-1, 0], 270: [0, -1] };
|
|
28
|
+
function quarter(r = 0) {
|
|
29
|
+
const t = QUARTER[((r % 360) + 360) % 360];
|
|
30
|
+
if (!t) throw new Error(`a part's rotation is ${r}°: boards take 0, 90, 180 or 270`);
|
|
31
|
+
return t;
|
|
32
|
+
} // solder paste, as a share of an SMD pad (what tscircuit uses)
|
|
21
33
|
// The board's manufacturing minimums (mm): tscircuit's defaults, within what common fabs make. The router keeps to them.
|
|
22
34
|
const FAB_MINIMUMS = { min_trace_width: 0.1, min_via_hole_diameter: 0.2, min_via_pad_diameter: 0.3, min_via_hole_edge_to_via_hole_edge_clearance: 0.1,
|
|
23
35
|
min_trace_to_hole_edge_clearance: 0.2, min_trace_to_pad_edge_clearance: 0.1, min_pad_edge_to_pad_edge_clearance: 0.1,
|
|
@@ -39,12 +51,17 @@ export async function circuitJson(board, { route = defaultRouter } = {}) {
|
|
|
39
51
|
are_pins_interchangeable: part.ftype === 'simple_switch', source_group_id: 'source_group_0', subcircuit_id: SUB });
|
|
40
52
|
const fpEls = part.footprint ? fp.string(part.footprint).circuitJson() : [];
|
|
41
53
|
const mirror = part.layer === 'bottom' ? -1 : 1;
|
|
42
|
-
const
|
|
43
|
-
|
|
54
|
+
const [cos, sin] = quarter(part.rotation), swap = sin !== 0;
|
|
55
|
+
const at = (x, y) => ({ x: part.x + mirror * (cos * x - sin * y), y: part.y + (sin * x + cos * y) });
|
|
56
|
+
const size = (w, h) => (swap ? { w: h, h: w } : { w, h }); // a rectangle turned a quarter swaps its sides
|
|
57
|
+
// Pads: from the description, or a footprint's SMD pads and plated holes (pin n: hints ['n'])
|
|
44
58
|
const pads = part.footprint
|
|
45
|
-
? fpEls.filter((e) => e.type === 'pcb_smtpad'
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
? fpEls.filter((e) => e.type === 'pcb_smtpad' || e.type === 'pcb_plated_hole').map((e) => e.type === 'pcb_smtpad'
|
|
60
|
+
? { smd: true, pin: `pin${e.port_hints[0]}`, hint: e.port_hints[0], ...at(e.x, e.y), ...size(e.width, e.height) }
|
|
61
|
+
: { pin: `pin${e.port_hints[0]}`, hint: e.port_hints[0], ...at(e.x, e.y), hole: e.hole_diameter ?? Math.min(e.hole_width, e.hole_height),
|
|
62
|
+
outer: e.outer_diameter ?? Math.max(e.rect_pad_width ?? e.outer_width ?? 0, e.rect_pad_height ?? e.outer_height ?? 0) })
|
|
63
|
+
: part.pads.map((p) => ({ ...p, ...at(p.x, p.y), ...(p.smd ? size(p.w, p.h) : {}) }));
|
|
64
|
+
const names = portNames(part, pads);
|
|
48
65
|
const sourcePorts = pads.map((p, i) => add('source_port', { name: names[i].name, pin_number: names[i].n, port_hints: names[i].hints,
|
|
49
66
|
source_component_id: comp.source_component_id, subcircuit_id: SUB }));
|
|
50
67
|
if (part.ftype === 'simple_chip') { // pins with the same label are joined inside the part (a Pico's GND pins)
|
|
@@ -53,18 +70,18 @@ export async function circuitJson(board, { route = defaultRouter } = {}) {
|
|
|
53
70
|
if (groups.length) comp.internally_connected_source_port_ids = groups.map((g) => g.map((s) => s.source_port_id));
|
|
54
71
|
for (const g of groups) add('source_component_internal_connection', { source_component_id: comp.source_component_id, subcircuit_id: SUB, source_port_ids: g.map((s) => s.source_port_id) });
|
|
55
72
|
}
|
|
56
|
-
placed.push({ part, comp, pads, sourcePorts, fpEls, at, mirror });
|
|
73
|
+
placed.push({ part, comp, pads, sourcePorts, fpEls, at, mirror, size });
|
|
57
74
|
}
|
|
58
75
|
add('pcb_board', { source_board_id: sourceBoard.source_board_id, subcircuit_id: SUB, center: board.center, thickness: board.thickness, num_layers: 2,
|
|
59
76
|
width: board.width, height: board.height, material: 'fr4', ...FAB_MINIMUMS });
|
|
60
77
|
|
|
61
|
-
for (const { part, comp, pads, sourcePorts, fpEls, at, mirror } of placed) {
|
|
78
|
+
for (const { part, comp, pads, sourcePorts, fpEls, at, mirror, size } of placed) {
|
|
62
79
|
// The component sits at the middle of its pads and holes (as tscircuit places it: pick-and-place reads this)
|
|
63
80
|
const extent = [...pads.map((p) => (p.smd ? [p.x - p.w / 2, p.y - p.h / 2, p.x + p.w / 2, p.y + p.h / 2] : [p.x - p.outer / 2, p.y - p.outer / 2, p.x + p.outer / 2, p.y + p.outer / 2])),
|
|
64
81
|
...(part.holes || []).map((h) => { const q = at(h.x, h.y); return [q.x - h.d / 2, q.y - h.d / 2, q.x + h.d / 2, q.y + h.d / 2]; })];
|
|
65
82
|
const box = [Math.min(...extent.map((e) => e[0])), Math.min(...extent.map((e) => e[1])), Math.max(...extent.map((e) => e[2])), Math.max(...extent.map((e) => e[3]))];
|
|
66
83
|
const pc = add('pcb_component', { center: { x: (box[0] + box[2]) / 2, y: (box[1] + box[3]) / 2 }, width: box[2] - box[0], height: box[3] - box[1],
|
|
67
|
-
layer: part.layer, rotation: 0, source_component_id: comp.source_component_id, subcircuit_id: SUB });
|
|
84
|
+
layer: part.layer, rotation: part.rotation ?? 0, source_component_id: comp.source_component_id, subcircuit_id: SUB });
|
|
68
85
|
const own = { pcb_component_id: pc.pcb_component_id, subcircuit_id: SUB };
|
|
69
86
|
pads.forEach((p, i) => {
|
|
70
87
|
const layers = p.smd ? [part.layer] : ['top', 'bottom'];
|
|
@@ -80,20 +97,20 @@ export async function circuitJson(board, { route = defaultRouter } = {}) {
|
|
|
80
97
|
for (const key of [sourcePorts[i].name, ...sourcePorts[i].port_hints]) ports.set(`${part.name}.${key}`, { source: sourcePorts[i], pcb: port, x: p.x, y: p.y, layers });
|
|
81
98
|
});
|
|
82
99
|
for (const h of part.holes || []) { const q = at(h.x, h.y); add('pcb_hole', { ...own, hole_shape: 'circle', hole_diameter: h.d, x: q.x, y: q.y, is_covered_with_solder_mask: false }); }
|
|
83
|
-
if (part.courtyard) add('pcb_courtyard_rect', { ...own, layer: part.layer, center: { x: part.x, y: part.y }, width:
|
|
100
|
+
if (part.courtyard) { const c = size(part.courtyard.w, part.courtyard.h); add('pcb_courtyard_rect', { ...own, layer: part.layer, center: { x: part.x, y: part.y }, width: c.w, height: c.h }); }
|
|
84
101
|
// A footprint's own marks, mirrored with it on the bottom: fab notes, the reference, the courtyard
|
|
85
102
|
for (const e of fpEls) {
|
|
86
103
|
if (e.type === 'pcb_fabrication_note_path') add(e.type, { ...own, layer: part.layer, route: e.route.map((q) => at(q.x, q.y)), stroke_width: e.stroke_width });
|
|
87
104
|
if (e.type === 'pcb_fabrication_note_text' || e.type === 'pcb_silkscreen_text') add(e.type, { anchor_alignment: 'center', anchor_position: at(e.anchor_position.x, e.anchor_position.y),
|
|
88
105
|
font: e.font, font_size: e.type === 'pcb_silkscreen_text' ? 0.5 : e.font_size, layer: part.layer, ...(e.type === 'pcb_silkscreen_text' ? { text: e.text.replace('{REF}', part.name), ccw_rotation: 0 } : { ccw_rotation: mirror < 0 ? 180 : 0, text: e.text }), ...own });
|
|
89
|
-
if (e.type === 'pcb_courtyard_rect') add(e.type, { ...own, layer: part.layer, center: at(e.center.x, e.center.y), width:
|
|
106
|
+
if (e.type === 'pcb_courtyard_rect') { const c = size(e.width, e.height); add(e.type, { ...own, layer: part.layer, center: at(e.center.x, e.center.y), width: c.w, height: c.h }); }
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
// ---- traces: drawn by rule, or left to the autorouter ----
|
|
94
111
|
const port = (part, name) => { const p = ports.get(`${part}.${name}`); if (!p) throw new Error(`no port ${part}.${name}`); return p; };
|
|
95
|
-
const toRoute = [];
|
|
96
|
-
for (const t of
|
|
112
|
+
const toRoute = [], traces = [...(board.traces || []), ...netTraces(board, port)];
|
|
113
|
+
for (const t of traces) {
|
|
97
114
|
const a = port(...t.from), b = port(...t.to);
|
|
98
115
|
const st = add('source_trace', { connected_source_port_ids: [a.source.source_port_id, b.source.source_port_id], connected_source_net_ids: [], subcircuit_id: SUB,
|
|
99
116
|
display_name: `.${t.from.join(' > .')} to .${t.to.join(' > .')}`, min_trace_thickness: t.width });
|
|
@@ -122,19 +139,40 @@ export async function circuitJson(board, { route = defaultRouter } = {}) {
|
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
141
|
|
|
125
|
-
schematic(out, add, placed, board, port);
|
|
142
|
+
schematic(out, add, placed, { ...board, traces }, port);
|
|
126
143
|
return out;
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
// A part's port names, in pad order: a switch's pin1 and pin2, a diode's anode and cathode, a chip's pins by label
|
|
130
|
-
function portNames(part) {
|
|
147
|
+
function portNames(part, pads) {
|
|
131
148
|
if (part.ftype === 'simple_diode') return [{ name: 'pin1', n: 1, hints: ['pin1', 'pos', 'anode', 'left', '1'] }, { name: 'pin2', n: 2, hints: ['pin2', 'neg', 'cathode', 'right', '2'] }];
|
|
132
|
-
return part.pads.map((p) => {
|
|
149
|
+
return (part.pads || pads).map((p) => {
|
|
133
150
|
const n = Number(p.pin.replace(/\D/g, '')), label = part.sch?.pinLabels?.[p.pin];
|
|
134
151
|
return { name: label || p.pin, n, hints: label ? [label, p.pin, String(n)] : [p.pin, String(n)] };
|
|
135
152
|
});
|
|
136
153
|
}
|
|
137
154
|
|
|
155
|
+
// A net's connections: its pins joined by the shortest set of straight links between them (Prim's minimum spanning
|
|
156
|
+
// tree, ties to the earlier pin), so the router gets short, sensible connections to route rather than a long chain
|
|
157
|
+
function netTraces(board, port) {
|
|
158
|
+
return Object.entries(board.nets || {}).flatMap(([net, spec]) => {
|
|
159
|
+
const list = Array.isArray(spec) ? spec : spec.pins, width = (Array.isArray(spec) ? undefined : spec.width) ?? board.track;
|
|
160
|
+
if (!width) throw new Error(`net ${net}: no track width. Give the board a track width (board.track) or the net one ({ pins, width })`);
|
|
161
|
+
const pins = list.map((p) => { const i = String(p).indexOf('.'); if (i < 1) throw new Error(`net ${net}: "${p}" should name a part and a pin, like U1.GND`); return [p.slice(0, i), p.slice(i + 1)]; });
|
|
162
|
+
const at = pins.map((p) => port(...p)), linked = [0], links = [];
|
|
163
|
+
while (linked.length < pins.length) {
|
|
164
|
+
let best = null;
|
|
165
|
+
for (const a of linked) for (let b = 0; b < pins.length; b++) {
|
|
166
|
+
if (linked.includes(b)) continue;
|
|
167
|
+
const d = Math.hypot(at[a].x - at[b].x, at[a].y - at[b].y);
|
|
168
|
+
if (!best || d < best.d) best = { a, b, d };
|
|
169
|
+
}
|
|
170
|
+
linked.push(best.b); links.push({ from: pins[best.a], to: pins[best.b], width, net });
|
|
171
|
+
}
|
|
172
|
+
return links;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
// ---- the schematic: each part at the place the board gives it, wires between neighbours, labels to the chip ----
|
|
139
177
|
function schematic(out, add, placed, board, port) {
|
|
140
178
|
const sch = new Map();
|
|
@@ -167,9 +205,9 @@ function schematic(out, add, placed, board, port) {
|
|
|
167
205
|
for (const t of board.traces) {
|
|
168
206
|
const a = port(...t.from), b = port(...t.to), pa = sch.get(a.source.source_port_id), pb = sch.get(b.source.source_port_id);
|
|
169
207
|
const chip = [a, b].find((q) => placed.find((p) => p.sourcePorts.includes(q.source)).part.ftype === 'simple_chip');
|
|
170
|
-
if (chip) {
|
|
208
|
+
if (chip || t.net) { // a net's ends are labelled with the net's name
|
|
171
209
|
// At the chip, the label points out from its side of the box; at the matrix, it hangs below the pin
|
|
172
|
-
for (const [q, sp] of [[pa, a], [pb, b]]) add('schematic_net_label', { text: chip.source.name, anchor_position: q.center, center: q.center,
|
|
210
|
+
for (const [q, sp] of [[pa, a], [pb, b]]) add('schematic_net_label', { text: t.net ?? chip.source.name, anchor_position: q.center, center: q.center,
|
|
173
211
|
anchor_side: q.side_of_component === 'left' ? 'right' : q.side_of_component === 'right' ? 'left' : 'top', source_port_id: sp.source.source_port_id });
|
|
174
212
|
continue;
|
|
175
213
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulecad/pcb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Circuit boards from rules: parts from rule sets or any standard footprint, quarter-turn rotation, nets and autorouting; keyboards with every matrix track drawn by rule. Gerbers, KiCad files and a JLCPCB BOM. Circuit JSON written directly: no React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./pcb.mjs",
|
|
7
7
|
"files": [
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"tscircuit",
|
|
18
18
|
"gerber",
|
|
19
19
|
"jlcpcb",
|
|
20
|
-
"kicad"
|
|
20
|
+
"kicad",
|
|
21
|
+
"circuit-board",
|
|
22
|
+
"autorouter"
|
|
21
23
|
],
|
|
22
24
|
"engines": {
|
|
23
25
|
"node": ">=20.6"
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
"@flatten-js/core": "1.6.14",
|
|
32
34
|
"@jscad/modeling": "^2.12.0",
|
|
33
35
|
"@resvg/resvg-js": "2.6.2",
|
|
34
|
-
"@rulecad/cad": "^0.
|
|
36
|
+
"@rulecad/cad": "^0.2.0",
|
|
35
37
|
"@tscircuit/capacity-autorouter": "0.0.982",
|
|
36
38
|
"@tscircuit/checks": "0.0.253",
|
|
37
39
|
"@tscircuit/circuit-json-util": "0.0.122",
|
package/pcb.mjs
CHANGED
|
@@ -51,6 +51,55 @@ export const picoPin = (n, p) => { const left = n <= 20, i = left ? n - 1 : 40 -
|
|
|
51
51
|
// The Pico's GPIO pins down one edge: [{ pin, gpio }]
|
|
52
52
|
export const picoGpio = (side) => Object.entries(loadRuleSet('pico').rules[`gpio_${side}`].pins).map(([pin, gpio]) => ({ pin: Number(pin), gpio }));
|
|
53
53
|
|
|
54
|
+
// ---------- any board ----------
|
|
55
|
+
// Parts for any board, not only keyboards. Each returns a part as circuit.mjs describes it: put them in board.parts, join
|
|
56
|
+
// their pins with board.nets, and set board.autoroute. o (all optional): { rotation: 0 | 90 | 180 | 270, layer,
|
|
57
|
+
// mpn, labels: { pin1: 'GND', ... }, sch: { x, y } }. Pin labels name the pins in nets ('U1.GND') and on the schematic.
|
|
58
|
+
const chip = (name, x, y, o, more) => ({ name, ftype: 'simple_chip', x, y, layer: o.layer ?? 'top', rotation: o.rotation ?? 0, mpn: o.mpn,
|
|
59
|
+
courtyard: o.courtyard ?? null, sch: { x: o.sch?.x ?? x / 2, y: o.sch?.y ?? y / 2, pinLabels: o.labels ?? {} }, ...more });
|
|
60
|
+
const pinNo = (p) => Number(p.pin.replace(/\D/g, ''));
|
|
61
|
+
// A rule set's pin map ({ "1": "GND", ... }) as pin labels ({ pin1: 'GND', ... })
|
|
62
|
+
const labelsFrom = (set, rule) => Object.fromEntries(Object.entries(loadRuleSet(set).rules[rule].pins).map(([n, l]) => [`pin${n}`, l]));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Any standard footprint as a part, by its footprinter name: 'jst4_sh', 'pinrow4', 'soic8', '0603', 'sot23' ...
|
|
66
|
+
* (SMD and through-hole). Pins are numbered as the footprint numbers them.
|
|
67
|
+
* @param {string} name @param {string} footprint @param {number} x @param {number} y @param {object} [o]
|
|
68
|
+
*/
|
|
69
|
+
export const part = (name, footprint, x, y, o = {}) => chip(name, x, y, o, { footprint });
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A pin header from rules (r: pitch, drill, pad; row_spacing for two rows): o.pins pins per row, o.rows 1 or 2.
|
|
73
|
+
* Two rows are numbered as a DIP: 1 to n down the left, n + 1 to 2n up the right.
|
|
74
|
+
*/
|
|
75
|
+
export function header(name, x, y, r, o = {}) {
|
|
76
|
+
const n = o.pins, two = (o.rows ?? 1) === 2, pads = [];
|
|
77
|
+
for (let i = 0; i < n; i++) {
|
|
78
|
+
const py = ((n - 1) / 2 - i) * r.pitch;
|
|
79
|
+
pads.push(pad(`pin${i + 1}`, two ? -r.row_spacing / 2 : 0, py, r.drill, r.pad));
|
|
80
|
+
if (two) pads.push(pad(`pin${2 * n - i}`, r.row_spacing / 2, py, r.drill, r.pad));
|
|
81
|
+
}
|
|
82
|
+
return chip(name, x, y, o, { pads: pads.sort((a, b) => pinNo(a) - pinNo(b)) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A Seeed Studio XIAO module (any of the family) on header pins, USB at +y, from the xiao rules; pins labelled D0..D10, 5V, GND, 3V3. */
|
|
86
|
+
export const xiao = (name, x, y, r, o = {}) =>
|
|
87
|
+
header(name, x, y, r, { pins: r.pins / 2, rows: 2, mpn: 'Seeed Studio XIAO', courtyard: { w: r.width, h: r.length }, labels: labelsFrom('xiao', 'pins'), ...o });
|
|
88
|
+
|
|
89
|
+
/** A Qwiic / STEMMA QT socket (JST SH, 4 pins, side entry), from the qwiic rules: the plug goes in from +y, the tabs' side. */
|
|
90
|
+
export function qwiic(name, x, y, r, o = {}) {
|
|
91
|
+
const n = r.pins, smd = (k, x, y, w, h) => ({ pin: `pin${k}`, smd: true, hint: String(k), x, y, w, h });
|
|
92
|
+
const pads = Array.from({ length: n }, (_, i) => smd(i + 1, (i - (n - 1) / 2) * r.pitch, r.pad_y, r.pad_w, r.pad_h))
|
|
93
|
+
.concat([-1, 1].map((side, i) => smd(n + i + 1, side * r.tab_x, r.tab_y, r.tab_w, r.tab_h)));
|
|
94
|
+
return chip(name, x, y, { mpn: 'JST SM04B-SRSS-TB', labels: { ...labelsFrom('qwiic', 'pins'), pin5: 'TAB1', pin6: 'TAB2' }, ...o }, { pads });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A JST PH socket (2.0 mm, through-hole), from the jstph rules: o.pins pins in a row along x (default 2). */
|
|
98
|
+
export function jstph(name, x, y, r, o = {}) {
|
|
99
|
+
const n = o.pins ?? 2;
|
|
100
|
+
return chip(name, x, y, { mpn: `JST B${n}B-PH-K-S`, ...o }, { pads: Array.from({ length: n }, (_, i) => pad(`pin${i + 1}`, (i - (n - 1) / 2) * r.pitch, 0, r.drill, r.pad)) });
|
|
101
|
+
}
|
|
102
|
+
|
|
54
103
|
// ---------- the keyboard ----------
|
|
55
104
|
// Keys from a layout file (lib/layout.mjs keys, in key units) -> [{ r, c, x, y, u, n, label }] in mm. The matrix row is
|
|
56
105
|
// the layout row; the matrix column comes from lining every row up with the widest one: each row's keys take, in order,
|
|
@@ -270,12 +319,12 @@ export function keyboard(v, keys) {
|
|
|
270
319
|
|
|
271
320
|
// ---------- building ----------
|
|
272
321
|
// render(board) -> Circuit JSON (circuit.mjs)
|
|
273
|
-
export const render = (board) => circuitJson(board);
|
|
322
|
+
export const render = (board, options) => circuitJson(board, options); // options.route: a router in place of the autorouter
|
|
274
323
|
|
|
275
324
|
// The board as a solid (previews, fits, the on-screen copy, the model page): the bare board, drilled, bottom at z = 0. Its
|
|
276
325
|
// copper is drawn in the 2D views (views), not in 3D. The board is a 2D outline with its holes, extruded: its faces
|
|
277
326
|
// triangulate cleanly (cylinders subtracted from a block leave long slivers).
|
|
278
|
-
export function solids(json, { thickness, smooth }) {
|
|
327
|
+
export function solids(json, { thickness, smooth = 16 }) { // smooth: segments per hole (a board without the variable gets 16)
|
|
279
328
|
const { rectangle, circle } = jscad.primitives, { subtract } = jscad.booleans, { extrudeLinear } = jscad.extrusions;
|
|
280
329
|
const b = json.find((e) => e.type === 'pcb_board');
|
|
281
330
|
const drills = json.filter((e) => e.type === 'pcb_hole' || e.type === 'pcb_plated_hole' || e.type === 'pcb_via')
|
|
@@ -291,7 +340,14 @@ export function outputs(json, matrix) {
|
|
|
291
340
|
return { board_x: b.center.x, board_y: b.center.y, board_w: b.width, board_d: b.height, ...(matrix?.pico ? { pico_x: matrix.pico.x, pico_y: matrix.pico.y } : {}) };
|
|
292
341
|
}
|
|
293
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A board's mounting holes (the board's own holes, not a part's): [{ x, y, d }] in mm, in the board's frame (as
|
|
345
|
+
* board_x and board_y). A case reads them to put its bosses under them; a holes fit checks screws find their inserts.
|
|
346
|
+
*/
|
|
347
|
+
export const holes = (json) => json.filter((e) => e.type === 'pcb_hole' && !e.pcb_component_id).map((e) => ({ x: e.x, y: e.y, d: e.hole_diameter }));
|
|
348
|
+
|
|
294
349
|
// ---------- checks ----------
|
|
350
|
+
const mm = (x) => String(Number(x.toFixed(3))); // 0.30000000000000004 reads as 0.3
|
|
295
351
|
// checks(json, v) -> findings ({ level, text }): tscircuit's own design checks, the fab's limits, and the matrix
|
|
296
352
|
export async function checks(json, v, matrix) {
|
|
297
353
|
const { runAllChecks } = await import('@tscircuit/checks');
|
|
@@ -302,7 +358,7 @@ export async function checks(json, v, matrix) {
|
|
|
302
358
|
const traces = json.filter((e) => e.type === 'pcb_trace'), unrouted = json.filter((e) => e.type === 'source_trace').length - traces.length;
|
|
303
359
|
out.push(unrouted > 0 ? { level: 'fail', text: `${unrouted} connections have no track` } : { level: 'pass', text: `every connection is routed (${traces.length} tracks, ${json.filter((e) => e.type === 'pcb_via').length} vias)` });
|
|
304
360
|
const thin = traces.flatMap((t) => t.route).filter((p) => p.width != null && p.width < j.track_min);
|
|
305
|
-
out.push(thin.length ? { level: 'fail', text: `tracks under JLCPCB's ${j.track_min} mm` } : { level: 'pass', text: `tracks ${v.track} mm
|
|
361
|
+
out.push(thin.length ? { level: 'fail', text: `tracks under JLCPCB's ${j.track_min} mm` } : { level: 'pass', text: `tracks ${mm(v.track ?? Math.min(...traces.flatMap((t) => t.route).map((p) => p.width).filter((w) => w != null)))} mm${v.clear != null ? `, gaps ${mm(v.clear)} mm` : ''}: over JLCPCB's ${j.track_min} / ${j.gap_min} mm` });
|
|
306
362
|
const drills = json.filter((e) => e.type === 'pcb_hole' || e.type === 'pcb_plated_hole').map((e) => e.hole_diameter ?? e.hole_width);
|
|
307
363
|
out.push(Math.min(...drills) < j.drill_min ? { level: 'fail', text: `a hole under JLCPCB's ${j.drill_min} mm` } : { level: 'pass', text: `${drills.length} holes, smallest ${Math.min(...drills)} mm (JLCPCB: ${j.drill_min} mm)` });
|
|
308
364
|
const b = json.find((e) => e.type === 'pcb_board'), size = `${b.width.toFixed(1)} × ${b.height.toFixed(1)} mm`;
|