circuitjson-toolkit 1.0.17 → 1.1.1
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/NOTICE.md +31 -0
- package/README.md +274 -107
- package/docs/api.md +507 -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 +126 -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/release-notes-v1.1.1.md +24 -0
- package/docs/testing.md +122 -7
- package/package.json +32 -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 +999 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +650 -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,906 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from '../CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from '../CircuitJsonUnits.mjs'
|
|
3
|
+
import { ToolkitDiagnostic } from '../contracts/ToolkitDiagnostic.mjs'
|
|
4
|
+
import { ToolkitError } from '../contracts/ToolkitError.mjs'
|
|
5
|
+
import { CircuitJsonDocumentContext } from '../context/CircuitJsonDocumentContext.mjs'
|
|
6
|
+
import { freezeScene } from './Scene3dFreeze.mjs'
|
|
7
|
+
import { Scene3dAssetIndex } from './Scene3dAssetIndex.mjs'
|
|
8
|
+
import { Scene3dBoardModel } from './Scene3dBoardModel.mjs'
|
|
9
|
+
import { Scene3dDocumentMetadata } from './Scene3dDocumentMetadata.mjs'
|
|
10
|
+
import { Scene3dInputPreflight } from './Scene3dInputPreflight.mjs'
|
|
11
|
+
import { Scene3dIdRegistry } from './Scene3dIdRegistry.mjs'
|
|
12
|
+
import { Scene3dMaterials } from './Scene3dMaterials.mjs'
|
|
13
|
+
import { Scene3dModelReference } from './Scene3dModelReference.mjs'
|
|
14
|
+
import { Scene3dOptions } from './Scene3dOptions.mjs'
|
|
15
|
+
|
|
16
|
+
const CANONICAL_SCENE_OWNERS = new WeakMap()
|
|
17
|
+
const ZONE_TYPES = new Set([
|
|
18
|
+
'pcb_copper_pour',
|
|
19
|
+
'pcb_ground_plane',
|
|
20
|
+
'pcb_ground_plane_region'
|
|
21
|
+
])
|
|
22
|
+
const PAD_TYPES = new Set(['pcb_smtpad', 'pcb_plated_hole'])
|
|
23
|
+
const MAIN_TYPES = new Set([
|
|
24
|
+
'pcb_board',
|
|
25
|
+
'pcb_component',
|
|
26
|
+
'pcb_trace',
|
|
27
|
+
'pcb_via',
|
|
28
|
+
...PAD_TYPES,
|
|
29
|
+
...ZONE_TYPES
|
|
30
|
+
])
|
|
31
|
+
/**
|
|
32
|
+
* Builds canonical, data-only PCB scene descriptions from CircuitJSON.
|
|
33
|
+
*/
|
|
34
|
+
export class PcbScene3dBuilder {
|
|
35
|
+
/**
|
|
36
|
+
* Builds one deterministic right-handed Z-up scene in millimeters.
|
|
37
|
+
* @param {unknown} input Document result, CircuitJSON model, or context.
|
|
38
|
+
* @param {unknown} [options] Scene options.
|
|
39
|
+
* @returns {object} Canonical scene description.
|
|
40
|
+
*/
|
|
41
|
+
static build(input, options = {}) {
|
|
42
|
+
const normalized = Scene3dOptions.normalize(options)
|
|
43
|
+
Scene3dOptions.assertNotAborted(normalized.signal)
|
|
44
|
+
const assetsPreflighted = Scene3dInputPreflight.check(input, normalized)
|
|
45
|
+
const context = CircuitJsonDocumentContext.prepare(input, {
|
|
46
|
+
indexes: ['elements']
|
|
47
|
+
})
|
|
48
|
+
if (!assetsPreflighted) {
|
|
49
|
+
Scene3dInputPreflight.checkAssets(context.assets, normalized)
|
|
50
|
+
}
|
|
51
|
+
const base = PcbScene3dBuilder.#canonical(context, normalized)
|
|
52
|
+
if (normalized.fidelity === 'canonical') {
|
|
53
|
+
return PcbScene3dBuilder.#sceneWithPlan(
|
|
54
|
+
base,
|
|
55
|
+
PcbScene3dBuilder.#cadPlan(context, false, normalized),
|
|
56
|
+
false
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const native = PcbScene3dBuilder.#nativePlan(context, normalized)
|
|
61
|
+
if (native) {
|
|
62
|
+
return PcbScene3dBuilder.#sceneWithPlan(base, native, true)
|
|
63
|
+
}
|
|
64
|
+
return PcbScene3dBuilder.#sceneWithPlan(
|
|
65
|
+
base,
|
|
66
|
+
PcbScene3dBuilder.#cadPlan(context, false, normalized),
|
|
67
|
+
false
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Adds CAD assets and placements to one cached geometry foundation.
|
|
73
|
+
* @param {object} base Cached canonical scene geometry.
|
|
74
|
+
* @param {{ assets: object[], externalPlacements: object[] }} plan CAD plan.
|
|
75
|
+
* @param {boolean} nativeFidelity Whether native fidelity was selected.
|
|
76
|
+
* @returns {object} Materialized scene.
|
|
77
|
+
*/
|
|
78
|
+
static #sceneWithPlan(base, plan, nativeFidelity) {
|
|
79
|
+
if (
|
|
80
|
+
!nativeFidelity &&
|
|
81
|
+
!plan.assets.length &&
|
|
82
|
+
!plan.externalPlacements.length
|
|
83
|
+
) {
|
|
84
|
+
return base
|
|
85
|
+
}
|
|
86
|
+
return freezeScene({
|
|
87
|
+
...base,
|
|
88
|
+
assets: plan.assets,
|
|
89
|
+
externalPlacements: plan.externalPlacements,
|
|
90
|
+
statistics: {
|
|
91
|
+
...base.statistics,
|
|
92
|
+
assetCount: plan.assets.length,
|
|
93
|
+
externalPlacementCount: plan.externalPlacements.length,
|
|
94
|
+
nativeFidelity: nativeFidelity ? 1 : 0
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns one context-cached canonical scene foundation.
|
|
101
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
102
|
+
* @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
|
|
103
|
+
* @returns {object} Frozen canonical scene.
|
|
104
|
+
*/
|
|
105
|
+
static #canonical(context, options) {
|
|
106
|
+
const firstBoard = PcbScene3dBuilder.#all(
|
|
107
|
+
context.getIndex('elements').elementsByType || new Map(),
|
|
108
|
+
'pcb_board'
|
|
109
|
+
)[0]
|
|
110
|
+
const sourceThickness = CircuitJsonUnits.optionalLength(
|
|
111
|
+
firstBoard?.thickness
|
|
112
|
+
)
|
|
113
|
+
const cacheThickness =
|
|
114
|
+
!options.boardThicknessProvided &&
|
|
115
|
+
sourceThickness !== null &&
|
|
116
|
+
sourceThickness > 0 &&
|
|
117
|
+
sourceThickness <= 1000
|
|
118
|
+
? sourceThickness
|
|
119
|
+
: options.boardThickness
|
|
120
|
+
const key = options.boardThicknessProvided
|
|
121
|
+
? `canonical:override:${options.boardThickness}`
|
|
122
|
+
: `canonical:${cacheThickness}`
|
|
123
|
+
const scene = context.getOrCreateDerived('scene3d', key, () => {
|
|
124
|
+
const built = PcbScene3dBuilder.#buildCanonical(context, options)
|
|
125
|
+
CANONICAL_SCENE_OWNERS.set(built, { context, key })
|
|
126
|
+
return built
|
|
127
|
+
})
|
|
128
|
+
const owner = CANONICAL_SCENE_OWNERS.get(scene)
|
|
129
|
+
if (owner?.context !== context || owner?.key !== key) {
|
|
130
|
+
throw new ToolkitError(
|
|
131
|
+
'CircuitJSON scene cache contains an unowned value.',
|
|
132
|
+
{
|
|
133
|
+
code: 'ERR_CONTEXT_CACHE_COLLISION',
|
|
134
|
+
category: 'runtime',
|
|
135
|
+
details: { namespace: 'scene3d', key }
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
return scene
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Builds the canonical scene once for one context and thickness.
|
|
144
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
145
|
+
* @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
|
|
146
|
+
* @returns {object} Frozen scene.
|
|
147
|
+
*/
|
|
148
|
+
static #buildCanonical(context, options) {
|
|
149
|
+
const index = context.getIndex('elements')
|
|
150
|
+
const byType = index.elementsByType || new Map()
|
|
151
|
+
const boards = PcbScene3dBuilder.#all(byType, 'pcb_board')
|
|
152
|
+
const cutouts = PcbScene3dBuilder.#all(byType, 'pcb_cutout')
|
|
153
|
+
const boardModel = new Scene3dBoardModel(
|
|
154
|
+
boards,
|
|
155
|
+
cutouts,
|
|
156
|
+
options.boardThickness,
|
|
157
|
+
options.boardThicknessProvided
|
|
158
|
+
)
|
|
159
|
+
Scene3dIdRegistry.assertUnique(
|
|
160
|
+
'board.outlines',
|
|
161
|
+
boardModel.board.outlines
|
|
162
|
+
)
|
|
163
|
+
const components = PcbScene3dBuilder.#all(byType, 'pcb_component').map(
|
|
164
|
+
(element) => PcbScene3dBuilder.#component(element, boardModel)
|
|
165
|
+
)
|
|
166
|
+
const pads = []
|
|
167
|
+
for (const type of PAD_TYPES) {
|
|
168
|
+
for (const element of PcbScene3dBuilder.#all(byType, type)) {
|
|
169
|
+
pads.push(PcbScene3dBuilder.#pad(element, boardModel))
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const tracks = PcbScene3dBuilder.#all(byType, 'pcb_trace').map(
|
|
173
|
+
(element) => PcbScene3dBuilder.#track(element, boardModel)
|
|
174
|
+
)
|
|
175
|
+
const vias = PcbScene3dBuilder.#all(byType, 'pcb_via').map((element) =>
|
|
176
|
+
PcbScene3dBuilder.#via(element)
|
|
177
|
+
)
|
|
178
|
+
const zones = []
|
|
179
|
+
for (const type of ZONE_TYPES) {
|
|
180
|
+
for (const element of PcbScene3dBuilder.#all(byType, type)) {
|
|
181
|
+
zones.push(PcbScene3dBuilder.#zone(element, boardModel))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const texts = []
|
|
185
|
+
const objects = []
|
|
186
|
+
for (const element of context.model) {
|
|
187
|
+
const type = String(element.type || '')
|
|
188
|
+
if (PcbScene3dBuilder.#isTextType(type)) {
|
|
189
|
+
texts.push(PcbScene3dBuilder.#text(element, boardModel))
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (PcbScene3dBuilder.#isObjectType(type)) {
|
|
193
|
+
objects.push(PcbScene3dBuilder.#object(element, boardModel))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const diagnostics = []
|
|
197
|
+
const materials = Scene3dMaterials.build(boards)
|
|
198
|
+
for (const [collection, rows] of Object.entries({
|
|
199
|
+
components,
|
|
200
|
+
pads,
|
|
201
|
+
tracks,
|
|
202
|
+
vias,
|
|
203
|
+
zones,
|
|
204
|
+
texts,
|
|
205
|
+
objects
|
|
206
|
+
})) {
|
|
207
|
+
Scene3dIdRegistry.assertUnique(collection, rows)
|
|
208
|
+
}
|
|
209
|
+
if (!boards.length) {
|
|
210
|
+
diagnostics.push(
|
|
211
|
+
ToolkitDiagnostic.create({
|
|
212
|
+
code: 'SCENE_BOARD_MISSING',
|
|
213
|
+
severity: 'warning',
|
|
214
|
+
message: 'The CircuitJSON model contains no PCB board.',
|
|
215
|
+
source: Scene3dDocumentMetadata.sourceFileName(context),
|
|
216
|
+
details: {}
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return freezeScene({
|
|
222
|
+
schema: 'ecad-toolkit.scene3d.v1',
|
|
223
|
+
units: 'mm',
|
|
224
|
+
coordinateSystem: 'right-handed-z-up',
|
|
225
|
+
board: boardModel.board,
|
|
226
|
+
components,
|
|
227
|
+
pads,
|
|
228
|
+
tracks,
|
|
229
|
+
vias,
|
|
230
|
+
zones,
|
|
231
|
+
texts,
|
|
232
|
+
objects,
|
|
233
|
+
materials,
|
|
234
|
+
assets: [],
|
|
235
|
+
externalPlacements: [],
|
|
236
|
+
diagnostics,
|
|
237
|
+
statistics: {
|
|
238
|
+
elementCount: context.model.length,
|
|
239
|
+
boardCount: boards.length,
|
|
240
|
+
componentCount: components.length,
|
|
241
|
+
padCount: pads.length,
|
|
242
|
+
trackCount: tracks.length,
|
|
243
|
+
viaCount: vias.length,
|
|
244
|
+
zoneCount: zones.length,
|
|
245
|
+
textCount: texts.length,
|
|
246
|
+
objectCount: objects.length,
|
|
247
|
+
materialCount: materials.length,
|
|
248
|
+
assetCount: 0,
|
|
249
|
+
externalPlacementCount: 0,
|
|
250
|
+
nativeFidelity: 0
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Builds source-native CAD placement and asset records when fully usable.
|
|
257
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
258
|
+
* @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
|
|
259
|
+
* @returns {{ assets: object[], externalPlacements: object[] } | null} Native plan or canonical fallback.
|
|
260
|
+
*/
|
|
261
|
+
static #nativePlan(context, options) {
|
|
262
|
+
const format = Scene3dDocumentMetadata.sourceFormat(context)
|
|
263
|
+
const extension = context.extensions?.[format]
|
|
264
|
+
if (!Scene3dDocumentMetadata.hasNativeExtension(format, extension)) {
|
|
265
|
+
if (options.fidelity === 'auto') return null
|
|
266
|
+
throw new ToolkitError(
|
|
267
|
+
`Native scene extension data is required for ${format}.`,
|
|
268
|
+
{
|
|
269
|
+
code: 'ERR_EXTENSION_DATA_REQUIRED',
|
|
270
|
+
category: 'unsupported',
|
|
271
|
+
format,
|
|
272
|
+
source: Scene3dDocumentMetadata.sourceFileName(context),
|
|
273
|
+
details: { format, feature: 'scene3d' }
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.fidelity === 'auto') {
|
|
279
|
+
const cadComponents = PcbScene3dBuilder.#all(
|
|
280
|
+
context.getIndex('elements').elementsByType || new Map(),
|
|
281
|
+
'cad_component'
|
|
282
|
+
)
|
|
283
|
+
for (const component of cadComponents) {
|
|
284
|
+
const reference =
|
|
285
|
+
Scene3dModelReference.fromCadComponent(component)
|
|
286
|
+
if (!reference?.requiresAsset) return null
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const plan = PcbScene3dBuilder.#cadPlan(context, true, options)
|
|
291
|
+
if (!plan.externalPlacements.length) {
|
|
292
|
+
if (options.fidelity === 'auto') return null
|
|
293
|
+
throw new ToolkitError(
|
|
294
|
+
`Native scene placement data is required for ${format}.`,
|
|
295
|
+
{
|
|
296
|
+
code: 'ERR_EXTENSION_DATA_REQUIRED',
|
|
297
|
+
category: 'unsupported',
|
|
298
|
+
format,
|
|
299
|
+
source: Scene3dDocumentMetadata.sourceFileName(context),
|
|
300
|
+
details: { format, feature: 'scene3d-placements' }
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
const unresolved = plan.assets.find((asset) => asset.data === null)
|
|
305
|
+
if (unresolved && !options.resolveAsset) {
|
|
306
|
+
if (options.fidelity === 'auto') return null
|
|
307
|
+
throw new ToolkitError(
|
|
308
|
+
`Native scene asset data is required: ${unresolved.id}.`,
|
|
309
|
+
{
|
|
310
|
+
code: 'ERR_ASSET_DATA_REQUIRED',
|
|
311
|
+
category: 'unsupported',
|
|
312
|
+
format,
|
|
313
|
+
source: Scene3dDocumentMetadata.sourceFileName(context),
|
|
314
|
+
details: { assetId: unresolved.id }
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
return plan
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Builds canonical CAD placement records and their asset requests.
|
|
323
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
324
|
+
* @param {boolean} requireReferences Whether every CAD row needs a model.
|
|
325
|
+
* @param {ReturnType<Scene3dOptions['normalize']>} options Scene options.
|
|
326
|
+
* @returns {{ assets: object[], externalPlacements: object[] }} CAD plan.
|
|
327
|
+
*/
|
|
328
|
+
static #cadPlan(context, requireReferences, options) {
|
|
329
|
+
const cadComponents = PcbScene3dBuilder.#all(
|
|
330
|
+
context.getIndex('elements').elementsByType || new Map(),
|
|
331
|
+
'cad_component'
|
|
332
|
+
)
|
|
333
|
+
if (cadComponents.length > Scene3dOptions.maxAssetCount) {
|
|
334
|
+
throw new ToolkitError(
|
|
335
|
+
'Scene asset count exceeds the safe limit.',
|
|
336
|
+
{
|
|
337
|
+
code: 'ERR_ASSET_LIMIT',
|
|
338
|
+
category: 'unsupported',
|
|
339
|
+
details: {
|
|
340
|
+
count: cadComponents.length,
|
|
341
|
+
maximum: Scene3dOptions.maxAssetCount
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
if (!cadComponents.length) {
|
|
347
|
+
return { assets: [], externalPlacements: [] }
|
|
348
|
+
}
|
|
349
|
+
const assetIndex = new Scene3dAssetIndex(context.assets, options)
|
|
350
|
+
const externalPlacements = []
|
|
351
|
+
const format = Scene3dDocumentMetadata.sourceFormat(context)
|
|
352
|
+
|
|
353
|
+
for (const cadComponent of cadComponents) {
|
|
354
|
+
const reference =
|
|
355
|
+
Scene3dModelReference.fromCadComponent(cadComponent)
|
|
356
|
+
if (!reference || (requireReferences && !reference.requiresAsset)) {
|
|
357
|
+
if (requireReferences) {
|
|
358
|
+
throw new ToolkitError(
|
|
359
|
+
`CAD component has no model asset reference: ${PcbScene3dBuilder.#id(cadComponent)}.`,
|
|
360
|
+
{
|
|
361
|
+
code: 'ERR_ASSET_DATA_REQUIRED',
|
|
362
|
+
category: 'unsupported',
|
|
363
|
+
format,
|
|
364
|
+
source: Scene3dDocumentMetadata.sourceFileName(
|
|
365
|
+
context
|
|
366
|
+
),
|
|
367
|
+
details: {
|
|
368
|
+
cadComponentId:
|
|
369
|
+
PcbScene3dBuilder.#id(cadComponent)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
const asset = reference.requiresAsset
|
|
377
|
+
? assetIndex.resolve(reference)
|
|
378
|
+
: null
|
|
379
|
+
externalPlacements.push(
|
|
380
|
+
PcbScene3dBuilder.#externalPlacement(
|
|
381
|
+
cadComponent,
|
|
382
|
+
reference,
|
|
383
|
+
asset,
|
|
384
|
+
format,
|
|
385
|
+
context
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
Scene3dIdRegistry.assertUnique('externalPlacements', externalPlacements)
|
|
391
|
+
return { assets: assetIndex.assets, externalPlacements }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Builds one canonical external placement.
|
|
396
|
+
* @param {object} cadComponent CAD component.
|
|
397
|
+
* @param {{ name: string, format: string, inlineModel?: unknown, generator?: string }} reference Model reference.
|
|
398
|
+
* @param {object | null} asset Canonical asset.
|
|
399
|
+
* @param {string} format Source format.
|
|
400
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
401
|
+
* @returns {object} External placement record.
|
|
402
|
+
*/
|
|
403
|
+
static #externalPlacement(cadComponent, reference, asset, format, context) {
|
|
404
|
+
const componentId = String(cadComponent.pcb_component_id || '')
|
|
405
|
+
const linked = context
|
|
406
|
+
.getIndex('elements')
|
|
407
|
+
.pcbComponentById?.get(componentId)
|
|
408
|
+
const side = PcbScene3dBuilder.#side(
|
|
409
|
+
cadComponent.layer || linked?.layer
|
|
410
|
+
)
|
|
411
|
+
return {
|
|
412
|
+
id: PcbScene3dBuilder.#id(cadComponent),
|
|
413
|
+
componentId,
|
|
414
|
+
sourceComponentId: String(cadComponent.source_component_id || ''),
|
|
415
|
+
side,
|
|
416
|
+
position: PcbScene3dBuilder.#point3(cadComponent.position),
|
|
417
|
+
rotation: PcbScene3dBuilder.#point3(cadComponent.rotation),
|
|
418
|
+
size: PcbScene3dBuilder.#point3(cadComponent.size),
|
|
419
|
+
scale: PcbScene3dBuilder.#positiveNumber(
|
|
420
|
+
cadComponent.model_unit_to_mm_scale_factor,
|
|
421
|
+
1
|
|
422
|
+
),
|
|
423
|
+
model: {
|
|
424
|
+
assetId: asset?.id || null,
|
|
425
|
+
format: reference.format,
|
|
426
|
+
name: reference.name,
|
|
427
|
+
inlineModel: reference.inlineModel ?? null,
|
|
428
|
+
generator: reference.generator || '',
|
|
429
|
+
boardNormalDirection: String(
|
|
430
|
+
cadComponent.model_board_normal_direction || 'z+'
|
|
431
|
+
),
|
|
432
|
+
originPosition: PcbScene3dBuilder.#point3(
|
|
433
|
+
cadComponent.model_origin_position
|
|
434
|
+
),
|
|
435
|
+
originAlignment: String(
|
|
436
|
+
cadComponent.model_origin_alignment || 'unknown'
|
|
437
|
+
),
|
|
438
|
+
objectFit: String(
|
|
439
|
+
cadComponent.model_object_fit || 'contain_within_bounds'
|
|
440
|
+
)
|
|
441
|
+
},
|
|
442
|
+
translucent: cadComponent.show_as_translucent_model === true,
|
|
443
|
+
boundingBox: cadComponent.show_as_bounding_box === true,
|
|
444
|
+
extensionRef: {
|
|
445
|
+
format,
|
|
446
|
+
cadComponentId: PcbScene3dBuilder.#id(cadComponent)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Maps one PCB component to a canonical placement.
|
|
453
|
+
* @param {object} element PCB component.
|
|
454
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
455
|
+
* @returns {object} Scene component.
|
|
456
|
+
*/
|
|
457
|
+
static #component(element, boardModel) {
|
|
458
|
+
const side = PcbScene3dBuilder.#side(element.layer)
|
|
459
|
+
const center = PcbScene3dBuilder.#point2(element.center)
|
|
460
|
+
return {
|
|
461
|
+
id: PcbScene3dBuilder.#id(element),
|
|
462
|
+
sourceComponentId: String(element.source_component_id || ''),
|
|
463
|
+
side,
|
|
464
|
+
position: {
|
|
465
|
+
...center,
|
|
466
|
+
z: boardModel.surfaceZ(side, center)
|
|
467
|
+
},
|
|
468
|
+
rotation: {
|
|
469
|
+
x: 0,
|
|
470
|
+
y: 0,
|
|
471
|
+
z: CircuitJsonUnits.angle(element.rotation, 0)
|
|
472
|
+
},
|
|
473
|
+
size: {
|
|
474
|
+
x: CircuitJsonUnits.length(element.width, 0),
|
|
475
|
+
y: CircuitJsonUnits.length(element.height, 0),
|
|
476
|
+
z: CircuitJsonUnits.length(
|
|
477
|
+
element.height_3d || element.depth,
|
|
478
|
+
0
|
|
479
|
+
)
|
|
480
|
+
},
|
|
481
|
+
materialId: 'component-body'
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Maps one SMT or plated pad.
|
|
487
|
+
* @param {object} element Pad element.
|
|
488
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
489
|
+
* @returns {object} Scene pad.
|
|
490
|
+
*/
|
|
491
|
+
static #pad(element, boardModel) {
|
|
492
|
+
const side = PcbScene3dBuilder.#side(element.layer)
|
|
493
|
+
const position = PcbScene3dBuilder.#elementPoint(element)
|
|
494
|
+
const diameter = CircuitJsonUnits.length(
|
|
495
|
+
element.outer_diameter || element.diameter,
|
|
496
|
+
0
|
|
497
|
+
)
|
|
498
|
+
return {
|
|
499
|
+
id: PcbScene3dBuilder.#id(element),
|
|
500
|
+
componentId: String(element.pcb_component_id || ''),
|
|
501
|
+
side,
|
|
502
|
+
layerId: String(element.layer || ''),
|
|
503
|
+
position: {
|
|
504
|
+
...position,
|
|
505
|
+
z: boardModel.surfaceZ(side, position)
|
|
506
|
+
},
|
|
507
|
+
rotation: {
|
|
508
|
+
x: 0,
|
|
509
|
+
y: 0,
|
|
510
|
+
z: CircuitJsonUnits.angle(
|
|
511
|
+
element.ccw_rotation || element.rotation,
|
|
512
|
+
0
|
|
513
|
+
)
|
|
514
|
+
},
|
|
515
|
+
shape: String(element.shape || element.hole_shape || 'circle'),
|
|
516
|
+
size: {
|
|
517
|
+
x: CircuitJsonUnits.length(element.width, diameter),
|
|
518
|
+
y: CircuitJsonUnits.length(element.height, diameter)
|
|
519
|
+
},
|
|
520
|
+
holeDiameter: CircuitJsonUnits.length(element.hole_diameter, 0),
|
|
521
|
+
points: PcbScene3dBuilder.#points(element.points),
|
|
522
|
+
netId: String(element.net_id || element.net || ''),
|
|
523
|
+
materialId: 'copper'
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Maps one routed trace with per-route-point surface coordinates.
|
|
529
|
+
* @param {object} element PCB trace.
|
|
530
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
531
|
+
* @returns {object} Scene track.
|
|
532
|
+
*/
|
|
533
|
+
static #track(element, boardModel) {
|
|
534
|
+
const route = []
|
|
535
|
+
for (const point of Array.isArray(element.route) ? element.route : []) {
|
|
536
|
+
const side = PcbScene3dBuilder.#side(point.layer || element.layer)
|
|
537
|
+
const position = PcbScene3dBuilder.#elementPoint(point)
|
|
538
|
+
route.push({
|
|
539
|
+
kind: String(point.route_type || point.type || 'wire'),
|
|
540
|
+
position: {
|
|
541
|
+
...position,
|
|
542
|
+
z: boardModel.surfaceZ(side, position)
|
|
543
|
+
},
|
|
544
|
+
width: CircuitJsonUnits.length(point.width || element.width, 0),
|
|
545
|
+
layerId: String(point.layer || element.layer || ''),
|
|
546
|
+
viaId: String(point.pcb_via_id || '')
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
id: PcbScene3dBuilder.#id(element),
|
|
551
|
+
netId: String(element.net_id || element.net || ''),
|
|
552
|
+
route,
|
|
553
|
+
materialId: 'copper'
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Maps one PCB via.
|
|
559
|
+
* @param {object} element Via element.
|
|
560
|
+
* @returns {object} Scene via.
|
|
561
|
+
*/
|
|
562
|
+
static #via(element) {
|
|
563
|
+
const layers = Array.isArray(element.layers)
|
|
564
|
+
? element.layers.map(String)
|
|
565
|
+
: [element.from_layer, element.to_layer].map(String).filter(Boolean)
|
|
566
|
+
return {
|
|
567
|
+
id: PcbScene3dBuilder.#id(element),
|
|
568
|
+
position: { ...PcbScene3dBuilder.#elementPoint(element), z: 0 },
|
|
569
|
+
diameter: CircuitJsonUnits.length(
|
|
570
|
+
element.outer_diameter || element.diameter,
|
|
571
|
+
0
|
|
572
|
+
),
|
|
573
|
+
holeDiameter: CircuitJsonUnits.length(element.hole_diameter, 0),
|
|
574
|
+
layerIds: layers,
|
|
575
|
+
netId: String(element.net_id || element.net || ''),
|
|
576
|
+
materialId: 'copper'
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Maps one copper zone.
|
|
582
|
+
* @param {object} element Zone element.
|
|
583
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
584
|
+
* @returns {object} Scene zone.
|
|
585
|
+
*/
|
|
586
|
+
static #zone(element, boardModel) {
|
|
587
|
+
const side = PcbScene3dBuilder.#side(element.layer)
|
|
588
|
+
const position = PcbScene3dBuilder.#elementPoint(element)
|
|
589
|
+
return {
|
|
590
|
+
id: PcbScene3dBuilder.#id(element),
|
|
591
|
+
side,
|
|
592
|
+
layerId: String(element.layer || ''),
|
|
593
|
+
position: {
|
|
594
|
+
...position,
|
|
595
|
+
z: boardModel.surfaceZ(side, position)
|
|
596
|
+
},
|
|
597
|
+
shape: String(element.shape || 'polygon'),
|
|
598
|
+
points: PcbScene3dBuilder.#points(element.points),
|
|
599
|
+
rings: PcbScene3dBuilder.#rings(element.rings),
|
|
600
|
+
netId: String(element.net_id || element.net || ''),
|
|
601
|
+
materialId: 'copper'
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Maps one PCB text element.
|
|
607
|
+
* @param {object} element Text element.
|
|
608
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
609
|
+
* @returns {object} Scene text.
|
|
610
|
+
*/
|
|
611
|
+
static #text(element, boardModel) {
|
|
612
|
+
const side = PcbScene3dBuilder.#side(element.layer)
|
|
613
|
+
const position = PcbScene3dBuilder.#elementPoint(element)
|
|
614
|
+
return {
|
|
615
|
+
id: PcbScene3dBuilder.#id(element),
|
|
616
|
+
kind: String(element.type),
|
|
617
|
+
side,
|
|
618
|
+
layerId: String(element.layer || ''),
|
|
619
|
+
position: {
|
|
620
|
+
...position,
|
|
621
|
+
z: boardModel.surfaceZ(side, position)
|
|
622
|
+
},
|
|
623
|
+
rotation: {
|
|
624
|
+
x: 0,
|
|
625
|
+
y: 0,
|
|
626
|
+
z: CircuitJsonUnits.angle(element.rotation, 0)
|
|
627
|
+
},
|
|
628
|
+
value: String(element.text || element.value || ''),
|
|
629
|
+
size: CircuitJsonUnits.length(
|
|
630
|
+
element.font_size || element.fontSize,
|
|
631
|
+
0
|
|
632
|
+
),
|
|
633
|
+
materialId: String(element.type).includes('silkscreen')
|
|
634
|
+
? 'silkscreen'
|
|
635
|
+
: 'copper'
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Maps an additional PCB geometry object.
|
|
641
|
+
* @param {object} element PCB element.
|
|
642
|
+
* @param {Scene3dBoardModel} boardModel Board geometry model.
|
|
643
|
+
* @returns {object} Generic scene object.
|
|
644
|
+
*/
|
|
645
|
+
static #object(element, boardModel) {
|
|
646
|
+
const side = PcbScene3dBuilder.#side(element.layer)
|
|
647
|
+
const position = PcbScene3dBuilder.#elementPoint(element)
|
|
648
|
+
return {
|
|
649
|
+
id: PcbScene3dBuilder.#id(element),
|
|
650
|
+
kind: String(element.type),
|
|
651
|
+
side,
|
|
652
|
+
layerId: String(element.layer || ''),
|
|
653
|
+
position: {
|
|
654
|
+
...position,
|
|
655
|
+
z: String(element.layer || '').includes('board')
|
|
656
|
+
? 0
|
|
657
|
+
: boardModel.surfaceZ(side, position)
|
|
658
|
+
},
|
|
659
|
+
rotation: {
|
|
660
|
+
x: 0,
|
|
661
|
+
y: 0,
|
|
662
|
+
z: CircuitJsonUnits.angle(
|
|
663
|
+
element.rotation || element.ccw_rotation,
|
|
664
|
+
0
|
|
665
|
+
)
|
|
666
|
+
},
|
|
667
|
+
size: {
|
|
668
|
+
x: CircuitJsonUnits.length(
|
|
669
|
+
element.width || element.diameter,
|
|
670
|
+
0
|
|
671
|
+
),
|
|
672
|
+
y: CircuitJsonUnits.length(
|
|
673
|
+
element.height || element.diameter,
|
|
674
|
+
0
|
|
675
|
+
),
|
|
676
|
+
z: 0
|
|
677
|
+
},
|
|
678
|
+
points: PcbScene3dBuilder.#points(element.points || element.route),
|
|
679
|
+
materialId: String(element.type).includes('silkscreen')
|
|
680
|
+
? 'silkscreen'
|
|
681
|
+
: 'board-core'
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Returns true for PCB text element types.
|
|
687
|
+
* @param {string} type Element type.
|
|
688
|
+
* @returns {boolean} Whether the type is text.
|
|
689
|
+
*/
|
|
690
|
+
static #isTextType(type) {
|
|
691
|
+
return (
|
|
692
|
+
type === 'pcb_text' ||
|
|
693
|
+
(type.startsWith('pcb_') && type.endsWith('_text'))
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Returns true for additional non-diagnostic PCB geometry.
|
|
699
|
+
* @param {string} type Element type.
|
|
700
|
+
* @returns {boolean} Whether the element belongs in objects.
|
|
701
|
+
*/
|
|
702
|
+
static #isObjectType(type) {
|
|
703
|
+
return (
|
|
704
|
+
type.startsWith('pcb_') &&
|
|
705
|
+
!MAIN_TYPES.has(type) &&
|
|
706
|
+
!PcbScene3dBuilder.#isTextType(type) &&
|
|
707
|
+
!/(?:error|warning)$/u.test(type)
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Returns an element list for one indexed type.
|
|
713
|
+
* @param {Map<string, object[]>} byType Element type map.
|
|
714
|
+
* @param {string} type Element type.
|
|
715
|
+
* @returns {object[]} Elements.
|
|
716
|
+
*/
|
|
717
|
+
static #all(byType, type) {
|
|
718
|
+
const values = byType.get(type)
|
|
719
|
+
return Array.isArray(values) ? values : []
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Resolves the standard element id.
|
|
724
|
+
* @param {object} element CircuitJSON element.
|
|
725
|
+
* @returns {string} Element id.
|
|
726
|
+
*/
|
|
727
|
+
static #id(element) {
|
|
728
|
+
return CircuitJsonIndexer.getElementId(element)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Resolves a canonical element position.
|
|
733
|
+
* @param {object} element CircuitJSON element or route point.
|
|
734
|
+
* @returns {{ x: number, y: number }} Position.
|
|
735
|
+
*/
|
|
736
|
+
static #elementPoint(element) {
|
|
737
|
+
if (element.center && typeof element.center === 'object') {
|
|
738
|
+
return PcbScene3dBuilder.#point2(element.center)
|
|
739
|
+
}
|
|
740
|
+
if (element.position && typeof element.position === 'object') {
|
|
741
|
+
return PcbScene3dBuilder.#point2(element.position)
|
|
742
|
+
}
|
|
743
|
+
if (element.x !== undefined || element.y !== undefined) {
|
|
744
|
+
return PcbScene3dBuilder.#point2({
|
|
745
|
+
x: element.x,
|
|
746
|
+
y: element.y
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
const points = PcbScene3dBuilder.#points(element.points)
|
|
750
|
+
if (points.length) return PcbScene3dBuilder.#representativePoint(points)
|
|
751
|
+
const rings = PcbScene3dBuilder.#rings(element.rings)
|
|
752
|
+
if (rings[0]?.length) {
|
|
753
|
+
return PcbScene3dBuilder.#representativePoint(rings[0])
|
|
754
|
+
}
|
|
755
|
+
return { x: 0, y: 0 }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Returns the arithmetic center of one non-empty geometry point set.
|
|
760
|
+
* @param {{ x: number, y: number }[]} points Canonical points.
|
|
761
|
+
* @returns {{ x: number, y: number }} Representative point.
|
|
762
|
+
*/
|
|
763
|
+
static #representativePoint(points) {
|
|
764
|
+
let x = 0
|
|
765
|
+
let y = 0
|
|
766
|
+
for (const point of points) {
|
|
767
|
+
x += point.x
|
|
768
|
+
y += point.y
|
|
769
|
+
}
|
|
770
|
+
const center = { x: x / points.length, y: y / points.length }
|
|
771
|
+
if (PcbScene3dBuilder.#polygonContains(center, points)) return center
|
|
772
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
773
|
+
const start = points[index]
|
|
774
|
+
const end = points[(index + 1) % points.length]
|
|
775
|
+
if (start.x === end.x && start.y === end.y) continue
|
|
776
|
+
return {
|
|
777
|
+
x: (start.x + end.x) / 2,
|
|
778
|
+
y: (start.y + end.y) / 2
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return { ...points[0] }
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Tests whether a point lies inside or on one polygon.
|
|
786
|
+
* @param {{ x: number, y: number }} point Candidate point.
|
|
787
|
+
* @param {{ x: number, y: number }[]} points Polygon points.
|
|
788
|
+
* @returns {boolean} Whether the polygon contains the point.
|
|
789
|
+
*/
|
|
790
|
+
static #polygonContains(point, points) {
|
|
791
|
+
let inside = false
|
|
792
|
+
for (
|
|
793
|
+
let current = 0, previous = points.length - 1;
|
|
794
|
+
current < points.length;
|
|
795
|
+
previous = current, current += 1
|
|
796
|
+
) {
|
|
797
|
+
const a = points[current]
|
|
798
|
+
const b = points[previous]
|
|
799
|
+
if (PcbScene3dBuilder.#pointOnSegment(point, a, b)) return true
|
|
800
|
+
const crosses =
|
|
801
|
+
a.y > point.y !== b.y > point.y &&
|
|
802
|
+
point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x
|
|
803
|
+
if (crosses) inside = !inside
|
|
804
|
+
}
|
|
805
|
+
return inside
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Tests whether a point lies on a closed line segment.
|
|
810
|
+
* @param {{ x: number, y: number }} point Candidate point.
|
|
811
|
+
* @param {{ x: number, y: number }} start Segment start.
|
|
812
|
+
* @param {{ x: number, y: number }} end Segment end.
|
|
813
|
+
* @returns {boolean} Whether the point lies on the segment.
|
|
814
|
+
*/
|
|
815
|
+
static #pointOnSegment(point, start, end) {
|
|
816
|
+
const cross =
|
|
817
|
+
(point.y - start.y) * (end.x - start.x) -
|
|
818
|
+
(point.x - start.x) * (end.y - start.y)
|
|
819
|
+
if (Math.abs(cross) > 1e-9) return false
|
|
820
|
+
return (
|
|
821
|
+
point.x >= Math.min(start.x, end.x) &&
|
|
822
|
+
point.x <= Math.max(start.x, end.x) &&
|
|
823
|
+
point.y >= Math.min(start.y, end.y) &&
|
|
824
|
+
point.y <= Math.max(start.y, end.y)
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Normalizes a two-dimensional point.
|
|
830
|
+
* @param {unknown} point Point candidate.
|
|
831
|
+
* @returns {{ x: number, y: number }} Point.
|
|
832
|
+
*/
|
|
833
|
+
static #point2(point) {
|
|
834
|
+
return {
|
|
835
|
+
x: CircuitJsonUnits.length(point?.x, 0),
|
|
836
|
+
y: CircuitJsonUnits.length(point?.y, 0)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Normalizes a three-dimensional point.
|
|
842
|
+
* @param {unknown} point Point candidate.
|
|
843
|
+
* @returns {{ x: number, y: number, z: number }} Point.
|
|
844
|
+
*/
|
|
845
|
+
static #point3(point) {
|
|
846
|
+
return {
|
|
847
|
+
x: CircuitJsonUnits.length(point?.x, 0),
|
|
848
|
+
y: CircuitJsonUnits.length(point?.y, 0),
|
|
849
|
+
z: CircuitJsonUnits.length(point?.z, 0)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Maps a bounded point array without variadic spreads.
|
|
855
|
+
* @param {unknown} points Point candidates.
|
|
856
|
+
* @returns {{ x: number, y: number }[]} Points.
|
|
857
|
+
*/
|
|
858
|
+
static #points(points) {
|
|
859
|
+
if (!Array.isArray(points)) return []
|
|
860
|
+
const result = []
|
|
861
|
+
for (const point of points) {
|
|
862
|
+
if (!point || typeof point !== 'object') continue
|
|
863
|
+
result.push(PcbScene3dBuilder.#point2(point))
|
|
864
|
+
}
|
|
865
|
+
return result
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Maps nested polygon rings.
|
|
870
|
+
* @param {unknown} rings Ring candidates.
|
|
871
|
+
* @returns {{ x: number, y: number }[][]} Rings.
|
|
872
|
+
*/
|
|
873
|
+
static #rings(rings) {
|
|
874
|
+
if (!Array.isArray(rings)) return []
|
|
875
|
+
const result = []
|
|
876
|
+
for (const ring of rings) {
|
|
877
|
+
if (Array.isArray(ring)) {
|
|
878
|
+
result.push(PcbScene3dBuilder.#points(ring))
|
|
879
|
+
} else if (Array.isArray(ring?.points)) {
|
|
880
|
+
result.push(PcbScene3dBuilder.#points(ring.points))
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return result
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Resolves top or bottom from a common layer value.
|
|
888
|
+
* @param {unknown} layer Layer candidate.
|
|
889
|
+
* @returns {'top' | 'bottom'} Canonical side.
|
|
890
|
+
*/
|
|
891
|
+
static #side(layer) {
|
|
892
|
+
const value = String(layer || '').toLowerCase()
|
|
893
|
+
return /(?:bottom|back|b\.)/u.test(value) ? 'bottom' : 'top'
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Normalizes a positive number.
|
|
898
|
+
* @param {unknown} value Candidate number.
|
|
899
|
+
* @param {number} fallback Fallback number.
|
|
900
|
+
* @returns {number} Positive number.
|
|
901
|
+
*/
|
|
902
|
+
static #positiveNumber(value, fallback) {
|
|
903
|
+
const number = Number(value)
|
|
904
|
+
return Number.isFinite(number) && number > 0 ? number : fallback
|
|
905
|
+
}
|
|
906
|
+
}
|