circuitjson-toolkit 1.0.16 → 1.1.0
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 +15 -0
- package/NOTICE.md +31 -0
- package/README.md +267 -107
- package/docs/api.md +501 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +120 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/testing.md +117 -7
- package/package.json +31 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +997 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +623 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- package/src/workers/parser.worker.mjs +59 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from '../context/CircuitJsonDocumentContext.mjs'
|
|
2
|
+
import { PcbPrimitivePreparation } from '../context/PcbPrimitivePreparation.mjs'
|
|
3
|
+
import { ToolkitDiagnostic } from '../contracts/ToolkitDiagnostic.mjs'
|
|
4
|
+
import { CircuitJsonPcbPrimitiveFields } from '../CircuitJsonPcbPrimitiveFields.mjs'
|
|
5
|
+
import { CanonicalRenderOptions } from './CanonicalRenderOptions.mjs'
|
|
6
|
+
|
|
7
|
+
const COPPER_KINDS = new Set([
|
|
8
|
+
'breakout-point',
|
|
9
|
+
'copper-text',
|
|
10
|
+
'pad',
|
|
11
|
+
'route-hint',
|
|
12
|
+
'thermal-spoke',
|
|
13
|
+
'track',
|
|
14
|
+
'via',
|
|
15
|
+
'zone'
|
|
16
|
+
])
|
|
17
|
+
const VIRTUAL_LAYER_ORDER = [
|
|
18
|
+
'top_silkscreen',
|
|
19
|
+
'bottom_silkscreen',
|
|
20
|
+
'top_fabrication',
|
|
21
|
+
'bottom_fabrication',
|
|
22
|
+
'top_courtyard',
|
|
23
|
+
'bottom_courtyard',
|
|
24
|
+
'top_soldermask',
|
|
25
|
+
'bottom_soldermask',
|
|
26
|
+
'top_paste',
|
|
27
|
+
'bottom_paste',
|
|
28
|
+
'keepouts',
|
|
29
|
+
'cutouts',
|
|
30
|
+
'panel',
|
|
31
|
+
'breakout_points',
|
|
32
|
+
'diagnostics',
|
|
33
|
+
'groups',
|
|
34
|
+
'anchor_offsets',
|
|
35
|
+
'trace_lengths',
|
|
36
|
+
'ratsnest'
|
|
37
|
+
]
|
|
38
|
+
const VIRTUAL_LAYER_IDS = new Set(VIRTUAL_LAYER_ORDER)
|
|
39
|
+
const PREPARED_PLANS = new WeakSet()
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prepares one reusable CircuitJSON PCB primitive model and layer selection.
|
|
43
|
+
*/
|
|
44
|
+
export class PcbRenderPlan {
|
|
45
|
+
/**
|
|
46
|
+
* Prepares a render plan from any canonical DocumentInput.
|
|
47
|
+
* @param {unknown} document DocumentResult, CircuitJSON model, or context.
|
|
48
|
+
* @param {Record<string, any>} [options] Canonical PCB render options.
|
|
49
|
+
* @returns {Record<string, any>} Prepared PCB render plan.
|
|
50
|
+
*/
|
|
51
|
+
static prepare(document, options = {}) {
|
|
52
|
+
const normalized = CanonicalRenderOptions.normalize(options, {
|
|
53
|
+
layers: true,
|
|
54
|
+
side: true,
|
|
55
|
+
svg: true
|
|
56
|
+
})
|
|
57
|
+
CanonicalRenderOptions.requireCanonicalFidelity(normalized.fidelity)
|
|
58
|
+
const context = CircuitJsonDocumentContext.prepare(document)
|
|
59
|
+
const model = PcbPrimitivePreparation.prepareComplete(context)
|
|
60
|
+
const availableLayers = PcbRenderPlan.#layers(model)
|
|
61
|
+
const byId = new Map(availableLayers.map((layer) => [layer.id, layer]))
|
|
62
|
+
const selectedIds = normalized.layers
|
|
63
|
+
if (selectedIds) {
|
|
64
|
+
const missing = selectedIds.filter((id) => !byId.has(id))
|
|
65
|
+
if (missing.length) {
|
|
66
|
+
throw CanonicalRenderOptions.error(
|
|
67
|
+
`Unknown render layers: ${missing.join(', ')}.`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
const wrongSide = selectedIds.filter((id) => {
|
|
71
|
+
const side = byId.get(id).side
|
|
72
|
+
return side === 'top' || side === 'bottom'
|
|
73
|
+
? side !== normalized.side
|
|
74
|
+
: false
|
|
75
|
+
})
|
|
76
|
+
if (wrongSide.length) {
|
|
77
|
+
throw CanonicalRenderOptions.error(
|
|
78
|
+
`Render layers do not belong to side ${normalized.side}: ${wrongSide.join(', ')}.`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const layers = (
|
|
83
|
+
selectedIds ||
|
|
84
|
+
PcbRenderPlan.#defaultLayerIds(availableLayers, normalized.side)
|
|
85
|
+
).map((id) => byId.get(id))
|
|
86
|
+
const statistics = context.statistics
|
|
87
|
+
const diagnostics = (model.diagnostics || []).map((row) =>
|
|
88
|
+
ToolkitDiagnostic.create({
|
|
89
|
+
code: row.code || row.type || row.id || 'PCB_RENDER_DIAGNOSTIC',
|
|
90
|
+
severity: row.severity || 'warning',
|
|
91
|
+
message: row.message || '',
|
|
92
|
+
source: context.source.fileName || '',
|
|
93
|
+
details: row
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
const plan = {
|
|
97
|
+
schema: 'ecad-toolkit.pcb-render-plan.v1',
|
|
98
|
+
model,
|
|
99
|
+
side: normalized.side,
|
|
100
|
+
fidelity: normalized.fidelity,
|
|
101
|
+
svg: normalized.svg,
|
|
102
|
+
layers,
|
|
103
|
+
selectedLayerIds: selectedIds ? [...selectedIds] : null,
|
|
104
|
+
diagnostics,
|
|
105
|
+
statistics: {
|
|
106
|
+
validationPasses: statistics.validationPasses,
|
|
107
|
+
primitiveBuilds:
|
|
108
|
+
statistics.derivedBuilds['render:pcb-primitives-v1'] || 0
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
PcbRenderPlan.#freeze(plan)
|
|
112
|
+
PREPARED_PLANS.add(plan)
|
|
113
|
+
return plan
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Requires a genuine immutable plan created by this module.
|
|
118
|
+
* @param {unknown} plan Prepared-plan candidate.
|
|
119
|
+
* @returns {Record<string, any>} Branded render plan.
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
static requirePrepared(plan) {
|
|
123
|
+
if (!plan || typeof plan !== 'object' || !PREPARED_PLANS.has(plan)) {
|
|
124
|
+
throw CanonicalRenderOptions.error(
|
|
125
|
+
'Prepared PCB rendering requires an immutable render plan.'
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return plan
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns a filtered primitive-model view for selected canonical layers.
|
|
133
|
+
* @param {Record<string, any>} plan Prepared render plan.
|
|
134
|
+
* @param {string[] | null} [layerIds] Canonical layer ids.
|
|
135
|
+
* @returns {Record<string, any>} Full or layer-filtered primitive model.
|
|
136
|
+
*/
|
|
137
|
+
static modelForLayers(plan, layerIds = plan.selectedLayerIds) {
|
|
138
|
+
PcbRenderPlan.requirePrepared(plan)
|
|
139
|
+
if (!layerIds) return plan.model
|
|
140
|
+
const ids = new Set(layerIds)
|
|
141
|
+
const descriptors = new Map(
|
|
142
|
+
plan.layers.map((layer) => [layer.id, layer])
|
|
143
|
+
)
|
|
144
|
+
const selected = layerIds
|
|
145
|
+
.map((id) => descriptors.get(id))
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
const copperLayers = new Set(
|
|
148
|
+
selected
|
|
149
|
+
.filter((layer) => layer.type === 'copper')
|
|
150
|
+
.map((layer) => layer.sourceLayerId)
|
|
151
|
+
)
|
|
152
|
+
const virtualLayers = new Set(
|
|
153
|
+
selected
|
|
154
|
+
.filter((layer) => layer.type !== 'copper')
|
|
155
|
+
.map((layer) => layer.id)
|
|
156
|
+
)
|
|
157
|
+
const primitives = (plan.model.primitives || []).filter(
|
|
158
|
+
(primitive) =>
|
|
159
|
+
primitive.kind === 'board' ||
|
|
160
|
+
(COPPER_KINDS.has(primitive.kind) &&
|
|
161
|
+
(copperLayers.has(primitive.layer) ||
|
|
162
|
+
(primitive.kind === 'via' &&
|
|
163
|
+
!primitive.side &&
|
|
164
|
+
PcbRenderPlan.#viaTouchesLayers(
|
|
165
|
+
primitive,
|
|
166
|
+
copperLayers,
|
|
167
|
+
plan.model.layers || []
|
|
168
|
+
)))) ||
|
|
169
|
+
virtualLayers.has(
|
|
170
|
+
PcbRenderPlan.#primitiveVirtualLayerId(primitive)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
...plan.model,
|
|
175
|
+
layers: (plan.model.layers || []).filter((layer) =>
|
|
176
|
+
selected.some(
|
|
177
|
+
(candidate) =>
|
|
178
|
+
candidate.type === 'copper' &&
|
|
179
|
+
candidate.sourceLayerId === layer.id
|
|
180
|
+
)
|
|
181
|
+
),
|
|
182
|
+
virtualLayers: selected.filter((layer) => layer.type !== 'copper'),
|
|
183
|
+
components: [],
|
|
184
|
+
primitives,
|
|
185
|
+
anchors: [],
|
|
186
|
+
diagnostics: ids.has('diagnostics')
|
|
187
|
+
? plan.model.diagnostics || []
|
|
188
|
+
: [],
|
|
189
|
+
airwires: ids.has('ratsnest') ? plan.model.airwires || [] : [],
|
|
190
|
+
traceLengths: ids.has('trace_lengths')
|
|
191
|
+
? plan.model.traceLengths || []
|
|
192
|
+
: [],
|
|
193
|
+
groups: ids.has('groups') ? plan.model.groups || [] : [],
|
|
194
|
+
anchorOffsets: ids.has('anchor_offsets')
|
|
195
|
+
? plan.model.anchorOffsets || []
|
|
196
|
+
: []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Builds canonical physical and virtual layer descriptors.
|
|
202
|
+
* @param {Record<string, any>} model Primitive model.
|
|
203
|
+
* @returns {object[]} Canonical layer descriptors.
|
|
204
|
+
*/
|
|
205
|
+
static #layers(model) {
|
|
206
|
+
const physical = (model.layers || []).map((layer) => ({
|
|
207
|
+
...layer,
|
|
208
|
+
id: `${layer.id}_copper`,
|
|
209
|
+
key: `${layer.id}_copper`,
|
|
210
|
+
sourceLayerId: layer.id,
|
|
211
|
+
type: 'copper'
|
|
212
|
+
}))
|
|
213
|
+
const reservedIds = new Set(physical.map((layer) => layer.id))
|
|
214
|
+
const byId = new Map()
|
|
215
|
+
for (const layer of model.virtualLayers || []) {
|
|
216
|
+
const id = String(layer.id || layer.key || '')
|
|
217
|
+
if (
|
|
218
|
+
!VIRTUAL_LAYER_IDS.has(id) ||
|
|
219
|
+
reservedIds.has(id) ||
|
|
220
|
+
byId.has(id)
|
|
221
|
+
) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
byId.set(id, {
|
|
225
|
+
...layer,
|
|
226
|
+
id,
|
|
227
|
+
key: id,
|
|
228
|
+
side: PcbRenderPlan.#layerSide(id) || layer.side,
|
|
229
|
+
sourceLayerId: id
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
for (const primitive of model.primitives || []) {
|
|
233
|
+
const id = PcbRenderPlan.#primitiveVirtualLayerId(primitive)
|
|
234
|
+
if (
|
|
235
|
+
!VIRTUAL_LAYER_IDS.has(id) ||
|
|
236
|
+
reservedIds.has(id) ||
|
|
237
|
+
byId.has(id)
|
|
238
|
+
) {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
byId.set(id, {
|
|
242
|
+
id,
|
|
243
|
+
key: id,
|
|
244
|
+
layer: id,
|
|
245
|
+
name: id,
|
|
246
|
+
side: PcbRenderPlan.#layerSide(id),
|
|
247
|
+
type: 'drawing',
|
|
248
|
+
sourceFormat: 'circuitjson',
|
|
249
|
+
sourceLayerId: id
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
const virtual = [
|
|
253
|
+
...VIRTUAL_LAYER_ORDER.filter((id) => byId.has(id)).map((id) =>
|
|
254
|
+
byId.get(id)
|
|
255
|
+
),
|
|
256
|
+
...[...byId]
|
|
257
|
+
.filter(([id]) => !VIRTUAL_LAYER_ORDER.includes(id))
|
|
258
|
+
.sort(([left], [right]) =>
|
|
259
|
+
left < right ? -1 : left > right ? 1 : 0
|
|
260
|
+
)
|
|
261
|
+
.map(([_id, layer]) => layer)
|
|
262
|
+
]
|
|
263
|
+
return [...physical, ...virtual]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolves a canonical virtual layer id from primitive kind and side.
|
|
268
|
+
* @param {object} primitive PCB primitive.
|
|
269
|
+
* @returns {string} Canonical virtual layer id or an empty string.
|
|
270
|
+
*/
|
|
271
|
+
static #primitiveVirtualLayerId(primitive) {
|
|
272
|
+
const kind = String(primitive.kind || '')
|
|
273
|
+
if (
|
|
274
|
+
['silkscreen', 'silkscreen_line', 'silkscreen_text'].includes(kind)
|
|
275
|
+
) {
|
|
276
|
+
return `${PcbRenderPlan.#primitiveSide(primitive)}_silkscreen`
|
|
277
|
+
}
|
|
278
|
+
if (['fabrication', 'note', 'dimension'].includes(kind)) {
|
|
279
|
+
return `${PcbRenderPlan.#primitiveSide(primitive)}_fabrication`
|
|
280
|
+
}
|
|
281
|
+
if (kind === 'courtyard') {
|
|
282
|
+
return `${PcbRenderPlan.#primitiveSide(primitive)}_courtyard`
|
|
283
|
+
}
|
|
284
|
+
if (kind === 'solder-mask') {
|
|
285
|
+
return `${PcbRenderPlan.#primitiveSide(primitive)}_soldermask`
|
|
286
|
+
}
|
|
287
|
+
if (kind === 'solder-paste') {
|
|
288
|
+
return `${PcbRenderPlan.#primitiveSide(primitive)}_paste`
|
|
289
|
+
}
|
|
290
|
+
if (kind === 'keepout') return 'keepouts'
|
|
291
|
+
if (kind === 'cutout') return 'cutouts'
|
|
292
|
+
if (kind === 'panel') return 'panel'
|
|
293
|
+
if (kind === 'breakout-point') return 'breakout_points'
|
|
294
|
+
if (kind === 'board') return ''
|
|
295
|
+
const layer = String(primitive.layer || '')
|
|
296
|
+
return /^(?:top|bottom|inner\d+)$/u.test(layer) ? '' : layer
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolves a primitive surface without changing the legacy primitive model.
|
|
301
|
+
* @param {object} primitive PCB primitive.
|
|
302
|
+
* @returns {'top' | 'bottom'} Canonical surface.
|
|
303
|
+
*/
|
|
304
|
+
static #primitiveSide(primitive) {
|
|
305
|
+
if (primitive.side === 'bottom' || primitive.side === 'top') {
|
|
306
|
+
return primitive.side
|
|
307
|
+
}
|
|
308
|
+
return PcbRenderPlan.#layerSide(primitive.layer) || 'top'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolves a side from physical or canonical virtual layer ids.
|
|
313
|
+
* @param {unknown} layer Layer candidate.
|
|
314
|
+
* @returns {'top' | 'bottom' | ''} Surface or empty string.
|
|
315
|
+
*/
|
|
316
|
+
static #layerSide(layer) {
|
|
317
|
+
const value = String(layer || '').toLowerCase()
|
|
318
|
+
if (value === 'top' || value.startsWith('top_')) return 'top'
|
|
319
|
+
if (value === 'bottom' || value.startsWith('bottom_')) return 'bottom'
|
|
320
|
+
return ''
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolves default layers compatible with one side.
|
|
325
|
+
* @param {object[]} layers Available layers.
|
|
326
|
+
* @param {'top' | 'bottom'} side Requested side.
|
|
327
|
+
* @returns {string[]} Default layer ids.
|
|
328
|
+
*/
|
|
329
|
+
static #defaultLayerIds(layers, side) {
|
|
330
|
+
return layers
|
|
331
|
+
.filter(
|
|
332
|
+
(layer) =>
|
|
333
|
+
!['top', 'bottom'].includes(layer.side) ||
|
|
334
|
+
layer.side === side
|
|
335
|
+
)
|
|
336
|
+
.map((layer) => layer.id)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Returns whether a drilled primitive spans any selected copper layer.
|
|
341
|
+
* @param {object} primitive Via-like primitive.
|
|
342
|
+
* @param {Set<string>} selectedLayers Selected physical layer ids.
|
|
343
|
+
* @param {object[]} physicalLayers Ordered board copper layers.
|
|
344
|
+
* @returns {boolean} Whether the via belongs in the layer view.
|
|
345
|
+
*/
|
|
346
|
+
static #viaTouchesLayers(primitive, selectedLayers, physicalLayers) {
|
|
347
|
+
if (!selectedLayers.size) return false
|
|
348
|
+
const source = primitive.sourceRoute || primitive.source || {}
|
|
349
|
+
const declared = Array.isArray(source.layers)
|
|
350
|
+
? source.layers
|
|
351
|
+
.map(CircuitJsonPcbPrimitiveFields.layer)
|
|
352
|
+
.filter(Boolean)
|
|
353
|
+
: []
|
|
354
|
+
const from = CircuitJsonPcbPrimitiveFields.layer(
|
|
355
|
+
declared.length
|
|
356
|
+
? declared[0]
|
|
357
|
+
: (source.from_layer ?? source.fromLayer)
|
|
358
|
+
)
|
|
359
|
+
const to = CircuitJsonPcbPrimitiveFields.layer(
|
|
360
|
+
declared.length
|
|
361
|
+
? declared.at(-1)
|
|
362
|
+
: (source.to_layer ?? source.toLayer)
|
|
363
|
+
)
|
|
364
|
+
if (!from && !to) return true
|
|
365
|
+
const ordered = physicalLayers.map((layer) => String(layer.id || ''))
|
|
366
|
+
const start = ordered.indexOf(from || to)
|
|
367
|
+
const end = ordered.indexOf(to || from)
|
|
368
|
+
if (start < 0 || end < 0) {
|
|
369
|
+
return [from, to, ...declared].some((layer) =>
|
|
370
|
+
selectedLayers.has(layer)
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
const minimum = Math.min(start, end)
|
|
374
|
+
const maximum = Math.max(start, end)
|
|
375
|
+
for (let index = minimum; index <= maximum; index += 1) {
|
|
376
|
+
if (selectedLayers.has(ordered[index])) return true
|
|
377
|
+
}
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Deep-freezes one cyclic primitive graph before it enters context cache.
|
|
383
|
+
* @param {any} value Value to freeze.
|
|
384
|
+
* @returns {any} Frozen value.
|
|
385
|
+
*/
|
|
386
|
+
static #freeze(value) {
|
|
387
|
+
if (!value || typeof value !== 'object') return value
|
|
388
|
+
const pending = [{ value, expanded: false }]
|
|
389
|
+
const visited = new WeakSet()
|
|
390
|
+
while (pending.length) {
|
|
391
|
+
const current = pending.pop()
|
|
392
|
+
if (current.expanded) {
|
|
393
|
+
Object.freeze(current.value)
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
if (visited.has(current.value) || Object.isFrozen(current.value)) {
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
visited.add(current.value)
|
|
400
|
+
pending.push({ value: current.value, expanded: true })
|
|
401
|
+
if (current.value instanceof Map) {
|
|
402
|
+
for (const [key, entry] of current.value) {
|
|
403
|
+
if (key && typeof key === 'object') {
|
|
404
|
+
pending.push({ value: key, expanded: false })
|
|
405
|
+
}
|
|
406
|
+
if (entry && typeof entry === 'object') {
|
|
407
|
+
pending.push({ value: entry, expanded: false })
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
for (const descriptor of Object.values(
|
|
413
|
+
Object.getOwnPropertyDescriptors(current.value)
|
|
414
|
+
)) {
|
|
415
|
+
if (
|
|
416
|
+
'value' in descriptor &&
|
|
417
|
+
descriptor.value &&
|
|
418
|
+
typeof descriptor.value === 'object'
|
|
419
|
+
) {
|
|
420
|
+
pending.push({
|
|
421
|
+
value: descriptor.value,
|
|
422
|
+
expanded: false
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return value
|
|
428
|
+
}
|
|
429
|
+
}
|