@tscircuit/checks 0.0.137 → 0.0.139
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/dist/index.js +368 -232
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,266 @@
|
|
|
1
|
+
// lib/check-traces-are-contiguous/is-point-in-pad.ts
|
|
2
|
+
import { pointToSegmentDistance } from "@tscircuit/math-utils";
|
|
3
|
+
|
|
4
|
+
// lib/check-each-pcb-trace-non-overlapping/segment-to-polygon-clearance.ts
|
|
5
|
+
import {
|
|
6
|
+
distSq,
|
|
7
|
+
getSegmentIntersection,
|
|
8
|
+
isPointInsidePolygon,
|
|
9
|
+
pointToSegmentClosestPoint,
|
|
10
|
+
segmentToSegmentMinDistance
|
|
11
|
+
} from "@tscircuit/math-utils";
|
|
12
|
+
var rotatePoint = (point, angleDegrees) => {
|
|
13
|
+
const angle = angleDegrees * Math.PI / 180;
|
|
14
|
+
return {
|
|
15
|
+
x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
|
|
16
|
+
y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
var getRotatedRectPoints = ({
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
ccwRotation
|
|
25
|
+
}) => {
|
|
26
|
+
const halfWidth = width / 2;
|
|
27
|
+
const halfHeight = height / 2;
|
|
28
|
+
return [
|
|
29
|
+
{ x: -halfWidth, y: -halfHeight },
|
|
30
|
+
{ x: halfWidth, y: -halfHeight },
|
|
31
|
+
{ x: halfWidth, y: halfHeight },
|
|
32
|
+
{ x: -halfWidth, y: halfHeight }
|
|
33
|
+
].map((point) => {
|
|
34
|
+
const rotated = rotatePoint(point, ccwRotation);
|
|
35
|
+
return { x: x + rotated.x, y: y + rotated.y };
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
var getPillCenterLineForPad = (pad) => {
|
|
39
|
+
const ccwRotation = pad.shape === "rotated_pill" ? pad.ccw_rotation : 0;
|
|
40
|
+
const halfLineLength = Math.max(
|
|
41
|
+
Math.max(pad.width, pad.height) / 2 - pad.radius,
|
|
42
|
+
0
|
|
43
|
+
);
|
|
44
|
+
const axis = pad.width >= pad.height ? { x: halfLineLength, y: 0 } : { x: 0, y: halfLineLength };
|
|
45
|
+
const rotatedAxis = rotatePoint(axis, ccwRotation);
|
|
46
|
+
return {
|
|
47
|
+
start: { x: pad.x - rotatedAxis.x, y: pad.y - rotatedAxis.y },
|
|
48
|
+
end: { x: pad.x + rotatedAxis.x, y: pad.y + rotatedAxis.y },
|
|
49
|
+
radius: pad.radius
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
var getPolygonPointsForPad = (pad) => {
|
|
53
|
+
if (pad.type === "pcb_smtpad") {
|
|
54
|
+
if (pad.shape === "polygon") return pad.points;
|
|
55
|
+
if (pad.shape === "rotated_rect") {
|
|
56
|
+
return getRotatedRectPoints({
|
|
57
|
+
x: pad.x,
|
|
58
|
+
y: pad.y,
|
|
59
|
+
width: pad.width,
|
|
60
|
+
height: pad.height,
|
|
61
|
+
ccwRotation: pad.ccw_rotation
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
|
|
66
|
+
return getRotatedRectPoints({
|
|
67
|
+
x: pad.x,
|
|
68
|
+
y: pad.y,
|
|
69
|
+
width: pad.rect_pad_width,
|
|
70
|
+
height: pad.rect_pad_height,
|
|
71
|
+
ccwRotation: "rect_ccw_rotation" in pad && typeof pad.rect_ccw_rotation === "number" ? pad.rect_ccw_rotation : 0
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Expected polygonal pad geometry, got ${pad.type} with shape "${pad.shape}"`
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
var getPolygonEdges = (points) => points.map(
|
|
79
|
+
(point, index) => [point, points[(index + 1) % points.length]]
|
|
80
|
+
);
|
|
81
|
+
var getClosestPointsBetweenSegments = (a1, a2, b1, b2) => {
|
|
82
|
+
const intersection = getSegmentIntersection(a1, a2, b1, b2);
|
|
83
|
+
if (intersection) {
|
|
84
|
+
return {
|
|
85
|
+
distance: 0,
|
|
86
|
+
pointOnA: intersection,
|
|
87
|
+
pointOnB: intersection,
|
|
88
|
+
center: intersection
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const candidates = [
|
|
92
|
+
{ pointOnA: a1, pointOnB: pointToSegmentClosestPoint(a1, b1, b2) },
|
|
93
|
+
{ pointOnA: a2, pointOnB: pointToSegmentClosestPoint(a2, b1, b2) },
|
|
94
|
+
{ pointOnA: pointToSegmentClosestPoint(b1, a1, a2), pointOnB: b1 },
|
|
95
|
+
{ pointOnA: pointToSegmentClosestPoint(b2, a1, a2), pointOnB: b2 }
|
|
96
|
+
];
|
|
97
|
+
let best = candidates[0];
|
|
98
|
+
let bestDistanceSquared = distSq(best.pointOnA, best.pointOnB);
|
|
99
|
+
for (const candidate of candidates.slice(1)) {
|
|
100
|
+
const candidateDistanceSquared = distSq(
|
|
101
|
+
candidate.pointOnA,
|
|
102
|
+
candidate.pointOnB
|
|
103
|
+
);
|
|
104
|
+
if (candidateDistanceSquared < bestDistanceSquared) {
|
|
105
|
+
best = candidate;
|
|
106
|
+
bestDistanceSquared = candidateDistanceSquared;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
distance: segmentToSegmentMinDistance(a1, a2, b1, b2),
|
|
111
|
+
pointOnA: best.pointOnA,
|
|
112
|
+
pointOnB: best.pointOnB,
|
|
113
|
+
center: {
|
|
114
|
+
x: (best.pointOnA.x + best.pointOnB.x) / 2,
|
|
115
|
+
y: (best.pointOnA.y + best.pointOnB.y) / 2
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
var getSegmentToPolygonClearanceFromPoints = (start, end, polygon) => {
|
|
120
|
+
if (polygon.length < 3) {
|
|
121
|
+
return { distance: Number.POSITIVE_INFINITY, center: start };
|
|
122
|
+
}
|
|
123
|
+
const intersections = getPolygonEdges(polygon).map(
|
|
124
|
+
([edgeStart, edgeEnd]) => getSegmentIntersection(start, end, edgeStart, edgeEnd)
|
|
125
|
+
).filter((point) => point !== null);
|
|
126
|
+
if (intersections.length > 0) {
|
|
127
|
+
const dx = end.x - start.x;
|
|
128
|
+
const dy = end.y - start.y;
|
|
129
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
130
|
+
intersections.sort((a, b) => {
|
|
131
|
+
const ta = ((a.x - start.x) * dx + (a.y - start.y) * dy) / lengthSquared;
|
|
132
|
+
const tb = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared;
|
|
133
|
+
return ta - tb;
|
|
134
|
+
});
|
|
135
|
+
return { distance: 0, center: intersections[0] };
|
|
136
|
+
}
|
|
137
|
+
if (isPointInsidePolygon(start, polygon) || isPointInsidePolygon(end, polygon)) {
|
|
138
|
+
return {
|
|
139
|
+
distance: 0,
|
|
140
|
+
center: {
|
|
141
|
+
x: (start.x + end.x) / 2,
|
|
142
|
+
y: (start.y + end.y) / 2
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let best = getClosestPointsBetweenSegments(
|
|
147
|
+
start,
|
|
148
|
+
end,
|
|
149
|
+
polygon[0],
|
|
150
|
+
polygon[1]
|
|
151
|
+
);
|
|
152
|
+
for (const [edgeStart, edgeEnd] of getPolygonEdges(polygon).slice(1)) {
|
|
153
|
+
const candidate = getClosestPointsBetweenSegments(
|
|
154
|
+
start,
|
|
155
|
+
end,
|
|
156
|
+
edgeStart,
|
|
157
|
+
edgeEnd
|
|
158
|
+
);
|
|
159
|
+
if (candidate.distance < best.distance) best = candidate;
|
|
160
|
+
}
|
|
161
|
+
return { distance: best.distance, center: best.center };
|
|
162
|
+
};
|
|
163
|
+
var getSegmentToPolygonClearance = (segment, polygon) => getSegmentToPolygonClearanceFromPoints(
|
|
164
|
+
{ x: segment.x1, y: segment.y1 },
|
|
165
|
+
{ x: segment.x2, y: segment.y2 },
|
|
166
|
+
polygon
|
|
167
|
+
);
|
|
168
|
+
var getSegmentToPillClearance = (segment, pad) => {
|
|
169
|
+
const pill = getPillCenterLineForPad(pad);
|
|
170
|
+
const closest = getClosestPointsBetweenSegments(
|
|
171
|
+
{ x: segment.x1, y: segment.y1 },
|
|
172
|
+
{ x: segment.x2, y: segment.y2 },
|
|
173
|
+
pill.start,
|
|
174
|
+
pill.end
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
distance: closest.distance,
|
|
178
|
+
center: closest.center,
|
|
179
|
+
radius: pill.radius
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// lib/check-traces-are-contiguous/is-point-in-pad.ts
|
|
184
|
+
function getDistanceBetweenPoints(pointA, pointB) {
|
|
185
|
+
return Math.sqrt((pointB.x - pointA.x) ** 2 + (pointB.y - pointA.y) ** 2);
|
|
186
|
+
}
|
|
187
|
+
var POINT_ON_SEGMENT_TOLERANCE_MM = 1e-9;
|
|
188
|
+
function isPointOnSegment(point, segment) {
|
|
189
|
+
const crossProduct = (point.y - segment.start.y) * (segment.end.x - segment.start.x) - (point.x - segment.start.x) * (segment.end.y - segment.start.y);
|
|
190
|
+
if (Math.abs(crossProduct) > POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
191
|
+
const dotProduct = (point.x - segment.start.x) * (segment.end.x - segment.start.x) + (point.y - segment.start.y) * (segment.end.y - segment.start.y);
|
|
192
|
+
if (dotProduct < -POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
193
|
+
const squaredLength = (segment.end.x - segment.start.x) ** 2 + (segment.end.y - segment.start.y) ** 2;
|
|
194
|
+
return dotProduct <= squaredLength + POINT_ON_SEGMENT_TOLERANCE_MM;
|
|
195
|
+
}
|
|
196
|
+
function isPointInPad(point, pad) {
|
|
197
|
+
if (pad.type === "pcb_smtpad") {
|
|
198
|
+
if (pad.shape === "circle") {
|
|
199
|
+
return getDistanceBetweenPoints(point, pad) <= pad.radius;
|
|
200
|
+
}
|
|
201
|
+
if (pad.shape === "rect") {
|
|
202
|
+
const halfWidth = pad.width / 2;
|
|
203
|
+
const halfHeight = pad.height / 2;
|
|
204
|
+
return Math.abs(point.x - pad.x) <= halfWidth && Math.abs(point.y - pad.y) <= halfHeight;
|
|
205
|
+
}
|
|
206
|
+
if (pad.shape === "rotated_rect") {
|
|
207
|
+
const dx = point.x - pad.x;
|
|
208
|
+
const dy = point.y - pad.y;
|
|
209
|
+
const angle = -pad.ccw_rotation;
|
|
210
|
+
const rotatedX = dx * Math.cos(angle) - dy * Math.sin(angle);
|
|
211
|
+
const rotatedY = dx * Math.sin(angle) + dy * Math.cos(angle);
|
|
212
|
+
return Math.abs(rotatedX) <= pad.width / 2 && Math.abs(rotatedY) <= pad.height / 2;
|
|
213
|
+
}
|
|
214
|
+
if (pad.shape === "pill" || pad.shape === "rotated_pill") {
|
|
215
|
+
if (pad.shape === "rotated_pill") {
|
|
216
|
+
const pill = getPillCenterLineForPad(pad);
|
|
217
|
+
return pointToSegmentDistance(point, pill.start, pill.end) <= pill.radius;
|
|
218
|
+
}
|
|
219
|
+
const halfWidth = pad.width / 2;
|
|
220
|
+
const halfHeight = pad.height / 2;
|
|
221
|
+
const radius = pad.radius;
|
|
222
|
+
if (Math.abs(point.x - pad.x) <= halfWidth - radius && Math.abs(point.y - pad.y) <= halfHeight) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
const cornerX = Math.max(
|
|
226
|
+
Math.abs(point.x - pad.x) - (halfWidth - radius),
|
|
227
|
+
0
|
|
228
|
+
);
|
|
229
|
+
const cornerY = Math.max(
|
|
230
|
+
Math.abs(point.y - pad.y) - (halfHeight - radius),
|
|
231
|
+
0
|
|
232
|
+
);
|
|
233
|
+
return cornerX * cornerX + cornerY * cornerY <= radius * radius;
|
|
234
|
+
}
|
|
235
|
+
if (pad.shape === "polygon") {
|
|
236
|
+
let inside = false;
|
|
237
|
+
for (let i = 0, j = pad.points.length - 1; i < pad.points.length; j = i++) {
|
|
238
|
+
const pi = pad.points[i];
|
|
239
|
+
const pj = pad.points[j];
|
|
240
|
+
if (isPointOnSegment(point, { start: pi, end: pj })) return true;
|
|
241
|
+
const intersects = pi.y > point.y !== pj.y > point.y && point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x;
|
|
242
|
+
if (intersects) inside = !inside;
|
|
243
|
+
}
|
|
244
|
+
return inside;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (pad.type === "pcb_plated_hole") {
|
|
248
|
+
if (pad.shape === "circle") {
|
|
249
|
+
return getDistanceBetweenPoints(point, pad) <= pad.outer_diameter / 2;
|
|
250
|
+
}
|
|
251
|
+
if (pad.shape === "oval" || pad.shape === "pill") {
|
|
252
|
+
return Math.abs(point.x - pad.x) <= pad.outer_width / 2 && Math.abs(point.y - pad.y) <= pad.outer_height / 2;
|
|
253
|
+
}
|
|
254
|
+
if (pad.shape === "circular_hole_with_rect_pad") {
|
|
255
|
+
return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
|
|
256
|
+
}
|
|
257
|
+
if (pad.shape === "pill_hole_with_rect_pad") {
|
|
258
|
+
return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
1
264
|
// lib/add-start-and-end-port-ids-if-missing.ts
|
|
2
265
|
function distance(x1, y1, x2, y2) {
|
|
3
266
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
@@ -20,6 +283,8 @@ var addStartAndEndPortIdsIfMissing = (soup) => {
|
|
|
20
283
|
return Math.abs(point.x - pad.x) < pad.width / 2 + traceWidth / 2 && Math.abs(point.y - pad.y) < pad.height / 2 + traceWidth / 2;
|
|
21
284
|
} else if (pad.shape === "circle") {
|
|
22
285
|
return distance(point.x, point.y, pad.x, pad.y) < pad.radius;
|
|
286
|
+
} else if (pad.shape === "pill" || pad.shape === "rotated_pill") {
|
|
287
|
+
return isPointInPad(point, pad);
|
|
23
288
|
}
|
|
24
289
|
});
|
|
25
290
|
if (smtPad) return smtPad.pcb_port_id ?? null;
|
|
@@ -636,156 +901,6 @@ var clamp = (value, min, max) => {
|
|
|
636
901
|
|
|
637
902
|
// lib/check-each-pcb-trace-non-overlapping/getCollidableBounds.ts
|
|
638
903
|
import { getBoundsOfPcbElements as getBoundsOfPcbElements2 } from "@tscircuit/circuit-json-util";
|
|
639
|
-
|
|
640
|
-
// lib/check-each-pcb-trace-non-overlapping/segment-to-polygon-clearance.ts
|
|
641
|
-
import {
|
|
642
|
-
distSq,
|
|
643
|
-
getSegmentIntersection,
|
|
644
|
-
isPointInsidePolygon,
|
|
645
|
-
pointToSegmentClosestPoint,
|
|
646
|
-
segmentToSegmentMinDistance
|
|
647
|
-
} from "@tscircuit/math-utils";
|
|
648
|
-
var rotatePoint = (point, angleDegrees) => {
|
|
649
|
-
const angle = angleDegrees * Math.PI / 180;
|
|
650
|
-
return {
|
|
651
|
-
x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
|
|
652
|
-
y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
|
|
653
|
-
};
|
|
654
|
-
};
|
|
655
|
-
var getRotatedRectPoints = ({
|
|
656
|
-
x,
|
|
657
|
-
y,
|
|
658
|
-
width,
|
|
659
|
-
height,
|
|
660
|
-
ccwRotation
|
|
661
|
-
}) => {
|
|
662
|
-
const halfWidth = width / 2;
|
|
663
|
-
const halfHeight = height / 2;
|
|
664
|
-
return [
|
|
665
|
-
{ x: -halfWidth, y: -halfHeight },
|
|
666
|
-
{ x: halfWidth, y: -halfHeight },
|
|
667
|
-
{ x: halfWidth, y: halfHeight },
|
|
668
|
-
{ x: -halfWidth, y: halfHeight }
|
|
669
|
-
].map((point) => {
|
|
670
|
-
const rotated = rotatePoint(point, ccwRotation);
|
|
671
|
-
return { x: x + rotated.x, y: y + rotated.y };
|
|
672
|
-
});
|
|
673
|
-
};
|
|
674
|
-
var getPolygonPointsForPad = (pad) => {
|
|
675
|
-
if (pad.type === "pcb_smtpad") {
|
|
676
|
-
if (pad.shape === "polygon") return pad.points;
|
|
677
|
-
if (pad.shape === "rotated_rect") {
|
|
678
|
-
return getRotatedRectPoints({
|
|
679
|
-
x: pad.x,
|
|
680
|
-
y: pad.y,
|
|
681
|
-
width: pad.width,
|
|
682
|
-
height: pad.height,
|
|
683
|
-
ccwRotation: pad.ccw_rotation
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
|
|
688
|
-
return getRotatedRectPoints({
|
|
689
|
-
x: pad.x,
|
|
690
|
-
y: pad.y,
|
|
691
|
-
width: pad.rect_pad_width,
|
|
692
|
-
height: pad.rect_pad_height,
|
|
693
|
-
ccwRotation: "rect_ccw_rotation" in pad && typeof pad.rect_ccw_rotation === "number" ? pad.rect_ccw_rotation : 0
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
throw new Error(
|
|
697
|
-
`Expected polygonal pad geometry, got ${pad.type} with shape "${pad.shape}"`
|
|
698
|
-
);
|
|
699
|
-
};
|
|
700
|
-
var getPolygonEdges = (points) => points.map(
|
|
701
|
-
(point, index) => [point, points[(index + 1) % points.length]]
|
|
702
|
-
);
|
|
703
|
-
var getClosestPointsBetweenSegments = (a1, a2, b1, b2) => {
|
|
704
|
-
const intersection = getSegmentIntersection(a1, a2, b1, b2);
|
|
705
|
-
if (intersection) {
|
|
706
|
-
return {
|
|
707
|
-
distance: 0,
|
|
708
|
-
pointOnA: intersection,
|
|
709
|
-
pointOnB: intersection,
|
|
710
|
-
center: intersection
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
const candidates = [
|
|
714
|
-
{ pointOnA: a1, pointOnB: pointToSegmentClosestPoint(a1, b1, b2) },
|
|
715
|
-
{ pointOnA: a2, pointOnB: pointToSegmentClosestPoint(a2, b1, b2) },
|
|
716
|
-
{ pointOnA: pointToSegmentClosestPoint(b1, a1, a2), pointOnB: b1 },
|
|
717
|
-
{ pointOnA: pointToSegmentClosestPoint(b2, a1, a2), pointOnB: b2 }
|
|
718
|
-
];
|
|
719
|
-
let best = candidates[0];
|
|
720
|
-
let bestDistanceSquared = distSq(best.pointOnA, best.pointOnB);
|
|
721
|
-
for (const candidate of candidates.slice(1)) {
|
|
722
|
-
const candidateDistanceSquared = distSq(
|
|
723
|
-
candidate.pointOnA,
|
|
724
|
-
candidate.pointOnB
|
|
725
|
-
);
|
|
726
|
-
if (candidateDistanceSquared < bestDistanceSquared) {
|
|
727
|
-
best = candidate;
|
|
728
|
-
bestDistanceSquared = candidateDistanceSquared;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
distance: segmentToSegmentMinDistance(a1, a2, b1, b2),
|
|
733
|
-
pointOnA: best.pointOnA,
|
|
734
|
-
pointOnB: best.pointOnB,
|
|
735
|
-
center: {
|
|
736
|
-
x: (best.pointOnA.x + best.pointOnB.x) / 2,
|
|
737
|
-
y: (best.pointOnA.y + best.pointOnB.y) / 2
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
};
|
|
741
|
-
var getSegmentToPolygonClearance = (segment, polygon) => {
|
|
742
|
-
const start = { x: segment.x1, y: segment.y1 };
|
|
743
|
-
const end = { x: segment.x2, y: segment.y2 };
|
|
744
|
-
if (polygon.length < 3) {
|
|
745
|
-
return { distance: Number.POSITIVE_INFINITY, center: start };
|
|
746
|
-
}
|
|
747
|
-
const intersections = getPolygonEdges(polygon).map(
|
|
748
|
-
([edgeStart, edgeEnd]) => getSegmentIntersection(start, end, edgeStart, edgeEnd)
|
|
749
|
-
).filter((point) => point !== null);
|
|
750
|
-
if (intersections.length > 0) {
|
|
751
|
-
const dx = end.x - start.x;
|
|
752
|
-
const dy = end.y - start.y;
|
|
753
|
-
const lengthSquared = dx * dx + dy * dy;
|
|
754
|
-
intersections.sort((a, b) => {
|
|
755
|
-
const ta = ((a.x - start.x) * dx + (a.y - start.y) * dy) / lengthSquared;
|
|
756
|
-
const tb = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared;
|
|
757
|
-
return ta - tb;
|
|
758
|
-
});
|
|
759
|
-
return { distance: 0, center: intersections[0] };
|
|
760
|
-
}
|
|
761
|
-
if (isPointInsidePolygon(start, polygon) || isPointInsidePolygon(end, polygon)) {
|
|
762
|
-
return {
|
|
763
|
-
distance: 0,
|
|
764
|
-
center: {
|
|
765
|
-
x: (start.x + end.x) / 2,
|
|
766
|
-
y: (start.y + end.y) / 2
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
let best = getClosestPointsBetweenSegments(
|
|
771
|
-
start,
|
|
772
|
-
end,
|
|
773
|
-
polygon[0],
|
|
774
|
-
polygon[1]
|
|
775
|
-
);
|
|
776
|
-
for (const [edgeStart, edgeEnd] of getPolygonEdges(polygon).slice(1)) {
|
|
777
|
-
const candidate = getClosestPointsBetweenSegments(
|
|
778
|
-
start,
|
|
779
|
-
end,
|
|
780
|
-
edgeStart,
|
|
781
|
-
edgeEnd
|
|
782
|
-
);
|
|
783
|
-
if (candidate.distance < best.distance) best = candidate;
|
|
784
|
-
}
|
|
785
|
-
return { distance: best.distance, center: best.center };
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
// lib/check-each-pcb-trace-non-overlapping/getCollidableBounds.ts
|
|
789
904
|
var getCollidableBounds = (collidable) => {
|
|
790
905
|
if (collidable.type === "pcb_trace_segment") {
|
|
791
906
|
return {
|
|
@@ -806,6 +921,15 @@ var getCollidableBounds = (collidable) => {
|
|
|
806
921
|
maxY: Math.max(...polygonPoints.map((point) => point.y))
|
|
807
922
|
};
|
|
808
923
|
}
|
|
924
|
+
if (collidable.type === "pcb_smtpad" && collidable.shape === "rotated_pill") {
|
|
925
|
+
const pill = getPillCenterLineForPad(collidable);
|
|
926
|
+
return {
|
|
927
|
+
minX: Math.min(pill.start.x, pill.end.x) - pill.radius,
|
|
928
|
+
minY: Math.min(pill.start.y, pill.end.y) - pill.radius,
|
|
929
|
+
maxX: Math.max(pill.start.x, pill.end.x) + pill.radius,
|
|
930
|
+
maxY: Math.max(pill.start.y, pill.end.y) + pill.radius
|
|
931
|
+
};
|
|
932
|
+
}
|
|
809
933
|
}
|
|
810
934
|
return getBoundsOfPcbElements2([collidable]);
|
|
811
935
|
};
|
|
@@ -1005,6 +1129,38 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
|
|
|
1005
1129
|
].filter(Boolean)
|
|
1006
1130
|
});
|
|
1007
1131
|
}
|
|
1132
|
+
if (obj.type === "pcb_smtpad" && obj.shape === "rotated_pill") {
|
|
1133
|
+
const { distance: distance3, center, radius } = getSegmentToPillClearance(
|
|
1134
|
+
segmentA,
|
|
1135
|
+
obj
|
|
1136
|
+
);
|
|
1137
|
+
const gap2 = distance3 - segmentA.thickness / 2 - radius;
|
|
1138
|
+
if (gap2 > minClearance - EPSILON) continue;
|
|
1139
|
+
const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
|
|
1140
|
+
if (errorIds.has(pcb_trace_error_id)) continue;
|
|
1141
|
+
errorIds.add(pcb_trace_error_id);
|
|
1142
|
+
errors.push({
|
|
1143
|
+
type: "pcb_trace_error",
|
|
1144
|
+
error_type: "pcb_trace_error",
|
|
1145
|
+
message: constructErrorMessage(
|
|
1146
|
+
getReadableName(segmentA.pcb_trace_id),
|
|
1147
|
+
`${obj.type} "${getReadableName(getPrimaryId(obj))}"`,
|
|
1148
|
+
gap2
|
|
1149
|
+
),
|
|
1150
|
+
pcb_trace_id: segmentA.pcb_trace_id,
|
|
1151
|
+
center,
|
|
1152
|
+
source_trace_id: "",
|
|
1153
|
+
pcb_trace_error_id,
|
|
1154
|
+
pcb_component_ids: [
|
|
1155
|
+
"pcb_component_id" in obj ? obj.pcb_component_id : void 0
|
|
1156
|
+
].filter(Boolean),
|
|
1157
|
+
pcb_port_ids: [
|
|
1158
|
+
...getPcbPortIdsConnectedToTraces([segmentA._pcbTrace]),
|
|
1159
|
+
"pcb_port_id" in obj ? obj.pcb_port_id : void 0
|
|
1160
|
+
].filter(Boolean)
|
|
1161
|
+
});
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1008
1164
|
const isPolygon = obj.type === "pcb_smtpad" && (obj.shape === "rotated_rect" || obj.shape === "polygon") || obj.type === "pcb_plated_hole" && "rect_pad_width" in obj && "rect_pad_height" in obj;
|
|
1009
1165
|
if (isPolygon) {
|
|
1010
1166
|
const padOutline = getPolygonPointsForPad(obj);
|
|
@@ -1872,7 +2028,11 @@ import {
|
|
|
1872
2028
|
distanceBetweenPolygonAndPolygon,
|
|
1873
2029
|
getBoundsOfPcbElements as getBoundsOfPcbElements5
|
|
1874
2030
|
} from "@tscircuit/circuit-json-util";
|
|
1875
|
-
import {
|
|
2031
|
+
import {
|
|
2032
|
+
midpoint,
|
|
2033
|
+
segmentToCircleMinDistance as segmentToCircleMinDistance2,
|
|
2034
|
+
segmentToSegmentMinDistance as segmentToSegmentMinDistance4
|
|
2035
|
+
} from "@tscircuit/math-utils";
|
|
1876
2036
|
var formatMm = (value) => {
|
|
1877
2037
|
const rounded = Math.round(value * 1e3) / 1e3;
|
|
1878
2038
|
return `${Number(rounded.toFixed(3))}mm`;
|
|
@@ -1890,6 +2050,7 @@ var getPadRadius = (pad) => {
|
|
|
1890
2050
|
return Math.min(bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) / 2;
|
|
1891
2051
|
};
|
|
1892
2052
|
var isCircularPad = (pad) => pad.shape === "circle";
|
|
2053
|
+
var isPillPad = (pad) => pad.type === "pcb_smtpad" && (pad.shape === "pill" || pad.shape === "rotated_pill");
|
|
1893
2054
|
var getCircleShape = (pad) => {
|
|
1894
2055
|
const center = getPadCenter(pad);
|
|
1895
2056
|
return {
|
|
@@ -1900,6 +2061,18 @@ var getCircleShape = (pad) => {
|
|
|
1900
2061
|
};
|
|
1901
2062
|
};
|
|
1902
2063
|
var getPolygonShape = (pad) => {
|
|
2064
|
+
if (pad.type === "pcb_smtpad" && (pad.shape === "polygon" || pad.shape === "rotated_rect")) {
|
|
2065
|
+
return {
|
|
2066
|
+
kind: "polygon",
|
|
2067
|
+
points: getPolygonPointsForPad(pad)
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
|
|
2071
|
+
return {
|
|
2072
|
+
kind: "polygon",
|
|
2073
|
+
points: getPolygonPointsForPad(pad)
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
1903
2076
|
const bounds = getPadBounds(pad);
|
|
1904
2077
|
return {
|
|
1905
2078
|
kind: "polygon",
|
|
@@ -1912,6 +2085,40 @@ var getPolygonShape = (pad) => {
|
|
|
1912
2085
|
};
|
|
1913
2086
|
};
|
|
1914
2087
|
var getPadToPadGap = (padA, padB) => {
|
|
2088
|
+
if (isPillPad(padA) && isPillPad(padB)) {
|
|
2089
|
+
const pillA = getPillCenterLineForPad(padA);
|
|
2090
|
+
const pillB = getPillCenterLineForPad(padB);
|
|
2091
|
+
return segmentToSegmentMinDistance4(
|
|
2092
|
+
pillA.start,
|
|
2093
|
+
pillA.end,
|
|
2094
|
+
pillB.start,
|
|
2095
|
+
pillB.end
|
|
2096
|
+
) - pillA.radius - pillB.radius;
|
|
2097
|
+
}
|
|
2098
|
+
if (isPillPad(padA) && isCircularPad(padB)) {
|
|
2099
|
+
const pill = getPillCenterLineForPad(padA);
|
|
2100
|
+
return segmentToCircleMinDistance2(pill.start, pill.end, getCircleShape(padB)) - pill.radius;
|
|
2101
|
+
}
|
|
2102
|
+
if (isCircularPad(padA) && isPillPad(padB)) {
|
|
2103
|
+
const pill = getPillCenterLineForPad(padB);
|
|
2104
|
+
return segmentToCircleMinDistance2(pill.start, pill.end, getCircleShape(padA)) - pill.radius;
|
|
2105
|
+
}
|
|
2106
|
+
if (isPillPad(padA)) {
|
|
2107
|
+
const pill = getPillCenterLineForPad(padA);
|
|
2108
|
+
return getSegmentToPolygonClearanceFromPoints(
|
|
2109
|
+
pill.start,
|
|
2110
|
+
pill.end,
|
|
2111
|
+
getPolygonShape(padB).points
|
|
2112
|
+
).distance - pill.radius;
|
|
2113
|
+
}
|
|
2114
|
+
if (isPillPad(padB)) {
|
|
2115
|
+
const pill = getPillCenterLineForPad(padB);
|
|
2116
|
+
return getSegmentToPolygonClearanceFromPoints(
|
|
2117
|
+
pill.start,
|
|
2118
|
+
pill.end,
|
|
2119
|
+
getPolygonShape(padA).points
|
|
2120
|
+
).distance - pill.radius;
|
|
2121
|
+
}
|
|
1915
2122
|
if (isCircularPad(padA) && isCircularPad(padB)) {
|
|
1916
2123
|
return distanceBetweenCircleAndCircle(
|
|
1917
2124
|
getCircleShape(padA),
|
|
@@ -2029,7 +2236,7 @@ import {
|
|
|
2029
2236
|
} from "@tscircuit/circuit-json-util";
|
|
2030
2237
|
import {
|
|
2031
2238
|
segmentToBoundsMinDistance as segmentToBoundsMinDistance2,
|
|
2032
|
-
segmentToCircleMinDistance as
|
|
2239
|
+
segmentToCircleMinDistance as segmentToCircleMinDistance3
|
|
2033
2240
|
} from "@tscircuit/math-utils";
|
|
2034
2241
|
import {
|
|
2035
2242
|
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson9
|
|
@@ -2060,7 +2267,13 @@ function checkPadTraceClearance(circuitJson, {
|
|
|
2060
2267
|
if (!getLayersOfPcbElement(pad).includes(segment.layer)) continue;
|
|
2061
2268
|
if (connMap.areIdsConnected(segment.pcb_trace_id, padId)) continue;
|
|
2062
2269
|
const center = getPadCenter(pad);
|
|
2063
|
-
const gap =
|
|
2270
|
+
const gap = pad.type === "pcb_smtpad" && pad.shape === "rotated_pill" ? (() => {
|
|
2271
|
+
const { distance: distance3, radius } = getSegmentToPillClearance(
|
|
2272
|
+
segment,
|
|
2273
|
+
pad
|
|
2274
|
+
);
|
|
2275
|
+
return distance3 - segment.thickness / 2 - radius;
|
|
2276
|
+
})() : isCircularPad(pad) ? segmentToCircleMinDistance3(
|
|
2064
2277
|
{ x: segment.x1, y: segment.y1 },
|
|
2065
2278
|
{ x: segment.x2, y: segment.y2 },
|
|
2066
2279
|
{ x: center.x, y: center.y, radius: getPadRadius(pad) }
|
|
@@ -2096,7 +2309,7 @@ function checkPadTraceClearance(circuitJson, {
|
|
|
2096
2309
|
|
|
2097
2310
|
// lib/check-via-trace-clearance.ts
|
|
2098
2311
|
import { getReadableNameForElement as getReadableNameForElement8 } from "@tscircuit/circuit-json-util";
|
|
2099
|
-
import { segmentToCircleMinDistance as
|
|
2312
|
+
import { segmentToCircleMinDistance as segmentToCircleMinDistance4 } from "@tscircuit/math-utils";
|
|
2100
2313
|
import {
|
|
2101
2314
|
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson10
|
|
2102
2315
|
} from "circuit-json-to-connectivity-map";
|
|
@@ -2134,7 +2347,7 @@ function checkViaTraceClearance(circuitJson, {
|
|
|
2134
2347
|
if (!getLayersOfPcbElement(via).includes(segment.layer)) continue;
|
|
2135
2348
|
if (connMap.areIdsConnected(segment.pcb_trace_id, via.pcb_via_id))
|
|
2136
2349
|
continue;
|
|
2137
|
-
const gap =
|
|
2350
|
+
const gap = segmentToCircleMinDistance4(
|
|
2138
2351
|
{ x: segment.x1, y: segment.y1 },
|
|
2139
2352
|
{ x: segment.x2, y: segment.y2 },
|
|
2140
2353
|
{
|
|
@@ -2571,83 +2784,6 @@ function checkCourtyardOverlap(circuitJson) {
|
|
|
2571
2784
|
return errors;
|
|
2572
2785
|
}
|
|
2573
2786
|
|
|
2574
|
-
// lib/check-traces-are-contiguous/is-point-in-pad.ts
|
|
2575
|
-
function getDistanceBetweenPoints(pointA, pointB) {
|
|
2576
|
-
return Math.sqrt((pointB.x - pointA.x) ** 2 + (pointB.y - pointA.y) ** 2);
|
|
2577
|
-
}
|
|
2578
|
-
var POINT_ON_SEGMENT_TOLERANCE_MM = 1e-9;
|
|
2579
|
-
function isPointOnSegment(point, segment) {
|
|
2580
|
-
const crossProduct = (point.y - segment.start.y) * (segment.end.x - segment.start.x) - (point.x - segment.start.x) * (segment.end.y - segment.start.y);
|
|
2581
|
-
if (Math.abs(crossProduct) > POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
2582
|
-
const dotProduct = (point.x - segment.start.x) * (segment.end.x - segment.start.x) + (point.y - segment.start.y) * (segment.end.y - segment.start.y);
|
|
2583
|
-
if (dotProduct < -POINT_ON_SEGMENT_TOLERANCE_MM) return false;
|
|
2584
|
-
const squaredLength = (segment.end.x - segment.start.x) ** 2 + (segment.end.y - segment.start.y) ** 2;
|
|
2585
|
-
return dotProduct <= squaredLength + POINT_ON_SEGMENT_TOLERANCE_MM;
|
|
2586
|
-
}
|
|
2587
|
-
function isPointInPad(point, pad) {
|
|
2588
|
-
if (pad.type === "pcb_smtpad") {
|
|
2589
|
-
if (pad.shape === "circle") {
|
|
2590
|
-
return getDistanceBetweenPoints(point, pad) <= pad.radius;
|
|
2591
|
-
}
|
|
2592
|
-
if (pad.shape === "rect") {
|
|
2593
|
-
const halfWidth = pad.width / 2;
|
|
2594
|
-
const halfHeight = pad.height / 2;
|
|
2595
|
-
return Math.abs(point.x - pad.x) <= halfWidth && Math.abs(point.y - pad.y) <= halfHeight;
|
|
2596
|
-
}
|
|
2597
|
-
if (pad.shape === "rotated_rect") {
|
|
2598
|
-
const dx = point.x - pad.x;
|
|
2599
|
-
const dy = point.y - pad.y;
|
|
2600
|
-
const angle = -pad.ccw_rotation;
|
|
2601
|
-
const rotatedX = dx * Math.cos(angle) - dy * Math.sin(angle);
|
|
2602
|
-
const rotatedY = dx * Math.sin(angle) + dy * Math.cos(angle);
|
|
2603
|
-
return Math.abs(rotatedX) <= pad.width / 2 && Math.abs(rotatedY) <= pad.height / 2;
|
|
2604
|
-
}
|
|
2605
|
-
if (pad.shape === "pill") {
|
|
2606
|
-
const halfWidth = pad.width / 2;
|
|
2607
|
-
const halfHeight = pad.height / 2;
|
|
2608
|
-
const radius = pad.radius;
|
|
2609
|
-
if (Math.abs(point.x - pad.x) <= halfWidth - radius && Math.abs(point.y - pad.y) <= halfHeight) {
|
|
2610
|
-
return true;
|
|
2611
|
-
}
|
|
2612
|
-
const cornerX = Math.max(
|
|
2613
|
-
Math.abs(point.x - pad.x) - (halfWidth - radius),
|
|
2614
|
-
0
|
|
2615
|
-
);
|
|
2616
|
-
const cornerY = Math.max(
|
|
2617
|
-
Math.abs(point.y - pad.y) - (halfHeight - radius),
|
|
2618
|
-
0
|
|
2619
|
-
);
|
|
2620
|
-
return cornerX * cornerX + cornerY * cornerY <= radius * radius;
|
|
2621
|
-
}
|
|
2622
|
-
if (pad.shape === "polygon") {
|
|
2623
|
-
let inside = false;
|
|
2624
|
-
for (let i = 0, j = pad.points.length - 1; i < pad.points.length; j = i++) {
|
|
2625
|
-
const pi = pad.points[i];
|
|
2626
|
-
const pj = pad.points[j];
|
|
2627
|
-
if (isPointOnSegment(point, { start: pi, end: pj })) return true;
|
|
2628
|
-
const intersects = pi.y > point.y !== pj.y > point.y && point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x;
|
|
2629
|
-
if (intersects) inside = !inside;
|
|
2630
|
-
}
|
|
2631
|
-
return inside;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
if (pad.type === "pcb_plated_hole") {
|
|
2635
|
-
if (pad.shape === "circle") {
|
|
2636
|
-
return getDistanceBetweenPoints(point, pad) <= pad.outer_diameter / 2;
|
|
2637
|
-
}
|
|
2638
|
-
if (pad.shape === "oval" || pad.shape === "pill") {
|
|
2639
|
-
return Math.abs(point.x - pad.x) <= pad.outer_width / 2 && Math.abs(point.y - pad.y) <= pad.outer_height / 2;
|
|
2640
|
-
}
|
|
2641
|
-
if (pad.shape === "circular_hole_with_rect_pad") {
|
|
2642
|
-
return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
|
|
2643
|
-
}
|
|
2644
|
-
if (pad.shape === "pill_hole_with_rect_pad") {
|
|
2645
|
-
return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
return false;
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
2787
|
// lib/check-traces-are-contiguous/check-traces-are-contiguous.ts
|
|
2652
2788
|
import {
|
|
2653
2789
|
getReadableNameForPcbPort as getReadableNameForPcbPort2,
|