@pcbjs/core 0.0.0 → 0.0.1-next.9029beb783788cfbb471cc9d57e1b47864c6eaa6

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/dist/board-spec.d.ts +54 -0
  3. package/dist/board-spec.js +320 -0
  4. package/dist/circuit.d.ts +96 -0
  5. package/dist/circuit.js +337 -0
  6. package/dist/clearance.d.ts +9 -0
  7. package/dist/clearance.js +61 -0
  8. package/dist/config.d.ts +41 -0
  9. package/dist/config.js +3 -0
  10. package/dist/defaults.d.ts +28 -0
  11. package/dist/defaults.js +81 -0
  12. package/dist/fonts/hershey-simplex.d.ts +8 -0
  13. package/dist/fonts/hershey-simplex.js +127 -0
  14. package/dist/fonts/newstroke.d.ts +8 -0
  15. package/dist/fonts/newstroke.js +433 -0
  16. package/dist/impedance.d.ts +30 -0
  17. package/dist/impedance.js +146 -0
  18. package/dist/index.d.ts +96 -0
  19. package/dist/index.js +425 -0
  20. package/dist/jsx-dev-runtime.d.ts +9 -0
  21. package/dist/jsx-dev-runtime.js +7 -0
  22. package/dist/jsx-runtime.d.ts +34 -0
  23. package/dist/jsx-runtime.js +107 -0
  24. package/dist/model-source.d.ts +13 -0
  25. package/dist/model-source.js +21 -0
  26. package/dist/model.d.ts +565 -0
  27. package/dist/model.js +3 -0
  28. package/dist/netclass.d.ts +57 -0
  29. package/dist/netclass.js +194 -0
  30. package/dist/outline.d.ts +73 -0
  31. package/dist/outline.js +320 -0
  32. package/dist/own-files.d.ts +2 -0
  33. package/dist/own-files.js +3 -0
  34. package/dist/preset.d.ts +54 -0
  35. package/dist/preset.js +10 -0
  36. package/dist/reference.d.ts +217 -0
  37. package/dist/reference.js +34 -0
  38. package/dist/rules.d.ts +37 -0
  39. package/dist/rules.js +144 -0
  40. package/dist/sch-elements.d.ts +93 -0
  41. package/dist/sch-elements.js +58 -0
  42. package/dist/sch-model.d.ts +120 -0
  43. package/dist/sch-model.js +1 -0
  44. package/dist/sch-symbol.d.ts +78 -0
  45. package/dist/sch-symbol.js +196 -0
  46. package/dist/schematic.d.ts +264 -0
  47. package/dist/schematic.js +791 -0
  48. package/dist/site-data.d.ts +355 -0
  49. package/dist/site-data.js +1 -0
  50. package/dist/sourcing.d.ts +7 -0
  51. package/dist/sourcing.js +48 -0
  52. package/dist/stackups.d.ts +34 -0
  53. package/dist/stackups.js +46 -0
  54. package/dist/teardrop.d.ts +23 -0
  55. package/dist/teardrop.js +57 -0
  56. package/dist/text.d.ts +35 -0
  57. package/dist/text.js +65 -0
  58. package/dist/types.d.ts +981 -0
  59. package/dist/types.js +1 -0
  60. package/dist/units.d.ts +14 -0
  61. package/dist/units.js +53 -0
  62. package/dist/variants.d.ts +51 -0
  63. package/dist/variants.js +107 -0
  64. package/package.json +57 -3
  65. package/src/fonts/SOURCES.md +17 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuxuan Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ import type { AutoRouteParams, BoardProps, ResolvedRules, Stackup } from './types.js';
2
+ import { type ResolvedNetClass } from './netclass.js';
3
+ import { type ResolvedTeardrops } from './teardrop.js';
4
+ /** The board spec a default layer may set: everything but the name and the children. */
5
+ export type BoardSpec = Partial<Omit<BoardProps, 'name' | 'children'>>;
6
+ /** A resolved board spec: what the compiler uses where props and defaults leave a key out. */
7
+ export interface ResolvedBoardSpec {
8
+ title?: string;
9
+ revision: string;
10
+ copper: 2 | 4 | 6 | 8;
11
+ thickness: number;
12
+ stackup: Stackup;
13
+ rules: ResolvedRules;
14
+ /** the ordered sourcing tags, [] for the catch-all MPN alone */
15
+ sourcing: string[];
16
+ /** the board-wide autoroute parameters */
17
+ autoroute: AutoRouteParams;
18
+ /** the net classes by name, lengths in mm, {} when none */
19
+ classes: Record<string, ResolvedNetClass>;
20
+ /** where cpl.csv places a part, absent when no level sets it: the footprint origin */
21
+ cpl?: {
22
+ origin: 'footprint' | 'center';
23
+ };
24
+ /** the README-fab notes of every level, lowest first, each line once */
25
+ fabNotes: string[];
26
+ /** on or off, and the parameters a class's `teardrops: true` takes */
27
+ teardrops: {
28
+ on: boolean;
29
+ spec: ResolvedTeardrops;
30
+ };
31
+ }
32
+ /** The built-in autoroute parameters; the optional ones are absent and mean automatic or unlimited. */
33
+ export declare const BASE_AUTOROUTE: AutoRouteParams;
34
+ /** Check partial autoroute parameters; throws a plain Error. */
35
+ export declare function checkAutoroute(v: unknown, where: string): Partial<AutoRouteParams>;
36
+ /** Autoroute parameters merged lowest first; `via` and `tune` merge their keys, the rest replace whole. */
37
+ export declare function mergeAutoroute<T extends object>(base: T, ...levels: (object | null | undefined)[]): T;
38
+ /** The sum of the layers, as a fab measures the finished board. */
39
+ export declare function stackupSum(s: Stackup): number;
40
+ export declare const stackupThickness: (s: Stackup) => number;
41
+ export declare const copperOf: (s: Stackup) => {
42
+ type: "copper";
43
+ name: import("./types.js").CopperName;
44
+ thickness: number;
45
+ weight?: number;
46
+ }[];
47
+ /** Check a stackup and return its copper count. */
48
+ export declare function validateStackup(s: Stackup): number;
49
+ /** Check an island mode: 'remove', 'keep' or { minArea } in mm². Throws naming `where`. */
50
+ export declare function checkIslands(v: unknown, where: string): void;
51
+ /** Check an edge band: { width, kinds?, except? }, see EdgeBand. Throws naming `where`. */
52
+ export declare function checkEdgeBand(v: unknown, where: string): void;
53
+ /** Resolve levels over the base, lowest first; the highest level giving `copper` or `stackup` decides the layers, and a `thickness` must agree. */
54
+ export declare function resolveBoardSpec(levels: BoardSpec[], levelNames?: string[]): ResolvedBoardSpec;
@@ -0,0 +1,320 @@
1
+ import { BASE, mergeRules, resolveRules, THERMAL } from './rules.js';
2
+ import { defaultStackup } from './stackups.js';
3
+ import { mm } from './units.js';
4
+ import { checkClasses, mergeClasses, resolveClasses } from './netclass.js';
5
+ import { checkTeardrops, resolveTeardrops } from './teardrop.js';
6
+ /** The built-in autoroute parameters; the optional ones are absent and mean automatic or unlimited. */
7
+ export const BASE_AUTOROUTE = { algorithm: 'maze', viaCost: 5, clearance: 0.02, bend: '45', priority: 0 };
8
+ const AUTO_KEYS = new Set(['algorithm', 'pitch', 'viaCost', 'layers', 'via', 'clearance', 'bend', 'prefer', 'maxVias', 'avoid', 'priority', 'ripup', 'maxDetour', 'tune']);
9
+ /** Check partial autoroute parameters; throws a plain Error. */
10
+ export function checkAutoroute(v, where) {
11
+ if (v === undefined || v === null)
12
+ return {};
13
+ if (typeof v !== 'object' || Array.isArray(v))
14
+ throw new Error(`${where} autoroute must be an object`);
15
+ const o = v;
16
+ const bad = (k, what) => { throw new Error(`${where} autoroute: ${k} must be ${what}, got ${typeof o[k] === 'function' ? 'a function' : JSON.stringify(o[k])}`); };
17
+ const num = (k, ok, what) => { if (o[k] !== undefined && !(typeof o[k] === 'number' && Number.isFinite(o[k]) && ok(o[k])))
18
+ bad(k, what); };
19
+ for (const k of Object.keys(o))
20
+ if (!AUTO_KEYS.has(k))
21
+ throw new Error(`${where} autoroute: unknown parameter ${JSON.stringify(k)} (known: ${[...AUTO_KEYS].join(', ')})`);
22
+ if (o.algorithm !== undefined && o.algorithm !== 'maze' && o.algorithm !== 'straight' && typeof o.algorithm !== 'function')
23
+ bad('algorithm', "'maze', 'straight' or a function");
24
+ num('pitch', x => x > 0, 'a positive number of mm');
25
+ num('viaCost', x => x >= 0, 'a number of mm, 0 or more');
26
+ num('clearance', x => x >= 0, 'a number of mm, 0 or more');
27
+ num('maxVias', x => x >= 0 && Number.isInteger(x), 'a whole number, 0 or more');
28
+ num('priority', () => true, 'a number');
29
+ num('ripup', x => x >= 0 && Number.isInteger(x), 'a whole number, 0 or more');
30
+ if (o.maxDetour !== undefined) {
31
+ const m = o.maxDetour, ok = (x) => x === undefined || (typeof x === 'number' && Number.isFinite(x) && x >= 0);
32
+ if (!(typeof m === 'number' ? ok(m) : m && typeof m === 'object' && !Array.isArray(m) && Object.keys(m).every(k => k === 'ratio' || k === 'floor') && ok(m.ratio) && ok(m.floor)))
33
+ bad('maxDetour', 'a ratio, 0 or more, or an object { ratio?, floor? } of numbers, 0 or more');
34
+ }
35
+ if (o.bend !== undefined && o.bend !== '45' && o.bend !== '90')
36
+ bad('bend', "'45' or '90'");
37
+ if (o.layers !== undefined && !(Array.isArray(o.layers) && o.layers.length && o.layers.every((l) => typeof l === 'string')))
38
+ bad('layers', 'a list of copper layer names');
39
+ if (o.via !== undefined && !(o.via && typeof o.via === 'object' && !Array.isArray(o.via)))
40
+ bad('via', 'an object { drill?, size? }');
41
+ if (o.prefer !== undefined && !(o.prefer && typeof o.prefer === 'object' && !Array.isArray(o.prefer) && Object.values(o.prefer).every(d => d === 'h' || d === 'v')))
42
+ bad('prefer', "an object of layer names to 'h' or 'v'");
43
+ if (o.avoid !== undefined && !Array.isArray(o.avoid))
44
+ bad('avoid', 'a list of positions and rectangles { x, y, w, h }');
45
+ if (o.tune !== undefined) {
46
+ if (!o.tune || typeof o.tune !== 'object' || Array.isArray(o.tune))
47
+ bad('tune', 'an object { amplitude?, spacing?, maxAmplitude? }');
48
+ for (const [k, x] of Object.entries(o.tune)) {
49
+ if (k !== 'amplitude' && k !== 'spacing' && k !== 'maxAmplitude')
50
+ throw new Error(`${where} autoroute: tune has an unknown field ${JSON.stringify(k)} (known: amplitude, spacing, maxAmplitude)`);
51
+ let n = NaN;
52
+ try {
53
+ n = mm(x);
54
+ }
55
+ catch { /* reported below */ }
56
+ if (!(Number.isFinite(n) && n > 0))
57
+ throw new Error(`${where} autoroute: tune.${k} must be a positive length, got ${JSON.stringify(x)}`);
58
+ }
59
+ }
60
+ return o;
61
+ }
62
+ /** Autoroute parameters merged lowest first; `via` and `tune` merge their keys, the rest replace whole. */
63
+ export function mergeAutoroute(base, ...levels) {
64
+ const out = { ...base };
65
+ for (const l of levels) {
66
+ if (!l)
67
+ continue;
68
+ for (const [k, v] of Object.entries(l)) {
69
+ if (v === undefined)
70
+ continue;
71
+ out[k] = (k === 'via' || k === 'tune') && out[k] ? { ...out[k], ...v } : v;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /** boolean rules absent from BASE: false unless set */
77
+ const FLAG_KEYS = new Set(['strictNets', 'requireSourcing']);
78
+ const RULE_KEYS = new Set([...Object.keys(BASE), ...FLAG_KEYS, 'thermal', 'islands', 'edge', 'maskWeb', 'maskWebByColor']);
79
+ const FINISHES = new Set(['HASL', 'LeadFreeHASL', 'ENIG', 'ImmersionSilver', 'ImmersionTin', 'OSP', 'HardGold']);
80
+ const r4 = (v) => Math.round(v * 1e4) / 1e4;
81
+ /** The sum of the layers, as a fab measures the finished board. */
82
+ export function stackupSum(s) {
83
+ return r4(s.layers.reduce((t, l) => t + (l.type === 'copper' || l.type === 'core' || l.type === 'prepreg' || l.type === 'mask' ? l.thickness ?? 0 : 0), 0));
84
+ }
85
+ export const stackupThickness = (s) => s.thickness ?? stackupSum(s);
86
+ export const copperOf = (s) => s.layers.filter((l) => l.type === 'copper');
87
+ /** Check a stackup and return its copper count. */
88
+ export function validateStackup(s) {
89
+ if (!s || typeof s !== 'object' || !Array.isArray(s.layers))
90
+ throw new Error('stackup needs a layers array, listed top to bottom');
91
+ const cu = copperOf(s);
92
+ const n = cu.length;
93
+ if (n < 2)
94
+ throw new Error(`stackup has ${n} copper layer${n === 1 ? '' : 's'}, it needs F.Cu and B.Cu at least`);
95
+ if (n > 8)
96
+ throw new Error(`stackup has ${n} copper layers, at most 8 are supported`);
97
+ if (n % 2)
98
+ throw new Error(`stackup has ${n} copper layers, the count must be even`);
99
+ const want = ['F.Cu', ...Array.from({ length: n - 2 }, (_, i) => `In${i + 1}.Cu`), 'B.Cu'];
100
+ cu.forEach((l, i) => { if (l.name !== want[i])
101
+ throw new Error(`stackup copper layer ${i + 1} is ${JSON.stringify(l.name)}, expected ${want[i]} (order: ${want.join(', ')})`); });
102
+ const first = s.layers.indexOf(cu[0]), last = s.layers.indexOf(cu[n - 1]);
103
+ s.layers.forEach((l, i) => {
104
+ const what = `stackup layer ${i + 1} (${l.type}${l.type === 'copper' ? ` ${l.name}` : ''})`;
105
+ switch (l.type) {
106
+ case 'copper':
107
+ case 'core':
108
+ case 'prepreg':
109
+ if (!(typeof l.thickness === 'number' && l.thickness > 0))
110
+ throw new Error(`${what} needs a positive thickness, got ${l.thickness}`);
111
+ if (l.type !== 'copper' && (i < first || i > last))
112
+ throw new Error(`${what} is outside the copper layers`);
113
+ if (l.type === 'copper' && l.weight !== undefined && !(l.weight > 0))
114
+ throw new Error(`${what} needs a positive weight, got ${l.weight}`);
115
+ if (l.type !== 'copper' && l.er !== undefined && !(l.er >= 1))
116
+ throw new Error(`${what} has er ${l.er}, expected 1 or more`);
117
+ break;
118
+ case 'mask':
119
+ case 'silk':
120
+ case 'paste':
121
+ if (l.side !== 'F' && l.side !== 'B')
122
+ throw new Error(`${what} needs side 'F' or 'B'`);
123
+ if (l.side === 'F' ? i > first : i < last)
124
+ throw new Error(`${what} on side ${l.side} must be ${l.side === 'F' ? 'above F.Cu' : 'below B.Cu'}`);
125
+ if (l.thickness !== undefined && !(l.thickness >= 0))
126
+ throw new Error(`${what} has a negative thickness`);
127
+ break;
128
+ default: throw new Error(`stackup layer ${i + 1} has an unknown type ${JSON.stringify(l.type)}`);
129
+ }
130
+ });
131
+ for (let k = 0; k + 1 < n; k++) {
132
+ const a = s.layers.indexOf(cu[k]), b = s.layers.indexOf(cu[k + 1]);
133
+ if (!s.layers.slice(a + 1, b).some(l => l.type === 'core' || l.type === 'prepreg'))
134
+ throw new Error(`stackup has no core or prepreg between ${cu[k].name} and ${cu[k + 1].name}`);
135
+ }
136
+ if (s.finish !== undefined && !FINISHES.has(s.finish))
137
+ throw new Error(`stackup finish ${JSON.stringify(s.finish)} is not one of ${[...FINISHES].join(', ')}`);
138
+ if (s.thickness !== undefined) {
139
+ if (!(s.thickness > 0))
140
+ throw new Error(`stackup thickness must be positive, got ${s.thickness}`);
141
+ const sum = stackupSum(s);
142
+ if (Math.abs(sum - s.thickness) > 0.1 * s.thickness)
143
+ throw new Error(`stackup layers add up to ${sum} mm, more than 10% off its thickness ${s.thickness} mm`);
144
+ }
145
+ return n;
146
+ }
147
+ function checkRules(r, where) {
148
+ if (r === undefined || r === null)
149
+ return {};
150
+ if (typeof r !== 'object')
151
+ throw new Error(`${where} rules must be an object`);
152
+ for (const [k, v] of Object.entries(r)) {
153
+ if (!RULE_KEYS.has(k))
154
+ throw new Error(`${where} rules: unknown rule ${JSON.stringify(k)} (known: ${[...RULE_KEYS].join(', ')})`);
155
+ if (k === 'thermal') {
156
+ checkThermal(v, where);
157
+ continue;
158
+ }
159
+ if (k === 'islands') {
160
+ checkIslands(v, `${where} rules`);
161
+ continue;
162
+ }
163
+ if (k === 'edge') {
164
+ checkEdgeBand(v, `${where} rules`);
165
+ continue;
166
+ }
167
+ if (k === 'maskWebByColor') {
168
+ if (!v || typeof v !== 'object' || Array.isArray(v) || Object.values(v).some(x => !(typeof x === 'number' && x >= 0 && Number.isFinite(x))))
169
+ throw new Error(`${where} rules: maskWebByColor must map mask colours to mm, such as { Black: 0.13 }, got ${JSON.stringify(v)}`);
170
+ continue;
171
+ }
172
+ if ((k === 'holeClearance' || k === 'minHoleToHole') && v && typeof v === 'object' && !Array.isArray(v)) {
173
+ const sides = k === 'holeClearance' ? ['inner', 'outer', 'via'] : ['pad', 'via'];
174
+ const ks = Object.keys(v).filter(s => v[s] !== undefined);
175
+ if (!ks.length || ks.some(s => !sides.includes(s) || !(typeof v[s] === 'number' && v[s] >= 0 && Number.isFinite(v[s]))))
176
+ throw new Error(`${where} rules: ${k} must be a number of mm or { ${sides.join(', ')} } of mm, got ${JSON.stringify(v)}`);
177
+ continue;
178
+ }
179
+ if (FLAG_KEYS.has(k)) {
180
+ if (v !== undefined && typeof v !== 'boolean')
181
+ throw new Error(`${where} rules: ${k} must be true or false, got ${JSON.stringify(v)}`);
182
+ continue;
183
+ }
184
+ if (v !== undefined && !(typeof v === 'number' && v >= 0 && Number.isFinite(v)))
185
+ throw new Error(`${where} rules: ${k} must be a number of mm, got ${JSON.stringify(v)}`);
186
+ }
187
+ return r;
188
+ }
189
+ /** Check an island mode: 'remove', 'keep' or { minArea } in mm². Throws naming `where`. */
190
+ export function checkIslands(v, where) {
191
+ if (v === undefined || v === 'remove' || v === 'keep')
192
+ return;
193
+ if (v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).every(k => k === 'minArea') && typeof v.minArea === 'number' && v.minArea >= 0 && Number.isFinite(v.minArea))
194
+ return;
195
+ throw new Error(`${where}: islands must be 'remove', 'keep' or { minArea } in mm², got ${JSON.stringify(v)}`);
196
+ }
197
+ const EDGE_KINDS = ['courtyard', 'silk', 'pad'];
198
+ /** Check an edge band: { width, kinds?, except? }, see EdgeBand. Throws naming `where`. */
199
+ export function checkEdgeBand(v, where) {
200
+ if (v === undefined)
201
+ return;
202
+ if (!v || typeof v !== 'object' || Array.isArray(v))
203
+ throw new Error(`${where}: edge must be an object such as { width: 1.2, except: [J1] }`);
204
+ for (const [k, x] of Object.entries(v)) {
205
+ if (x === undefined)
206
+ continue;
207
+ if (k === 'width') {
208
+ if (!(typeof x === 'number' && x >= 0 && Number.isFinite(x)))
209
+ throw new Error(`${where}: edge.width must be a number of mm, got ${JSON.stringify(x)}`);
210
+ continue;
211
+ }
212
+ if (k === 'kinds') {
213
+ if (!Array.isArray(x) || x.some(q => !EDGE_KINDS.includes(q)))
214
+ throw new Error(`${where}: edge.kinds must list ${EDGE_KINDS.map(q => `'${q}'`).join(', ')}, got ${JSON.stringify(x)}`);
215
+ continue;
216
+ }
217
+ if (k === 'except') {
218
+ // a Part is a function, a handle an object branded 'pin', a ref a string such as 'J1.3'
219
+ const ok = (q) => (typeof q === 'string' && q.trim() !== '') || typeof q === 'function' || (!!q && typeof q === 'object' && q.$$ === 'pin');
220
+ if (!Array.isArray(x) || !x.every(ok))
221
+ throw new Error(`${where}: edge.except must list parts, pads such as J1.pad(3), or refs such as 'J1' and 'J1.3'`);
222
+ continue;
223
+ }
224
+ throw new Error(`${where}: edge has no key ${JSON.stringify(k)} (known: width, kinds, except)`);
225
+ }
226
+ }
227
+ function checkThermal(v, where) {
228
+ if (v === undefined)
229
+ return;
230
+ if (!v || typeof v !== 'object' || Array.isArray(v))
231
+ throw new Error(`${where} rules: thermal must be an object such as { gap: 0.5, spokeWidth: 0.5 }`);
232
+ for (const [k, x] of Object.entries(v)) {
233
+ if (x === undefined)
234
+ continue;
235
+ if (!(k in THERMAL))
236
+ throw new Error(`${where} rules: thermal has no key ${JSON.stringify(k)} (known: ${Object.keys(THERMAL).join(', ')})`);
237
+ if (k === 'angle' ? x !== 90 && x !== 45 : k === 'spokes' || k === 'minSpokes' ? !(Number.isInteger(x) && x >= 0 && x <= 16) : !(typeof x === 'number' && x > 0 && Number.isFinite(x)))
238
+ throw new Error(`${where} rules: thermal.${k} ${k === 'angle' ? 'must be 90 or 45' : k === 'spokes' || k === 'minSpokes' ? 'must be a whole number from 0 to 16' : 'must be a positive number of mm'}, got ${JSON.stringify(x)}`);
239
+ }
240
+ }
241
+ function checkSourcing(v, where) {
242
+ if (!Array.isArray(v) || v.some(t => typeof t !== 'string' || !t.trim() || t.includes(':')))
243
+ throw new Error(`${where} sourcing must be a list of tags such as ['LCSC', 'CN'], got ${JSON.stringify(v)}`);
244
+ return v.map(t => t.trim());
245
+ }
246
+ function checkFabNotes(v, where) {
247
+ if (!Array.isArray(v) || v.some(t => typeof t !== 'string'))
248
+ throw new Error(`${where} fabNotes must be a list of lines such as ['Single board, no panel.'], got ${JSON.stringify(v)}`);
249
+ return v.flatMap(t => t.split(/\r?\n/)).map(t => t.trim()).filter(Boolean);
250
+ }
251
+ const has = (o, k) => o[k] !== undefined && o[k] !== null;
252
+ /** Resolve levels over the base, lowest first; the highest level giving `copper` or `stackup` decides the layers, and a `thickness` must agree. */
253
+ export function resolveBoardSpec(levels, levelNames) {
254
+ const names = levelNames ?? (levels.length === 1 ? ['default'] : levels.map((_, i) => (i === levels.length - 1 ? '<PCB>' : 'default')));
255
+ const rules = resolveRules(mergeRules(BASE, ...levels.map((l, i) => checkRules(l.rules, names[i]))));
256
+ const autoroute = mergeAutoroute(BASE_AUTOROUTE, ...levels.map((l, i) => checkAutoroute(l.autoroute, names[i])));
257
+ const classes = resolveClasses(mergeClasses(...levels.map((l, i) => checkClasses(l.classes, names[i]))));
258
+ let title, revision = '', sourcing = [], cpl;
259
+ const fabNotes = [];
260
+ levels.forEach((l, i) => {
261
+ if (has(l, 'fabNotes'))
262
+ for (const n of checkFabNotes(l.fabNotes, names[i]))
263
+ if (!fabNotes.includes(n))
264
+ fabNotes.push(n);
265
+ if (has(l, 'cpl')) {
266
+ const o = l.cpl?.origin;
267
+ if (typeof l.cpl !== 'object' || (o !== undefined && o !== 'footprint' && o !== 'center'))
268
+ throw new Error(`${names[i]} cpl must be { origin: 'footprint' } or { origin: 'center' }, got ${JSON.stringify(l.cpl)}`);
269
+ if (o !== undefined)
270
+ cpl = { origin: o };
271
+ }
272
+ if (has(l, 'title'))
273
+ title = String(l.title);
274
+ if (has(l, 'revision'))
275
+ revision = String(l.revision);
276
+ if (has(l, 'sourcing'))
277
+ sourcing = checkSourcing(l.sourcing, names[i]);
278
+ });
279
+ let top = -1;
280
+ levels.forEach((l, i) => { if (has(l, 'copper') || has(l, 'stackup'))
281
+ top = i; });
282
+ let thickAt = -1, thick;
283
+ levels.forEach((l, i) => { if (has(l, 'thickness')) {
284
+ thickAt = i;
285
+ thick = mm(l.thickness);
286
+ } });
287
+ if (thick !== undefined && !(thick > 0))
288
+ throw new Error(`thickness must be positive, got ${thick}`);
289
+ const lv = top >= 0 ? levels[top] : {};
290
+ let copper, stackup, thickness;
291
+ if (has(lv, 'stackup')) {
292
+ const where = `${names[top]} stackup`;
293
+ try {
294
+ copper = validateStackup(lv.stackup);
295
+ }
296
+ catch (e) {
297
+ throw new Error(`${where}: ${e.message}`);
298
+ }
299
+ if (has(lv, 'copper') && Number(lv.copper) !== copper)
300
+ throw new Error(`${names[top]}: copper={${lv.copper}} disagrees with the stackup, which has ${copper} copper layers`);
301
+ stackup = structuredClone(lv.stackup);
302
+ thickness = stackupThickness(stackup);
303
+ if (thick !== undefined && thickAt >= top && Math.abs(thick - thickness) > 0.005)
304
+ throw new Error(`${names[thickAt]}: thickness ${thick} mm disagrees with the stackup, which is ${thickness} mm`);
305
+ }
306
+ else {
307
+ copper = has(lv, 'copper') ? Number(lv.copper) : 2;
308
+ if (![2, 4, 6, 8].includes(copper))
309
+ throw new Error(`${names[top] ?? '<PCB>'}: copper must be 2, 4, 6 or 8, got ${JSON.stringify(lv.copper)}`);
310
+ thickness = thick ?? 1.6;
311
+ stackup = defaultStackup(copper, thickness);
312
+ }
313
+ const teardrops = resolveTeardrops(levels.map((l, i) => (has(l, 'teardrops') ? checkTeardrops(l.teardrops, names[i]) : undefined)));
314
+ const out = { revision, copper: copper, thickness, stackup, rules, sourcing, autoroute, classes, fabNotes, teardrops };
315
+ if (title !== undefined)
316
+ out.title = title;
317
+ if (cpl)
318
+ out.cpl = cpl;
319
+ return out;
320
+ }
@@ -0,0 +1,96 @@
1
+ import type { At, Child, Net, NoConnect, PcbNode, Side } from './types.js';
2
+ import { type InstanceState } from './schematic.js';
3
+ type Required_<K extends string> = K extends `${string}?` ? never : K;
4
+ type Optional_<K extends string> = K extends `${infer N}?` ? N : never;
5
+ /** The ports of a circuit by name, `Ports<'VIN' | 'GND'>`; a name ending in `?` may be passed `NC`. */
6
+ export type Ports<K extends string = never> = {
7
+ readonly [N in Required_<K>]: Net;
8
+ } & {
9
+ readonly [N in Optional_<K>]: Net | NoConnect;
10
+ };
11
+ /** A proxy of nets: every key read gives the net of that name, the same object for the same key. */
12
+ export type NetsProxy = {
13
+ readonly [name: string]: Net;
14
+ };
15
+ /** The second argument of a build: the instance's private nets, such as `Buck1/FB`. */
16
+ export interface CircuitOwn {
17
+ nets: NetsProxy;
18
+ /** `nets` under the name `local()` has for components */
19
+ local: NetsProxy;
20
+ }
21
+ /** What a circuit holds: parts and nested circuit instances or groups, by key. */
22
+ export type CircuitMembers = {
23
+ readonly [key: string]: {
24
+ readonly $$: 'part';
25
+ } | GroupBase;
26
+ };
27
+ /** The fixed properties of every circuit instance and every group. */
28
+ export interface GroupBase {
29
+ readonly $$: 'circuit';
30
+ /** given by `named()`, the call, group() or its key in another circuit, else the circuit name plus a counter */
31
+ readonly name: string;
32
+ /** the circuit() name, or "group" for a group */
33
+ readonly circuit: string;
34
+ /** the instance path, outermost first, such as ['Power', 'LDO'] */
35
+ readonly path: readonly string[];
36
+ }
37
+ /** A group: the object given to group() plus `name`, `circuit` and `path`. */
38
+ export type Group<T extends object> = T & GroupBase;
39
+ /** The fixed properties of every circuit instance. */
40
+ export interface CircuitInstanceBase<P = {}> extends GroupBase {
41
+ /** the nets the caller connected to the ports */
42
+ readonly ports: Readonly<P>;
43
+ /** private nets, `Buck1.nets.FB` is `Buck1/FB` */
44
+ readonly nets: NetsProxy;
45
+ }
46
+ /** A circuit instance: the members by key, `Buck1.R1`, plus `name`, `circuit`, `path`, `ports` and `nets`. */
47
+ export type CircuitInstance<T extends CircuitMembers = CircuitMembers, P = {}> = CircuitInstanceBase<P> & {
48
+ readonly [K in keyof T]: T[K];
49
+ };
50
+ type CallArgs<P> = {} extends P ? [ports?: P, name?: string] : [ports: P, name?: string];
51
+ /** A circuit definition: call it with the port nets and an optional name to make an instance. */
52
+ export interface Circuit<P = {}, T extends CircuitMembers = CircuitMembers> {
53
+ (...args: CallArgs<P>): CircuitInstance<T, P>;
54
+ /** the same circuit naming its instance: `buck.named('Buck1')({ VIN, VOUT, GND })` */
55
+ named(name: string): {} extends P ? (ports?: P) => CircuitInstance<T, P> : (ports: P) => CircuitInstance<T, P>;
56
+ /** the circuit() name */
57
+ readonly name: string;
58
+ readonly $$: 'circuit-def';
59
+ }
60
+ /** The props of a layout: where it goes, and anything else the render function takes. */
61
+ export interface LayoutProps {
62
+ at?: At;
63
+ side?: Side;
64
+ [k: string]: any;
65
+ }
66
+ /** The instance type of a circuit definition. */
67
+ export type InstanceOf<C> = C extends Circuit<infer P, infer T> ? CircuitInstance<T, P> : C extends GroupBase ? C : never;
68
+ /** A component that places one instance, `<BuckCompact g={Buck1} at="10 10" />`. */
69
+ export interface Layout<I = CircuitInstance<any, any>> {
70
+ (props: {
71
+ g: I;
72
+ } & LayoutProps): PcbNode;
73
+ /** the layout name, which the viewer shows */
74
+ readonly name: string;
75
+ /** the circuit() name it lays out */
76
+ readonly circuit: string;
77
+ }
78
+ /** Define a circuit with ports and private nets, `circuit('buck', (ports, { nets }) => ({ U1, R1 }))`, or the object form for parts made beforehand. */
79
+ export declare function circuit<P extends object = {}, T extends CircuitMembers = CircuitMembers>(name: string, build: (ports: P, own: CircuitOwn) => T): Circuit<P, T>;
80
+ export declare function circuit<T extends CircuitMembers>(name: string, members: T): CircuitInstance<T, {}>;
81
+ /** Name the parts of a plain object, `group('P1', { J, SW })`, so findings say `P1/SW`; no ports, refs stay flat, returns the object. */
82
+ export declare function group<T extends object>(name: string, members: T): Group<T>;
83
+ /** True for a circuit instance. */
84
+ export declare const isCircuitInstance: (v: unknown) => v is CircuitInstance;
85
+ /** True for a circuit definition made by circuit() with a build function. */
86
+ export declare const isCircuit: (v: unknown) => v is Circuit<any, any>;
87
+ /** The marker the flattener reads on a <g> a layout renders. */
88
+ export declare const LAYOUT_PROP = "__layout";
89
+ export interface LayoutMark {
90
+ inst: InstanceState;
91
+ name: string;
92
+ src?: unknown;
93
+ }
94
+ /** A component placing one instance, `layout(buck, (g, { at }) => <g at={at}>…</g>)`; the name defaults to the render function's. */
95
+ export declare function layout<C extends Circuit<any, any> | GroupBase>(target: C, render: (g: InstanceOf<C>, props: LayoutProps) => Child, name?: string): Layout<InstanceOf<C>>;
96
+ export {};