altium-toolkit 0.1.23 → 1.0.2
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 +4 -3
- package/docs/api.md +47 -8
- package/docs/model-format.md +30 -6
- package/package.json +2 -1
- package/spec/library-scope.md +2 -1
- package/src/core/altium/AltiumParser.mjs +17 -4
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +826 -0
- package/src/core/circuit-json/CircuitJsonModelAdapterPrimitives.mjs +354 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +83 -0
- package/src/core/netlist-query/CircuitTraversal.mjs +325 -0
- package/src/core/netlist-query/ComponentGrouping.mjs +355 -0
- package/src/core/netlist-query/LoadedDesignNetlistService.mjs +732 -0
- package/src/core/netlist-query/QueryNetlistBuilder.mjs +180 -0
- package/src/core/netlist-query/RegexPattern.mjs +89 -0
- package/src/netlist-query.mjs +12 -0
- package/src/parser.mjs +2 -0
- package/src/styles/altium-renderers.css +12 -1
- package/src/ui/PcbNativeTextKnockoutDetector.mjs +130 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +402 -0
- package/src/ui/PcbScene3dBuilder.mjs +16 -5
- package/src/ui/PcbScene3dDrillCutoutBuilder.mjs +26 -15
- package/src/ui/PcbSvgRenderer.mjs +83 -10
- package/src/ui/PcbTextPrimitiveRenderer.mjs +648 -19
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Refines Altium 3D board outlines when parser recovery emitted a rasterized
|
|
7
|
+
* stair-step contour even though the document still carries a smoother board
|
|
8
|
+
* region contour.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbScene3dBoardOutlineRefiner {
|
|
11
|
+
static #MIN_RASTERIZED_SEGMENTS = 16
|
|
12
|
+
static #MIN_SHORT_SEGMENT_RATIO = 0.35
|
|
13
|
+
static #MIN_AXIS_ALIGNED_RATIO = 0.9
|
|
14
|
+
static #SHORT_SEGMENT_MAX_MIL = 24
|
|
15
|
+
static #POINT_EPSILON_MIL = 0.25
|
|
16
|
+
static #AREA_RATIO_MIN = 0.75
|
|
17
|
+
static #AREA_RATIO_MAX = 1.25
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns a scene description with a refined board outline when a better
|
|
21
|
+
* board-region contour is available.
|
|
22
|
+
* @param {object} sceneDescription Built scene description.
|
|
23
|
+
* @param {object} documentModel Source PCB document model.
|
|
24
|
+
* @returns {object}
|
|
25
|
+
*/
|
|
26
|
+
static refine(sceneDescription, documentModel) {
|
|
27
|
+
const board = sceneDescription?.board
|
|
28
|
+
const segments = Array.isArray(board?.segments) ? board.segments : []
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
!PcbScene3dBoardOutlineRefiner.#isRasterizedManhattanOutline(
|
|
32
|
+
segments
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
return sceneDescription
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentOutline =
|
|
39
|
+
documentModel?.pcb?.boardOutline || sceneDescription?.board || {}
|
|
40
|
+
const candidate = PcbScene3dBoardOutlineRefiner.#selectCandidate(
|
|
41
|
+
documentModel?.pcb?.boardRegions,
|
|
42
|
+
currentOutline
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if (!candidate) {
|
|
46
|
+
return sceneDescription
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...sceneDescription,
|
|
51
|
+
board: {
|
|
52
|
+
...board,
|
|
53
|
+
minX: candidate.minX,
|
|
54
|
+
minY: candidate.minY,
|
|
55
|
+
widthMil: candidate.widthMil,
|
|
56
|
+
heightMil: candidate.heightMil,
|
|
57
|
+
centerX: candidate.minX + candidate.widthMil / 2,
|
|
58
|
+
centerY: candidate.minY + candidate.heightMil / 2,
|
|
59
|
+
segments: candidate.segments
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns true when an outline looks like a raster-grid recovery of a
|
|
66
|
+
* curved board route instead of authored geometric segments.
|
|
67
|
+
* @param {Array<Record<string, number | string>>} segments Outline segments.
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
static #isRasterizedManhattanOutline(segments) {
|
|
71
|
+
if (
|
|
72
|
+
segments.length <
|
|
73
|
+
PcbScene3dBoardOutlineRefiner.#MIN_RASTERIZED_SEGMENTS
|
|
74
|
+
) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (segments.some((segment) => segment.type === 'arc')) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let axisAlignedCount = 0
|
|
83
|
+
let shortSegmentCount = 0
|
|
84
|
+
|
|
85
|
+
for (const segment of segments) {
|
|
86
|
+
const dx = Math.abs(
|
|
87
|
+
Number(segment.x2 || 0) - Number(segment.x1 || 0)
|
|
88
|
+
)
|
|
89
|
+
const dy = Math.abs(
|
|
90
|
+
Number(segment.y2 || 0) - Number(segment.y1 || 0)
|
|
91
|
+
)
|
|
92
|
+
const length = Math.hypot(dx, dy)
|
|
93
|
+
|
|
94
|
+
if (dx <= 0.001 || dy <= 0.001) {
|
|
95
|
+
axisAlignedCount += 1
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
length <= PcbScene3dBoardOutlineRefiner.#SHORT_SEGMENT_MAX_MIL
|
|
99
|
+
) {
|
|
100
|
+
shortSegmentCount += 1
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
axisAlignedCount / segments.length >=
|
|
106
|
+
PcbScene3dBoardOutlineRefiner.#MIN_AXIS_ALIGNED_RATIO &&
|
|
107
|
+
shortSegmentCount / segments.length >=
|
|
108
|
+
PcbScene3dBoardOutlineRefiner.#MIN_SHORT_SEGMENT_RATIO
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Chooses the best compatible board-region contour.
|
|
114
|
+
* @param {object[] | undefined} regions Candidate board regions.
|
|
115
|
+
* @param {object} currentOutline Current outline bounds and segments.
|
|
116
|
+
* @returns {object | null}
|
|
117
|
+
*/
|
|
118
|
+
static #selectCandidate(regions, currentOutline) {
|
|
119
|
+
const currentBounds =
|
|
120
|
+
PcbScene3dBoardOutlineRefiner.#resolveOutlineBounds(currentOutline)
|
|
121
|
+
if (!currentBounds) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const currentArea = Math.abs(
|
|
126
|
+
PcbScene3dBoardOutlineRefiner.#computeAreaFromSegments(
|
|
127
|
+
currentOutline?.segments || []
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
const candidates = (Array.isArray(regions) ? regions : [])
|
|
131
|
+
.filter((region) =>
|
|
132
|
+
PcbScene3dBoardOutlineRefiner.#isBoardRegionCandidate(region)
|
|
133
|
+
)
|
|
134
|
+
.map((region) =>
|
|
135
|
+
PcbScene3dBoardOutlineRefiner.#buildOutlineFromPoints(
|
|
136
|
+
region.points
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.filter((outline) =>
|
|
141
|
+
PcbScene3dBoardOutlineRefiner.#boundsAreCompatible(
|
|
142
|
+
currentBounds,
|
|
143
|
+
outline
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
.filter((outline) =>
|
|
147
|
+
PcbScene3dBoardOutlineRefiner.#areaIsCompatible(
|
|
148
|
+
currentArea,
|
|
149
|
+
outline
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
.map((outline) => ({
|
|
153
|
+
outline,
|
|
154
|
+
score: PcbScene3dBoardOutlineRefiner.#scoreBounds(
|
|
155
|
+
currentBounds,
|
|
156
|
+
outline
|
|
157
|
+
)
|
|
158
|
+
}))
|
|
159
|
+
.sort((left, right) => left.score - right.score)
|
|
160
|
+
|
|
161
|
+
return candidates[0]?.outline || null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns true when a region can represent the board body boundary.
|
|
166
|
+
* @param {object} region Source board region.
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
static #isBoardRegionCandidate(region) {
|
|
170
|
+
return (
|
|
171
|
+
Array.isArray(region?.points) &&
|
|
172
|
+
region.points.length >= 3 &&
|
|
173
|
+
(region?.objectKind === 'BoardRegion' ||
|
|
174
|
+
region?.isBoardCutout === true ||
|
|
175
|
+
Number.isInteger(region?.boardRegionIndex))
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Converts one point loop into outline bounds and line segments.
|
|
181
|
+
* @param {{ x?: number, y?: number }[] | undefined} points Source points.
|
|
182
|
+
* @returns {object | null}
|
|
183
|
+
*/
|
|
184
|
+
static #buildOutlineFromPoints(points) {
|
|
185
|
+
const normalizedPoints =
|
|
186
|
+
PcbScene3dBoardOutlineRefiner.#normalizePointLoop(points)
|
|
187
|
+
|
|
188
|
+
if (normalizedPoints.length < 3) {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let minX = Number.POSITIVE_INFINITY
|
|
193
|
+
let minY = Number.POSITIVE_INFINITY
|
|
194
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
195
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
196
|
+
|
|
197
|
+
for (const point of normalizedPoints) {
|
|
198
|
+
minX = Math.min(minX, point.x)
|
|
199
|
+
minY = Math.min(minY, point.y)
|
|
200
|
+
maxX = Math.max(maxX, point.x)
|
|
201
|
+
maxY = Math.max(maxY, point.y)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const segments = normalizedPoints.map((point, index) => {
|
|
205
|
+
const next = normalizedPoints[(index + 1) % normalizedPoints.length]
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
type: 'line',
|
|
209
|
+
x1: point.x,
|
|
210
|
+
y1: point.y,
|
|
211
|
+
x2: next.x,
|
|
212
|
+
y2: next.y
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
minX,
|
|
218
|
+
minY,
|
|
219
|
+
widthMil: maxX - minX,
|
|
220
|
+
heightMil: maxY - minY,
|
|
221
|
+
segments
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Normalizes finite points and drops duplicated adjacent points.
|
|
227
|
+
* @param {{ x?: number, y?: number }[] | undefined} points Source points.
|
|
228
|
+
* @returns {{ x: number, y: number }[]}
|
|
229
|
+
*/
|
|
230
|
+
static #normalizePointLoop(points) {
|
|
231
|
+
const output = []
|
|
232
|
+
|
|
233
|
+
for (const point of Array.isArray(points) ? points : []) {
|
|
234
|
+
const nextPoint = {
|
|
235
|
+
x: Number(point?.x),
|
|
236
|
+
y: Number(point?.y)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
!Number.isFinite(nextPoint.x) ||
|
|
241
|
+
!Number.isFinite(nextPoint.y)
|
|
242
|
+
) {
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
output.length &&
|
|
248
|
+
PcbScene3dBoardOutlineRefiner.#distanceBetween(
|
|
249
|
+
output[output.length - 1],
|
|
250
|
+
nextPoint
|
|
251
|
+
) <= PcbScene3dBoardOutlineRefiner.#POINT_EPSILON_MIL
|
|
252
|
+
) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
output.push(nextPoint)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
output.length > 1 &&
|
|
261
|
+
PcbScene3dBoardOutlineRefiner.#distanceBetween(
|
|
262
|
+
output[0],
|
|
263
|
+
output[output.length - 1]
|
|
264
|
+
) <= PcbScene3dBoardOutlineRefiner.#POINT_EPSILON_MIL
|
|
265
|
+
) {
|
|
266
|
+
output.pop()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return output
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolves bounds from an outline object.
|
|
274
|
+
* @param {object} outline Outline-like object.
|
|
275
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number, widthMil: number, heightMil: number } | null}
|
|
276
|
+
*/
|
|
277
|
+
static #resolveOutlineBounds(outline) {
|
|
278
|
+
const minX = Number(outline?.minX)
|
|
279
|
+
const minY = Number(outline?.minY)
|
|
280
|
+
const widthMil = Number(outline?.widthMil)
|
|
281
|
+
const heightMil = Number(outline?.heightMil)
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
!Number.isFinite(minX) ||
|
|
285
|
+
!Number.isFinite(minY) ||
|
|
286
|
+
!Number.isFinite(widthMil) ||
|
|
287
|
+
!Number.isFinite(heightMil) ||
|
|
288
|
+
widthMil <= 0 ||
|
|
289
|
+
heightMil <= 0
|
|
290
|
+
) {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
minX,
|
|
296
|
+
minY,
|
|
297
|
+
maxX: minX + widthMil,
|
|
298
|
+
maxY: minY + heightMil,
|
|
299
|
+
widthMil,
|
|
300
|
+
heightMil
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Returns true when a candidate's envelope matches the current board.
|
|
306
|
+
* @param {object} current Current outline bounds.
|
|
307
|
+
* @param {object} candidate Candidate outline bounds.
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
static #boundsAreCompatible(current, candidate) {
|
|
311
|
+
const tolerance = Math.max(
|
|
312
|
+
Math.max(current.widthMil, current.heightMil) * 0.06,
|
|
313
|
+
40
|
|
314
|
+
)
|
|
315
|
+
const candidateBounds =
|
|
316
|
+
PcbScene3dBoardOutlineRefiner.#resolveOutlineBounds(candidate)
|
|
317
|
+
|
|
318
|
+
if (!candidateBounds) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
Math.abs(current.minX - candidateBounds.minX) <= tolerance &&
|
|
324
|
+
Math.abs(current.minY - candidateBounds.minY) <= tolerance &&
|
|
325
|
+
Math.abs(current.maxX - candidateBounds.maxX) <= tolerance &&
|
|
326
|
+
Math.abs(current.maxY - candidateBounds.maxY) <= tolerance
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Returns true when the candidate area is close enough to the fallback.
|
|
332
|
+
* @param {number} currentArea Current outline area.
|
|
333
|
+
* @param {object} candidate Candidate outline.
|
|
334
|
+
* @returns {boolean}
|
|
335
|
+
*/
|
|
336
|
+
static #areaIsCompatible(currentArea, candidate) {
|
|
337
|
+
if (!currentArea) {
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const candidateArea = Math.abs(
|
|
342
|
+
PcbScene3dBoardOutlineRefiner.#computeAreaFromSegments(
|
|
343
|
+
candidate?.segments || []
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
const ratio = candidateArea / currentArea
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
ratio >= PcbScene3dBoardOutlineRefiner.#AREA_RATIO_MIN &&
|
|
350
|
+
ratio <= PcbScene3dBoardOutlineRefiner.#AREA_RATIO_MAX
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Scores how closely candidate bounds match current bounds.
|
|
356
|
+
* @param {object} current Current outline bounds.
|
|
357
|
+
* @param {object} candidate Candidate outline bounds.
|
|
358
|
+
* @returns {number}
|
|
359
|
+
*/
|
|
360
|
+
static #scoreBounds(current, candidate) {
|
|
361
|
+
const candidateBounds =
|
|
362
|
+
PcbScene3dBoardOutlineRefiner.#resolveOutlineBounds(candidate)
|
|
363
|
+
|
|
364
|
+
if (!candidateBounds) {
|
|
365
|
+
return Number.POSITIVE_INFINITY
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
Math.abs(current.minX - candidateBounds.minX) +
|
|
370
|
+
Math.abs(current.minY - candidateBounds.minY) +
|
|
371
|
+
Math.abs(current.maxX - candidateBounds.maxX) +
|
|
372
|
+
Math.abs(current.maxY - candidateBounds.maxY)
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Computes signed area from line segments.
|
|
378
|
+
* @param {Array<Record<string, number | string>>} segments Outline segments.
|
|
379
|
+
* @returns {number}
|
|
380
|
+
*/
|
|
381
|
+
static #computeAreaFromSegments(segments) {
|
|
382
|
+
let area = 0
|
|
383
|
+
|
|
384
|
+
for (const segment of segments) {
|
|
385
|
+
area +=
|
|
386
|
+
Number(segment.x1 || 0) * Number(segment.y2 || 0) -
|
|
387
|
+
Number(segment.x2 || 0) * Number(segment.y1 || 0)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return area / 2
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Measures the distance between two points.
|
|
395
|
+
* @param {{ x: number, y: number }} left First point.
|
|
396
|
+
* @param {{ x: number, y: number }} right Second point.
|
|
397
|
+
* @returns {number}
|
|
398
|
+
*/
|
|
399
|
+
static #distanceBetween(left, right) {
|
|
400
|
+
return Math.hypot(right.x - left.x, right.y - left.y)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs'
|
|
6
|
+
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
6
7
|
import { PcbScene3dDrillCutoutBuilder } from './PcbScene3dDrillCutoutBuilder.mjs'
|
|
7
8
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
8
9
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
@@ -99,7 +100,7 @@ export class PcbScene3dBuilder {
|
|
|
99
100
|
appearance3d
|
|
100
101
|
)
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
const sceneDescription = {
|
|
103
104
|
sourceFormat: 'altium',
|
|
104
105
|
board,
|
|
105
106
|
components: components.map((component) =>
|
|
@@ -139,6 +140,11 @@ export class PcbScene3dBuilder {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
return PcbScene3dBoardOutlineRefiner.refine(
|
|
145
|
+
sceneDescription,
|
|
146
|
+
documentModel
|
|
147
|
+
)
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
/**
|
|
@@ -729,6 +735,11 @@ export class PcbScene3dBuilder {
|
|
|
729
735
|
boardOutline
|
|
730
736
|
)
|
|
731
737
|
|
|
738
|
+
const drillCutouts = PcbScene3dDrillCutoutBuilder.buildCutouts(
|
|
739
|
+
pads,
|
|
740
|
+
vias
|
|
741
|
+
)
|
|
742
|
+
|
|
732
743
|
return {
|
|
733
744
|
...normalized,
|
|
734
745
|
denseOverlayArtwork,
|
|
@@ -743,11 +754,11 @@ export class PcbScene3dBuilder {
|
|
|
743
754
|
side
|
|
744
755
|
),
|
|
745
756
|
tracks: normalized.tracks,
|
|
746
|
-
fills: PcbScene3dDrillCutoutBuilder.
|
|
757
|
+
fills: PcbScene3dDrillCutoutBuilder.clipFillsWithCutouts(
|
|
747
758
|
fillsWithRegions,
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
)
|
|
759
|
+
drillCutouts
|
|
760
|
+
),
|
|
761
|
+
drillCutouts: drillCutouts.map((cutout) => cutout.points)
|
|
751
762
|
}
|
|
752
763
|
}
|
|
753
764
|
|
|
@@ -10,6 +10,19 @@ export class PcbScene3dDrillCutoutBuilder {
|
|
|
10
10
|
static #SLOT_CAP_SEGMENTS = 12
|
|
11
11
|
static #EPSILON = 0.001
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Builds all known drill cutout contours.
|
|
15
|
+
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number, holeSlotLength?: number, slotLength?: number, rotation?: number, holeRotation?: number }[]} pads
|
|
16
|
+
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number }[]} vias
|
|
17
|
+
* @returns {{ x: number, y: number, bounds: { minX: number, minY: number, maxX: number, maxY: number }, points: { x: number, y: number }[] }[]}
|
|
18
|
+
*/
|
|
19
|
+
static buildCutouts(pads, vias) {
|
|
20
|
+
return [
|
|
21
|
+
...PcbScene3dDrillCutoutBuilder.#buildPadCutouts(pads),
|
|
22
|
+
...PcbScene3dDrillCutoutBuilder.#buildViaCutouts(vias)
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* Adds drill-shaped holes to every fill intersected by a pad or via drill.
|
|
15
28
|
* @param {{ points?: { x: number, y: number }[], holes?: { x: number, y: number }[][], x1?: number, y1?: number, x2?: number, y2?: number }[]} fills
|
|
@@ -18,9 +31,20 @@ export class PcbScene3dDrillCutoutBuilder {
|
|
|
18
31
|
* @returns {{ points?: { x: number, y: number }[], holes?: { x: number, y: number }[][], x1?: number, y1?: number, x2?: number, y2?: number }[]}
|
|
19
32
|
*/
|
|
20
33
|
static clipFills(fills, pads, vias) {
|
|
21
|
-
|
|
34
|
+
return PcbScene3dDrillCutoutBuilder.clipFillsWithCutouts(
|
|
35
|
+
fills,
|
|
36
|
+
PcbScene3dDrillCutoutBuilder.buildCutouts(pads, vias)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Adds drill-shaped holes from precomputed cutouts.
|
|
42
|
+
* @param {{ points?: { x: number, y: number }[], holes?: { x: number, y: number }[][], x1?: number, y1?: number, x2?: number, y2?: number }[]} fills
|
|
43
|
+
* @param {{ x: number, y: number, bounds: { minX: number, minY: number, maxX: number, maxY: number }, points: { x: number, y: number }[] }[]} cutouts
|
|
44
|
+
* @returns {{ points?: { x: number, y: number }[], holes?: { x: number, y: number }[][], x1?: number, y1?: number, x2?: number, y2?: number }[]}
|
|
45
|
+
*/
|
|
46
|
+
static clipFillsWithCutouts(fills, cutouts) {
|
|
47
|
+
if (!Array.isArray(cutouts) || !cutouts.length) {
|
|
24
48
|
return fills
|
|
25
49
|
}
|
|
26
50
|
|
|
@@ -48,19 +72,6 @@ export class PcbScene3dDrillCutoutBuilder {
|
|
|
48
72
|
})
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
/**
|
|
52
|
-
* Builds all known drill cutout contours.
|
|
53
|
-
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number, holeSlotLength?: number, slotLength?: number, rotation?: number, holeRotation?: number }[]} pads
|
|
54
|
-
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number }[]} vias
|
|
55
|
-
* @returns {{ x: number, y: number, bounds: { minX: number, minY: number, maxX: number, maxY: number }, points: { x: number, y: number }[] }[]}
|
|
56
|
-
*/
|
|
57
|
-
static #buildCutouts(pads, vias) {
|
|
58
|
-
return [
|
|
59
|
-
...PcbScene3dDrillCutoutBuilder.#buildPadCutouts(pads),
|
|
60
|
-
...PcbScene3dDrillCutoutBuilder.#buildViaCutouts(vias)
|
|
61
|
-
]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
75
|
/**
|
|
65
76
|
* Builds drill contours for drilled pads.
|
|
66
77
|
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number, holeSlotLength?: number, slotLength?: number, rotation?: number, holeRotation?: number }[]} pads
|
|
@@ -7,7 +7,9 @@ import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs
|
|
|
7
7
|
import { PcbEmbeddedFontFaceRenderer } from './PcbEmbeddedFontFaceRenderer.mjs'
|
|
8
8
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
9
9
|
import { PcbCopperPrimitiveSplitter } from './PcbCopperPrimitiveSplitter.mjs'
|
|
10
|
+
import { PcbNativeTextKnockoutDetector } from './PcbNativeTextKnockoutDetector.mjs'
|
|
10
11
|
import { PcbRegionPrimitiveRenderer } from './PcbRegionPrimitiveRenderer.mjs'
|
|
12
|
+
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
11
13
|
import { PcbTextPrimitiveRenderer } from './PcbTextPrimitiveRenderer.mjs'
|
|
12
14
|
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
13
15
|
/**
|
|
@@ -27,7 +29,10 @@ export class PcbSvgRenderer {
|
|
|
27
29
|
if (!pcb) {
|
|
28
30
|
return '<section class="altium-renderer-empty">No PCB entities were recovered from this file.</section>'
|
|
29
31
|
}
|
|
30
|
-
const outline =
|
|
32
|
+
const outline = PcbScene3dBoardOutlineRefiner.refine(
|
|
33
|
+
{ board: pcb.boardOutline },
|
|
34
|
+
documentModel
|
|
35
|
+
).board
|
|
31
36
|
const polygons = pcb.polygons || []
|
|
32
37
|
const fills = pcb.fills || []
|
|
33
38
|
const tracks = pcb.tracks || []
|
|
@@ -43,11 +48,6 @@ export class PcbSvgRenderer {
|
|
|
43
48
|
const stackLayers = Array.isArray(pcb.layers) ? pcb.layers : []
|
|
44
49
|
const primitiveLayers = pcb.primitiveLayers || []
|
|
45
50
|
const displayLayers = stackLayers.length ? stackLayers : primitiveLayers
|
|
46
|
-
const texts = PcbTextPrimitiveRenderer.select(
|
|
47
|
-
primitiveLayers,
|
|
48
|
-
pcb.texts || [],
|
|
49
|
-
'top'
|
|
50
|
-
)
|
|
51
51
|
const copperGroups = PcbCopperPrimitiveSplitter.split(
|
|
52
52
|
polygons,
|
|
53
53
|
fills,
|
|
@@ -66,6 +66,18 @@ export class PcbSvgRenderer {
|
|
|
66
66
|
),
|
|
67
67
|
outline
|
|
68
68
|
)
|
|
69
|
+
const texts = PcbTextPrimitiveRenderer.select(
|
|
70
|
+
primitiveLayers,
|
|
71
|
+
pcb.texts || [],
|
|
72
|
+
'top',
|
|
73
|
+
{
|
|
74
|
+
nativeTextKnockouts:
|
|
75
|
+
PcbNativeTextKnockoutDetector.hasNativeTextKnockouts(
|
|
76
|
+
footprintPrimitives,
|
|
77
|
+
outline
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
69
81
|
const path = PcbSvgRenderer.#buildBoardPath(outline.segments)
|
|
70
82
|
const clipPathId = 'pcb-board-clip'
|
|
71
83
|
const viewBox = PcbSvgRenderer.#buildViewBox(
|
|
@@ -256,6 +268,9 @@ export class PcbSvgRenderer {
|
|
|
256
268
|
'pcb-footprint-region'
|
|
257
269
|
)
|
|
258
270
|
const textMarkup = PcbTextPrimitiveRenderer.render(texts)
|
|
271
|
+
const textGroupTransform = PcbSvgRenderer.#renderTextGroupTransform(
|
|
272
|
+
pcb.textGroupTransform
|
|
273
|
+
)
|
|
259
274
|
const fontFaceMarkup = PcbEmbeddedFontFaceRenderer.buildMarkup(
|
|
260
275
|
pcb.embeddedFonts || []
|
|
261
276
|
)
|
|
@@ -355,17 +370,19 @@ export class PcbSvgRenderer {
|
|
|
355
370
|
footprintArcMarkup +
|
|
356
371
|
footprintRegionMarkup +
|
|
357
372
|
'</g>' +
|
|
373
|
+
'<g class="pcb-components">' +
|
|
374
|
+
componentMarkup +
|
|
375
|
+
'</g>' +
|
|
358
376
|
'<g class="pcb-texts" clip-path="url(#' +
|
|
359
377
|
clipPathId +
|
|
360
|
-
')"
|
|
378
|
+
')"' +
|
|
379
|
+
textGroupTransform +
|
|
380
|
+
'>' +
|
|
361
381
|
textMarkup +
|
|
362
382
|
'</g>' +
|
|
363
383
|
'<path class="board-outline board-outline--stroke" d="' +
|
|
364
384
|
SchematicSvgUtils.escapeHtml(path) +
|
|
365
385
|
'" />' +
|
|
366
|
-
'<g class="pcb-components">' +
|
|
367
|
-
componentMarkup +
|
|
368
|
-
'</g>' +
|
|
369
386
|
'</svg></div></section>'
|
|
370
387
|
)
|
|
371
388
|
}
|
|
@@ -415,6 +432,62 @@ export class PcbSvgRenderer {
|
|
|
415
432
|
return path + ' Z'
|
|
416
433
|
}
|
|
417
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Renders an optional transform for the whole PCB text layer.
|
|
437
|
+
* @param {{ translateX?: number, translateY?: number, scaleX?: number, scaleY?: number } | undefined} transform Text group transform.
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
static #renderTextGroupTransform(transform) {
|
|
441
|
+
if (!transform || typeof transform !== 'object') {
|
|
442
|
+
return ''
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const translateX = Number(transform.translateX || 0)
|
|
446
|
+
const translateY = Number(transform.translateY || 0)
|
|
447
|
+
const scaleX =
|
|
448
|
+
transform.scaleX === null || transform.scaleX === undefined
|
|
449
|
+
? 1
|
|
450
|
+
: Number(transform.scaleX)
|
|
451
|
+
const scaleY =
|
|
452
|
+
transform.scaleY === null || transform.scaleY === undefined
|
|
453
|
+
? 1
|
|
454
|
+
: Number(transform.scaleY)
|
|
455
|
+
if (
|
|
456
|
+
!Number.isFinite(translateX) ||
|
|
457
|
+
!Number.isFinite(translateY) ||
|
|
458
|
+
!Number.isFinite(scaleX) ||
|
|
459
|
+
!Number.isFinite(scaleY) ||
|
|
460
|
+
(translateX === 0 &&
|
|
461
|
+
translateY === 0 &&
|
|
462
|
+
scaleX === 1 &&
|
|
463
|
+
scaleY === 1)
|
|
464
|
+
) {
|
|
465
|
+
return ''
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const parts = []
|
|
469
|
+
if (translateX !== 0 || translateY !== 0) {
|
|
470
|
+
parts.push(
|
|
471
|
+
'translate(' +
|
|
472
|
+
SchematicSvgUtils.formatNumber(translateX) +
|
|
473
|
+
' ' +
|
|
474
|
+
SchematicSvgUtils.formatNumber(translateY) +
|
|
475
|
+
')'
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
479
|
+
parts.push(
|
|
480
|
+
'scale(' +
|
|
481
|
+
SchematicSvgUtils.formatNumber(scaleX) +
|
|
482
|
+
' ' +
|
|
483
|
+
SchematicSvgUtils.formatNumber(scaleY) +
|
|
484
|
+
')'
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return ' transform="' + parts.join(' ') + '"'
|
|
489
|
+
}
|
|
490
|
+
|
|
418
491
|
/**
|
|
419
492
|
* Computes a reasonable viewBox.
|
|
420
493
|
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }} outline
|