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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds downloadable manufacturing metadata artifacts from parsed documents.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonManufacturingDownloadBuilder {
|
|
5
|
+
/**
|
|
6
|
+
* Returns true when a format is handled by this builder.
|
|
7
|
+
* @param {string} format Export format.
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
static supportsFormat(format) {
|
|
11
|
+
return [
|
|
12
|
+
'pick-place-csv',
|
|
13
|
+
'routing-dsn',
|
|
14
|
+
'fabrication-notes-json'
|
|
15
|
+
].includes(String(format || ''))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds a manufacturing metadata download.
|
|
20
|
+
* @param {object} documentModel Parsed document model.
|
|
21
|
+
* @param {string} format Export format.
|
|
22
|
+
* @returns {{ fileName: string, bytes: Uint8Array, contentType: string }}
|
|
23
|
+
*/
|
|
24
|
+
static build(documentModel, format) {
|
|
25
|
+
if (format === 'pick-place-csv') {
|
|
26
|
+
return CircuitJsonManufacturingDownloadBuilder.#pickPlaceCsv(
|
|
27
|
+
documentModel
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
if (format === 'routing-dsn') {
|
|
31
|
+
return CircuitJsonManufacturingDownloadBuilder.#routingDsn(
|
|
32
|
+
documentModel
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (format === 'fabrication-notes-json') {
|
|
36
|
+
return CircuitJsonManufacturingDownloadBuilder.#fabricationNotesJson(
|
|
37
|
+
documentModel
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
throw new Error('Unsupported manufacturing export format')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds a pick-and-place CSV download.
|
|
45
|
+
* @param {object} documentModel Parsed document model.
|
|
46
|
+
* @returns {{ fileName: string, bytes: Uint8Array, contentType: string }}
|
|
47
|
+
*/
|
|
48
|
+
static #pickPlaceCsv(documentModel) {
|
|
49
|
+
const rows = Array.isArray(
|
|
50
|
+
documentModel?.manufacturing?.pickAndPlaceRows
|
|
51
|
+
)
|
|
52
|
+
? documentModel.manufacturing.pickAndPlaceRows
|
|
53
|
+
: []
|
|
54
|
+
if (!rows.length) {
|
|
55
|
+
throw new Error('No placement metadata is available')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const headers = [
|
|
59
|
+
'Designator',
|
|
60
|
+
'Component ID',
|
|
61
|
+
'Source Component ID',
|
|
62
|
+
'X',
|
|
63
|
+
'Y',
|
|
64
|
+
'Rotation',
|
|
65
|
+
'Layer',
|
|
66
|
+
'Side',
|
|
67
|
+
'Value',
|
|
68
|
+
'Package',
|
|
69
|
+
'Manufacturer Part Number'
|
|
70
|
+
]
|
|
71
|
+
const body = rows.map((row) => [
|
|
72
|
+
row.designator,
|
|
73
|
+
row.componentId,
|
|
74
|
+
row.sourceComponentId,
|
|
75
|
+
row.x,
|
|
76
|
+
row.y,
|
|
77
|
+
row.rotation,
|
|
78
|
+
row.layer,
|
|
79
|
+
row.side,
|
|
80
|
+
row.value,
|
|
81
|
+
row.package,
|
|
82
|
+
row.manufacturerPartNumber
|
|
83
|
+
])
|
|
84
|
+
const csv = CircuitJsonManufacturingDownloadBuilder.#csv([
|
|
85
|
+
headers,
|
|
86
|
+
...body
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
fileName:
|
|
91
|
+
CircuitJsonManufacturingDownloadBuilder.#fileBase(
|
|
92
|
+
documentModel
|
|
93
|
+
) + '-pick-place.csv',
|
|
94
|
+
bytes: new TextEncoder().encode(csv),
|
|
95
|
+
contentType: 'text/csv;charset=utf-8'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds a routing DSN download.
|
|
101
|
+
* @param {object} documentModel Parsed document model.
|
|
102
|
+
* @returns {{ fileName: string, bytes: Uint8Array, contentType: string }}
|
|
103
|
+
*/
|
|
104
|
+
static #routingDsn(documentModel) {
|
|
105
|
+
const dsn = String(documentModel?.manufacturing?.routingDsn || '')
|
|
106
|
+
if (!dsn.trim()) {
|
|
107
|
+
throw new Error('No routing metadata is available')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
fileName:
|
|
112
|
+
CircuitJsonManufacturingDownloadBuilder.#fileBase(
|
|
113
|
+
documentModel
|
|
114
|
+
) + '-routing.dsn',
|
|
115
|
+
bytes: new TextEncoder().encode(dsn),
|
|
116
|
+
contentType: 'application/specctra-dsn'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds a fabrication notes JSON download.
|
|
122
|
+
* @param {object} documentModel Parsed document model.
|
|
123
|
+
* @returns {{ fileName: string, bytes: Uint8Array, contentType: string }}
|
|
124
|
+
*/
|
|
125
|
+
static #fabricationNotesJson(documentModel) {
|
|
126
|
+
const notes = Array.isArray(
|
|
127
|
+
documentModel?.manufacturing?.fabricationNotes
|
|
128
|
+
)
|
|
129
|
+
? documentModel.manufacturing.fabricationNotes
|
|
130
|
+
: []
|
|
131
|
+
if (!notes.length) {
|
|
132
|
+
throw new Error('No fabrication note metadata is available')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const payload = {
|
|
136
|
+
fileName: String(documentModel?.fileName || ''),
|
|
137
|
+
notes
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
fileName:
|
|
142
|
+
CircuitJsonManufacturingDownloadBuilder.#fileBase(
|
|
143
|
+
documentModel
|
|
144
|
+
) + '-fabrication-notes.json',
|
|
145
|
+
bytes: new TextEncoder().encode(
|
|
146
|
+
JSON.stringify(payload, null, 2) + '\n'
|
|
147
|
+
),
|
|
148
|
+
contentType: 'application/json;charset=utf-8'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serializes CSV rows.
|
|
154
|
+
* @param {unknown[][]} rows CSV row values.
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
static #csv(rows) {
|
|
158
|
+
return (
|
|
159
|
+
rows
|
|
160
|
+
.map((row) =>
|
|
161
|
+
row
|
|
162
|
+
.map((value) =>
|
|
163
|
+
CircuitJsonManufacturingDownloadBuilder.#csvCell(
|
|
164
|
+
value
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
.join(',')
|
|
168
|
+
)
|
|
169
|
+
.join('\n') + '\n'
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Serializes one CSV cell.
|
|
175
|
+
* @param {unknown} value Cell value.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static #csvCell(value) {
|
|
179
|
+
const text = String(value ?? '')
|
|
180
|
+
if (!/[",\n\r]/u.test(text)) return text
|
|
181
|
+
return '"' + text.replaceAll('"', '""') + '"'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Builds a filesystem-safe base file name.
|
|
186
|
+
* @param {object} documentModel Parsed document model.
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
static #fileBase(documentModel) {
|
|
190
|
+
const raw = String(documentModel?.fileName || 'manufacturing')
|
|
191
|
+
.replace(/\.[^.]+$/u, '')
|
|
192
|
+
.trim()
|
|
193
|
+
const safe = raw.replace(/[^a-z0-9._-]+/giu, '-').replace(/^-|-$/gu, '')
|
|
194
|
+
return safe || 'manufacturing'
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
|
+
import { CircuitJsonBomBuilder } from './CircuitJsonBomBuilder.mjs'
|
|
3
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
4
|
+
import { CircuitJsonManufacturingBuilder } from './CircuitJsonManufacturingBuilder.mjs'
|
|
5
|
+
import { CircuitJsonSupportMatrixBuilder } from './CircuitJsonSupportMatrixBuilder.mjs'
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Parses standalone CircuitJSON files.
|
|
@@ -22,10 +26,15 @@ export class CircuitJsonParser {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
CircuitJsonDocument.assertModel(parsed)
|
|
29
|
+
const index = CircuitJsonIndexer.index(parsed)
|
|
25
30
|
return CircuitJsonDocument.attachMetadata(parsed, {
|
|
26
31
|
fileName: options.fileName || '',
|
|
27
32
|
fileType: 'circuitjson',
|
|
28
|
-
kind: CircuitJsonParser.#resolveKind(
|
|
33
|
+
kind: CircuitJsonParser.#resolveKind(index),
|
|
34
|
+
diagnostics: index.diagnostics,
|
|
35
|
+
bom: CircuitJsonBomBuilder.build(parsed),
|
|
36
|
+
supportMatrix: CircuitJsonSupportMatrixBuilder.build(parsed),
|
|
37
|
+
manufacturing: CircuitJsonManufacturingBuilder.build(parsed, index)
|
|
29
38
|
})
|
|
30
39
|
}
|
|
31
40
|
|
|
@@ -44,12 +53,19 @@ export class CircuitJsonParser {
|
|
|
44
53
|
|
|
45
54
|
/**
|
|
46
55
|
* Resolves a broad document kind from available elements.
|
|
47
|
-
* @param {object[]}
|
|
56
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Model index.
|
|
48
57
|
* @returns {string}
|
|
49
58
|
*/
|
|
50
|
-
static #resolveKind(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
static #resolveKind(index) {
|
|
60
|
+
if (index.elementsByType?.has('pcb_board')) return 'pcb'
|
|
61
|
+
if (
|
|
62
|
+
[...(index.elementsByType?.keys() || [])].some((type) =>
|
|
63
|
+
String(type).startsWith('schematic_')
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
66
|
+
return 'schematic'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'circuitjson'
|
|
54
70
|
}
|
|
55
71
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
2
|
+
import { CircuitJsonPcbCopperGeometry } from './CircuitJsonPcbCopperGeometry.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds copper clearance diagnostics for CircuitJSON PCB primitives.
|
|
6
|
+
*/
|
|
7
|
+
export class CircuitJsonPcbClearanceDiagnostics {
|
|
8
|
+
/**
|
|
9
|
+
* Builds generic copper clearance diagnostics when board rules are present.
|
|
10
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
11
|
+
* @param {object[]} primitives Primitive rows.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static build(index, primitives) {
|
|
15
|
+
const minimum = this.#minimumClearance(index)
|
|
16
|
+
if (minimum === null || minimum <= 0) return []
|
|
17
|
+
|
|
18
|
+
const copper = primitives.filter((primitive) =>
|
|
19
|
+
this.#isCopperPrimitive(primitive)
|
|
20
|
+
)
|
|
21
|
+
const keepouts = primitives.filter((primitive) =>
|
|
22
|
+
this.#isKeepoutPrimitive(primitive)
|
|
23
|
+
)
|
|
24
|
+
const diagnostics = []
|
|
25
|
+
for (let leftIndex = 0; leftIndex < copper.length; leftIndex += 1) {
|
|
26
|
+
for (
|
|
27
|
+
let rightIndex = leftIndex + 1;
|
|
28
|
+
rightIndex < copper.length;
|
|
29
|
+
rightIndex += 1
|
|
30
|
+
) {
|
|
31
|
+
const left = copper[leftIndex]
|
|
32
|
+
const right = copper[rightIndex]
|
|
33
|
+
if (left.netName === right.netName) continue
|
|
34
|
+
if (!this.#sameClearanceLayer(left, right)) continue
|
|
35
|
+
const actual =
|
|
36
|
+
CircuitJsonPcbCopperGeometry.clearance(left, right) ??
|
|
37
|
+
this.#boundsClearance(left.bounds, right.bounds)
|
|
38
|
+
if (actual >= minimum) continue
|
|
39
|
+
diagnostics.push(
|
|
40
|
+
this.#clearanceDiagnostic(
|
|
41
|
+
left,
|
|
42
|
+
right,
|
|
43
|
+
minimum,
|
|
44
|
+
actual,
|
|
45
|
+
diagnostics.length
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const copperPrimitive of copper) {
|
|
51
|
+
for (const keepout of keepouts) {
|
|
52
|
+
if (!this.#keepoutAppliesToCopper(copperPrimitive, keepout)) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
const actual =
|
|
56
|
+
CircuitJsonPcbCopperGeometry.clearance(
|
|
57
|
+
copperPrimitive,
|
|
58
|
+
keepout
|
|
59
|
+
) ??
|
|
60
|
+
this.#boundsClearance(
|
|
61
|
+
copperPrimitive.bounds,
|
|
62
|
+
keepout.bounds
|
|
63
|
+
)
|
|
64
|
+
if (actual >= minimum) continue
|
|
65
|
+
diagnostics.push(
|
|
66
|
+
this.#keepoutDiagnostic(
|
|
67
|
+
copperPrimitive,
|
|
68
|
+
keepout,
|
|
69
|
+
minimum,
|
|
70
|
+
actual,
|
|
71
|
+
diagnostics.length
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return diagnostics
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the configured minimum copper clearance.
|
|
81
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
82
|
+
* @returns {number | null}
|
|
83
|
+
*/
|
|
84
|
+
static #minimumClearance(index) {
|
|
85
|
+
for (const board of index.elementsByType.get('pcb_board') || []) {
|
|
86
|
+
const value = CircuitJsonUnits.optionalLength(
|
|
87
|
+
board.min_trace_clearance ??
|
|
88
|
+
board.minimum_trace_clearance ??
|
|
89
|
+
board.minimum_copper_clearance ??
|
|
90
|
+
board.minimumCopperClearance ??
|
|
91
|
+
board.minCopperClearance
|
|
92
|
+
)
|
|
93
|
+
if (value !== null) return value
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns true when a primitive participates in copper spacing checks.
|
|
100
|
+
* @param {object} primitive Primitive row.
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
static #isCopperPrimitive(primitive) {
|
|
104
|
+
return (
|
|
105
|
+
['pad', 'track', 'via', 'zone'].includes(primitive.kind) &&
|
|
106
|
+
String(primitive.netName || '').trim() &&
|
|
107
|
+
primitive.bounds
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns true when a primitive represents a keepout region.
|
|
113
|
+
* @param {object} primitive Primitive row.
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
static #isKeepoutPrimitive(primitive) {
|
|
117
|
+
return primitive.kind === 'keepout' && Boolean(primitive.bounds)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns true when two copper primitives should share clearance checks.
|
|
122
|
+
* @param {object} left First primitive.
|
|
123
|
+
* @param {object} right Second primitive.
|
|
124
|
+
* @returns {boolean}
|
|
125
|
+
*/
|
|
126
|
+
static #sameClearanceLayer(left, right) {
|
|
127
|
+
if (left.kind === 'via' || right.kind === 'via') return true
|
|
128
|
+
const leftLayer = String(left.layer || '').trim()
|
|
129
|
+
const rightLayer = String(right.layer || '').trim()
|
|
130
|
+
return !leftLayer || !rightLayer || leftLayer === rightLayer
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns true when a keepout applies to the copper primitive layer.
|
|
135
|
+
* @param {object} copper Copper primitive.
|
|
136
|
+
* @param {object} keepout Keepout primitive.
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
static #keepoutAppliesToCopper(copper, keepout) {
|
|
140
|
+
if (copper.kind === 'via') return true
|
|
141
|
+
const keepoutSides = this.#keepoutSides(keepout)
|
|
142
|
+
if (!keepoutSides.length) return true
|
|
143
|
+
const copperSide = this.#surfaceSide(copper.layer || copper.side)
|
|
144
|
+
return !copperSide || keepoutSides.includes(copperSide)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolves the positive distance between two axis-aligned bounds.
|
|
149
|
+
* @param {object} left First bounds.
|
|
150
|
+
* @param {object} right Second bounds.
|
|
151
|
+
* @returns {number}
|
|
152
|
+
*/
|
|
153
|
+
static #boundsClearance(left, right) {
|
|
154
|
+
const gapX = Math.max(left.minX - right.maxX, right.minX - left.maxX, 0)
|
|
155
|
+
const gapY = Math.max(left.minY - right.maxY, right.minY - left.maxY, 0)
|
|
156
|
+
return Math.hypot(gapX, gapY)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Builds one copper clearance diagnostic.
|
|
161
|
+
* @param {object} left First primitive.
|
|
162
|
+
* @param {object} right Second primitive.
|
|
163
|
+
* @param {number} minimum Minimum clearance.
|
|
164
|
+
* @param {number} actual Actual clearance.
|
|
165
|
+
* @param {number} index Diagnostic index.
|
|
166
|
+
* @returns {object}
|
|
167
|
+
*/
|
|
168
|
+
static #clearanceDiagnostic(left, right, minimum, actual, index) {
|
|
169
|
+
const leftCenter = this.#boundsCenter(left.bounds)
|
|
170
|
+
const rightCenter = this.#boundsCenter(right.bounds)
|
|
171
|
+
const netName = [left.netName, right.netName].sort().join(' / ')
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
id: 'clearance:' + index,
|
|
175
|
+
kind: 'error',
|
|
176
|
+
severity: 'error',
|
|
177
|
+
category: 'clearance',
|
|
178
|
+
code: 'pcb_copper_clearance',
|
|
179
|
+
message:
|
|
180
|
+
'Copper clearance is below the configured minimum for ' +
|
|
181
|
+
netName +
|
|
182
|
+
'.',
|
|
183
|
+
point: {
|
|
184
|
+
x: (leftCenter.x + rightCenter.x) / 2,
|
|
185
|
+
y: (leftCenter.y + rightCenter.y) / 2
|
|
186
|
+
},
|
|
187
|
+
bounds: this.#mergeBounds([left.bounds, right.bounds]),
|
|
188
|
+
relatedPrimitiveIds: [left.id, right.id].filter(Boolean).sort(),
|
|
189
|
+
componentKey: '',
|
|
190
|
+
netName,
|
|
191
|
+
clearance: {
|
|
192
|
+
minimum,
|
|
193
|
+
actual: Number(actual.toFixed(6))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Builds one keepout clearance diagnostic.
|
|
200
|
+
* @param {object} copper Copper primitive.
|
|
201
|
+
* @param {object} keepout Keepout primitive.
|
|
202
|
+
* @param {number} minimum Minimum clearance.
|
|
203
|
+
* @param {number} actual Actual clearance.
|
|
204
|
+
* @param {number} index Diagnostic index.
|
|
205
|
+
* @returns {object}
|
|
206
|
+
*/
|
|
207
|
+
static #keepoutDiagnostic(copper, keepout, minimum, actual, index) {
|
|
208
|
+
const copperCenter = this.#boundsCenter(copper.bounds)
|
|
209
|
+
const keepoutCenter = this.#boundsCenter(keepout.bounds)
|
|
210
|
+
const keepoutId = String(keepout.id || '')
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
id: 'keepout-clearance:' + index,
|
|
214
|
+
kind: 'error',
|
|
215
|
+
severity: 'error',
|
|
216
|
+
category: 'clearance',
|
|
217
|
+
code: 'pcb_keepout_clearance',
|
|
218
|
+
message:
|
|
219
|
+
'Copper clearance is below the configured minimum around keepout ' +
|
|
220
|
+
keepoutId +
|
|
221
|
+
'.',
|
|
222
|
+
point: {
|
|
223
|
+
x: (copperCenter.x + keepoutCenter.x) / 2,
|
|
224
|
+
y: (copperCenter.y + keepoutCenter.y) / 2
|
|
225
|
+
},
|
|
226
|
+
bounds: this.#mergeBounds([copper.bounds, keepout.bounds]),
|
|
227
|
+
relatedPrimitiveIds: [copper.id, keepout.id].filter(Boolean).sort(),
|
|
228
|
+
componentKey: String(copper.componentKey || ''),
|
|
229
|
+
netName: String(copper.netName || ''),
|
|
230
|
+
keepoutId,
|
|
231
|
+
clearance: {
|
|
232
|
+
minimum,
|
|
233
|
+
actual: Number(actual.toFixed(6))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolves the center point of bounds.
|
|
240
|
+
* @param {object} bounds Bounds record.
|
|
241
|
+
* @returns {{ x: number, y: number }}
|
|
242
|
+
*/
|
|
243
|
+
static #boundsCenter(bounds) {
|
|
244
|
+
return {
|
|
245
|
+
x: bounds.minX + bounds.width / 2,
|
|
246
|
+
y: bounds.minY + bounds.height / 2
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Merges bounds rows.
|
|
252
|
+
* @param {object[]} rows Bounds rows.
|
|
253
|
+
* @returns {object | null}
|
|
254
|
+
*/
|
|
255
|
+
static #mergeBounds(rows) {
|
|
256
|
+
const validRows = rows.filter(Boolean)
|
|
257
|
+
if (!validRows.length) return null
|
|
258
|
+
const minX = Math.min(...validRows.map((bounds) => bounds.minX))
|
|
259
|
+
const minY = Math.min(...validRows.map((bounds) => bounds.minY))
|
|
260
|
+
const maxX = Math.max(...validRows.map((bounds) => bounds.maxX))
|
|
261
|
+
const maxY = Math.max(...validRows.map((bounds) => bounds.maxY))
|
|
262
|
+
return {
|
|
263
|
+
minX: this.#round(minX),
|
|
264
|
+
minY: this.#round(minY),
|
|
265
|
+
maxX: this.#round(maxX),
|
|
266
|
+
maxY: this.#round(maxY),
|
|
267
|
+
width: this.#round(maxX - minX),
|
|
268
|
+
height: this.#round(maxY - minY)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolves keepout surface sides from source layer fields.
|
|
274
|
+
* @param {object} keepout Keepout primitive.
|
|
275
|
+
* @returns {string[]}
|
|
276
|
+
*/
|
|
277
|
+
static #keepoutSides(keepout) {
|
|
278
|
+
const source = keepout.source || {}
|
|
279
|
+
const layers = [
|
|
280
|
+
...(Array.isArray(source.layers) ? source.layers : []),
|
|
281
|
+
source.layer,
|
|
282
|
+
source.side
|
|
283
|
+
]
|
|
284
|
+
return [
|
|
285
|
+
...new Set(
|
|
286
|
+
layers.map((layer) => this.#surfaceSide(layer)).filter(Boolean)
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolves top or bottom from common layer values.
|
|
293
|
+
* @param {unknown} layer Layer value.
|
|
294
|
+
* @returns {'top' | 'bottom' | ''}
|
|
295
|
+
*/
|
|
296
|
+
static #surfaceSide(layer) {
|
|
297
|
+
const value =
|
|
298
|
+
typeof layer === 'object' && layer !== null ? layer.name : layer
|
|
299
|
+
const normalized = String(value || '')
|
|
300
|
+
.trim()
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
if (
|
|
303
|
+
normalized === 'top' ||
|
|
304
|
+
normalized === 'front' ||
|
|
305
|
+
normalized === 'f.cu' ||
|
|
306
|
+
normalized === '1'
|
|
307
|
+
) {
|
|
308
|
+
return 'top'
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
normalized === 'bottom' ||
|
|
312
|
+
normalized === 'back' ||
|
|
313
|
+
normalized === 'b.cu' ||
|
|
314
|
+
normalized === '32'
|
|
315
|
+
) {
|
|
316
|
+
return 'bottom'
|
|
317
|
+
}
|
|
318
|
+
return ''
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Rounds one computed geometry value.
|
|
323
|
+
* @param {number} value Numeric value.
|
|
324
|
+
* @returns {number}
|
|
325
|
+
*/
|
|
326
|
+
static #round(value) {
|
|
327
|
+
return Number(Number(value).toFixed(6))
|
|
328
|
+
}
|
|
329
|
+
}
|