@tscircuit/checks 0.0.196 → 0.0.197
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 +2 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +904 -534
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,711 @@
|
|
|
1
|
+
// lib/check-copper-pour-shorts.ts
|
|
2
|
+
import Flatbush from "flatbush";
|
|
3
|
+
import "@flatten-js/core";
|
|
4
|
+
import { getReadableNameForElement } from "@tscircuit/circuit-json-util";
|
|
5
|
+
|
|
6
|
+
// node_modules/@tscircuit/circuit-json-to-flattenjs/dist/index.js
|
|
7
|
+
import * as F from "@flatten-js/core";
|
|
8
|
+
import { Vector as Vector2 } from "@flatten-js/core";
|
|
9
|
+
import { BooleanOperations } from "@flatten-js/core";
|
|
10
|
+
import { Arc as Arc2 } from "@flatten-js/core";
|
|
11
|
+
var EPS = 1e-9;
|
|
12
|
+
var point = (p) => new F.Point(p.x, p.y);
|
|
13
|
+
function positive(value, name) {
|
|
14
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
15
|
+
throw new Error(`${name} must be finite and positive`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function circle(center, radius) {
|
|
19
|
+
positive(radius, "radius");
|
|
20
|
+
return new F.Polygon(new F.Circle(point(center), radius));
|
|
21
|
+
}
|
|
22
|
+
function ring(vertices) {
|
|
23
|
+
const clean = vertices.filter(
|
|
24
|
+
(p, i) => i === 0 || point(p).distanceTo(point(vertices[i - 1]))[0] > EPS
|
|
25
|
+
);
|
|
26
|
+
if (clean.length > 1 && point(clean[0]).distanceTo(point(clean.at(-1)))[0] <= EPS)
|
|
27
|
+
clean.pop();
|
|
28
|
+
if (clean.length < 2)
|
|
29
|
+
throw new Error("A ring needs at least two distinct vertices");
|
|
30
|
+
const edges = clean.map((a, i) => {
|
|
31
|
+
const b = clean[(i + 1) % clean.length];
|
|
32
|
+
if (![a.x, a.y, a.bulge ?? 0].every(Number.isFinite))
|
|
33
|
+
throw new Error("Non-finite ring vertex");
|
|
34
|
+
const bulge = a.bulge ?? 0;
|
|
35
|
+
if (Math.abs(bulge) <= EPS) return new F.Segment(point(a), point(b));
|
|
36
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
37
|
+
const length = Math.hypot(dx, dy);
|
|
38
|
+
const offset = length * (1 - bulge * bulge) / (4 * bulge);
|
|
39
|
+
const center = new F.Point(
|
|
40
|
+
(a.x + b.x) / 2 - dy / length * offset,
|
|
41
|
+
(a.y + b.y) / 2 + dx / length * offset
|
|
42
|
+
);
|
|
43
|
+
return new F.Arc(
|
|
44
|
+
center,
|
|
45
|
+
length * (1 + bulge * bulge) / (4 * Math.abs(bulge)),
|
|
46
|
+
Math.atan2(a.y - center.y, a.x - center.x),
|
|
47
|
+
Math.atan2(b.y - center.y, b.x - center.x),
|
|
48
|
+
bulge > 0
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
const polygon = new F.Polygon();
|
|
52
|
+
polygon.addFace(edges);
|
|
53
|
+
if (polygon.area() <= EPS)
|
|
54
|
+
throw new Error("A ring must enclose a non-zero area");
|
|
55
|
+
return polygon;
|
|
56
|
+
}
|
|
57
|
+
function rectangle(center, width, height, rotation = 0, radius = 0) {
|
|
58
|
+
positive(width, "width");
|
|
59
|
+
positive(height, "height");
|
|
60
|
+
if (!Number.isFinite(rotation) || !Number.isFinite(radius) || radius < 0)
|
|
61
|
+
throw new Error("Invalid rectangle rotation or corner radius");
|
|
62
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
63
|
+
const x = width / 2, y = height / 2;
|
|
64
|
+
let polygon;
|
|
65
|
+
if (r <= EPS)
|
|
66
|
+
polygon = ring([
|
|
67
|
+
{ x: -x, y: -y },
|
|
68
|
+
{ x, y: -y },
|
|
69
|
+
{ x, y },
|
|
70
|
+
{ x: -x, y }
|
|
71
|
+
]);
|
|
72
|
+
else {
|
|
73
|
+
const corners = [
|
|
74
|
+
new F.Point(x - r, -y + r),
|
|
75
|
+
new F.Point(x - r, y - r),
|
|
76
|
+
new F.Point(-x + r, y - r),
|
|
77
|
+
new F.Point(-x + r, -y + r)
|
|
78
|
+
];
|
|
79
|
+
const edges = [];
|
|
80
|
+
for (let i = 0; i < 4; i++) {
|
|
81
|
+
const angle = -Math.PI / 2 + i * Math.PI / 2;
|
|
82
|
+
const arc = new F.Arc(corners[i], r, angle, angle + Math.PI / 2, true);
|
|
83
|
+
const next = corners[(i + 1) % 4];
|
|
84
|
+
const nextStart = new F.Point(
|
|
85
|
+
next.x + r * Math.cos(angle + Math.PI / 2),
|
|
86
|
+
next.y + r * Math.sin(angle + Math.PI / 2)
|
|
87
|
+
);
|
|
88
|
+
edges.push(arc);
|
|
89
|
+
if (arc.end.distanceTo(nextStart)[0] > EPS)
|
|
90
|
+
edges.push(new F.Segment(arc.end, nextStart));
|
|
91
|
+
}
|
|
92
|
+
polygon = new F.Polygon();
|
|
93
|
+
polygon.addFace(edges);
|
|
94
|
+
}
|
|
95
|
+
return polygon.rotate(rotation * Math.PI / 180).translate(new F.Vector(center.x, center.y));
|
|
96
|
+
}
|
|
97
|
+
function stroke(a, b, widthA, widthB = widthA) {
|
|
98
|
+
const r1 = positive(widthA, "trace width") / 2, r2 = positive(widthB, "trace width") / 2;
|
|
99
|
+
const d = point(a).distanceTo(point(b))[0];
|
|
100
|
+
if (d <= Math.abs(r1 - r2) + EPS)
|
|
101
|
+
return circle(r1 >= r2 ? a : b, Math.max(r1, r2));
|
|
102
|
+
const theta = Math.atan2(b.y - a.y, b.x - a.x);
|
|
103
|
+
const alpha = Math.acos((r1 - r2) / d);
|
|
104
|
+
const low = theta - alpha, high = theta + alpha;
|
|
105
|
+
const p = (c, r, angle) => new F.Point(c.x + r * Math.cos(angle), c.y + r * Math.sin(angle));
|
|
106
|
+
const polygon = new F.Polygon();
|
|
107
|
+
polygon.addFace([
|
|
108
|
+
new F.Segment(p(a, r1, low), p(b, r2, low)),
|
|
109
|
+
new F.Arc(point(b), r2, low, high, true),
|
|
110
|
+
new F.Segment(p(b, r2, high), p(a, r1, high)),
|
|
111
|
+
new F.Arc(point(a), r1, high, low, true)
|
|
112
|
+
]);
|
|
113
|
+
return polygon;
|
|
114
|
+
}
|
|
115
|
+
function withHoles(outer, holes) {
|
|
116
|
+
const result = outer.clone();
|
|
117
|
+
const orientation = [...result.faces][0].orientation();
|
|
118
|
+
for (const hole of holes) {
|
|
119
|
+
const copy = hole.clone();
|
|
120
|
+
if ([...copy.faces][0].orientation() === orientation) copy.reverse();
|
|
121
|
+
for (const face of copy.faces) result.addFace(face.shapes);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
function ellipse(center, width, height, rotation = 0, tolerance = 1e-3) {
|
|
126
|
+
positive(width, "ellipse width");
|
|
127
|
+
positive(height, "ellipse height");
|
|
128
|
+
positive(tolerance, "curveTolerance");
|
|
129
|
+
if (Math.abs(width - height) < EPS) return circle(center, width / 2);
|
|
130
|
+
const radius = Math.max(width, height) / 2;
|
|
131
|
+
const count = Math.max(
|
|
132
|
+
16,
|
|
133
|
+
Math.ceil(Math.PI / Math.acos(Math.max(-1, 1 - tolerance / radius)))
|
|
134
|
+
);
|
|
135
|
+
if (count > 1e5)
|
|
136
|
+
throw new Error("curveTolerance requires more than 100000 ellipse segments");
|
|
137
|
+
return ring(
|
|
138
|
+
Array.from({ length: count }, (_, i) => ({
|
|
139
|
+
x: width / 2 * Math.cos(i * 2 * Math.PI / count),
|
|
140
|
+
y: height / 2 * Math.sin(i * 2 * Math.PI / count)
|
|
141
|
+
}))
|
|
142
|
+
).rotate(rotation * Math.PI / 180).translate(new F.Vector(center.x, center.y));
|
|
143
|
+
}
|
|
144
|
+
function getCopperLayers(json, options) {
|
|
145
|
+
if (options.copperLayers) {
|
|
146
|
+
if (!options.copperLayers.length || new Set(options.copperLayers).size !== options.copperLayers.length)
|
|
147
|
+
throw new Error(
|
|
148
|
+
"copperLayers must be a non-empty list without duplicates"
|
|
149
|
+
);
|
|
150
|
+
return [...options.copperLayers];
|
|
151
|
+
}
|
|
152
|
+
const board = json.find((e) => e.type === "pcb_board");
|
|
153
|
+
let count = board?.num_layers ?? 2;
|
|
154
|
+
for (const element of json) {
|
|
155
|
+
const layers = "layers" in element ? element.layers : "layer" in element ? [element.layer] : [];
|
|
156
|
+
if (Array.isArray(layers))
|
|
157
|
+
for (const layer of layers) {
|
|
158
|
+
const match = typeof layer === "string" && /^inner(\d+)$/.exec(layer);
|
|
159
|
+
if (match) count = Math.max(count, Number(match[1]) + 2);
|
|
160
|
+
}
|
|
161
|
+
if (element.type === "pcb_trace")
|
|
162
|
+
for (const p of element.route) {
|
|
163
|
+
for (const layer of p.route_type === "wire" ? [p.layer] : p.route_type === "via" ? [p.from_layer, p.to_layer] : [p.start_layer, p.end_layer]) {
|
|
164
|
+
const match = /^inner(\d+)$/.exec(layer);
|
|
165
|
+
if (match) count = Math.max(count, Number(match[1]) + 2);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!Number.isInteger(count) || count < 1 || count > 10)
|
|
170
|
+
throw new Error("Supported copper layer count is 1\u201310");
|
|
171
|
+
return count === 1 ? ["top"] : [
|
|
172
|
+
"top",
|
|
173
|
+
...Array.from(
|
|
174
|
+
{ length: count - 2 },
|
|
175
|
+
(_, i) => `inner${i + 1}`
|
|
176
|
+
),
|
|
177
|
+
"bottom"
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
function spanLayers(layers, stack) {
|
|
181
|
+
if (!layers.length) return [];
|
|
182
|
+
const indices = layers.map((layer) => stack.indexOf(layer));
|
|
183
|
+
if (indices.some((i) => i < 0))
|
|
184
|
+
throw new Error(`Layer is missing from copperLayers: ${layers.join(", ")}`);
|
|
185
|
+
return stack.slice(Math.min(...indices), Math.max(...indices) + 1);
|
|
186
|
+
}
|
|
187
|
+
function smtPad(pad) {
|
|
188
|
+
switch (pad.shape) {
|
|
189
|
+
case "circle":
|
|
190
|
+
return circle(pad, pad.radius);
|
|
191
|
+
case "polygon":
|
|
192
|
+
return ring(pad.points);
|
|
193
|
+
case "rect":
|
|
194
|
+
case "rotated_rect":
|
|
195
|
+
return rectangle(
|
|
196
|
+
pad,
|
|
197
|
+
pad.width,
|
|
198
|
+
pad.height,
|
|
199
|
+
"ccw_rotation" in pad ? pad.ccw_rotation : 0,
|
|
200
|
+
pad.rect_border_radius ?? pad.corner_radius ?? 0
|
|
201
|
+
);
|
|
202
|
+
case "pill":
|
|
203
|
+
case "rotated_pill":
|
|
204
|
+
return rectangle(
|
|
205
|
+
pad,
|
|
206
|
+
pad.width,
|
|
207
|
+
pad.height,
|
|
208
|
+
"ccw_rotation" in pad ? pad.ccw_rotation : 0,
|
|
209
|
+
pad.radius
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function nonPlatedHole(hole, tolerance) {
|
|
214
|
+
switch (hole.hole_shape) {
|
|
215
|
+
case "circle":
|
|
216
|
+
return circle(hole, hole.hole_diameter / 2);
|
|
217
|
+
case "square":
|
|
218
|
+
return rectangle(hole, hole.hole_diameter, hole.hole_diameter);
|
|
219
|
+
case "rect":
|
|
220
|
+
return rectangle(hole, hole.hole_width, hole.hole_height);
|
|
221
|
+
case "oval":
|
|
222
|
+
return ellipse(hole, hole.hole_width, hole.hole_height, 0, tolerance);
|
|
223
|
+
case "pill":
|
|
224
|
+
case "rotated_pill":
|
|
225
|
+
return rectangle(
|
|
226
|
+
hole,
|
|
227
|
+
hole.hole_width,
|
|
228
|
+
hole.hole_height,
|
|
229
|
+
"ccw_rotation" in hole ? hole.ccw_rotation : 0,
|
|
230
|
+
Math.min(hole.hole_width, hole.hole_height) / 2
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function platedHole(hole, componentRotation, includeDrill, tolerance) {
|
|
235
|
+
let outer;
|
|
236
|
+
let drill;
|
|
237
|
+
const drillCenter = {
|
|
238
|
+
x: hole.x + ("hole_offset_x" in hole ? hole.hole_offset_x ?? 0 : 0),
|
|
239
|
+
y: hole.y + ("hole_offset_y" in hole ? hole.hole_offset_y ?? 0 : 0)
|
|
240
|
+
};
|
|
241
|
+
switch (hole.shape) {
|
|
242
|
+
case "circle":
|
|
243
|
+
outer = circle(hole, hole.outer_diameter / 2);
|
|
244
|
+
drill = circle(hole, hole.hole_diameter / 2);
|
|
245
|
+
break;
|
|
246
|
+
case "oval":
|
|
247
|
+
outer = ellipse(
|
|
248
|
+
hole,
|
|
249
|
+
hole.outer_width,
|
|
250
|
+
hole.outer_height,
|
|
251
|
+
hole.ccw_rotation,
|
|
252
|
+
tolerance
|
|
253
|
+
);
|
|
254
|
+
drill = ellipse(
|
|
255
|
+
hole,
|
|
256
|
+
hole.hole_width,
|
|
257
|
+
hole.hole_height,
|
|
258
|
+
hole.ccw_rotation,
|
|
259
|
+
tolerance
|
|
260
|
+
);
|
|
261
|
+
break;
|
|
262
|
+
case "pill":
|
|
263
|
+
outer = rectangle(
|
|
264
|
+
hole,
|
|
265
|
+
hole.outer_width,
|
|
266
|
+
hole.outer_height,
|
|
267
|
+
hole.ccw_rotation,
|
|
268
|
+
Math.min(hole.outer_width, hole.outer_height) / 2
|
|
269
|
+
);
|
|
270
|
+
drill = rectangle(
|
|
271
|
+
hole,
|
|
272
|
+
hole.hole_width,
|
|
273
|
+
hole.hole_height,
|
|
274
|
+
hole.ccw_rotation,
|
|
275
|
+
Math.min(hole.hole_width, hole.hole_height) / 2
|
|
276
|
+
);
|
|
277
|
+
break;
|
|
278
|
+
case "circular_hole_with_rect_pad":
|
|
279
|
+
case "pill_hole_with_rect_pad":
|
|
280
|
+
case "rotated_pill_hole_with_rect_pad":
|
|
281
|
+
outer = rectangle(
|
|
282
|
+
hole,
|
|
283
|
+
hole.rect_pad_width,
|
|
284
|
+
hole.rect_pad_height,
|
|
285
|
+
"rect_ccw_rotation" in hole ? hole.rect_ccw_rotation : 0,
|
|
286
|
+
hole.rect_border_radius ?? 0
|
|
287
|
+
);
|
|
288
|
+
drill = hole.shape === "circular_hole_with_rect_pad" ? circle(drillCenter, hole.hole_diameter / 2) : rectangle(
|
|
289
|
+
drillCenter,
|
|
290
|
+
hole.hole_width,
|
|
291
|
+
hole.hole_height,
|
|
292
|
+
"hole_ccw_rotation" in hole ? hole.hole_ccw_rotation : 0,
|
|
293
|
+
Math.min(hole.hole_width, hole.hole_height) / 2
|
|
294
|
+
);
|
|
295
|
+
break;
|
|
296
|
+
case "hole_with_polygon_pad": {
|
|
297
|
+
const rotation = hole.ccw_rotation ?? componentRotation;
|
|
298
|
+
outer = ring(hole.pad_outline).rotate(rotation * Math.PI / 180).translate(new Vector2(hole.x, hole.y));
|
|
299
|
+
drill = hole.hole_shape === "circle" ? circle(drillCenter, hole.hole_diameter / 2) : hole.hole_shape === "oval" ? ellipse(
|
|
300
|
+
drillCenter,
|
|
301
|
+
hole.hole_width,
|
|
302
|
+
hole.hole_height,
|
|
303
|
+
rotation,
|
|
304
|
+
tolerance
|
|
305
|
+
) : rectangle(
|
|
306
|
+
drillCenter,
|
|
307
|
+
hole.hole_width,
|
|
308
|
+
hole.hole_height,
|
|
309
|
+
rotation,
|
|
310
|
+
Math.min(hole.hole_width, hole.hole_height) / 2
|
|
311
|
+
);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return includeDrill ? withHoles(outer, [drill]) : outer;
|
|
316
|
+
}
|
|
317
|
+
var samePoint = (a, b) => a.x === b.x && a.y === b.y;
|
|
318
|
+
var unit = (x, y) => {
|
|
319
|
+
const length = Math.hypot(x, y);
|
|
320
|
+
return length ? { x: x / length, y: y / length } : { x: 1, y: 0 };
|
|
321
|
+
};
|
|
322
|
+
function interpolatedPolygon(segments) {
|
|
323
|
+
const points = [
|
|
324
|
+
{ ...segments[0].start, width: segments[0].startWidth },
|
|
325
|
+
...segments.map((s) => ({ ...s.end, width: s.endWidth }))
|
|
326
|
+
];
|
|
327
|
+
const directions = segments.map(
|
|
328
|
+
(s) => unit(s.end.x - s.start.x, s.end.y - s.start.y)
|
|
329
|
+
);
|
|
330
|
+
const left = [], right = [];
|
|
331
|
+
for (let i = 0; i < points.length; i++) {
|
|
332
|
+
const p = points[i];
|
|
333
|
+
positive(p.width, "interpolated trace width");
|
|
334
|
+
const before = directions[Math.max(0, i - 1)], after = directions[Math.min(i, directions.length - 1)];
|
|
335
|
+
const d = unit(before.x + after.x, before.y + after.y);
|
|
336
|
+
const nx = -d.y * p.width / 2, ny = d.x * p.width / 2;
|
|
337
|
+
left.push({ x: p.x + nx, y: p.y + ny });
|
|
338
|
+
right.push({ x: p.x - nx, y: p.y - ny });
|
|
339
|
+
}
|
|
340
|
+
return ring([...left, ...right.reverse()]);
|
|
341
|
+
}
|
|
342
|
+
function traceGeometry(trace, stack, includeDrill) {
|
|
343
|
+
const result = /* @__PURE__ */ new Map();
|
|
344
|
+
const drills = /* @__PURE__ */ new Map();
|
|
345
|
+
const segments = [];
|
|
346
|
+
const add = (layer, shape) => result.set(layer, [...result.get(layer) ?? [], shape]);
|
|
347
|
+
for (const p of trace.route) {
|
|
348
|
+
if (p.route_type === "via") {
|
|
349
|
+
const legacy = p;
|
|
350
|
+
const outerDiameter = p.outer_diameter ?? legacy.via_diameter;
|
|
351
|
+
if (outerDiameter === void 0) continue;
|
|
352
|
+
const holeDiameter = p.hole_diameter ?? legacy.via_hole_diameter;
|
|
353
|
+
const shape = circle(p, outerDiameter / 2);
|
|
354
|
+
const drill = includeDrill && holeDiameter ? circle(p, holeDiameter / 2) : void 0;
|
|
355
|
+
for (const layer of spanLayers([p.from_layer, p.to_layer], stack)) {
|
|
356
|
+
add(layer, drill ? withHoles(shape, [drill]) : shape);
|
|
357
|
+
if (drill) drills.set(layer, [...drills.get(layer) ?? [], drill]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (p.route_type === "through_pad") {
|
|
361
|
+
for (const layer of /* @__PURE__ */ new Set([p.start_layer, p.end_layer]))
|
|
362
|
+
add(layer, stroke(p.start, p.end, p.width));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (let i = 0; i < trace.route.length - 1; i++) {
|
|
366
|
+
const a = trace.route[i], b = trace.route[i + 1];
|
|
367
|
+
const start = a.route_type === "through_pad" ? a.end : a;
|
|
368
|
+
const end = b.route_type === "through_pad" ? b.start : b;
|
|
369
|
+
if (samePoint(start, end)) continue;
|
|
370
|
+
const startLayers = a.route_type === "wire" ? [a.layer] : a.route_type === "via" ? spanLayers([a.from_layer, a.to_layer], stack) : [a.end_layer];
|
|
371
|
+
const endLayers = b.route_type === "wire" ? [b.layer] : b.route_type === "via" ? spanLayers([b.from_layer, b.to_layer], stack) : [b.start_layer];
|
|
372
|
+
const sharedLayers = startLayers.filter(
|
|
373
|
+
(layer) => endLayers.includes(layer)
|
|
374
|
+
);
|
|
375
|
+
if (sharedLayers.length !== 1)
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Cannot determine one wire layer between route points ${i} and ${i + 1}`
|
|
378
|
+
);
|
|
379
|
+
const startLayer = sharedLayers[0];
|
|
380
|
+
const width = "width" in a ? a.width : "width" in b ? b.width : void 0;
|
|
381
|
+
if (width === void 0 || samePoint(start, end)) continue;
|
|
382
|
+
segments.push({
|
|
383
|
+
start,
|
|
384
|
+
end,
|
|
385
|
+
layer: startLayer,
|
|
386
|
+
startWidth: width,
|
|
387
|
+
endWidth: "width" in b ? b.width : width
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (trace.route_thickness_mode === "interpolated") {
|
|
391
|
+
let group = [];
|
|
392
|
+
const flush = () => {
|
|
393
|
+
if (group.length) {
|
|
394
|
+
add(group[0].layer, interpolatedPolygon(group));
|
|
395
|
+
group = [];
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
for (const segment of segments) {
|
|
399
|
+
const last = group.at(-1);
|
|
400
|
+
if (last && (last.layer !== segment.layer || !samePoint(last.end, segment.start) || last.endWidth !== segment.startWidth))
|
|
401
|
+
flush();
|
|
402
|
+
group.push(segment);
|
|
403
|
+
}
|
|
404
|
+
flush();
|
|
405
|
+
} else
|
|
406
|
+
for (const s of segments) add(s.layer, stroke(s.start, s.end, s.startWidth));
|
|
407
|
+
for (const [layer, holes] of drills) {
|
|
408
|
+
result.set(
|
|
409
|
+
layer,
|
|
410
|
+
result.get(layer).map(
|
|
411
|
+
(shape) => holes.reduce(
|
|
412
|
+
(shape2, hole) => BooleanOperations.subtract(shape2, hole),
|
|
413
|
+
shape
|
|
414
|
+
)
|
|
415
|
+
).filter((shape) => shape.faces.size > 0)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
var supportedElementTypes = [
|
|
421
|
+
"pcb_board",
|
|
422
|
+
"pcb_smtpad",
|
|
423
|
+
"pcb_plated_hole",
|
|
424
|
+
"pcb_hole",
|
|
425
|
+
"pcb_via",
|
|
426
|
+
"pcb_trace",
|
|
427
|
+
"pcb_copper_pour",
|
|
428
|
+
"pcb_cutout",
|
|
429
|
+
"pcb_keepout",
|
|
430
|
+
"pcb_courtyard_rect",
|
|
431
|
+
"pcb_courtyard_circle",
|
|
432
|
+
"pcb_courtyard_pill",
|
|
433
|
+
"pcb_courtyard_polygon",
|
|
434
|
+
"pcb_courtyard_outline"
|
|
435
|
+
];
|
|
436
|
+
function cutoutGeometry(cutout) {
|
|
437
|
+
switch (cutout.shape) {
|
|
438
|
+
case "circle":
|
|
439
|
+
return [circle(cutout.center, cutout.radius)];
|
|
440
|
+
case "rect":
|
|
441
|
+
return [
|
|
442
|
+
rectangle(
|
|
443
|
+
cutout.center,
|
|
444
|
+
cutout.width,
|
|
445
|
+
cutout.height,
|
|
446
|
+
cutout.rotation,
|
|
447
|
+
cutout.corner_radius
|
|
448
|
+
)
|
|
449
|
+
];
|
|
450
|
+
case "polygon":
|
|
451
|
+
return [ring(cutout.points)];
|
|
452
|
+
case "path": {
|
|
453
|
+
if (cutout.slot_length !== void 0 || cutout.space_between_slots !== void 0 || cutout.slot_corner_radius !== void 0) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
"Patterned/custom-corner path cutouts are not supported; use explicit rect/polygon cutouts"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return cutout.route.slice(1).map((p, i) => stroke(cutout.route[i], p, cutout.slot_width));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function convertCircuitJsonToFlattenJs(json, options = {}) {
|
|
463
|
+
if (options.layer && options.layers)
|
|
464
|
+
throw new Error("Specify layer or layers, not both");
|
|
465
|
+
const selectedLayers = options.layer ? [options.layer] : options.layers;
|
|
466
|
+
const stack = getCopperLayers(json, options);
|
|
467
|
+
const result = {
|
|
468
|
+
elements: [],
|
|
469
|
+
warnings: [],
|
|
470
|
+
bounds: void 0,
|
|
471
|
+
copperLayers: stack
|
|
472
|
+
};
|
|
473
|
+
const rotations = new Map(
|
|
474
|
+
json.filter((e) => e.type === "pcb_component").map((e) => [e.pcb_component_id, e.rotation])
|
|
475
|
+
);
|
|
476
|
+
const tolerance = options.curveTolerance ?? 1e-3;
|
|
477
|
+
if (!Number.isFinite(tolerance) || tolerance <= 0)
|
|
478
|
+
throw new Error("curveTolerance must be finite and positive");
|
|
479
|
+
const includeDrill = options.includeDrillHoles !== false;
|
|
480
|
+
for (const element of json) {
|
|
481
|
+
if (!supportedElementTypes.includes(element.type))
|
|
482
|
+
continue;
|
|
483
|
+
const elementId = element[`${element.type}_id`];
|
|
484
|
+
if (options.elementTypes && !options.elementTypes.includes(element.type))
|
|
485
|
+
continue;
|
|
486
|
+
if (options.elementIds && !options.elementIds.includes(elementId)) continue;
|
|
487
|
+
let role = "copper";
|
|
488
|
+
let layers = [null];
|
|
489
|
+
let shapes = [];
|
|
490
|
+
let byLayer;
|
|
491
|
+
try {
|
|
492
|
+
switch (element.type) {
|
|
493
|
+
case "pcb_smtpad":
|
|
494
|
+
layers = [element.layer];
|
|
495
|
+
shapes = [smtPad(element)];
|
|
496
|
+
break;
|
|
497
|
+
case "pcb_plated_hole":
|
|
498
|
+
layers = spanLayers(element.layers, stack);
|
|
499
|
+
shapes = [
|
|
500
|
+
platedHole(
|
|
501
|
+
element,
|
|
502
|
+
rotations.get(element.pcb_component_id ?? "") ?? 0,
|
|
503
|
+
includeDrill,
|
|
504
|
+
tolerance
|
|
505
|
+
)
|
|
506
|
+
];
|
|
507
|
+
break;
|
|
508
|
+
case "pcb_via": {
|
|
509
|
+
layers = spanLayers(element.layers, stack);
|
|
510
|
+
const outer = circle(element, element.outer_diameter / 2);
|
|
511
|
+
shapes = [
|
|
512
|
+
includeDrill ? withHoles(outer, [circle(element, element.hole_diameter / 2)]) : outer
|
|
513
|
+
];
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "pcb_trace":
|
|
517
|
+
byLayer = traceGeometry(element, stack, includeDrill);
|
|
518
|
+
layers = [...byLayer.keys()];
|
|
519
|
+
break;
|
|
520
|
+
case "pcb_copper_pour":
|
|
521
|
+
layers = [element.layer];
|
|
522
|
+
shapes = [
|
|
523
|
+
element.shape === "rect" ? rectangle(
|
|
524
|
+
element.center,
|
|
525
|
+
element.width,
|
|
526
|
+
element.height,
|
|
527
|
+
element.rotation
|
|
528
|
+
) : element.shape === "polygon" ? ring(element.points) : withHoles(
|
|
529
|
+
ring(element.brep_shape.outer_ring.vertices),
|
|
530
|
+
element.brep_shape.inner_rings.map(
|
|
531
|
+
(h) => ring(h.vertices)
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
];
|
|
535
|
+
break;
|
|
536
|
+
case "pcb_board":
|
|
537
|
+
role = "board";
|
|
538
|
+
shapes = [
|
|
539
|
+
element.outline?.length ? ring(element.outline) : rectangle(element.center, element.width, element.height)
|
|
540
|
+
];
|
|
541
|
+
break;
|
|
542
|
+
case "pcb_hole":
|
|
543
|
+
role = "drill";
|
|
544
|
+
shapes = [nonPlatedHole(element, tolerance)];
|
|
545
|
+
break;
|
|
546
|
+
case "pcb_cutout":
|
|
547
|
+
role = "cutout";
|
|
548
|
+
shapes = cutoutGeometry(element);
|
|
549
|
+
break;
|
|
550
|
+
case "pcb_keepout":
|
|
551
|
+
role = "keepout";
|
|
552
|
+
layers = element.layers;
|
|
553
|
+
shapes = [
|
|
554
|
+
element.shape === "circle" ? circle(element.center, element.radius) : element.shape === "rect" ? rectangle(element.center, element.width, element.height) : ring(element.outline)
|
|
555
|
+
];
|
|
556
|
+
break;
|
|
557
|
+
case "pcb_courtyard_rect":
|
|
558
|
+
role = "courtyard";
|
|
559
|
+
layers = [element.layer];
|
|
560
|
+
shapes = [
|
|
561
|
+
rectangle(
|
|
562
|
+
element.center,
|
|
563
|
+
element.width,
|
|
564
|
+
element.height,
|
|
565
|
+
element.ccw_rotation
|
|
566
|
+
)
|
|
567
|
+
];
|
|
568
|
+
break;
|
|
569
|
+
case "pcb_courtyard_circle":
|
|
570
|
+
role = "courtyard";
|
|
571
|
+
layers = [element.layer];
|
|
572
|
+
shapes = [circle(element.center, element.radius)];
|
|
573
|
+
break;
|
|
574
|
+
case "pcb_courtyard_pill":
|
|
575
|
+
role = "courtyard";
|
|
576
|
+
layers = [element.layer];
|
|
577
|
+
shapes = [
|
|
578
|
+
rectangle(
|
|
579
|
+
element.center,
|
|
580
|
+
element.width,
|
|
581
|
+
element.height,
|
|
582
|
+
0,
|
|
583
|
+
element.radius
|
|
584
|
+
)
|
|
585
|
+
];
|
|
586
|
+
break;
|
|
587
|
+
case "pcb_courtyard_polygon":
|
|
588
|
+
role = "courtyard";
|
|
589
|
+
layers = [element.layer];
|
|
590
|
+
shapes = [ring(element.points)];
|
|
591
|
+
break;
|
|
592
|
+
case "pcb_courtyard_outline":
|
|
593
|
+
role = "courtyard";
|
|
594
|
+
layers = [element.layer];
|
|
595
|
+
shapes = [ring(element.outline)];
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
if (options.roles && !options.roles.includes(role)) continue;
|
|
599
|
+
const convertedElements = [];
|
|
600
|
+
for (const layer of new Set(layers)) {
|
|
601
|
+
if (layer === null ? options.includeLayerless === false : selectedLayers && !selectedLayers.includes(layer))
|
|
602
|
+
continue;
|
|
603
|
+
const layerShapes = byLayer?.get(layer) ?? shapes;
|
|
604
|
+
if (!layerShapes.length) continue;
|
|
605
|
+
const bounds = layerShapes.reduce(
|
|
606
|
+
(box, shape) => box.merge(shape.box),
|
|
607
|
+
layerShapes[0].box
|
|
608
|
+
);
|
|
609
|
+
if (![bounds.xmin, bounds.ymin, bounds.xmax, bounds.ymax].every(
|
|
610
|
+
Number.isFinite
|
|
611
|
+
))
|
|
612
|
+
throw new Error("Non-finite geometry bounds");
|
|
613
|
+
const converted = {
|
|
614
|
+
elementId,
|
|
615
|
+
elementType: element.type,
|
|
616
|
+
sourceElement: element,
|
|
617
|
+
role,
|
|
618
|
+
layer,
|
|
619
|
+
shapes: layerShapes,
|
|
620
|
+
bounds
|
|
621
|
+
};
|
|
622
|
+
convertedElements.push(converted);
|
|
623
|
+
}
|
|
624
|
+
for (const converted of convertedElements) {
|
|
625
|
+
result.elements.push(converted);
|
|
626
|
+
result.bounds = result.bounds ? result.bounds.merge(converted.bounds) : converted.bounds;
|
|
627
|
+
}
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
630
|
+
if (options.strict) throw new Error(`${elementId}: ${message}`);
|
|
631
|
+
result.warnings.push({ elementId, elementType: element.type, message });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// lib/check-copper-pour-shorts.ts
|
|
638
|
+
import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map";
|
|
639
|
+
function touchesPour(pour, geometry) {
|
|
640
|
+
if (!pour.box.intersect(geometry.box)) return false;
|
|
641
|
+
return pour.intersect(geometry).length > 0 || [...geometry.faces].some((face) => pour.contains(face.first.start)) || [...pour.faces].some((face) => geometry.contains(face.first.start));
|
|
642
|
+
}
|
|
643
|
+
function checkCopperPourShorts(circuitJson) {
|
|
644
|
+
if (!circuitJson.some((e) => e.type === "pcb_copper_pour")) return [];
|
|
645
|
+
const connMap = getFullConnectivityMapFromCircuitJson(circuitJson);
|
|
646
|
+
const { elements: copper } = convertCircuitJsonToFlattenJs(circuitJson, {
|
|
647
|
+
elementTypes: [
|
|
648
|
+
"pcb_smtpad",
|
|
649
|
+
"pcb_plated_hole",
|
|
650
|
+
"pcb_via",
|
|
651
|
+
"pcb_trace",
|
|
652
|
+
"pcb_copper_pour"
|
|
653
|
+
],
|
|
654
|
+
strict: true
|
|
655
|
+
});
|
|
656
|
+
const layers = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const element of copper) {
|
|
658
|
+
const entries = layers.get(element.layer) ?? [];
|
|
659
|
+
for (const shape of element.shapes) entries.push({ element, shape });
|
|
660
|
+
layers.set(element.layer, entries);
|
|
661
|
+
}
|
|
662
|
+
const indexes = new Map(
|
|
663
|
+
[...layers].map(([layer, entries]) => {
|
|
664
|
+
const index = new Flatbush(entries.length);
|
|
665
|
+
for (const { shape } of entries) {
|
|
666
|
+
const b = shape.box;
|
|
667
|
+
index.add(b.xmin, b.ymin, b.xmax, b.ymax);
|
|
668
|
+
}
|
|
669
|
+
index.finish();
|
|
670
|
+
return [layer, { index, entries }];
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
const netNames = new Map(
|
|
674
|
+
circuitJson.filter((e) => e.type === "source_net").map((e) => [e.source_net_id, e.name])
|
|
675
|
+
);
|
|
676
|
+
const errors = /* @__PURE__ */ new Map();
|
|
677
|
+
for (const pour of copper) {
|
|
678
|
+
const element = pour.sourceElement;
|
|
679
|
+
if (element.type !== "pcb_copper_pour") continue;
|
|
680
|
+
const netId = element.source_net_id;
|
|
681
|
+
const { index, entries } = indexes.get(pour.layer);
|
|
682
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
683
|
+
for (const shape of pour.shapes) {
|
|
684
|
+
const b = shape.box;
|
|
685
|
+
for (const i of index.search(b.xmin, b.ymin, b.xmax, b.ymax))
|
|
686
|
+
candidates.add(i);
|
|
687
|
+
}
|
|
688
|
+
for (const i of [...candidates].sort((a, b) => a - b)) {
|
|
689
|
+
const { element: other, shape: otherShape } = entries[i];
|
|
690
|
+
if (pour.elementId === other.elementId) continue;
|
|
691
|
+
const otherNetId = other.sourceElement.type === "pcb_copper_pour" ? other.sourceElement.source_net_id : other.elementId;
|
|
692
|
+
if (netId && otherNetId && (netId === otherNetId || connMap.areIdsConnected(netId, otherNetId)))
|
|
693
|
+
continue;
|
|
694
|
+
const id = `copper_pour_short_${[pour.elementId, other.elementId].sort().join("_")}`;
|
|
695
|
+
if (errors.has(id) || !pour.shapes.some((shape) => touchesPour(shape, otherShape)))
|
|
696
|
+
continue;
|
|
697
|
+
errors.set(id, {
|
|
698
|
+
type: "pcb_placement_error",
|
|
699
|
+
pcb_placement_error_id: id,
|
|
700
|
+
error_type: "pcb_placement_error",
|
|
701
|
+
message: `Copper pour ${pour.elementId} (${netNames.get(netId) ?? netId ?? "unassigned net"}) shorts to ${getReadableNameForElement(circuitJson, other.elementId)} (${other.elementId}) on ${pour.layer} (accidental copper contact)`,
|
|
702
|
+
subcircuit_id: element.subcircuit_id
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return [...errors.values()];
|
|
707
|
+
}
|
|
708
|
+
|
|
1
709
|
// lib/check-traces-are-contiguous/is-point-in-pad.ts
|
|
2
710
|
import { pointToSegmentDistance } from "@tscircuit/math-utils";
|
|
3
711
|
|
|
@@ -9,11 +717,11 @@ import {
|
|
|
9
717
|
pointToSegmentClosestPoint,
|
|
10
718
|
segmentToSegmentMinDistance
|
|
11
719
|
} from "@tscircuit/math-utils";
|
|
12
|
-
var rotatePoint = (
|
|
720
|
+
var rotatePoint = (point2, angleDegrees) => {
|
|
13
721
|
const angle = angleDegrees * Math.PI / 180;
|
|
14
722
|
return {
|
|
15
|
-
x:
|
|
16
|
-
y:
|
|
723
|
+
x: point2.x * Math.cos(angle) - point2.y * Math.sin(angle),
|
|
724
|
+
y: point2.x * Math.sin(angle) + point2.y * Math.cos(angle)
|
|
17
725
|
};
|
|
18
726
|
};
|
|
19
727
|
var getRotatedRectPoints = ({
|
|
@@ -30,8 +738,8 @@ var getRotatedRectPoints = ({
|
|
|
30
738
|
{ x: halfWidth, y: -halfHeight },
|
|
31
739
|
{ x: halfWidth, y: halfHeight },
|
|
32
740
|
{ x: -halfWidth, y: halfHeight }
|
|
33
|
-
].map((
|
|
34
|
-
const rotated = rotatePoint(
|
|
741
|
+
].map((point2) => {
|
|
742
|
+
const rotated = rotatePoint(point2, ccwRotation);
|
|
35
743
|
return { x: x + rotated.x, y: y + rotated.y };
|
|
36
744
|
});
|
|
37
745
|
};
|
|
@@ -76,7 +784,7 @@ var getPolygonPointsForPad = (pad) => {
|
|
|
76
784
|
);
|
|
77
785
|
};
|
|
78
786
|
var getPolygonEdges = (points) => points.map(
|
|
79
|
-
(
|
|
787
|
+
(point2, index) => [point2, points[(index + 1) % points.length]]
|
|
80
788
|
);
|
|
81
789
|
var getClosestPointsBetweenSegments = (a1, a2, b1, b2) => {
|
|
82
790
|
const intersection = getSegmentIntersection(a1, a2, b1, b2);
|
|
@@ -127,7 +835,7 @@ var getSegmentToPolygonClearanceFromPoints = (start, end, polygon) => {
|
|
|
127
835
|
}
|
|
128
836
|
const intersections = getPolygonEdges(polygon).map(
|
|
129
837
|
([edgeStart, edgeEnd]) => getSegmentIntersection(start, end, edgeStart, edgeEnd)
|
|
130
|
-
).filter((
|
|
838
|
+
).filter((point2) => point2 !== null);
|
|
131
839
|
if (intersections.length > 0) {
|
|
132
840
|
const dx = end.x - start.x;
|
|
133
841
|
const dy = end.y - start.y;
|
|
@@ -179,17 +887,17 @@ var getSegmentToPolygonClearanceFromPoints = (start, end, polygon) => {
|
|
|
179
887
|
};
|
|
180
888
|
};
|
|
181
889
|
var getSegmentToPillClearance = (segment, pad) => {
|
|
182
|
-
const
|
|
890
|
+
const pill = getPillCenterLineForPad(pad);
|
|
183
891
|
const closest = getClosestPointsBetweenSegments(
|
|
184
892
|
{ x: segment.x1, y: segment.y1 },
|
|
185
893
|
{ x: segment.x2, y: segment.y2 },
|
|
186
|
-
|
|
187
|
-
|
|
894
|
+
pill.start,
|
|
895
|
+
pill.end
|
|
188
896
|
);
|
|
189
897
|
return {
|
|
190
898
|
distance: closest.distance,
|
|
191
899
|
center: closest.center,
|
|
192
|
-
radius:
|
|
900
|
+
radius: pill.radius,
|
|
193
901
|
tracePoint: closest.pointOnA,
|
|
194
902
|
obstaclePoint: closest.pointOnB
|
|
195
903
|
};
|
|
@@ -201,73 +909,73 @@ function getDistanceBetweenPoints(pointA, pointB) {
|
|
|
201
909
|
}
|
|
202
910
|
var POINT_ON_SEGMENT_TOLERANCE_MM = 1e-9;
|
|
203
911
|
var POINT_IN_PAD_TOLERANCE_MM = 1e-9;
|
|
204
|
-
function isPointOnSegment(
|
|
205
|
-
const crossProduct = (
|
|
912
|
+
function isPointOnSegment(point2, segment) {
|
|
913
|
+
const crossProduct = (point2.y - segment.start.y) * (segment.end.x - segment.start.x) - (point2.x - segment.start.x) * (segment.end.y - segment.start.y);
|
|
206
914
|
if (Math.abs(crossProduct) > POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
207
|
-
const dotProduct = (
|
|
915
|
+
const dotProduct = (point2.x - segment.start.x) * (segment.end.x - segment.start.x) + (point2.y - segment.start.y) * (segment.end.y - segment.start.y);
|
|
208
916
|
if (dotProduct < -POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
209
917
|
const squaredLength = (segment.end.x - segment.start.x) ** 2 + (segment.end.y - segment.start.y) ** 2;
|
|
210
918
|
return dotProduct <= squaredLength + POINT_ON_SEGMENT_TOLERANCE_MM;
|
|
211
919
|
}
|
|
212
|
-
function isPointInPolygon(
|
|
920
|
+
function isPointInPolygon(point2, polygon) {
|
|
213
921
|
let inside = false;
|
|
214
922
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
215
923
|
const pi = polygon[i];
|
|
216
924
|
const pj = polygon[j];
|
|
217
|
-
if (isPointOnSegment(
|
|
218
|
-
const intersects = pi.y >
|
|
925
|
+
if (isPointOnSegment(point2, { start: pi, end: pj })) return true;
|
|
926
|
+
const intersects = pi.y > point2.y !== pj.y > point2.y && point2.x < (pj.x - pi.x) * (point2.y - pi.y) / (pj.y - pi.y) + pi.x;
|
|
219
927
|
if (intersects) inside = !inside;
|
|
220
928
|
}
|
|
221
929
|
return inside;
|
|
222
930
|
}
|
|
223
|
-
function isPointInPad(
|
|
931
|
+
function isPointInPad(point2, pad) {
|
|
224
932
|
if (pad.type === "pcb_smtpad") {
|
|
225
933
|
if (pad.shape === "circle") {
|
|
226
|
-
return getDistanceBetweenPoints(
|
|
934
|
+
return getDistanceBetweenPoints(point2, pad) <= pad.radius + POINT_IN_PAD_TOLERANCE_MM;
|
|
227
935
|
}
|
|
228
936
|
if (pad.shape === "rect") {
|
|
229
937
|
const halfWidth = pad.width / 2;
|
|
230
938
|
const halfHeight = pad.height / 2;
|
|
231
|
-
return Math.abs(
|
|
939
|
+
return Math.abs(point2.x - pad.x) <= halfWidth + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point2.y - pad.y) <= halfHeight + POINT_IN_PAD_TOLERANCE_MM;
|
|
232
940
|
}
|
|
233
941
|
if (pad.shape === "rotated_rect") {
|
|
234
|
-
return isPointInPolygon(
|
|
942
|
+
return isPointInPolygon(point2, getPolygonPointsForPad(pad));
|
|
235
943
|
}
|
|
236
944
|
if (pad.shape === "pill" || pad.shape === "rotated_pill") {
|
|
237
945
|
if (pad.shape === "rotated_pill") {
|
|
238
|
-
const
|
|
239
|
-
return pointToSegmentDistance(
|
|
946
|
+
const pill = getPillCenterLineForPad(pad);
|
|
947
|
+
return pointToSegmentDistance(point2, pill.start, pill.end) <= pill.radius + POINT_IN_PAD_TOLERANCE_MM;
|
|
240
948
|
}
|
|
241
949
|
const halfWidth = pad.width / 2;
|
|
242
950
|
const halfHeight = pad.height / 2;
|
|
243
951
|
const radius = pad.radius;
|
|
244
|
-
if (Math.abs(
|
|
952
|
+
if (Math.abs(point2.x - pad.x) <= halfWidth - radius + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point2.y - pad.y) <= halfHeight + POINT_IN_PAD_TOLERANCE_MM) {
|
|
245
953
|
return true;
|
|
246
954
|
}
|
|
247
955
|
const cornerX = Math.max(
|
|
248
|
-
Math.abs(
|
|
956
|
+
Math.abs(point2.x - pad.x) - (halfWidth - radius),
|
|
249
957
|
0
|
|
250
958
|
);
|
|
251
959
|
const cornerY = Math.max(
|
|
252
|
-
Math.abs(
|
|
960
|
+
Math.abs(point2.y - pad.y) - (halfHeight - radius),
|
|
253
961
|
0
|
|
254
962
|
);
|
|
255
963
|
const radiusWithTolerance = radius + POINT_IN_PAD_TOLERANCE_MM;
|
|
256
964
|
return cornerX * cornerX + cornerY * cornerY <= radiusWithTolerance * radiusWithTolerance;
|
|
257
965
|
}
|
|
258
966
|
if (pad.shape === "polygon") {
|
|
259
|
-
return isPointInPolygon(
|
|
967
|
+
return isPointInPolygon(point2, pad.points);
|
|
260
968
|
}
|
|
261
969
|
}
|
|
262
970
|
if (pad.type === "pcb_plated_hole") {
|
|
263
971
|
if (pad.shape === "circle") {
|
|
264
|
-
return getDistanceBetweenPoints(
|
|
972
|
+
return getDistanceBetweenPoints(point2, pad) <= pad.outer_diameter / 2 + POINT_IN_PAD_TOLERANCE_MM;
|
|
265
973
|
}
|
|
266
974
|
if ("rect_pad_width" in pad && "rect_pad_height" in pad) {
|
|
267
|
-
return isPointInPolygon(
|
|
975
|
+
return isPointInPolygon(point2, getPolygonPointsForPad(pad));
|
|
268
976
|
}
|
|
269
977
|
if (pad.shape === "oval" || pad.shape === "pill") {
|
|
270
|
-
return Math.abs(
|
|
978
|
+
return Math.abs(point2.x - pad.x) <= pad.outer_width / 2 + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point2.y - pad.y) <= pad.outer_height / 2 + POINT_IN_PAD_TOLERANCE_MM;
|
|
271
979
|
}
|
|
272
980
|
}
|
|
273
981
|
return false;
|
|
@@ -283,23 +991,23 @@ var addStartAndEndPortIdsIfMissing = (soup) => {
|
|
|
283
991
|
(item) => item.type === "pcb_smtpad"
|
|
284
992
|
);
|
|
285
993
|
const pcbTraces = soup.filter((item) => item.type === "pcb_trace");
|
|
286
|
-
function findPortIdOverlappingPoint(
|
|
994
|
+
function findPortIdOverlappingPoint(point2, options = {}) {
|
|
287
995
|
const traceWidth = options.traceWidth || 0;
|
|
288
996
|
const directPort = pcbPorts.find(
|
|
289
|
-
(port) => distance(port.x, port.y,
|
|
997
|
+
(port) => distance(port.x, port.y, point2.x, point2.y) < 0.01
|
|
290
998
|
);
|
|
291
999
|
if (directPort) return directPort.pcb_port_id;
|
|
292
1000
|
if (options.isFirstOrLastPoint) {
|
|
293
|
-
const
|
|
1001
|
+
const smtPad2 = pcbSmtPads.find((pad) => {
|
|
294
1002
|
if (pad.shape === "rect") {
|
|
295
|
-
return Math.abs(
|
|
1003
|
+
return Math.abs(point2.x - pad.x) < pad.width / 2 + traceWidth / 2 && Math.abs(point2.y - pad.y) < pad.height / 2 + traceWidth / 2;
|
|
296
1004
|
} else if (pad.shape === "circle") {
|
|
297
|
-
return distance(
|
|
1005
|
+
return distance(point2.x, point2.y, pad.x, pad.y) < pad.radius;
|
|
298
1006
|
} else if (pad.shape === "pill" || pad.shape === "rotated_pill") {
|
|
299
|
-
return isPointInPad(
|
|
1007
|
+
return isPointInPad(point2, pad);
|
|
300
1008
|
}
|
|
301
1009
|
});
|
|
302
|
-
if (
|
|
1010
|
+
if (smtPad2) return smtPad2.pcb_port_id ?? null;
|
|
303
1011
|
}
|
|
304
1012
|
return null;
|
|
305
1013
|
}
|
|
@@ -333,13 +1041,13 @@ var addStartAndEndPortIdsIfMissing = (soup) => {
|
|
|
333
1041
|
|
|
334
1042
|
// lib/check-each-pcb-port-connected-to-pcb-trace.ts
|
|
335
1043
|
import {
|
|
336
|
-
getFullConnectivityMapFromCircuitJson,
|
|
1044
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson2,
|
|
337
1045
|
PcbConnectivityMap
|
|
338
1046
|
} from "circuit-json-to-connectivity-map";
|
|
339
1047
|
|
|
340
1048
|
// lib/util/get-readable-names.ts
|
|
341
1049
|
import {
|
|
342
|
-
getReadableNameForElement,
|
|
1050
|
+
getReadableNameForElement as getReadableNameForElement2,
|
|
343
1051
|
getReadableNameForPcbPort,
|
|
344
1052
|
getBoundsOfPcbElements
|
|
345
1053
|
} from "@tscircuit/circuit-json-util";
|
|
@@ -358,7 +1066,7 @@ var firstReadableName = (candidates, id) => {
|
|
|
358
1066
|
return "";
|
|
359
1067
|
};
|
|
360
1068
|
var getReadableNameForComponent = (circuitJson, pcbComponentId) => sanitizeReadableName(
|
|
361
|
-
|
|
1069
|
+
getReadableNameForElement2(circuitJson, pcbComponentId),
|
|
362
1070
|
pcbComponentId,
|
|
363
1071
|
"component"
|
|
364
1072
|
);
|
|
@@ -395,7 +1103,7 @@ var getReadableNameForPort = (circuitJson, pcbPortId) => {
|
|
|
395
1103
|
}
|
|
396
1104
|
}
|
|
397
1105
|
return sanitizeReadableName(
|
|
398
|
-
getReadableNameForPcbPort(circuitJson, pcbPortId) ??
|
|
1106
|
+
getReadableNameForPcbPort(circuitJson, pcbPortId) ?? getReadableNameForElement2(circuitJson, pcbPortId),
|
|
399
1107
|
pcbPortId,
|
|
400
1108
|
"port"
|
|
401
1109
|
);
|
|
@@ -448,7 +1156,7 @@ var getReadableNameForSourceTrace = (circuitJson, sourceTrace) => {
|
|
|
448
1156
|
return `trace ${sourceTrace.source_trace_id}`;
|
|
449
1157
|
};
|
|
450
1158
|
var getReadableNameForElementId = (circuitJson, elementId) => sanitizeReadableName(
|
|
451
|
-
|
|
1159
|
+
getReadableNameForElement2(circuitJson, elementId),
|
|
452
1160
|
elementId,
|
|
453
1161
|
"element"
|
|
454
1162
|
);
|
|
@@ -640,7 +1348,7 @@ function checkEachPcbPortConnectedToPcbTraces(circuitJson) {
|
|
|
640
1348
|
(item) => item.type === "source_net"
|
|
641
1349
|
);
|
|
642
1350
|
const errors = [];
|
|
643
|
-
const connectivityMap =
|
|
1351
|
+
const connectivityMap = getFullConnectivityMapFromCircuitJson2(circuitJson);
|
|
644
1352
|
const pcbConnectivityMap = new PcbConnectivityMap(circuitJson);
|
|
645
1353
|
let pourConnectivity;
|
|
646
1354
|
const getPourConnectivity = () => pourConnectivity ??= getCopperPourConnectivity(
|
|
@@ -727,7 +1435,7 @@ function checkEachPcbPortConnectedToPcbTraces(circuitJson) {
|
|
|
727
1435
|
}
|
|
728
1436
|
|
|
729
1437
|
// lib/check-each-pcb-trace-non-overlapping/check-each-pcb-trace-non-overlapping.ts
|
|
730
|
-
import { cju as cju2, getReadableNameForElement as
|
|
1438
|
+
import { cju as cju2, getReadableNameForElement as getReadableNameForElement3 } from "@tscircuit/circuit-json-util";
|
|
731
1439
|
import { getPrimaryId as getPrimaryId2 } from "@tscircuit/circuit-json-util";
|
|
732
1440
|
import {
|
|
733
1441
|
segmentToBoundsMinDistance,
|
|
@@ -735,7 +1443,7 @@ import {
|
|
|
735
1443
|
} from "@tscircuit/math-utils";
|
|
736
1444
|
import { segmentToSegmentMinDistance as segmentToSegmentMinDistance3 } from "@tscircuit/math-utils";
|
|
737
1445
|
import {
|
|
738
|
-
getFullConnectivityMapFromCircuitJson as
|
|
1446
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson3
|
|
739
1447
|
} from "circuit-json-to-connectivity-map";
|
|
740
1448
|
|
|
741
1449
|
// lib/check-pad-clearance/common.ts
|
|
@@ -782,10 +1490,10 @@ var getPadBounds = (pad) => {
|
|
|
782
1490
|
if (pad.type === "pcb_keepout") {
|
|
783
1491
|
if (pad.shape === "outline") {
|
|
784
1492
|
return {
|
|
785
|
-
minX: Math.min(...pad.outline.map((
|
|
786
|
-
minY: Math.min(...pad.outline.map((
|
|
787
|
-
maxX: Math.max(...pad.outline.map((
|
|
788
|
-
maxY: Math.max(...pad.outline.map((
|
|
1493
|
+
minX: Math.min(...pad.outline.map((point2) => point2.x)),
|
|
1494
|
+
minY: Math.min(...pad.outline.map((point2) => point2.y)),
|
|
1495
|
+
maxX: Math.max(...pad.outline.map((point2) => point2.x)),
|
|
1496
|
+
maxY: Math.max(...pad.outline.map((point2) => point2.y))
|
|
789
1497
|
};
|
|
790
1498
|
}
|
|
791
1499
|
if (pad.shape === "circle") {
|
|
@@ -897,28 +1605,28 @@ var getPadToPadGap = (padA, padB) => {
|
|
|
897
1605
|
) - pillA.radius - pillB.radius;
|
|
898
1606
|
}
|
|
899
1607
|
if (isPillPad(padA) && isCircularPad(padB)) {
|
|
900
|
-
const
|
|
901
|
-
return segmentToCircleMinDistance(
|
|
1608
|
+
const pill = getPillCenterLineForPad(padA);
|
|
1609
|
+
return segmentToCircleMinDistance(pill.start, pill.end, getCircleShape(padB)) - pill.radius;
|
|
902
1610
|
}
|
|
903
1611
|
if (isCircularPad(padA) && isPillPad(padB)) {
|
|
904
|
-
const
|
|
905
|
-
return segmentToCircleMinDistance(
|
|
1612
|
+
const pill = getPillCenterLineForPad(padB);
|
|
1613
|
+
return segmentToCircleMinDistance(pill.start, pill.end, getCircleShape(padA)) - pill.radius;
|
|
906
1614
|
}
|
|
907
1615
|
if (isPillPad(padA)) {
|
|
908
|
-
const
|
|
1616
|
+
const pill = getPillCenterLineForPad(padA);
|
|
909
1617
|
return getSegmentToPolygonClearanceFromPoints(
|
|
910
|
-
|
|
911
|
-
|
|
1618
|
+
pill.start,
|
|
1619
|
+
pill.end,
|
|
912
1620
|
getPolygonShape(padB).points
|
|
913
|
-
).distance -
|
|
1621
|
+
).distance - pill.radius;
|
|
914
1622
|
}
|
|
915
1623
|
if (isPillPad(padB)) {
|
|
916
|
-
const
|
|
1624
|
+
const pill = getPillCenterLineForPad(padB);
|
|
917
1625
|
return getSegmentToPolygonClearanceFromPoints(
|
|
918
|
-
|
|
919
|
-
|
|
1626
|
+
pill.start,
|
|
1627
|
+
pill.end,
|
|
920
1628
|
getPolygonShape(padA).points
|
|
921
|
-
).distance -
|
|
1629
|
+
).distance - pill.radius;
|
|
922
1630
|
}
|
|
923
1631
|
if (isCircularPad(padA) && isCircularPad(padB)) {
|
|
924
1632
|
return distanceBetweenCircleAndCircle(
|
|
@@ -1008,19 +1716,19 @@ var getTraceObstacleClearance = (segment, obstacle) => {
|
|
|
1008
1716
|
const end = { x: segment.x2, y: segment.y2 };
|
|
1009
1717
|
const traceRadius = segment.thickness / 2;
|
|
1010
1718
|
if (obstacle.type === "pcb_via" || isCircularPad(obstacle)) {
|
|
1011
|
-
const
|
|
1719
|
+
const circle2 = obstacle.type === "pcb_via" ? {
|
|
1012
1720
|
x: obstacle.x,
|
|
1013
1721
|
y: obstacle.y,
|
|
1014
1722
|
radius: obstacle.outer_diameter / 2
|
|
1015
1723
|
} : getCircleShape(obstacle);
|
|
1016
|
-
const closestPoint = pointToSegmentClosestPoint2(
|
|
1724
|
+
const closestPoint = pointToSegmentClosestPoint2(circle2, start, end);
|
|
1017
1725
|
return {
|
|
1018
|
-
gap: segmentToCircleMinDistance(start, end,
|
|
1726
|
+
gap: segmentToCircleMinDistance(start, end, circle2) - traceRadius,
|
|
1019
1727
|
center: getCenterBetweenCopperEdges({
|
|
1020
1728
|
tracePoint: closestPoint,
|
|
1021
|
-
obstaclePoint:
|
|
1729
|
+
obstaclePoint: circle2,
|
|
1022
1730
|
traceRadius,
|
|
1023
|
-
obstacleRadius:
|
|
1731
|
+
obstacleRadius: circle2.radius
|
|
1024
1732
|
})
|
|
1025
1733
|
};
|
|
1026
1734
|
}
|
|
@@ -1440,19 +2148,19 @@ var getCollidableBounds = (collidable) => {
|
|
|
1440
2148
|
if (isPolygon) {
|
|
1441
2149
|
const polygonPoints = getPolygonPointsForPad(collidable);
|
|
1442
2150
|
return {
|
|
1443
|
-
minX: Math.min(...polygonPoints.map((
|
|
1444
|
-
minY: Math.min(...polygonPoints.map((
|
|
1445
|
-
maxX: Math.max(...polygonPoints.map((
|
|
1446
|
-
maxY: Math.max(...polygonPoints.map((
|
|
2151
|
+
minX: Math.min(...polygonPoints.map((point2) => point2.x)),
|
|
2152
|
+
minY: Math.min(...polygonPoints.map((point2) => point2.y)),
|
|
2153
|
+
maxX: Math.max(...polygonPoints.map((point2) => point2.x)),
|
|
2154
|
+
maxY: Math.max(...polygonPoints.map((point2) => point2.y))
|
|
1447
2155
|
};
|
|
1448
2156
|
}
|
|
1449
2157
|
if (collidable.type === "pcb_smtpad" && collidable.shape === "rotated_pill") {
|
|
1450
|
-
const
|
|
2158
|
+
const pill = getPillCenterLineForPad(collidable);
|
|
1451
2159
|
return {
|
|
1452
|
-
minX: Math.min(
|
|
1453
|
-
minY: Math.min(
|
|
1454
|
-
maxX: Math.max(
|
|
1455
|
-
maxY: Math.max(
|
|
2160
|
+
minX: Math.min(pill.start.x, pill.end.x) - pill.radius,
|
|
2161
|
+
minY: Math.min(pill.start.y, pill.end.y) - pill.radius,
|
|
2162
|
+
maxX: Math.max(pill.start.x, pill.end.x) + pill.radius,
|
|
2163
|
+
maxY: Math.max(pill.start.y, pill.end.y) + pill.radius
|
|
1456
2164
|
};
|
|
1457
2165
|
}
|
|
1458
2166
|
}
|
|
@@ -1516,7 +2224,7 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
|
|
|
1516
2224
|
} = {}) {
|
|
1517
2225
|
const errors = [];
|
|
1518
2226
|
addStartAndEndPortIdsIfMissing(circuitJson);
|
|
1519
|
-
connMap ??=
|
|
2227
|
+
connMap ??= getFullConnectivityMapFromCircuitJson3(circuitJson);
|
|
1520
2228
|
const board = getPcbBoard(circuitJson);
|
|
1521
2229
|
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? DEFAULT_TRACE_MARGIN;
|
|
1522
2230
|
const pcbTraces = cju2(circuitJson).pcb_trace.list();
|
|
@@ -1578,7 +2286,7 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
|
|
|
1578
2286
|
objects: allObjects,
|
|
1579
2287
|
getBounds: getCollidableBounds
|
|
1580
2288
|
});
|
|
1581
|
-
const getReadableName = (id) =>
|
|
2289
|
+
const getReadableName = (id) => getReadableNameForElement3(circuitJson, id);
|
|
1582
2290
|
const constructErrorMessage = (traceName, otherName, gap) => {
|
|
1583
2291
|
if (isTraceObstacleOverlap(gap)) {
|
|
1584
2292
|
return `PCB trace ${traceName} overlaps with ${otherName} (accidental contact)`;
|
|
@@ -1792,400 +2500,60 @@ var NetManager = class {
|
|
|
1792
2500
|
};
|
|
1793
2501
|
|
|
1794
2502
|
// lib/check-pcb-components-out-of-board/checkViasOffBoard.ts
|
|
1795
|
-
import { getReadableNameForElement as
|
|
2503
|
+
import { getReadableNameForElement as getReadableNameForElement4 } from "@tscircuit/circuit-json-util";
|
|
1796
2504
|
|
|
1797
2505
|
// lib/check-copper-to-board-edge-clearance.ts
|
|
1798
|
-
import * as Flatten from "@flatten-js/core";
|
|
1799
|
-
import { applyToPoint, rotateDEG } from "transformation-matrix";
|
|
1800
|
-
var toPcbComponentId = (id) => id;
|
|
1801
2506
|
var GEOMETRY_EPSILON = 1e-9;
|
|
1802
|
-
var pointsToPolygon = (points) => {
|
|
1803
|
-
if (points.length < 3) return null;
|
|
1804
|
-
return new Flatten.Polygon(points.map(({ x, y }) => new Flatten.Point(x, y)));
|
|
1805
|
-
};
|
|
1806
|
-
var brepRingToPolygon = (vertices) => {
|
|
1807
|
-
const ring = vertices.filter((vertex, index) => {
|
|
1808
|
-
const previous = vertices[index - 1];
|
|
1809
|
-
return !previous || Math.abs(previous.x - vertex.x) > GEOMETRY_EPSILON || Math.abs(previous.y - vertex.y) > GEOMETRY_EPSILON;
|
|
1810
|
-
});
|
|
1811
|
-
if (ring.length > 1 && Math.abs(ring[0].x - ring.at(-1).x) <= GEOMETRY_EPSILON && Math.abs(ring[0].y - ring.at(-1).y) <= GEOMETRY_EPSILON) {
|
|
1812
|
-
ring.pop();
|
|
1813
|
-
}
|
|
1814
|
-
if (ring.length < 3) return null;
|
|
1815
|
-
const edges = [];
|
|
1816
|
-
for (let index = 0; index < ring.length; index++) {
|
|
1817
|
-
const start = ring[index];
|
|
1818
|
-
const end = ring[(index + 1) % ring.length];
|
|
1819
|
-
const startPoint = new Flatten.Point(start.x, start.y);
|
|
1820
|
-
const endPoint = new Flatten.Point(end.x, end.y);
|
|
1821
|
-
const bulge = start.bulge ?? 0;
|
|
1822
|
-
if (Math.abs(bulge) <= GEOMETRY_EPSILON) {
|
|
1823
|
-
edges.push(new Flatten.Segment(startPoint, endPoint));
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
const chordLength = startPoint.distanceTo(endPoint)[0];
|
|
1827
|
-
if (chordLength <= GEOMETRY_EPSILON) continue;
|
|
1828
|
-
const midpoint2 = {
|
|
1829
|
-
x: (start.x + end.x) / 2,
|
|
1830
|
-
y: (start.y + end.y) / 2
|
|
1831
|
-
};
|
|
1832
|
-
const leftNormal = {
|
|
1833
|
-
x: -(end.y - start.y) / chordLength,
|
|
1834
|
-
y: (end.x - start.x) / chordLength
|
|
1835
|
-
};
|
|
1836
|
-
const centerOffset = chordLength * (1 - bulge * bulge) / (4 * bulge);
|
|
1837
|
-
const center = new Flatten.Point(
|
|
1838
|
-
midpoint2.x + leftNormal.x * centerOffset,
|
|
1839
|
-
midpoint2.y + leftNormal.y * centerOffset
|
|
1840
|
-
);
|
|
1841
|
-
const radius = chordLength * (1 + bulge * bulge) / (4 * Math.abs(bulge));
|
|
1842
|
-
edges.push(
|
|
1843
|
-
new Flatten.Arc(
|
|
1844
|
-
center,
|
|
1845
|
-
radius,
|
|
1846
|
-
Math.atan2(start.y - center.y, start.x - center.x),
|
|
1847
|
-
Math.atan2(end.y - center.y, end.x - center.x),
|
|
1848
|
-
bulge > 0
|
|
1849
|
-
)
|
|
1850
|
-
);
|
|
1851
|
-
}
|
|
1852
|
-
if (edges.length < 3) return null;
|
|
1853
|
-
const polygon = new Flatten.Polygon();
|
|
1854
|
-
polygon.addFace(edges);
|
|
1855
|
-
return polygon;
|
|
1856
|
-
};
|
|
1857
|
-
var boardToPolygon = (board) => {
|
|
1858
|
-
if (board.outline && board.outline.length >= 3) {
|
|
1859
|
-
return pointsToPolygon(board.outline);
|
|
1860
|
-
}
|
|
1861
|
-
if (!board.center || typeof board.width !== "number" || typeof board.height !== "number") {
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
const halfWidth = board.width / 2;
|
|
1865
|
-
const halfHeight = board.height / 2;
|
|
1866
|
-
return pointsToPolygon([
|
|
1867
|
-
{ x: board.center.x - halfWidth, y: board.center.y - halfHeight },
|
|
1868
|
-
{ x: board.center.x + halfWidth, y: board.center.y - halfHeight },
|
|
1869
|
-
{ x: board.center.x + halfWidth, y: board.center.y + halfHeight },
|
|
1870
|
-
{ x: board.center.x - halfWidth, y: board.center.y + halfHeight }
|
|
1871
|
-
]);
|
|
1872
|
-
};
|
|
1873
|
-
var getRectanglePolygon = ({
|
|
1874
|
-
x,
|
|
1875
|
-
y,
|
|
1876
|
-
width,
|
|
1877
|
-
height,
|
|
1878
|
-
ccwRotationDegrees = 0
|
|
1879
|
-
}) => {
|
|
1880
|
-
const halfWidth = width / 2;
|
|
1881
|
-
const halfHeight = height / 2;
|
|
1882
|
-
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
|
|
1883
|
-
return pointsToPolygon(
|
|
1884
|
-
[
|
|
1885
|
-
{ x: x - halfWidth, y: y - halfHeight },
|
|
1886
|
-
{ x: x + halfWidth, y: y - halfHeight },
|
|
1887
|
-
{ x: x + halfWidth, y: y + halfHeight },
|
|
1888
|
-
{ x: x - halfWidth, y: y + halfHeight }
|
|
1889
|
-
].map((point) => applyToPoint(rotationMatrix, point))
|
|
1890
|
-
);
|
|
1891
|
-
};
|
|
1892
|
-
var roundedRect = ({
|
|
1893
|
-
x,
|
|
1894
|
-
y,
|
|
1895
|
-
width,
|
|
1896
|
-
height,
|
|
1897
|
-
cornerRadius,
|
|
1898
|
-
ccwRotationDegrees = 0
|
|
1899
|
-
}) => {
|
|
1900
|
-
const radius = Math.max(0, Math.min(cornerRadius, width / 2, height / 2));
|
|
1901
|
-
if (radius <= GEOMETRY_EPSILON) {
|
|
1902
|
-
const polygon = getRectanglePolygon({
|
|
1903
|
-
x,
|
|
1904
|
-
y,
|
|
1905
|
-
width,
|
|
1906
|
-
height,
|
|
1907
|
-
ccwRotationDegrees
|
|
1908
|
-
});
|
|
1909
|
-
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
1910
|
-
}
|
|
1911
|
-
const shapes = [];
|
|
1912
|
-
const innerWidth = width - 2 * radius;
|
|
1913
|
-
const innerHeight = height - 2 * radius;
|
|
1914
|
-
if (innerWidth > GEOMETRY_EPSILON) {
|
|
1915
|
-
const verticalBand = getRectanglePolygon({
|
|
1916
|
-
x,
|
|
1917
|
-
y,
|
|
1918
|
-
width: innerWidth,
|
|
1919
|
-
height,
|
|
1920
|
-
ccwRotationDegrees
|
|
1921
|
-
});
|
|
1922
|
-
if (verticalBand) shapes.push(verticalBand);
|
|
1923
|
-
}
|
|
1924
|
-
if (innerHeight > GEOMETRY_EPSILON) {
|
|
1925
|
-
const horizontalBand = getRectanglePolygon({
|
|
1926
|
-
x,
|
|
1927
|
-
y,
|
|
1928
|
-
width,
|
|
1929
|
-
height: innerHeight,
|
|
1930
|
-
ccwRotationDegrees
|
|
1931
|
-
});
|
|
1932
|
-
if (horizontalBand) shapes.push(horizontalBand);
|
|
1933
|
-
}
|
|
1934
|
-
const halfInnerWidth = innerWidth / 2;
|
|
1935
|
-
const halfInnerHeight = innerHeight / 2;
|
|
1936
|
-
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
|
|
1937
|
-
const cornerCenters = [
|
|
1938
|
-
{ x: x - halfInnerWidth, y: y - halfInnerHeight },
|
|
1939
|
-
{ x: x + halfInnerWidth, y: y - halfInnerHeight },
|
|
1940
|
-
{ x: x + halfInnerWidth, y: y + halfInnerHeight },
|
|
1941
|
-
{ x: x - halfInnerWidth, y: y + halfInnerHeight }
|
|
1942
|
-
].map((point) => applyToPoint(rotationMatrix, point)).filter(
|
|
1943
|
-
(point, index, points) => points.findIndex(
|
|
1944
|
-
(candidate) => Math.abs(candidate.x - point.x) <= GEOMETRY_EPSILON && Math.abs(candidate.y - point.y) <= GEOMETRY_EPSILON
|
|
1945
|
-
) === index
|
|
1946
|
-
);
|
|
1947
|
-
shapes.push(
|
|
1948
|
-
...cornerCenters.map(
|
|
1949
|
-
(center) => new Flatten.Circle(new Flatten.Point(center.x, center.y), radius)
|
|
1950
|
-
)
|
|
1951
|
-
);
|
|
1952
|
-
return shapes.length > 0 ? { kind: "shapes", shapes } : null;
|
|
1953
|
-
};
|
|
1954
|
-
var pill = ({
|
|
1955
|
-
x,
|
|
1956
|
-
y,
|
|
1957
|
-
width,
|
|
1958
|
-
height,
|
|
1959
|
-
radius,
|
|
1960
|
-
ccwRotationDegrees = 0
|
|
1961
|
-
}) => {
|
|
1962
|
-
const halfLineLength = Math.max(Math.max(width, height) / 2 - radius, 0);
|
|
1963
|
-
const localAxis = width >= height ? { x: halfLineLength, y: 0 } : { x: 0, y: halfLineLength };
|
|
1964
|
-
const axis = applyToPoint(rotateDEG(ccwRotationDegrees), localAxis);
|
|
1965
|
-
if (halfLineLength <= GEOMETRY_EPSILON) {
|
|
1966
|
-
return {
|
|
1967
|
-
kind: "shapes",
|
|
1968
|
-
shapes: [new Flatten.Circle(new Flatten.Point(x, y), radius)]
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
|
-
return {
|
|
1972
|
-
kind: "pill",
|
|
1973
|
-
centerLine: new Flatten.Segment(
|
|
1974
|
-
new Flatten.Point(x - axis.x, y - axis.y),
|
|
1975
|
-
new Flatten.Point(x + axis.x, y + axis.y)
|
|
1976
|
-
),
|
|
1977
|
-
radius
|
|
1978
|
-
};
|
|
1979
|
-
};
|
|
1980
|
-
var getSmtPadGeometry = (pad) => {
|
|
1981
|
-
switch (pad.shape) {
|
|
1982
|
-
case "circle":
|
|
1983
|
-
return {
|
|
1984
|
-
kind: "shapes",
|
|
1985
|
-
shapes: [
|
|
1986
|
-
new Flatten.Circle(new Flatten.Point(pad.x, pad.y), pad.radius)
|
|
1987
|
-
]
|
|
1988
|
-
};
|
|
1989
|
-
case "rect":
|
|
1990
|
-
return roundedRect({
|
|
1991
|
-
x: pad.x,
|
|
1992
|
-
y: pad.y,
|
|
1993
|
-
width: pad.width,
|
|
1994
|
-
height: pad.height,
|
|
1995
|
-
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0
|
|
1996
|
-
});
|
|
1997
|
-
case "rotated_rect":
|
|
1998
|
-
return roundedRect({
|
|
1999
|
-
x: pad.x,
|
|
2000
|
-
y: pad.y,
|
|
2001
|
-
width: pad.width,
|
|
2002
|
-
height: pad.height,
|
|
2003
|
-
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0,
|
|
2004
|
-
ccwRotationDegrees: pad.ccw_rotation
|
|
2005
|
-
});
|
|
2006
|
-
case "pill":
|
|
2007
|
-
return pill({
|
|
2008
|
-
x: pad.x,
|
|
2009
|
-
y: pad.y,
|
|
2010
|
-
width: pad.width,
|
|
2011
|
-
height: pad.height,
|
|
2012
|
-
radius: pad.radius
|
|
2013
|
-
});
|
|
2014
|
-
case "rotated_pill":
|
|
2015
|
-
return pill({
|
|
2016
|
-
x: pad.x,
|
|
2017
|
-
y: pad.y,
|
|
2018
|
-
width: pad.width,
|
|
2019
|
-
height: pad.height,
|
|
2020
|
-
radius: pad.radius,
|
|
2021
|
-
ccwRotationDegrees: pad.ccw_rotation
|
|
2022
|
-
});
|
|
2023
|
-
case "polygon": {
|
|
2024
|
-
const polygon = pointsToPolygon(pad.points);
|
|
2025
|
-
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
};
|
|
2029
|
-
var getPlatedHoleGeometry = (platedHole, componentCcwRotationDegrees) => {
|
|
2030
|
-
switch (platedHole.shape) {
|
|
2031
|
-
case "circle":
|
|
2032
|
-
return {
|
|
2033
|
-
kind: "shapes",
|
|
2034
|
-
shapes: [
|
|
2035
|
-
new Flatten.Circle(
|
|
2036
|
-
new Flatten.Point(platedHole.x, platedHole.y),
|
|
2037
|
-
platedHole.outer_diameter / 2
|
|
2038
|
-
)
|
|
2039
|
-
]
|
|
2040
|
-
};
|
|
2041
|
-
case "oval":
|
|
2042
|
-
case "pill":
|
|
2043
|
-
return pill({
|
|
2044
|
-
x: platedHole.x,
|
|
2045
|
-
y: platedHole.y,
|
|
2046
|
-
width: platedHole.outer_width,
|
|
2047
|
-
height: platedHole.outer_height,
|
|
2048
|
-
radius: Math.min(
|
|
2049
|
-
platedHole.outer_width / 2,
|
|
2050
|
-
platedHole.outer_height / 2
|
|
2051
|
-
),
|
|
2052
|
-
ccwRotationDegrees: platedHole.ccw_rotation
|
|
2053
|
-
});
|
|
2054
|
-
case "circular_hole_with_rect_pad":
|
|
2055
|
-
case "pill_hole_with_rect_pad":
|
|
2056
|
-
case "rotated_pill_hole_with_rect_pad":
|
|
2057
|
-
return roundedRect({
|
|
2058
|
-
x: platedHole.x,
|
|
2059
|
-
y: platedHole.y,
|
|
2060
|
-
width: platedHole.rect_pad_width,
|
|
2061
|
-
height: platedHole.rect_pad_height,
|
|
2062
|
-
cornerRadius: platedHole.rect_border_radius ?? 0,
|
|
2063
|
-
ccwRotationDegrees: "rect_ccw_rotation" in platedHole ? platedHole.rect_ccw_rotation ?? 0 : 0
|
|
2064
|
-
});
|
|
2065
|
-
case "hole_with_polygon_pad": {
|
|
2066
|
-
const ccwRotationDegrees = platedHole.ccw_rotation ?? componentCcwRotationDegrees;
|
|
2067
|
-
const rotationMatrix = rotateDEG(ccwRotationDegrees);
|
|
2068
|
-
const polygon = pointsToPolygon(
|
|
2069
|
-
platedHole.pad_outline.map((point) => {
|
|
2070
|
-
const rotatedPoint = applyToPoint(rotationMatrix, point);
|
|
2071
|
-
return {
|
|
2072
|
-
x: platedHole.x + rotatedPoint.x,
|
|
2073
|
-
y: platedHole.y + rotatedPoint.y
|
|
2074
|
-
};
|
|
2075
|
-
})
|
|
2076
|
-
);
|
|
2077
|
-
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
};
|
|
2081
|
-
var getCopperGeometry = (element, componentCcwRotationsById) => {
|
|
2082
|
-
if (element.type === "pcb_via") {
|
|
2083
|
-
return {
|
|
2084
|
-
kind: "shapes",
|
|
2085
|
-
shapes: [
|
|
2086
|
-
new Flatten.Circle(
|
|
2087
|
-
new Flatten.Point(element.x, element.y),
|
|
2088
|
-
element.outer_diameter / 2
|
|
2089
|
-
)
|
|
2090
|
-
]
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
if (element.type === "pcb_smtpad") return getSmtPadGeometry(element);
|
|
2094
|
-
if (element.type === "pcb_plated_hole") {
|
|
2095
|
-
return getPlatedHoleGeometry(
|
|
2096
|
-
element,
|
|
2097
|
-
element.pcb_component_id ? componentCcwRotationsById.get(
|
|
2098
|
-
toPcbComponentId(element.pcb_component_id)
|
|
2099
|
-
) ?? 0 : 0
|
|
2100
|
-
);
|
|
2101
|
-
}
|
|
2102
|
-
let polygon;
|
|
2103
|
-
switch (element.shape) {
|
|
2104
|
-
case "rect":
|
|
2105
|
-
polygon = getRectanglePolygon({
|
|
2106
|
-
x: element.center.x,
|
|
2107
|
-
y: element.center.y,
|
|
2108
|
-
width: element.width,
|
|
2109
|
-
height: element.height,
|
|
2110
|
-
ccwRotationDegrees: element.rotation ?? 0
|
|
2111
|
-
});
|
|
2112
|
-
break;
|
|
2113
|
-
case "polygon":
|
|
2114
|
-
polygon = pointsToPolygon(element.points);
|
|
2115
|
-
break;
|
|
2116
|
-
case "brep":
|
|
2117
|
-
polygon = brepRingToPolygon(element.brep_shape.outer_ring.vertices);
|
|
2118
|
-
break;
|
|
2119
|
-
}
|
|
2120
|
-
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
2121
|
-
};
|
|
2122
|
-
var getCopperElementId = (element) => {
|
|
2123
|
-
if (element.type === "pcb_via") return element.pcb_via_id;
|
|
2124
|
-
if (element.type === "pcb_smtpad") return element.pcb_smtpad_id;
|
|
2125
|
-
if (element.type === "pcb_plated_hole") return element.pcb_plated_hole_id;
|
|
2126
|
-
return element.pcb_copper_pour_id;
|
|
2127
|
-
};
|
|
2128
2507
|
var getCopperElementLabel = (element) => {
|
|
2129
2508
|
if (element.type === "pcb_via") return "Via";
|
|
2130
2509
|
if (element.type === "pcb_smtpad") return "SMT pad";
|
|
2131
2510
|
if (element.type === "pcb_plated_hole") return "Plated hole";
|
|
2132
2511
|
return "Copper pour";
|
|
2133
2512
|
};
|
|
2134
|
-
var measureClearance = (board, geometry) => {
|
|
2135
|
-
if (geometry.kind === "shapes") {
|
|
2136
|
-
const isInside2 = geometry.shapes.every((shape) => board.contains(shape));
|
|
2137
|
-
return {
|
|
2138
|
-
isInside: isInside2,
|
|
2139
|
-
clearance: isInside2 ? Math.min(
|
|
2140
|
-
...geometry.shapes.map((shape) => board.distanceTo(shape)[0])
|
|
2141
|
-
) : 0
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
const centerLineClearance = board.distanceTo(geometry.centerLine)[0];
|
|
2145
|
-
const clearance = centerLineClearance - geometry.radius;
|
|
2146
|
-
const isInside = board.contains(geometry.centerLine) && clearance >= -GEOMETRY_EPSILON;
|
|
2147
|
-
return {
|
|
2148
|
-
isInside,
|
|
2149
|
-
clearance: isInside ? Math.max(0, clearance) : 0
|
|
2150
|
-
};
|
|
2151
|
-
};
|
|
2152
2513
|
function checkCopperToBoardEdgeClearance(circuitJson) {
|
|
2153
2514
|
const board = getPcbBoard(circuitJson);
|
|
2154
2515
|
if (!board) return [];
|
|
2155
|
-
const boardPolygon =
|
|
2516
|
+
const boardPolygon = convertCircuitJsonToFlattenJs([board], { strict: true }).elements[0]?.shapes[0];
|
|
2156
2517
|
if (!boardPolygon) return [];
|
|
2157
2518
|
const requiredClearance = getBoardDrcValue(board, "min_board_edge_clearance") ?? jlcMinTolerances.min_board_edge_clearance;
|
|
2158
2519
|
if (requiredClearance === void 0) return [];
|
|
2159
2520
|
const allowedOffBoardComponentIds = new Set(
|
|
2160
2521
|
circuitJson.filter(
|
|
2161
2522
|
(element) => element.type === "pcb_component"
|
|
2162
|
-
).filter((component) => component.is_allowed_to_be_off_board).map((component) =>
|
|
2163
|
-
);
|
|
2164
|
-
const copperElements = circuitJson.filter(
|
|
2165
|
-
(element) => element.type === "pcb_via" || element.type === "pcb_smtpad" || element.type === "pcb_plated_hole" || element.type === "pcb_copper_pour"
|
|
2523
|
+
).filter((component) => component.is_allowed_to_be_off_board).map((component) => component.pcb_component_id)
|
|
2166
2524
|
);
|
|
2167
|
-
const
|
|
2168
|
-
circuitJson
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2525
|
+
const { elements: copperElements } = convertCircuitJsonToFlattenJs(
|
|
2526
|
+
circuitJson,
|
|
2527
|
+
{
|
|
2528
|
+
elementTypes: [
|
|
2529
|
+
"pcb_via",
|
|
2530
|
+
"pcb_smtpad",
|
|
2531
|
+
"pcb_plated_hole",
|
|
2532
|
+
"pcb_copper_pour"
|
|
2533
|
+
],
|
|
2534
|
+
includeDrillHoles: false,
|
|
2535
|
+
strict: true
|
|
2536
|
+
}
|
|
2174
2537
|
);
|
|
2175
2538
|
const errors = [];
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
)
|
|
2539
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2540
|
+
for (const geometry of copperElements) {
|
|
2541
|
+
if (seen.has(geometry.elementId)) continue;
|
|
2542
|
+
seen.add(geometry.elementId);
|
|
2543
|
+
const element = geometry.sourceElement;
|
|
2544
|
+
if ((element.type === "pcb_smtpad" || element.type === "pcb_plated_hole") && element.pcb_component_id && allowedOffBoardComponentIds.has(element.pcb_component_id)) {
|
|
2180
2545
|
continue;
|
|
2181
2546
|
}
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2184
|
-
|
|
2547
|
+
const isInside = geometry.shapes.every(
|
|
2548
|
+
(shape) => boardPolygon.contains(shape)
|
|
2549
|
+
);
|
|
2550
|
+
const clearance = isInside ? Math.min(
|
|
2551
|
+
...geometry.shapes.map((shape) => boardPolygon.distanceTo(shape)[0])
|
|
2552
|
+
) : 0;
|
|
2185
2553
|
if (isInside && clearance + GEOMETRY_EPSILON >= requiredClearance) {
|
|
2186
2554
|
continue;
|
|
2187
2555
|
}
|
|
2188
|
-
const id =
|
|
2556
|
+
const id = geometry.elementId;
|
|
2189
2557
|
const label = getCopperElementLabel(element);
|
|
2190
2558
|
errors.push({
|
|
2191
2559
|
type: "pcb_placement_error",
|
|
@@ -2216,7 +2584,7 @@ function checkViasOffBoard(circuitJson) {
|
|
|
2216
2584
|
return vias.flatMap((via) => {
|
|
2217
2585
|
const violation = violationsById.get(via.pcb_via_id);
|
|
2218
2586
|
if (!violation) return [];
|
|
2219
|
-
const viaName =
|
|
2587
|
+
const viaName = getReadableNameForElement4(circuitJson, via.pcb_via_id);
|
|
2220
2588
|
return [
|
|
2221
2589
|
{
|
|
2222
2590
|
...violation,
|
|
@@ -2229,7 +2597,7 @@ function checkViasOffBoard(circuitJson) {
|
|
|
2229
2597
|
|
|
2230
2598
|
// lib/check-pcb-components-out-of-board/checkPcbComponentsOutOfBoard.ts
|
|
2231
2599
|
import * as Flatten2 from "@flatten-js/core";
|
|
2232
|
-
import { rotateDEG
|
|
2600
|
+
import { rotateDEG, applyToPoint } from "transformation-matrix";
|
|
2233
2601
|
function isPolygonCCW(poly) {
|
|
2234
2602
|
return poly.area() >= 0;
|
|
2235
2603
|
}
|
|
@@ -2250,9 +2618,9 @@ function rectanglePolygon({
|
|
|
2250
2618
|
];
|
|
2251
2619
|
let poly = new Flatten2.Polygon(corners);
|
|
2252
2620
|
if (rotationDeg) {
|
|
2253
|
-
const matrix =
|
|
2621
|
+
const matrix = rotateDEG(rotationDeg, cx, cy);
|
|
2254
2622
|
const rotatedCorners = corners.map((pt) => {
|
|
2255
|
-
const p =
|
|
2623
|
+
const p = applyToPoint(matrix, { x: pt.x, y: pt.y });
|
|
2256
2624
|
return new Flatten2.Point(p.x, p.y);
|
|
2257
2625
|
});
|
|
2258
2626
|
poly = new Flatten2.Polygon(rotatedCorners);
|
|
@@ -2260,7 +2628,7 @@ function rectanglePolygon({
|
|
|
2260
2628
|
if (!isPolygonCCW(poly)) poly.reverse();
|
|
2261
2629
|
return poly;
|
|
2262
2630
|
}
|
|
2263
|
-
function
|
|
2631
|
+
function boardToPolygon({
|
|
2264
2632
|
board
|
|
2265
2633
|
}) {
|
|
2266
2634
|
if (board.outline && board.outline.length > 0) {
|
|
@@ -2316,9 +2684,9 @@ function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentW
|
|
|
2316
2684
|
y: (corners[i].y + corners[next].y) / 2
|
|
2317
2685
|
});
|
|
2318
2686
|
}
|
|
2319
|
-
const matrix =
|
|
2687
|
+
const matrix = rotateDEG(rotationDeg, componentCenter.x, componentCenter.y);
|
|
2320
2688
|
const rotatePoint2 = (pt) => {
|
|
2321
|
-
const p =
|
|
2689
|
+
const p = applyToPoint(matrix, pt);
|
|
2322
2690
|
return new Flatten2.Point(p.x, p.y);
|
|
2323
2691
|
};
|
|
2324
2692
|
const rotatedPoints = corners.concat(midpoints).map(rotatePoint2);
|
|
@@ -2404,7 +2772,7 @@ function checkPcbComponentsOutOfBoard(circuitJson) {
|
|
|
2404
2772
|
(el) => el.type === "pcb_board"
|
|
2405
2773
|
);
|
|
2406
2774
|
if (!board) return [];
|
|
2407
|
-
const boardPoly =
|
|
2775
|
+
const boardPoly = boardToPolygon({ board });
|
|
2408
2776
|
if (!boardPoly) return [];
|
|
2409
2777
|
const components = circuitJson.filter(
|
|
2410
2778
|
(el) => el.type === "pcb_component"
|
|
@@ -2462,7 +2830,7 @@ function checkPcbComponentsOutOfBoard(circuitJson) {
|
|
|
2462
2830
|
// lib/check-pcb-component-over-cutout.ts
|
|
2463
2831
|
import { doBoundsOverlap } from "@tscircuit/math-utils";
|
|
2464
2832
|
import * as Flatten3 from "@flatten-js/core";
|
|
2465
|
-
import { applyToPoint as
|
|
2833
|
+
import { applyToPoint as applyToPoint2, rotateDEG as rotateDEG2 } from "transformation-matrix";
|
|
2466
2834
|
var CUTOUT_CIRCLE_SEGMENTS = 32;
|
|
2467
2835
|
function rectanglePolygon2({
|
|
2468
2836
|
center,
|
|
@@ -2478,10 +2846,10 @@ function rectanglePolygon2({
|
|
|
2478
2846
|
{ x: center.x + halfWidth, y: center.y + halfHeight },
|
|
2479
2847
|
{ x: center.x - halfWidth, y: center.y + halfHeight }
|
|
2480
2848
|
];
|
|
2481
|
-
const matrix =
|
|
2849
|
+
const matrix = rotateDEG2(rotation, center.x, center.y);
|
|
2482
2850
|
return new Flatten3.Polygon(
|
|
2483
2851
|
corners.map((corner) => {
|
|
2484
|
-
const rotated = rotation ?
|
|
2852
|
+
const rotated = rotation ? applyToPoint2(matrix, corner) : corner;
|
|
2485
2853
|
return new Flatten3.Point(rotated.x, rotated.y);
|
|
2486
2854
|
})
|
|
2487
2855
|
);
|
|
@@ -2514,7 +2882,7 @@ function cutoutToPolygon(cutout) {
|
|
|
2514
2882
|
}
|
|
2515
2883
|
if (cutout.shape === "polygon") {
|
|
2516
2884
|
return new Flatten3.Polygon(
|
|
2517
|
-
cutout.points.map((
|
|
2885
|
+
cutout.points.map((point2) => new Flatten3.Point(point2.x, point2.y))
|
|
2518
2886
|
);
|
|
2519
2887
|
}
|
|
2520
2888
|
return null;
|
|
@@ -2643,9 +3011,9 @@ function checkPcbCopperOverKeepout(circuitJson) {
|
|
|
2643
3011
|
}
|
|
2644
3012
|
|
|
2645
3013
|
// lib/check-same-net-via-spacing.ts
|
|
2646
|
-
import { getReadableNameForElement as
|
|
3014
|
+
import { getReadableNameForElement as getReadableNameForElement5 } from "@tscircuit/circuit-json-util";
|
|
2647
3015
|
import {
|
|
2648
|
-
getFullConnectivityMapFromCircuitJson as
|
|
3016
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson4
|
|
2649
3017
|
} from "circuit-json-to-connectivity-map";
|
|
2650
3018
|
|
|
2651
3019
|
// lib/util/distance.ts
|
|
@@ -2667,7 +3035,7 @@ function checkSameNetViaSpacing(circuitJson, {
|
|
|
2667
3035
|
if (vias.length < 2) return [];
|
|
2668
3036
|
const board = getPcbBoard(circuitJson);
|
|
2669
3037
|
minClearance ??= getBoardDrcValue(board, "min_via_hole_edge_to_via_hole_edge_clearance") ?? jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
|
|
2670
|
-
connMap ??=
|
|
3038
|
+
connMap ??= getFullConnectivityMapFromCircuitJson4(circuitJson);
|
|
2671
3039
|
const errors = [];
|
|
2672
3040
|
const reported = /* @__PURE__ */ new Set();
|
|
2673
3041
|
for (let i = 0; i < vias.length; i++) {
|
|
@@ -2684,10 +3052,10 @@ function checkSameNetViaSpacing(circuitJson, {
|
|
|
2684
3052
|
errors.push({
|
|
2685
3053
|
type: "pcb_via_clearance_error",
|
|
2686
3054
|
pcb_error_id: `same_net_vias_close_${pairId}`,
|
|
2687
|
-
message: `Vias ${
|
|
3055
|
+
message: `Vias ${getReadableNameForElement5(
|
|
2688
3056
|
circuitJson,
|
|
2689
3057
|
viaA.pcb_via_id
|
|
2690
|
-
)} and ${
|
|
3058
|
+
)} and ${getReadableNameForElement5(
|
|
2691
3059
|
circuitJson,
|
|
2692
3060
|
viaB.pcb_via_id
|
|
2693
3061
|
)} are too close together (gap: ${gap.toFixed(3)}mm)`,
|
|
@@ -2706,9 +3074,9 @@ function checkSameNetViaSpacing(circuitJson, {
|
|
|
2706
3074
|
}
|
|
2707
3075
|
|
|
2708
3076
|
// lib/check-different-net-via-spacing.ts
|
|
2709
|
-
import { getReadableNameForElement as
|
|
3077
|
+
import { getReadableNameForElement as getReadableNameForElement6 } from "@tscircuit/circuit-json-util";
|
|
2710
3078
|
import {
|
|
2711
|
-
getFullConnectivityMapFromCircuitJson as
|
|
3079
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson5
|
|
2712
3080
|
} from "circuit-json-to-connectivity-map";
|
|
2713
3081
|
function checkDifferentNetViaSpacing(circuitJson, {
|
|
2714
3082
|
connMap,
|
|
@@ -2718,7 +3086,7 @@ function checkDifferentNetViaSpacing(circuitJson, {
|
|
|
2718
3086
|
if (vias.length < 2) return [];
|
|
2719
3087
|
const board = getPcbBoard(circuitJson);
|
|
2720
3088
|
minClearance ??= getBoardDrcValue(board, "min_via_hole_edge_to_via_hole_edge_clearance") ?? jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
|
|
2721
|
-
connMap ??=
|
|
3089
|
+
connMap ??= getFullConnectivityMapFromCircuitJson5(circuitJson);
|
|
2722
3090
|
const errors = [];
|
|
2723
3091
|
const reported = /* @__PURE__ */ new Set();
|
|
2724
3092
|
for (let i = 0; i < vias.length; i++) {
|
|
@@ -2735,10 +3103,10 @@ function checkDifferentNetViaSpacing(circuitJson, {
|
|
|
2735
3103
|
errors.push({
|
|
2736
3104
|
type: "pcb_via_clearance_error",
|
|
2737
3105
|
pcb_error_id: `different_net_vias_close_${pairId}`,
|
|
2738
|
-
message: `Vias ${
|
|
3106
|
+
message: `Vias ${getReadableNameForElement6(
|
|
2739
3107
|
circuitJson,
|
|
2740
3108
|
viaA.pcb_via_id
|
|
2741
|
-
)} and ${
|
|
3109
|
+
)} and ${getReadableNameForElement6(
|
|
2742
3110
|
circuitJson,
|
|
2743
3111
|
viaB.pcb_via_id
|
|
2744
3112
|
)} from different nets are too close together (gap: ${gap.toFixed(
|
|
@@ -2760,14 +3128,14 @@ function checkDifferentNetViaSpacing(circuitJson, {
|
|
|
2760
3128
|
|
|
2761
3129
|
// lib/check-source-traces-match-pcb-trace-thickness.ts
|
|
2762
3130
|
import { cju as cju4 } from "@tscircuit/circuit-json-util";
|
|
2763
|
-
import { getFullConnectivityMapFromCircuitJson as
|
|
3131
|
+
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson6 } from "circuit-json-to-connectivity-map";
|
|
2764
3132
|
function checkSourceTracesMatchPcbTraceThickness(circuitJson) {
|
|
2765
3133
|
const warnings = [];
|
|
2766
3134
|
const db = cju4(circuitJson);
|
|
2767
3135
|
const sourceTraces = db.source_trace.list();
|
|
2768
3136
|
const pcbTraces = db.pcb_trace.list();
|
|
2769
3137
|
const pcbPorts = db.pcb_port.list();
|
|
2770
|
-
const connectivityMap =
|
|
3138
|
+
const connectivityMap = getFullConnectivityMapFromCircuitJson6(circuitJson);
|
|
2771
3139
|
for (const sourceTrace of sourceTraces) {
|
|
2772
3140
|
const requestedThickness = sourceTrace.min_trace_thickness;
|
|
2773
3141
|
if (requestedThickness === void 0) continue;
|
|
@@ -2785,7 +3153,7 @@ function checkSourceTracesMatchPcbTraceThickness(circuitJson) {
|
|
|
2785
3153
|
);
|
|
2786
3154
|
if (relatedPcbTraces.length === 0) continue;
|
|
2787
3155
|
const actualWireWidths = relatedPcbTraces.flatMap(
|
|
2788
|
-
(pcbTrace) => pcbTrace.route.filter((
|
|
3156
|
+
(pcbTrace) => pcbTrace.route.filter((point2) => point2.route_type === "wire").map((point2) => point2.width)
|
|
2789
3157
|
);
|
|
2790
3158
|
if (actualWireWidths.length === 0) continue;
|
|
2791
3159
|
const actualThickness = Math.min(...actualWireWidths);
|
|
@@ -2793,18 +3161,18 @@ function checkSourceTracesMatchPcbTraceThickness(circuitJson) {
|
|
|
2793
3161
|
let undersizedSegment;
|
|
2794
3162
|
for (const relatedPcbTrace of relatedPcbTraces) {
|
|
2795
3163
|
for (let i = 0; i < relatedPcbTrace.route.length - 1; i++) {
|
|
2796
|
-
const
|
|
3164
|
+
const point2 = relatedPcbTrace.route[i];
|
|
2797
3165
|
const nextPoint = relatedPcbTrace.route[i + 1];
|
|
2798
|
-
if (!
|
|
2799
|
-
if (
|
|
3166
|
+
if (!point2 || !nextPoint) continue;
|
|
3167
|
+
if (point2.route_type !== "wire" || nextPoint.route_type !== "wire") {
|
|
2800
3168
|
continue;
|
|
2801
3169
|
}
|
|
2802
|
-
if (
|
|
3170
|
+
if (point2.width !== actualThickness) continue;
|
|
2803
3171
|
undersizedSegment = {
|
|
2804
3172
|
pcb_trace_id: relatedPcbTrace.pcb_trace_id,
|
|
2805
3173
|
center: {
|
|
2806
|
-
x: (
|
|
2807
|
-
y: (
|
|
3174
|
+
x: (point2.x + nextPoint.x) / 2,
|
|
3175
|
+
y: (point2.y + nextPoint.y) / 2
|
|
2808
3176
|
}
|
|
2809
3177
|
};
|
|
2810
3178
|
break;
|
|
@@ -2832,7 +3200,7 @@ function checkSourceTracesMatchPcbTraceThickness(circuitJson) {
|
|
|
2832
3200
|
}
|
|
2833
3201
|
|
|
2834
3202
|
// lib/check-source-traces-have-pcb-traces.ts
|
|
2835
|
-
import { getFullConnectivityMapFromCircuitJson as
|
|
3203
|
+
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson7 } from "circuit-json-to-connectivity-map";
|
|
2836
3204
|
function checkSourceTracesHavePcbTraces(circuitJson) {
|
|
2837
3205
|
const errors = [];
|
|
2838
3206
|
const sourceTraces = circuitJson.filter(
|
|
@@ -2847,7 +3215,7 @@ function checkSourceTracesHavePcbTraces(circuitJson) {
|
|
|
2847
3215
|
const sourcePortToPcbPort = new Map(
|
|
2848
3216
|
pcbPorts.map((pcbPort) => [pcbPort.source_port_id, pcbPort])
|
|
2849
3217
|
);
|
|
2850
|
-
const connectivityMap =
|
|
3218
|
+
const connectivityMap = getFullConnectivityMapFromCircuitJson7(circuitJson);
|
|
2851
3219
|
let pourConnectivity;
|
|
2852
3220
|
const getPourConnectivity = () => pourConnectivity ??= getCopperPourConnectivity(
|
|
2853
3221
|
circuitJson,
|
|
@@ -2895,7 +3263,7 @@ import {
|
|
|
2895
3263
|
getReadableNameForPcbTrace
|
|
2896
3264
|
} from "@tscircuit/circuit-json-util";
|
|
2897
3265
|
import {
|
|
2898
|
-
getFullConnectivityMapFromCircuitJson as
|
|
3266
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson8,
|
|
2899
3267
|
PcbConnectivityMap as PcbConnectivityMap2
|
|
2900
3268
|
} from "circuit-json-to-connectivity-map";
|
|
2901
3269
|
|
|
@@ -2977,27 +3345,27 @@ function getViaContactIndex(circuitJson, connectivity) {
|
|
|
2977
3345
|
}
|
|
2978
3346
|
for (const trace of circuitJson) {
|
|
2979
3347
|
if (trace.type !== "pcb_trace") continue;
|
|
2980
|
-
for (const
|
|
2981
|
-
if (
|
|
3348
|
+
for (const point2 of trace.route) {
|
|
3349
|
+
if (point2.route_type !== "via") continue;
|
|
2982
3350
|
if (vias.some(
|
|
2983
|
-
(via) => Math.hypot(via.x -
|
|
3351
|
+
(via) => Math.hypot(via.x - point2.x, via.y - point2.y) <= CONTACT_EPSILON && (via.pcb_trace_id === trace.pcb_trace_id || !via.pcb_trace_id && connectivity.areIdsConnected(
|
|
2984
3352
|
via.pcb_via_id,
|
|
2985
3353
|
trace.pcb_trace_id
|
|
2986
3354
|
))
|
|
2987
3355
|
))
|
|
2988
3356
|
continue;
|
|
2989
|
-
const from = stack.indexOf(
|
|
2990
|
-
const to = stack.indexOf(
|
|
3357
|
+
const from = stack.indexOf(point2.from_layer);
|
|
3358
|
+
const to = stack.indexOf(point2.to_layer);
|
|
2991
3359
|
if (from < 0 || to < 0) continue;
|
|
2992
|
-
const diameter =
|
|
3360
|
+
const diameter = point2.outer_diameter;
|
|
2993
3361
|
if (diameter !== void 0 && (!Number.isFinite(diameter) || diameter <= 0))
|
|
2994
3362
|
continue;
|
|
2995
3363
|
add(
|
|
2996
3364
|
trace.pcb_trace_id,
|
|
2997
3365
|
stack.slice(Math.min(from, to), Math.max(from, to) + 1),
|
|
2998
3366
|
{
|
|
2999
|
-
x:
|
|
3000
|
-
y:
|
|
3367
|
+
x: point2.x,
|
|
3368
|
+
y: point2.y,
|
|
3001
3369
|
ownerTraceId: trace.pcb_trace_id,
|
|
3002
3370
|
radius: diameter === void 0 ? void 0 : diameter / 2
|
|
3003
3371
|
}
|
|
@@ -3007,30 +3375,30 @@ function getViaContactIndex(circuitJson, connectivity) {
|
|
|
3007
3375
|
return index;
|
|
3008
3376
|
}
|
|
3009
3377
|
function endpointTouchesVia({
|
|
3010
|
-
point,
|
|
3378
|
+
point: point2,
|
|
3011
3379
|
width,
|
|
3012
3380
|
ownerTrace,
|
|
3013
3381
|
index,
|
|
3014
3382
|
connectivity
|
|
3015
3383
|
}) {
|
|
3016
|
-
if (
|
|
3384
|
+
if (point2.route_type !== "wire" || !Number.isFinite(width) || width <= 0)
|
|
3017
3385
|
return false;
|
|
3018
3386
|
const net = connectivity.getNetConnectedToId(ownerTrace.pcb_trace_id);
|
|
3019
3387
|
if (!net) return false;
|
|
3020
|
-
return (index.get(net)?.get(
|
|
3388
|
+
return (index.get(net)?.get(point2.layer) ?? []).some((via) => {
|
|
3021
3389
|
if (via.ownerTraceId === ownerTrace.pcb_trace_id) return false;
|
|
3022
3390
|
if (!via.touchesPad && ![...via.touchingTraceIds].some((id) => id !== ownerTrace.pcb_trace_id))
|
|
3023
3391
|
return false;
|
|
3024
3392
|
const contactDistance = via.radius === void 0 ? 0 : via.radius + width / 2;
|
|
3025
|
-
return Math.hypot(
|
|
3393
|
+
return Math.hypot(point2.x - via.x, point2.y - via.y) <= contactDistance + CONTACT_EPSILON;
|
|
3026
3394
|
});
|
|
3027
3395
|
}
|
|
3028
3396
|
|
|
3029
3397
|
// lib/check-traces-are-contiguous/check-traces-are-contiguous.ts
|
|
3030
3398
|
var ENDPOINT_CONTACT_EPSILON = 1e-9;
|
|
3031
3399
|
var TRACE_SEGMENT_GEOMETRY_EPSILON = 1e-9;
|
|
3032
|
-
function routePointTouchesPad(
|
|
3033
|
-
return
|
|
3400
|
+
function routePointTouchesPad(point2, pad) {
|
|
3401
|
+
return point2.route_type === "wire" && getLayersOfPcbElement(pad).includes(point2.layer) && isPointInPad(point2, pad);
|
|
3034
3402
|
}
|
|
3035
3403
|
function getTraceWireSegmentsByNetAndLayer(pcbTraces, fullConnectivityMap) {
|
|
3036
3404
|
const segmentsByNetAndLayer = /* @__PURE__ */ new Map();
|
|
@@ -3073,43 +3441,43 @@ function getEndpointTraceCopperWidth(trace, endpoint) {
|
|
|
3073
3441
|
return void 0;
|
|
3074
3442
|
}
|
|
3075
3443
|
function routePointTouchesLogicallyConnectedTraceCopper({
|
|
3076
|
-
point,
|
|
3444
|
+
point: point2,
|
|
3077
3445
|
endpointTraceCopperWidth,
|
|
3078
3446
|
ownerTrace,
|
|
3079
3447
|
traceWireSegmentsByNetAndLayer,
|
|
3080
3448
|
fullConnectivityMap
|
|
3081
3449
|
}) {
|
|
3082
|
-
if (
|
|
3450
|
+
if (point2.route_type !== "wire") return false;
|
|
3083
3451
|
const ownerNetId = fullConnectivityMap.getNetConnectedToId(
|
|
3084
3452
|
ownerTrace.pcb_trace_id
|
|
3085
3453
|
);
|
|
3086
3454
|
if (!ownerNetId) return false;
|
|
3087
|
-
const candidateSegments = traceWireSegmentsByNetAndLayer.get(ownerNetId)?.get(
|
|
3455
|
+
const candidateSegments = traceWireSegmentsByNetAndLayer.get(ownerNetId)?.get(point2.layer) ?? [];
|
|
3088
3456
|
for (const segment of candidateSegments) {
|
|
3089
3457
|
if (segment.trace.pcb_trace_id === ownerTrace.pcb_trace_id) continue;
|
|
3090
3458
|
const maximumContactDistance = endpointTraceCopperWidth / 2 + segment.start.width / 2 + ENDPOINT_CONTACT_EPSILON;
|
|
3091
|
-
if (pointToSegmentDistance3(
|
|
3459
|
+
if (pointToSegmentDistance3(point2, segment.start, segment.end) <= maximumContactDistance) {
|
|
3092
3460
|
return true;
|
|
3093
3461
|
}
|
|
3094
3462
|
}
|
|
3095
3463
|
return false;
|
|
3096
3464
|
}
|
|
3097
|
-
function getRoutePointCenter(
|
|
3098
|
-
if (
|
|
3465
|
+
function getRoutePointCenter(point2) {
|
|
3466
|
+
if (point2.route_type === "through_pad") {
|
|
3099
3467
|
return {
|
|
3100
|
-
x: (
|
|
3101
|
-
y: (
|
|
3468
|
+
x: (point2.start.x + point2.end.x) / 2,
|
|
3469
|
+
y: (point2.start.y + point2.end.y) / 2
|
|
3102
3470
|
};
|
|
3103
3471
|
}
|
|
3104
|
-
return { x:
|
|
3472
|
+
return { x: point2.x, y: point2.y };
|
|
3105
3473
|
}
|
|
3106
|
-
function routePointConnectsToAnotherExpectedPort(
|
|
3474
|
+
function routePointConnectsToAnotherExpectedPort(point2, expectedPorts, missingPcbPortId, padMap) {
|
|
3107
3475
|
return expectedPorts.some((expectedPort) => {
|
|
3108
3476
|
if (!expectedPort.pcb_port_id || expectedPort.pcb_port_id === missingPcbPortId) {
|
|
3109
3477
|
return false;
|
|
3110
3478
|
}
|
|
3111
3479
|
const expectedPads = padMap.get(expectedPort.pcb_port_id);
|
|
3112
|
-
return expectedPads?.some((pad) => routePointTouchesPad(
|
|
3480
|
+
return expectedPads?.some((pad) => routePointTouchesPad(point2, pad)) ?? false;
|
|
3113
3481
|
});
|
|
3114
3482
|
}
|
|
3115
3483
|
function getMissingConnectionErrorCenter({
|
|
@@ -3182,7 +3550,7 @@ function checkTracesAreContiguous(circuitJson) {
|
|
|
3182
3550
|
let fullConnectivityMap;
|
|
3183
3551
|
let traceWireSegmentsByNetAndLayer;
|
|
3184
3552
|
const getFullConnectivityMap = () => {
|
|
3185
|
-
fullConnectivityMap ??=
|
|
3553
|
+
fullConnectivityMap ??= getFullConnectivityMapFromCircuitJson8(circuitJson);
|
|
3186
3554
|
return fullConnectivityMap;
|
|
3187
3555
|
};
|
|
3188
3556
|
const getTraceWireSegmentIndex = () => {
|
|
@@ -3220,10 +3588,10 @@ function checkTracesAreContiguous(circuitJson) {
|
|
|
3220
3588
|
const touchedPortIds = /* @__PURE__ */ new Set();
|
|
3221
3589
|
const firstPoint = trace.route[0];
|
|
3222
3590
|
const lastPoint = trace.route.at(-1);
|
|
3223
|
-
for (const
|
|
3224
|
-
if (!
|
|
3591
|
+
for (const point2 of [firstPoint, lastPoint]) {
|
|
3592
|
+
if (!point2) continue;
|
|
3225
3593
|
for (const [pcbPortId, pads] of padMap) {
|
|
3226
|
-
if (pads.some((pad) => routePointTouchesPad(
|
|
3594
|
+
if (pads.some((pad) => routePointTouchesPad(point2, pad))) {
|
|
3227
3595
|
touchedPortIds.add(pcbPortId);
|
|
3228
3596
|
}
|
|
3229
3597
|
}
|
|
@@ -3521,7 +3889,7 @@ import {
|
|
|
3521
3889
|
getPrimaryId as getPrimaryId5
|
|
3522
3890
|
} from "@tscircuit/circuit-json-util";
|
|
3523
3891
|
import { doBoundsOverlap as doBoundsOverlap3 } from "@tscircuit/math-utils";
|
|
3524
|
-
import { getFullConnectivityMapFromCircuitJson as
|
|
3892
|
+
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson9 } from "circuit-json-to-connectivity-map";
|
|
3525
3893
|
|
|
3526
3894
|
// lib/check-pcb-components-overlap/doPcbElementsOverlap.ts
|
|
3527
3895
|
import { getBoundsOfPcbElements as getBoundsOfPcbElements4 } from "@tscircuit/circuit-json-util";
|
|
@@ -3608,7 +3976,7 @@ var formatOverlapElementDescription = (circuitJson, element) => {
|
|
|
3608
3976
|
};
|
|
3609
3977
|
function checkPcbComponentOverlap(circuitJson) {
|
|
3610
3978
|
const errors = [];
|
|
3611
|
-
const connMap =
|
|
3979
|
+
const connMap = getFullConnectivityMapFromCircuitJson9(circuitJson);
|
|
3612
3980
|
const smtPads = cju6(circuitJson).pcb_smtpad.list();
|
|
3613
3981
|
const platedHoles = cju6(circuitJson).pcb_plated_hole.list();
|
|
3614
3982
|
const holes = cju6(circuitJson).pcb_hole.list();
|
|
@@ -3933,11 +4301,11 @@ var checkPcbTraceViaCounts = (circuitJson) => {
|
|
|
3933
4301
|
// lib/check-pad-pad-clearance.ts
|
|
3934
4302
|
import {
|
|
3935
4303
|
getPrimaryId as getPrimaryId6,
|
|
3936
|
-
getReadableNameForElement as
|
|
4304
|
+
getReadableNameForElement as getReadableNameForElement7
|
|
3937
4305
|
} from "@tscircuit/circuit-json-util";
|
|
3938
4306
|
import { formatMm } from "format-si-unit";
|
|
3939
4307
|
import {
|
|
3940
|
-
getFullConnectivityMapFromCircuitJson as
|
|
4308
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson10
|
|
3941
4309
|
} from "circuit-json-to-connectivity-map";
|
|
3942
4310
|
function checkPadPadClearance(circuitJson, {
|
|
3943
4311
|
connMap,
|
|
@@ -3947,7 +4315,7 @@ function checkPadPadClearance(circuitJson, {
|
|
|
3947
4315
|
if (pads.length < 2) return [];
|
|
3948
4316
|
const board = getPcbBoard(circuitJson);
|
|
3949
4317
|
minClearance ??= getBoardDrcValue(board, "min_pad_edge_to_pad_edge_clearance") ?? jlcMinTolerances.min_pad_edge_to_pad_edge_clearance;
|
|
3950
|
-
connMap ??=
|
|
4318
|
+
connMap ??= getFullConnectivityMapFromCircuitJson10(circuitJson);
|
|
3951
4319
|
const spatialIndex = new SpatialObjectIndex({
|
|
3952
4320
|
objects: pads,
|
|
3953
4321
|
getBounds: getPadBounds,
|
|
@@ -3978,7 +4346,7 @@ function checkPadPadClearance(circuitJson, {
|
|
|
3978
4346
|
type: "pcb_pad_pad_clearance_error",
|
|
3979
4347
|
pcb_pad_pad_clearance_error_id: `pad_pad_clearance_${pairId}`,
|
|
3980
4348
|
error_type: "pcb_pad_pad_clearance_error",
|
|
3981
|
-
message: `Pads ${
|
|
4349
|
+
message: `Pads ${getReadableNameForElement7(circuitJson, padAId)} and ${getReadableNameForElement7(circuitJson, padBId)} are too close (clearance: ${formatMm(gap)}, minimum: ${formatMm(minClearance)})`,
|
|
3982
4350
|
pcb_pad_ids: [padAId, padBId],
|
|
3983
4351
|
minimum_clearance: minClearance,
|
|
3984
4352
|
actual_clearance: gap,
|
|
@@ -3998,11 +4366,11 @@ function checkPadPadClearance(circuitJson, {
|
|
|
3998
4366
|
// lib/check-pad-trace-clearance.ts
|
|
3999
4367
|
import {
|
|
4000
4368
|
getPrimaryId as getPrimaryId7,
|
|
4001
|
-
getReadableNameForElement as
|
|
4369
|
+
getReadableNameForElement as getReadableNameForElement8
|
|
4002
4370
|
} from "@tscircuit/circuit-json-util";
|
|
4003
4371
|
import { formatMm as formatMm2 } from "format-si-unit";
|
|
4004
4372
|
import {
|
|
4005
|
-
getFullConnectivityMapFromCircuitJson as
|
|
4373
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson11
|
|
4006
4374
|
} from "circuit-json-to-connectivity-map";
|
|
4007
4375
|
function checkPadTraceClearance(circuitJson, {
|
|
4008
4376
|
connMap,
|
|
@@ -4013,7 +4381,7 @@ function checkPadTraceClearance(circuitJson, {
|
|
|
4013
4381
|
if (pads.length === 0 || segments.length === 0) return [];
|
|
4014
4382
|
const board = getPcbBoard(circuitJson);
|
|
4015
4383
|
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? jlcMinTolerances.min_trace_to_pad_edge_clearance;
|
|
4016
|
-
connMap ??=
|
|
4384
|
+
connMap ??= getFullConnectivityMapFromCircuitJson11(circuitJson);
|
|
4017
4385
|
const spatialIndex = new SpatialObjectIndex({
|
|
4018
4386
|
objects: pads,
|
|
4019
4387
|
getBounds: getPadBounds,
|
|
@@ -4043,7 +4411,7 @@ function checkPadTraceClearance(circuitJson, {
|
|
|
4043
4411
|
type: "pcb_pad_trace_clearance_error",
|
|
4044
4412
|
pcb_pad_trace_clearance_error_id: `pad_trace_clearance_${pairId}`,
|
|
4045
4413
|
error_type: "pcb_pad_trace_clearance_error",
|
|
4046
|
-
message: `Pad ${
|
|
4414
|
+
message: `Pad ${getReadableNameForElement8(circuitJson, padId)} and trace ${getReadableNameForElement8(circuitJson, segment.pcb_trace_id)} are too close (clearance: ${formatMm2(gap)}, minimum: ${formatMm2(minClearance)})`,
|
|
4047
4415
|
pcb_pad_id: padId,
|
|
4048
4416
|
pcb_trace_id: segment.pcb_trace_id,
|
|
4049
4417
|
minimum_clearance: minClearance,
|
|
@@ -4060,10 +4428,10 @@ function checkPadTraceClearance(circuitJson, {
|
|
|
4060
4428
|
}
|
|
4061
4429
|
|
|
4062
4430
|
// lib/check-via-trace-clearance.ts
|
|
4063
|
-
import { getReadableNameForElement as
|
|
4431
|
+
import { getReadableNameForElement as getReadableNameForElement9 } from "@tscircuit/circuit-json-util";
|
|
4064
4432
|
import { formatMm as formatMm3 } from "format-si-unit";
|
|
4065
4433
|
import {
|
|
4066
|
-
getFullConnectivityMapFromCircuitJson as
|
|
4434
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson12
|
|
4067
4435
|
} from "circuit-json-to-connectivity-map";
|
|
4068
4436
|
function checkViaTraceClearance(circuitJson, {
|
|
4069
4437
|
connMap,
|
|
@@ -4074,7 +4442,7 @@ function checkViaTraceClearance(circuitJson, {
|
|
|
4074
4442
|
if (vias.length === 0 || segments.length === 0) return [];
|
|
4075
4443
|
const board = getPcbBoard(circuitJson);
|
|
4076
4444
|
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? jlcMinTolerances.min_trace_to_pad_edge_clearance;
|
|
4077
|
-
connMap ??=
|
|
4445
|
+
connMap ??= getFullConnectivityMapFromCircuitJson12(circuitJson);
|
|
4078
4446
|
const errors = /* @__PURE__ */ new Map();
|
|
4079
4447
|
const overlappingPairIds = /* @__PURE__ */ new Set();
|
|
4080
4448
|
for (const via of vias) {
|
|
@@ -4095,7 +4463,7 @@ function checkViaTraceClearance(circuitJson, {
|
|
|
4095
4463
|
type: "pcb_via_trace_clearance_error",
|
|
4096
4464
|
pcb_via_trace_clearance_error_id: `via_trace_clearance_${pairId}`,
|
|
4097
4465
|
error_type: "pcb_via_trace_clearance_error",
|
|
4098
|
-
message: `Via ${
|
|
4466
|
+
message: `Via ${getReadableNameForElement9(circuitJson, via.pcb_via_id)} and trace ${getReadableNameForElement9(circuitJson, segment.pcb_trace_id)} are too close (clearance: ${formatMm3(gap)}, minimum: ${formatMm3(minClearance)})`,
|
|
4099
4467
|
pcb_via_id: via.pcb_via_id,
|
|
4100
4468
|
pcb_trace_id: segment.pcb_trace_id,
|
|
4101
4469
|
minimum_clearance: minClearance,
|
|
@@ -4114,11 +4482,11 @@ function checkViaTraceClearance(circuitJson, {
|
|
|
4114
4482
|
// lib/check-via-pad-clearance.ts
|
|
4115
4483
|
import {
|
|
4116
4484
|
getPrimaryId as getPrimaryId8,
|
|
4117
|
-
getReadableNameForElement as
|
|
4485
|
+
getReadableNameForElement as getReadableNameForElement10
|
|
4118
4486
|
} from "@tscircuit/circuit-json-util";
|
|
4119
4487
|
import { formatMm as formatMm4 } from "format-si-unit";
|
|
4120
4488
|
import {
|
|
4121
|
-
getFullConnectivityMapFromCircuitJson as
|
|
4489
|
+
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson13
|
|
4122
4490
|
} from "circuit-json-to-connectivity-map";
|
|
4123
4491
|
function checkViaPadClearance(circuitJson, {
|
|
4124
4492
|
connMap,
|
|
@@ -4131,7 +4499,7 @@ function checkViaPadClearance(circuitJson, {
|
|
|
4131
4499
|
if (vias.length === 0 || pads.length === 0) return [];
|
|
4132
4500
|
const board = getPcbBoard(circuitJson);
|
|
4133
4501
|
const requiredClearance = minClearance ?? getBoardDrcValue(board, "min_pad_edge_to_pad_edge_clearance") ?? jlcMinTolerances.min_pad_edge_to_pad_edge_clearance;
|
|
4134
|
-
connMap ??=
|
|
4502
|
+
connMap ??= getFullConnectivityMapFromCircuitJson13(circuitJson);
|
|
4135
4503
|
const padIndex = new SpatialObjectIndex({
|
|
4136
4504
|
objects: pads,
|
|
4137
4505
|
getBounds: getPadBounds,
|
|
@@ -4157,7 +4525,7 @@ function checkViaPadClearance(circuitJson, {
|
|
|
4157
4525
|
type: "pcb_pad_pad_clearance_error",
|
|
4158
4526
|
pcb_pad_pad_clearance_error_id: `via_pad_clearance_${via.pcb_via_id}_${padId}`,
|
|
4159
4527
|
error_type: "pcb_pad_pad_clearance_error",
|
|
4160
|
-
message: `Via ${
|
|
4528
|
+
message: `Via ${getReadableNameForElement10(circuitJson, via.pcb_via_id)} and pad ${getReadableNameForElement10(circuitJson, padId)} are too close (clearance: ${formatMm4(gap)}, minimum: ${formatMm4(requiredClearance)})`,
|
|
4161
4529
|
pcb_pad_ids: [via.pcb_via_id, padId],
|
|
4162
4530
|
minimum_clearance: requiredClearance,
|
|
4163
4531
|
actual_clearance: gap,
|
|
@@ -4170,7 +4538,7 @@ function checkViaPadClearance(circuitJson, {
|
|
|
4170
4538
|
|
|
4171
4539
|
// lib/check-vias-in-pads.ts
|
|
4172
4540
|
import { getPrimaryId as getPrimaryId9 } from "@tscircuit/circuit-json-util";
|
|
4173
|
-
import { getFullConnectivityMapFromCircuitJson as
|
|
4541
|
+
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson14 } from "circuit-json-to-connectivity-map";
|
|
4174
4542
|
function checkViasInPads(circuitJson) {
|
|
4175
4543
|
const board = getPcbBoard(circuitJson);
|
|
4176
4544
|
if (board && "is_via_in_pad_allowed" in board && board.is_via_in_pad_allowed === true) {
|
|
@@ -4181,7 +4549,7 @@ function checkViasInPads(circuitJson) {
|
|
|
4181
4549
|
);
|
|
4182
4550
|
const pads = getPads(circuitJson);
|
|
4183
4551
|
if (vias.length === 0 || pads.length === 0) return [];
|
|
4184
|
-
const connMap =
|
|
4552
|
+
const connMap = getFullConnectivityMapFromCircuitJson14(circuitJson);
|
|
4185
4553
|
const padOrdinals = new Map(
|
|
4186
4554
|
pads.map((pad, index) => [getPrimaryId9(pad), index])
|
|
4187
4555
|
);
|
|
@@ -5119,22 +5487,22 @@ function checkCourtyardOverlap(circuitJson) {
|
|
|
5119
5487
|
// lib/check-testpoint-accessibility.ts
|
|
5120
5488
|
import { isPointInsidePolygon as isPointInsidePolygon3 } from "@tscircuit/math-utils";
|
|
5121
5489
|
var isCourtyardElement3 = (element) => element.type === "pcb_courtyard_circle" || element.type === "pcb_courtyard_outline" || element.type === "pcb_courtyard_polygon" || element.type === "pcb_courtyard_rect";
|
|
5122
|
-
var isPointInsideCourtyard = (
|
|
5490
|
+
var isPointInsideCourtyard = (point2, courtyard) => {
|
|
5123
5491
|
if (courtyard.type === "pcb_courtyard_circle") {
|
|
5124
|
-
const dx =
|
|
5125
|
-
const dy =
|
|
5492
|
+
const dx = point2.x - courtyard.center.x;
|
|
5493
|
+
const dy = point2.y - courtyard.center.y;
|
|
5126
5494
|
return dx * dx + dy * dy <= courtyard.radius * courtyard.radius;
|
|
5127
5495
|
}
|
|
5128
5496
|
if (courtyard.type === "pcb_courtyard_rect") {
|
|
5129
5497
|
const angle = -1 * (courtyard.ccw_rotation ?? 0) * Math.PI / 180;
|
|
5130
|
-
const dx =
|
|
5131
|
-
const dy =
|
|
5498
|
+
const dx = point2.x - courtyard.center.x;
|
|
5499
|
+
const dy = point2.y - courtyard.center.y;
|
|
5132
5500
|
const localX = dx * Math.cos(angle) - dy * Math.sin(angle);
|
|
5133
5501
|
const localY = dx * Math.sin(angle) + dy * Math.cos(angle);
|
|
5134
5502
|
return Math.abs(localX) <= courtyard.width / 2 && Math.abs(localY) <= courtyard.height / 2;
|
|
5135
5503
|
}
|
|
5136
5504
|
const polygon = courtyard.type === "pcb_courtyard_polygon" ? courtyard.points : courtyard.outline;
|
|
5137
|
-
return isPointInsidePolygon3(
|
|
5505
|
+
return isPointInsidePolygon3(point2, polygon);
|
|
5138
5506
|
};
|
|
5139
5507
|
var getPcbComponentName = (circuitJson, pcbComponentId) => {
|
|
5140
5508
|
const pcbComponent = circuitJson.find(
|
|
@@ -5234,6 +5602,7 @@ async function runAllRoutingChecks(circuitJson) {
|
|
|
5234
5602
|
...checkPcbTraceLengths(circuitJson),
|
|
5235
5603
|
...checkPcbTraceViaCounts(circuitJson),
|
|
5236
5604
|
...checkEachPcbTraceNonOverlapping(circuitJson),
|
|
5605
|
+
...checkCopperPourShorts(circuitJson),
|
|
5237
5606
|
...checkPadTraceClearance(circuitJson),
|
|
5238
5607
|
...checkViaTraceClearance(circuitJson),
|
|
5239
5608
|
...checkViaPadClearance(circuitJson),
|
|
@@ -5256,6 +5625,7 @@ export {
|
|
|
5256
5625
|
NetManager,
|
|
5257
5626
|
checkAllPinsInComponentAreUnderspecified,
|
|
5258
5627
|
checkConnectorAccessibleOrientation,
|
|
5628
|
+
checkCopperPourShorts,
|
|
5259
5629
|
checkCopperToBoardEdgeClearance,
|
|
5260
5630
|
checkDifferentNetViaSpacing,
|
|
5261
5631
|
checkEachPcbPortConnectedToPcbTraces,
|