circuitjson-toolkit 1.0.3 → 1.0.16
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/AGENTS.md +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes copper-to-copper distances from normalized PCB primitives.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonPcbCopperGeometry {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the nearest edge clearance between two copper primitives.
|
|
7
|
+
* @param {object} left First primitive.
|
|
8
|
+
* @param {object} right Second primitive.
|
|
9
|
+
* @returns {number | null}
|
|
10
|
+
*/
|
|
11
|
+
static clearance(left, right) {
|
|
12
|
+
const leftShapes = CircuitJsonPcbCopperGeometry.#shapes(left)
|
|
13
|
+
const rightShapes = CircuitJsonPcbCopperGeometry.#shapes(right)
|
|
14
|
+
if (!leftShapes.length || !rightShapes.length) return null
|
|
15
|
+
|
|
16
|
+
let best = Infinity
|
|
17
|
+
for (const leftShape of leftShapes) {
|
|
18
|
+
for (const rightShape of rightShapes) {
|
|
19
|
+
best = Math.min(
|
|
20
|
+
best,
|
|
21
|
+
CircuitJsonPcbCopperGeometry.#shapeDistance(
|
|
22
|
+
leftShape,
|
|
23
|
+
rightShape
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Number.isFinite(best) ? Math.max(best, 0) : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds copper shapes for one primitive.
|
|
34
|
+
* @param {object} primitive Primitive row.
|
|
35
|
+
* @returns {object[]}
|
|
36
|
+
*/
|
|
37
|
+
static #shapes(primitive) {
|
|
38
|
+
if (primitive.kind === 'track') {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
type: 'capsule',
|
|
42
|
+
start: { x: primitive.x1, y: primitive.y1 },
|
|
43
|
+
end: { x: primitive.x2, y: primitive.y2 },
|
|
44
|
+
radius: Number(primitive.width || 0) / 2
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
if (primitive.kind === 'via') {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
type: 'circle',
|
|
52
|
+
center: { x: primitive.x, y: primitive.y },
|
|
53
|
+
radius: Number(primitive.diameter || 0) / 2
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
if (primitive.kind === 'zone') {
|
|
58
|
+
return CircuitJsonPcbCopperGeometry.#polygonShape(primitive.points)
|
|
59
|
+
}
|
|
60
|
+
if (primitive.kind === 'pad') {
|
|
61
|
+
return CircuitJsonPcbCopperGeometry.#padShapes(primitive)
|
|
62
|
+
}
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds copper shapes for a pad primitive.
|
|
68
|
+
* @param {object} primitive Pad primitive.
|
|
69
|
+
* @returns {object[]}
|
|
70
|
+
*/
|
|
71
|
+
static #padShapes(primitive) {
|
|
72
|
+
const shape = String(primitive.shape || 'rect').toLowerCase()
|
|
73
|
+
if (shape === 'circle') {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
type: 'circle',
|
|
77
|
+
center: { x: primitive.x, y: primitive.y },
|
|
78
|
+
radius:
|
|
79
|
+
Math.max(
|
|
80
|
+
Number(primitive.width || 0),
|
|
81
|
+
Number(primitive.height || 0)
|
|
82
|
+
) / 2
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
if (shape === 'pill' || shape === 'rotated_pill') {
|
|
87
|
+
return [
|
|
88
|
+
CircuitJsonPcbCopperGeometry.#pillShape(
|
|
89
|
+
primitive,
|
|
90
|
+
shape === 'rotated_pill'
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
if (shape === 'polygon') {
|
|
95
|
+
return CircuitJsonPcbCopperGeometry.#polygonShape(primitive.points)
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
type: 'polygon',
|
|
100
|
+
points: CircuitJsonPcbCopperGeometry.#rectPoints(primitive)
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builds a capsule for a pill pad.
|
|
107
|
+
* @param {object} primitive Pad primitive.
|
|
108
|
+
* @param {boolean} forceRotation Whether rotation should be applied.
|
|
109
|
+
* @returns {object}
|
|
110
|
+
*/
|
|
111
|
+
static #pillShape(primitive, forceRotation) {
|
|
112
|
+
const width = Number(primitive.width || primitive.bounds?.width || 0)
|
|
113
|
+
const height = Number(primitive.height || primitive.bounds?.height || 0)
|
|
114
|
+
const radius = Math.min(width, height) / 2
|
|
115
|
+
const run = Math.max(width, height) / 2 - radius
|
|
116
|
+
const angle =
|
|
117
|
+
((forceRotation ? Number(primitive.rotation || 0) : 0) * Math.PI) /
|
|
118
|
+
180
|
|
119
|
+
const axis = width >= height ? 0 : Math.PI / 2
|
|
120
|
+
const dx = Math.cos(angle + axis) * run
|
|
121
|
+
const dy = Math.sin(angle + axis) * run
|
|
122
|
+
return {
|
|
123
|
+
type: 'capsule',
|
|
124
|
+
start: { x: Number(primitive.x) - dx, y: Number(primitive.y) - dy },
|
|
125
|
+
end: { x: Number(primitive.x) + dx, y: Number(primitive.y) + dy },
|
|
126
|
+
radius
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds a polygon shape when enough points are present.
|
|
132
|
+
* @param {object[]} points Source points.
|
|
133
|
+
* @returns {object[]}
|
|
134
|
+
*/
|
|
135
|
+
static #polygonShape(points) {
|
|
136
|
+
const normalized = (Array.isArray(points) ? points : [])
|
|
137
|
+
.map((point) => ({
|
|
138
|
+
x: Number(point?.x),
|
|
139
|
+
y: Number(point?.y)
|
|
140
|
+
}))
|
|
141
|
+
.filter(
|
|
142
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
143
|
+
)
|
|
144
|
+
return normalized.length >= 3
|
|
145
|
+
? [{ type: 'polygon', points: normalized }]
|
|
146
|
+
: []
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Builds rotated rectangle corner points.
|
|
151
|
+
* @param {object} primitive Rectangular primitive.
|
|
152
|
+
* @returns {{ x: number, y: number }[]}
|
|
153
|
+
*/
|
|
154
|
+
static #rectPoints(primitive) {
|
|
155
|
+
const center = { x: Number(primitive.x), y: Number(primitive.y) }
|
|
156
|
+
const width = Number(primitive.width || primitive.bounds?.width || 0)
|
|
157
|
+
const height = Number(primitive.height || primitive.bounds?.height || 0)
|
|
158
|
+
const angle = (Number(primitive.rotation || 0) * Math.PI) / 180
|
|
159
|
+
return [
|
|
160
|
+
{ x: -width / 2, y: -height / 2 },
|
|
161
|
+
{ x: width / 2, y: -height / 2 },
|
|
162
|
+
{ x: width / 2, y: height / 2 },
|
|
163
|
+
{ x: -width / 2, y: height / 2 }
|
|
164
|
+
].map((point) => ({
|
|
165
|
+
x: center.x + point.x * Math.cos(angle) - point.y * Math.sin(angle),
|
|
166
|
+
y: center.y + point.x * Math.sin(angle) + point.y * Math.cos(angle)
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Computes distance between two primitive shapes.
|
|
172
|
+
* @param {object} left First shape.
|
|
173
|
+
* @param {object} right Second shape.
|
|
174
|
+
* @returns {number}
|
|
175
|
+
*/
|
|
176
|
+
static #shapeDistance(left, right) {
|
|
177
|
+
if (left.type === 'circle' && right.type === 'circle') {
|
|
178
|
+
return (
|
|
179
|
+
CircuitJsonPcbCopperGeometry.#pointDistance(
|
|
180
|
+
left.center,
|
|
181
|
+
right.center
|
|
182
|
+
) -
|
|
183
|
+
left.radius -
|
|
184
|
+
right.radius
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
if (left.type === 'capsule' && right.type === 'capsule') {
|
|
188
|
+
return (
|
|
189
|
+
CircuitJsonPcbCopperGeometry.#segmentDistance(
|
|
190
|
+
left.start,
|
|
191
|
+
left.end,
|
|
192
|
+
right.start,
|
|
193
|
+
right.end
|
|
194
|
+
) -
|
|
195
|
+
left.radius -
|
|
196
|
+
right.radius
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
if (left.type === 'circle' && right.type === 'capsule') {
|
|
200
|
+
return CircuitJsonPcbCopperGeometry.#circleCapsuleDistance(
|
|
201
|
+
left,
|
|
202
|
+
right
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
if (left.type === 'capsule' && right.type === 'circle') {
|
|
206
|
+
return CircuitJsonPcbCopperGeometry.#circleCapsuleDistance(
|
|
207
|
+
right,
|
|
208
|
+
left
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (left.type === 'polygon' && right.type === 'polygon') {
|
|
212
|
+
return CircuitJsonPcbCopperGeometry.#polygonDistance(
|
|
213
|
+
left.points,
|
|
214
|
+
right.points
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
if (left.type === 'circle' && right.type === 'polygon') {
|
|
218
|
+
return CircuitJsonPcbCopperGeometry.#circlePolygonDistance(
|
|
219
|
+
left,
|
|
220
|
+
right.points
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
if (left.type === 'polygon' && right.type === 'circle') {
|
|
224
|
+
return CircuitJsonPcbCopperGeometry.#circlePolygonDistance(
|
|
225
|
+
right,
|
|
226
|
+
left.points
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
if (left.type === 'capsule' && right.type === 'polygon') {
|
|
230
|
+
return CircuitJsonPcbCopperGeometry.#capsulePolygonDistance(
|
|
231
|
+
left,
|
|
232
|
+
right.points
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
return CircuitJsonPcbCopperGeometry.#capsulePolygonDistance(
|
|
236
|
+
right,
|
|
237
|
+
left.points
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Computes circle-to-capsule edge distance.
|
|
243
|
+
* @param {object} circle Circle shape.
|
|
244
|
+
* @param {object} capsule Capsule shape.
|
|
245
|
+
* @returns {number}
|
|
246
|
+
*/
|
|
247
|
+
static #circleCapsuleDistance(circle, capsule) {
|
|
248
|
+
return (
|
|
249
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
250
|
+
circle.center,
|
|
251
|
+
capsule.start,
|
|
252
|
+
capsule.end
|
|
253
|
+
) -
|
|
254
|
+
circle.radius -
|
|
255
|
+
capsule.radius
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Computes circle-to-polygon edge distance.
|
|
261
|
+
* @param {object} circle Circle shape.
|
|
262
|
+
* @param {object[]} polygon Polygon points.
|
|
263
|
+
* @returns {number}
|
|
264
|
+
*/
|
|
265
|
+
static #circlePolygonDistance(circle, polygon) {
|
|
266
|
+
if (
|
|
267
|
+
CircuitJsonPcbCopperGeometry.#pointInPolygon(circle.center, polygon)
|
|
268
|
+
) {
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
return (
|
|
272
|
+
Math.min(
|
|
273
|
+
...CircuitJsonPcbCopperGeometry.#edges(polygon).map((edge) =>
|
|
274
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
275
|
+
circle.center,
|
|
276
|
+
edge[0],
|
|
277
|
+
edge[1]
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
) - circle.radius
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Computes capsule-to-polygon edge distance.
|
|
286
|
+
* @param {object} capsule Capsule shape.
|
|
287
|
+
* @param {object[]} polygon Polygon points.
|
|
288
|
+
* @returns {number}
|
|
289
|
+
*/
|
|
290
|
+
static #capsulePolygonDistance(capsule, polygon) {
|
|
291
|
+
if (
|
|
292
|
+
CircuitJsonPcbCopperGeometry.#pointInPolygon(
|
|
293
|
+
capsule.start,
|
|
294
|
+
polygon
|
|
295
|
+
) ||
|
|
296
|
+
CircuitJsonPcbCopperGeometry.#pointInPolygon(capsule.end, polygon)
|
|
297
|
+
) {
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
const distance = Math.min(
|
|
301
|
+
...CircuitJsonPcbCopperGeometry.#edges(polygon).map((edge) =>
|
|
302
|
+
CircuitJsonPcbCopperGeometry.#segmentDistance(
|
|
303
|
+
capsule.start,
|
|
304
|
+
capsule.end,
|
|
305
|
+
edge[0],
|
|
306
|
+
edge[1]
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
return distance - capsule.radius
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Computes polygon-to-polygon edge distance.
|
|
315
|
+
* @param {object[]} left First polygon.
|
|
316
|
+
* @param {object[]} right Second polygon.
|
|
317
|
+
* @returns {number}
|
|
318
|
+
*/
|
|
319
|
+
static #polygonDistance(left, right) {
|
|
320
|
+
if (
|
|
321
|
+
left.some((point) =>
|
|
322
|
+
CircuitJsonPcbCopperGeometry.#pointInPolygon(point, right)
|
|
323
|
+
) ||
|
|
324
|
+
right.some((point) =>
|
|
325
|
+
CircuitJsonPcbCopperGeometry.#pointInPolygon(point, left)
|
|
326
|
+
)
|
|
327
|
+
) {
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
return Math.min(
|
|
331
|
+
...CircuitJsonPcbCopperGeometry.#edges(left).flatMap((leftEdge) =>
|
|
332
|
+
CircuitJsonPcbCopperGeometry.#edges(right).map((rightEdge) =>
|
|
333
|
+
CircuitJsonPcbCopperGeometry.#segmentDistance(
|
|
334
|
+
leftEdge[0],
|
|
335
|
+
leftEdge[1],
|
|
336
|
+
rightEdge[0],
|
|
337
|
+
rightEdge[1]
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Builds polygon edge pairs.
|
|
346
|
+
* @param {object[]} points Polygon points.
|
|
347
|
+
* @returns {object[][]}
|
|
348
|
+
*/
|
|
349
|
+
static #edges(points) {
|
|
350
|
+
return points.map((point, index) => [
|
|
351
|
+
point,
|
|
352
|
+
points[(index + 1) % points.length]
|
|
353
|
+
])
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Computes point distance.
|
|
358
|
+
* @param {object} left First point.
|
|
359
|
+
* @param {object} right Second point.
|
|
360
|
+
* @returns {number}
|
|
361
|
+
*/
|
|
362
|
+
static #pointDistance(left, right) {
|
|
363
|
+
return Math.hypot(
|
|
364
|
+
Number(left.x) - Number(right.x),
|
|
365
|
+
Number(left.y) - Number(right.y)
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Computes point-to-segment distance.
|
|
371
|
+
* @param {object} point Point.
|
|
372
|
+
* @param {object} start Segment start.
|
|
373
|
+
* @param {object} end Segment end.
|
|
374
|
+
* @returns {number}
|
|
375
|
+
*/
|
|
376
|
+
static #pointSegmentDistance(point, start, end) {
|
|
377
|
+
const dx = end.x - start.x
|
|
378
|
+
const dy = end.y - start.y
|
|
379
|
+
const lengthSq = dx * dx + dy * dy
|
|
380
|
+
if (!lengthSq)
|
|
381
|
+
return CircuitJsonPcbCopperGeometry.#pointDistance(point, start)
|
|
382
|
+
const t = Math.max(
|
|
383
|
+
0,
|
|
384
|
+
Math.min(
|
|
385
|
+
1,
|
|
386
|
+
((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSq
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
return CircuitJsonPcbCopperGeometry.#pointDistance(point, {
|
|
390
|
+
x: start.x + dx * t,
|
|
391
|
+
y: start.y + dy * t
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Computes segment-to-segment distance.
|
|
397
|
+
* @param {object} leftStart First segment start.
|
|
398
|
+
* @param {object} leftEnd First segment end.
|
|
399
|
+
* @param {object} rightStart Second segment start.
|
|
400
|
+
* @param {object} rightEnd Second segment end.
|
|
401
|
+
* @returns {number}
|
|
402
|
+
*/
|
|
403
|
+
static #segmentDistance(leftStart, leftEnd, rightStart, rightEnd) {
|
|
404
|
+
if (
|
|
405
|
+
CircuitJsonPcbCopperGeometry.#segmentsIntersect(
|
|
406
|
+
leftStart,
|
|
407
|
+
leftEnd,
|
|
408
|
+
rightStart,
|
|
409
|
+
rightEnd
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
return Math.min(
|
|
415
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
416
|
+
leftStart,
|
|
417
|
+
rightStart,
|
|
418
|
+
rightEnd
|
|
419
|
+
),
|
|
420
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
421
|
+
leftEnd,
|
|
422
|
+
rightStart,
|
|
423
|
+
rightEnd
|
|
424
|
+
),
|
|
425
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
426
|
+
rightStart,
|
|
427
|
+
leftStart,
|
|
428
|
+
leftEnd
|
|
429
|
+
),
|
|
430
|
+
CircuitJsonPcbCopperGeometry.#pointSegmentDistance(
|
|
431
|
+
rightEnd,
|
|
432
|
+
leftStart,
|
|
433
|
+
leftEnd
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Returns true when two segments intersect.
|
|
440
|
+
* @param {object} leftStart First segment start.
|
|
441
|
+
* @param {object} leftEnd First segment end.
|
|
442
|
+
* @param {object} rightStart Second segment start.
|
|
443
|
+
* @param {object} rightEnd Second segment end.
|
|
444
|
+
* @returns {boolean}
|
|
445
|
+
*/
|
|
446
|
+
static #segmentsIntersect(leftStart, leftEnd, rightStart, rightEnd) {
|
|
447
|
+
const o1 = CircuitJsonPcbCopperGeometry.#orientation(
|
|
448
|
+
leftStart,
|
|
449
|
+
leftEnd,
|
|
450
|
+
rightStart
|
|
451
|
+
)
|
|
452
|
+
const o2 = CircuitJsonPcbCopperGeometry.#orientation(
|
|
453
|
+
leftStart,
|
|
454
|
+
leftEnd,
|
|
455
|
+
rightEnd
|
|
456
|
+
)
|
|
457
|
+
const o3 = CircuitJsonPcbCopperGeometry.#orientation(
|
|
458
|
+
rightStart,
|
|
459
|
+
rightEnd,
|
|
460
|
+
leftStart
|
|
461
|
+
)
|
|
462
|
+
const o4 = CircuitJsonPcbCopperGeometry.#orientation(
|
|
463
|
+
rightStart,
|
|
464
|
+
rightEnd,
|
|
465
|
+
leftEnd
|
|
466
|
+
)
|
|
467
|
+
return o1 * o2 < 0 && o3 * o4 < 0
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Computes segment orientation.
|
|
472
|
+
* @param {object} a First point.
|
|
473
|
+
* @param {object} b Second point.
|
|
474
|
+
* @param {object} c Third point.
|
|
475
|
+
* @returns {number}
|
|
476
|
+
*/
|
|
477
|
+
static #orientation(a, b, c) {
|
|
478
|
+
return Math.sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Returns true when a point is inside a polygon.
|
|
483
|
+
* @param {object} point Point.
|
|
484
|
+
* @param {object[]} polygon Polygon points.
|
|
485
|
+
* @returns {boolean}
|
|
486
|
+
*/
|
|
487
|
+
static #pointInPolygon(point, polygon) {
|
|
488
|
+
let inside = false
|
|
489
|
+
for (
|
|
490
|
+
let index = 0, previous = polygon.length - 1;
|
|
491
|
+
index < polygon.length;
|
|
492
|
+
previous = index, index += 1
|
|
493
|
+
) {
|
|
494
|
+
const a = polygon[index]
|
|
495
|
+
const b = polygon[previous]
|
|
496
|
+
const intersects =
|
|
497
|
+
a.y > point.y !== b.y > point.y &&
|
|
498
|
+
point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x
|
|
499
|
+
if (intersects) inside = !inside
|
|
500
|
+
}
|
|
501
|
+
return inside
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes drawing style fields from PCB documentation rows.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonPcbDrawingStyle {
|
|
7
|
+
/**
|
|
8
|
+
* Resolves drawing style metadata from one element row.
|
|
9
|
+
* @param {object} element Element row.
|
|
10
|
+
* @returns {{ strokeColor?: string, fillColor?: string, dashArray?: string }}
|
|
11
|
+
*/
|
|
12
|
+
static fromElement(element) {
|
|
13
|
+
return CircuitJsonPcbDrawingStyle.#clean({
|
|
14
|
+
strokeColor: CircuitJsonPcbDrawingStyle.#safeColor(
|
|
15
|
+
element.stroke_color || element.strokeColor || element.color
|
|
16
|
+
),
|
|
17
|
+
fillColor: CircuitJsonPcbDrawingStyle.#safeColor(
|
|
18
|
+
element.fill_color || element.fillColor
|
|
19
|
+
),
|
|
20
|
+
dashArray: CircuitJsonPcbDrawingStyle.#dashArray(element)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds a normalized dash array.
|
|
26
|
+
* @param {object} element Element row.
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
static #dashArray(element) {
|
|
30
|
+
const explicit =
|
|
31
|
+
element.stroke_dasharray || element.dash_array || element.dashArray
|
|
32
|
+
if (Array.isArray(explicit)) {
|
|
33
|
+
return CircuitJsonPcbDrawingStyle.#dashValues(explicit)
|
|
34
|
+
}
|
|
35
|
+
if (String(explicit || '').trim()) {
|
|
36
|
+
return CircuitJsonPcbDrawingStyle.#dashValues(
|
|
37
|
+
String(explicit).trim().split(/\s+/u)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (element.is_dashed !== true && element.dashed !== true) return ''
|
|
41
|
+
|
|
42
|
+
const dashLength = CircuitJsonUnits.optionalLength(
|
|
43
|
+
element.dash_length ?? element.dashLength
|
|
44
|
+
)
|
|
45
|
+
const dashGap = CircuitJsonUnits.optionalLength(
|
|
46
|
+
element.dash_gap ?? element.dashGap
|
|
47
|
+
)
|
|
48
|
+
return CircuitJsonPcbDrawingStyle.#dashValues([
|
|
49
|
+
dashLength ?? 0.4,
|
|
50
|
+
dashGap ?? dashLength ?? 0.4
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalizes dash values.
|
|
56
|
+
* @param {unknown[]} values Dash values.
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
static #dashValues(values) {
|
|
60
|
+
const numbers = values.map(Number).filter((value) => value > 0)
|
|
61
|
+
return numbers.length
|
|
62
|
+
? numbers.map((value) => String(Number(value.toFixed(6)))).join(' ')
|
|
63
|
+
: ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns a safe hex color.
|
|
68
|
+
* @param {unknown} value Color candidate.
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
static #safeColor(value) {
|
|
72
|
+
const text = String(value || '').trim()
|
|
73
|
+
return /^#[0-9a-f]{3,8}$/iu.test(text) ? text : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Removes empty style fields.
|
|
78
|
+
* @param {object} value Style metadata.
|
|
79
|
+
* @returns {object}
|
|
80
|
+
*/
|
|
81
|
+
static #clean(value) {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(value).filter(([_key, entry]) =>
|
|
84
|
+
String(entry || '').trim()
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|