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,333 @@
|
|
|
1
|
+
import { ToolkitAsset } from './contracts/ToolkitAsset.mjs'
|
|
2
|
+
import { AttachedValueLimits } from './AttachedValueLimits.mjs'
|
|
3
|
+
|
|
4
|
+
const ASSET_MODES = new Set(['none', 'metadata', 'full'])
|
|
5
|
+
const EXTENSION_MODES = new Set(['none', 'metadata', 'canonical', 'full'])
|
|
6
|
+
const RETAIN_SOURCE_MODES = new Set(['none', 'reference'])
|
|
7
|
+
const WORKER_MODES = new Set(['auto', true, false])
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes the common standalone parser input and option contract.
|
|
11
|
+
*/
|
|
12
|
+
export class ParserOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes one parser request without decoding its CircuitJSON payload.
|
|
15
|
+
* @param {unknown} input Parser input candidate.
|
|
16
|
+
* @param {unknown} [options] Common parser options candidate.
|
|
17
|
+
* @returns {{ input: { fileName: string, data: string | ArrayBuffer | Uint8Array, assets: object[] }, sourceReference: object, options: { preserveRaw: boolean, decodeAssets: string, extensions: string | string[], reports: string[], retainSource: string, worker: 'auto' | boolean, transferInput: boolean, signal: any, onProgress: Function | undefined } }} Normalized request.
|
|
18
|
+
*/
|
|
19
|
+
static normalize(input, options = {}) {
|
|
20
|
+
const inputFields = ParserOptions.#plainDescriptors(
|
|
21
|
+
input,
|
|
22
|
+
'CircuitJSON parser input must be an object.'
|
|
23
|
+
)
|
|
24
|
+
const optionFields = ParserOptions.#plainDescriptors(
|
|
25
|
+
options,
|
|
26
|
+
'CircuitJSON parser options must be an object.'
|
|
27
|
+
)
|
|
28
|
+
const data = ParserOptions.#dataValue(inputFields.data)
|
|
29
|
+
const inputAssets = ParserOptions.#dataValue(inputFields.assets)
|
|
30
|
+
if (!ParserOptions.#isData(data)) {
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
'CircuitJSON parser data must be a string, ArrayBuffer, or Uint8Array.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (inputAssets !== undefined && !Array.isArray(inputAssets)) {
|
|
36
|
+
throw new TypeError('CircuitJSON parser assets must be an array.')
|
|
37
|
+
}
|
|
38
|
+
if (inputAssets !== undefined) AttachedValueLimits.add(inputAssets)
|
|
39
|
+
|
|
40
|
+
const decodeAssetsValue = ParserOptions.#dataValue(
|
|
41
|
+
optionFields.decodeAssets
|
|
42
|
+
)
|
|
43
|
+
const decodeAssets = String(
|
|
44
|
+
decodeAssetsValue === undefined ? 'metadata' : decodeAssetsValue
|
|
45
|
+
)
|
|
46
|
+
if (!ASSET_MODES.has(decodeAssets)) {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`Unsupported CircuitJSON asset decode mode: ${decodeAssets}.`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
const extensions = ParserOptions.#extensions(
|
|
52
|
+
ParserOptions.#dataValue(optionFields.extensions)
|
|
53
|
+
)
|
|
54
|
+
const reports = ParserOptions.#stringList(
|
|
55
|
+
ParserOptions.#dataValue(optionFields.reports),
|
|
56
|
+
'report'
|
|
57
|
+
)
|
|
58
|
+
const retainSourceValue = ParserOptions.#dataValue(
|
|
59
|
+
optionFields.retainSource
|
|
60
|
+
)
|
|
61
|
+
const retainSource = String(
|
|
62
|
+
retainSourceValue === undefined ? 'none' : retainSourceValue
|
|
63
|
+
)
|
|
64
|
+
if (!RETAIN_SOURCE_MODES.has(retainSource)) {
|
|
65
|
+
throw new TypeError(
|
|
66
|
+
`Unsupported CircuitJSON source retention mode: ${retainSource}.`
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const workerValue = ParserOptions.#dataValue(optionFields.worker)
|
|
70
|
+
const worker = workerValue === undefined ? 'auto' : workerValue
|
|
71
|
+
if (!WORKER_MODES.has(worker)) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
'CircuitJSON worker must be auto, true, or false.'
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
ParserOptions.#dataValue(optionFields.onProgress) !== undefined &&
|
|
78
|
+
typeof ParserOptions.#dataValue(optionFields.onProgress) !==
|
|
79
|
+
'function'
|
|
80
|
+
) {
|
|
81
|
+
throw new TypeError('CircuitJSON onProgress must be a function.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
input: {
|
|
86
|
+
fileName: ParserOptions.fileName(input),
|
|
87
|
+
data,
|
|
88
|
+
assets: inputAssets || []
|
|
89
|
+
},
|
|
90
|
+
sourceReference: input,
|
|
91
|
+
options: {
|
|
92
|
+
preserveRaw:
|
|
93
|
+
ParserOptions.#dataValue(optionFields.preserveRaw) === true,
|
|
94
|
+
decodeAssets,
|
|
95
|
+
extensions,
|
|
96
|
+
reports,
|
|
97
|
+
retainSource,
|
|
98
|
+
worker,
|
|
99
|
+
transferInput:
|
|
100
|
+
ParserOptions.#dataValue(optionFields.transferInput) ===
|
|
101
|
+
true,
|
|
102
|
+
signal: ParserOptions.#dataValue(optionFields.signal),
|
|
103
|
+
onProgress: ParserOptions.#dataValue(optionFields.onProgress)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decodes supported parser data into JSON text.
|
|
110
|
+
* @param {string | ArrayBuffer | Uint8Array} data Parser payload.
|
|
111
|
+
* @returns {string} Decoded text.
|
|
112
|
+
*/
|
|
113
|
+
static text(data) {
|
|
114
|
+
if (typeof data === 'string') return data
|
|
115
|
+
if (data instanceof ArrayBuffer) {
|
|
116
|
+
return new TextDecoder().decode(new Uint8Array(data))
|
|
117
|
+
}
|
|
118
|
+
if (data instanceof Uint8Array) return new TextDecoder().decode(data)
|
|
119
|
+
throw new TypeError(
|
|
120
|
+
'CircuitJSON parser data must be a string, ArrayBuffer, or Uint8Array.'
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Selects supplied assets for one canonicalization by DocumentResult.
|
|
126
|
+
* @param {object[]} assets Supplied asset records.
|
|
127
|
+
* @param {string} mode Asset decode mode.
|
|
128
|
+
* @returns {object[]} Selected canonical assets.
|
|
129
|
+
*/
|
|
130
|
+
static assets(assets, mode) {
|
|
131
|
+
return ToolkitAsset.prepareAll(assets, { mode })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Performs bounded CircuitJSON array detection without full JSON parsing.
|
|
136
|
+
* @param {unknown} input Parser input candidate.
|
|
137
|
+
* @returns {boolean} Whether the input has a CircuitJSON array prefix.
|
|
138
|
+
*/
|
|
139
|
+
static supports(input) {
|
|
140
|
+
try {
|
|
141
|
+
const descriptors = ParserOptions.#plainDescriptors(
|
|
142
|
+
input,
|
|
143
|
+
'CircuitJSON parser input must be an object.'
|
|
144
|
+
)
|
|
145
|
+
const data = ParserOptions.#dataValue(descriptors.data)
|
|
146
|
+
if (!ParserOptions.#isData(data)) return false
|
|
147
|
+
return ParserOptions.#prefix(data).trimStart().startsWith('[')
|
|
148
|
+
} catch {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Normalizes a source file name for errors and result identity.
|
|
155
|
+
* @param {unknown} input Parser input candidate.
|
|
156
|
+
* @returns {string} Normalized file name.
|
|
157
|
+
*/
|
|
158
|
+
static fileName(input) {
|
|
159
|
+
try {
|
|
160
|
+
const descriptors = ParserOptions.#plainDescriptors(
|
|
161
|
+
input,
|
|
162
|
+
'CircuitJSON parser input must be an object.'
|
|
163
|
+
)
|
|
164
|
+
const fileName = ParserOptions.#dataValue(descriptors.fileName)
|
|
165
|
+
if (
|
|
166
|
+
fileName !== undefined &&
|
|
167
|
+
!['string', 'number', 'boolean', 'bigint'].includes(
|
|
168
|
+
typeof fileName
|
|
169
|
+
)
|
|
170
|
+
) {
|
|
171
|
+
return ''
|
|
172
|
+
}
|
|
173
|
+
return String(fileName || '')
|
|
174
|
+
.replaceAll('\\', '/')
|
|
175
|
+
.replace(/^\.\//u, '')
|
|
176
|
+
} catch {
|
|
177
|
+
return ''
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Normalizes the extensions mode or selected feature ids.
|
|
183
|
+
* @param {unknown} value Extensions option candidate.
|
|
184
|
+
* @returns {string | string[]} Normalized extensions option.
|
|
185
|
+
*/
|
|
186
|
+
static #extensions(value) {
|
|
187
|
+
if (value === undefined) return 'canonical'
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return ParserOptions.#stringList(value, 'extension feature')
|
|
190
|
+
}
|
|
191
|
+
const mode = String(value)
|
|
192
|
+
if (!EXTENSION_MODES.has(mode)) {
|
|
193
|
+
throw new TypeError(
|
|
194
|
+
`Unsupported CircuitJSON extensions mode: ${mode}.`
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
return mode
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Normalizes one option list into unique non-empty strings.
|
|
202
|
+
* @param {unknown} value List candidate.
|
|
203
|
+
* @param {string} label Item label.
|
|
204
|
+
* @returns {string[]} Normalized list.
|
|
205
|
+
*/
|
|
206
|
+
static #stringList(value, label) {
|
|
207
|
+
if (value === undefined) return []
|
|
208
|
+
const descriptors = ParserOptions.#arrayDescriptors(value, label)
|
|
209
|
+
const items = []
|
|
210
|
+
for (let index = 0; index < descriptors.length.value; index += 1) {
|
|
211
|
+
const item = descriptors[String(index)].value
|
|
212
|
+
if (typeof item !== 'string' || !item.trim()) {
|
|
213
|
+
throw new TypeError(
|
|
214
|
+
`CircuitJSON ${label} ids must be non-empty strings.`
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
items.push(item.trim())
|
|
218
|
+
}
|
|
219
|
+
return [...new Set(items)]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns true for the canonical parser payload types.
|
|
224
|
+
* @param {unknown} data Payload candidate.
|
|
225
|
+
* @returns {boolean} Whether the payload type is supported.
|
|
226
|
+
*/
|
|
227
|
+
static #isData(data) {
|
|
228
|
+
return (
|
|
229
|
+
typeof data === 'string' ||
|
|
230
|
+
data instanceof ArrayBuffer ||
|
|
231
|
+
data instanceof Uint8Array
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Decodes only a bounded prefix for format detection.
|
|
237
|
+
* @param {string | ArrayBuffer | Uint8Array} data Parser payload.
|
|
238
|
+
* @returns {string} Bounded text prefix.
|
|
239
|
+
*/
|
|
240
|
+
static #prefix(data) {
|
|
241
|
+
const limit = 512
|
|
242
|
+
if (typeof data === 'string') return data.slice(0, limit)
|
|
243
|
+
const bytes =
|
|
244
|
+
data instanceof Uint8Array
|
|
245
|
+
? data.subarray(0, limit)
|
|
246
|
+
: new Uint8Array(data, 0, Math.min(data.byteLength, limit))
|
|
247
|
+
return new TextDecoder().decode(bytes)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns own data descriptors for a plain request record.
|
|
252
|
+
* @param {unknown} value Record candidate.
|
|
253
|
+
* @param {string} message Failure message.
|
|
254
|
+
* @returns {Record<string, PropertyDescriptor>} Data descriptors.
|
|
255
|
+
*/
|
|
256
|
+
static #plainDescriptors(value, message) {
|
|
257
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
258
|
+
throw new TypeError(message)
|
|
259
|
+
}
|
|
260
|
+
let prototype
|
|
261
|
+
let descriptors
|
|
262
|
+
try {
|
|
263
|
+
prototype = Object.getPrototypeOf(value)
|
|
264
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
265
|
+
} catch {
|
|
266
|
+
throw new TypeError(message)
|
|
267
|
+
}
|
|
268
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
269
|
+
throw new TypeError(message)
|
|
270
|
+
}
|
|
271
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
272
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
273
|
+
throw new TypeError(
|
|
274
|
+
'CircuitJSON parser fields must be data properties.'
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return descriptors
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Returns exact dense-array descriptors without caller iteration.
|
|
283
|
+
* @param {unknown} value Array candidate.
|
|
284
|
+
* @param {string} label Item label.
|
|
285
|
+
* @returns {Record<string, PropertyDescriptor>} Array descriptors.
|
|
286
|
+
*/
|
|
287
|
+
static #arrayDescriptors(value, label) {
|
|
288
|
+
if (!Array.isArray(value)) {
|
|
289
|
+
throw new TypeError(`CircuitJSON ${label}s must be an array.`)
|
|
290
|
+
}
|
|
291
|
+
let prototype
|
|
292
|
+
let descriptors
|
|
293
|
+
try {
|
|
294
|
+
prototype = Object.getPrototypeOf(value)
|
|
295
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
296
|
+
} catch {
|
|
297
|
+
throw new TypeError(`CircuitJSON ${label}s must be a dense array.`)
|
|
298
|
+
}
|
|
299
|
+
const length = ParserOptions.#dataValue(descriptors.length)
|
|
300
|
+
if (
|
|
301
|
+
prototype !== Array.prototype ||
|
|
302
|
+
!Number.isSafeInteger(length) ||
|
|
303
|
+
length < 0 ||
|
|
304
|
+
Reflect.ownKeys(descriptors).length !== length + 1
|
|
305
|
+
) {
|
|
306
|
+
throw new TypeError(`CircuitJSON ${label}s must be a dense array.`)
|
|
307
|
+
}
|
|
308
|
+
for (let index = 0; index < length; index += 1) {
|
|
309
|
+
const descriptor = descriptors[String(index)]
|
|
310
|
+
if (
|
|
311
|
+
!descriptor ||
|
|
312
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
313
|
+
descriptor.enumerable !== true
|
|
314
|
+
) {
|
|
315
|
+
throw new TypeError(
|
|
316
|
+
`CircuitJSON ${label}s must contain data properties.`
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return descriptors
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reads one data descriptor value.
|
|
325
|
+
* @param {PropertyDescriptor | undefined} descriptor Descriptor.
|
|
326
|
+
* @returns {unknown} Data value.
|
|
327
|
+
*/
|
|
328
|
+
static #dataValue(descriptor) {
|
|
329
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
330
|
+
? descriptor.value
|
|
331
|
+
: undefined
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -13,13 +13,29 @@ export class PcbBoundsSelectionModel {
|
|
|
13
13
|
* @returns {{ bounds: object | null, point: object | null, candidates: object[], selectedCandidate: object | null, componentKeys: string[], netNames: string[] }}
|
|
14
14
|
*/
|
|
15
15
|
static resolve(documentModel, bounds, options = {}) {
|
|
16
|
+
return PcbBoundsSelectionModel.resolvePrimitives(
|
|
17
|
+
PcbInteractionPrimitiveModel.build(documentModel).primitives,
|
|
18
|
+
bounds,
|
|
19
|
+
options
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves area selection from an already prepared primitive list.
|
|
25
|
+
* @param {object[]} primitives Prepared PCB primitives.
|
|
26
|
+
* @param {{ minX?: unknown, minY?: unknown, maxX?: unknown, maxY?: unknown }} bounds Board-space bounds.
|
|
27
|
+
* @param {{ side?: string, hiddenLayers?: string[], hiddenObjects?: string[] }} [options] Selection options.
|
|
28
|
+
* @returns {{ bounds: object | null, point: object | null, candidates: object[], selectedCandidate: object | null, componentKeys: string[], netNames: string[] }}
|
|
29
|
+
*/
|
|
30
|
+
static resolvePrimitives(primitives, bounds, options = {}) {
|
|
16
31
|
const normalizedBounds = PcbBoundsSelectionModel.normalizeBounds(bounds)
|
|
17
32
|
if (!normalizedBounds) return PcbBoundsSelectionModel.#empty()
|
|
18
33
|
|
|
34
|
+
const visibility = PcbBoundsSelectionModel.#visibilityOptions(options)
|
|
19
35
|
const candidates = PcbBoundsSelectionModel.#containedPrimitives(
|
|
20
|
-
|
|
36
|
+
primitives,
|
|
21
37
|
normalizedBounds,
|
|
22
|
-
|
|
38
|
+
visibility
|
|
23
39
|
).map((primitive) =>
|
|
24
40
|
PcbCandidateSelectionModel.fromPrimitive(primitive)
|
|
25
41
|
)
|
|
@@ -90,15 +106,13 @@ export class PcbBoundsSelectionModel {
|
|
|
90
106
|
|
|
91
107
|
/**
|
|
92
108
|
* Returns primitives that are visible and touch the measured bounds.
|
|
93
|
-
* @param {object
|
|
109
|
+
* @param {object[]} primitives Prepared primitives.
|
|
94
110
|
* @param {object} bounds Normalized bounds.
|
|
95
111
|
* @param {object} options Selection options.
|
|
96
112
|
* @returns {object[]}
|
|
97
113
|
*/
|
|
98
|
-
static #containedPrimitives(
|
|
99
|
-
return
|
|
100
|
-
documentModel
|
|
101
|
-
).primitives.filter(
|
|
114
|
+
static #containedPrimitives(primitives, bounds, options) {
|
|
115
|
+
return (primitives || []).filter(
|
|
102
116
|
(primitive) =>
|
|
103
117
|
PcbBoundsSelectionModel.#isVisible(primitive, options) &&
|
|
104
118
|
PcbBoundsSelectionModel.#touchesBounds(primitive, bounds)
|
|
@@ -112,24 +126,46 @@ export class PcbBoundsSelectionModel {
|
|
|
112
126
|
* @returns {boolean}
|
|
113
127
|
*/
|
|
114
128
|
static #isVisible(primitive, options) {
|
|
115
|
-
const side =
|
|
129
|
+
const side = options.side
|
|
116
130
|
if (side && primitive.side && primitive.side !== side) return false
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
if (primitive.layer && hiddenLayers.has(String(primitive.layer))) {
|
|
132
|
+
if (
|
|
133
|
+
primitive.layer &&
|
|
134
|
+
options.hiddenLayers.has(String(primitive.layer))
|
|
135
|
+
) {
|
|
124
136
|
return false
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
.map(String)
|
|
130
|
-
.filter(Boolean)
|
|
139
|
+
return !options.hiddenObjects.has(
|
|
140
|
+
PcbBoundsSelectionModel.#objectKey(primitive)
|
|
131
141
|
)
|
|
132
|
-
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds visibility lookup sets once for one area-selection operation.
|
|
146
|
+
* @param {{ side?: string, hiddenLayers?: string[], hiddenObjects?: string[] }} options Raw options.
|
|
147
|
+
* @returns {{ side: string, hiddenLayers: Set<string>, hiddenObjects: Set<string> }} Prepared visibility.
|
|
148
|
+
*/
|
|
149
|
+
static #visibilityOptions(options) {
|
|
150
|
+
return {
|
|
151
|
+
side: String(options?.side || ''),
|
|
152
|
+
hiddenLayers: new Set(
|
|
153
|
+
(Array.isArray(options?.hiddenLayers)
|
|
154
|
+
? options.hiddenLayers
|
|
155
|
+
: []
|
|
156
|
+
)
|
|
157
|
+
.map(String)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
),
|
|
160
|
+
hiddenObjects: new Set(
|
|
161
|
+
(Array.isArray(options?.hiddenObjects)
|
|
162
|
+
? options.hiddenObjects
|
|
163
|
+
: []
|
|
164
|
+
)
|
|
165
|
+
.map(String)
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
2
|
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
3
|
import { PcbInteractionPrimitiveModel } from './PcbInteractionPrimitiveModel.mjs'
|
|
4
|
+
import { CircuitJsonDocumentContext } from './context/CircuitJsonDocumentContext.mjs'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolves viewport focus targets for PCB diagnostic rows.
|
|
@@ -12,7 +13,23 @@ export class PcbDiagnosticFocusModel {
|
|
|
12
13
|
* @returns {Map<string, object>}
|
|
13
14
|
*/
|
|
14
15
|
static build(documentModel) {
|
|
15
|
-
|
|
16
|
+
let context
|
|
17
|
+
try {
|
|
18
|
+
context = CircuitJsonDocumentContext.prepare(documentModel)
|
|
19
|
+
} catch {
|
|
20
|
+
return new Map()
|
|
21
|
+
}
|
|
22
|
+
const model = PcbInteractionPrimitiveModel.build(context)
|
|
23
|
+
return PcbDiagnosticFocusModel.buildPrepared(context, model)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds focus rows from one already prepared complete primitive model.
|
|
28
|
+
* @param {object | object[]} documentModel Parsed PCB document.
|
|
29
|
+
* @param {object} model Complete primitive model.
|
|
30
|
+
* @returns {Map<string, object>} Diagnostic focus rows.
|
|
31
|
+
*/
|
|
32
|
+
static buildPrepared(documentModel, model) {
|
|
16
33
|
const context = PcbDiagnosticFocusModel.#context(documentModel, model)
|
|
17
34
|
const rows = new Map()
|
|
18
35
|
|
|
@@ -178,7 +195,12 @@ export class PcbDiagnosticFocusModel {
|
|
|
178
195
|
['pcb_trace_id', [element?.pcb_trace_id, element?.pcb_trace_ids]],
|
|
179
196
|
[
|
|
180
197
|
'pcb_smtpad_id',
|
|
181
|
-
[
|
|
198
|
+
[
|
|
199
|
+
element?.pcb_smtpad_id,
|
|
200
|
+
element?.pcb_smtpad_ids,
|
|
201
|
+
element?.pcb_pad_id,
|
|
202
|
+
element?.pcb_pad_ids
|
|
203
|
+
]
|
|
182
204
|
],
|
|
183
205
|
['pcb_via_id', [element?.pcb_via_id, element?.pcb_via_ids]],
|
|
184
206
|
[
|
|
@@ -280,6 +302,11 @@ export class PcbDiagnosticFocusModel {
|
|
|
280
302
|
* @returns {object[]}
|
|
281
303
|
*/
|
|
282
304
|
static #elements(documentModel) {
|
|
305
|
+
try {
|
|
306
|
+
return CircuitJsonDocumentContext.prepare(documentModel).model
|
|
307
|
+
} catch {
|
|
308
|
+
// Preserve the tolerant legacy wrapper fallbacks below.
|
|
309
|
+
}
|
|
283
310
|
if (Array.isArray(documentModel)) return documentModel
|
|
284
311
|
if (Array.isArray(documentModel?.elements))
|
|
285
312
|
return documentModel.elements
|
|
@@ -355,15 +382,19 @@ export class PcbDiagnosticFocusModel {
|
|
|
355
382
|
* @returns {object | null}
|
|
356
383
|
*/
|
|
357
384
|
static #mergeBounds(boundsRows) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
385
|
+
let minX = Infinity
|
|
386
|
+
let minY = Infinity
|
|
387
|
+
let maxX = -Infinity
|
|
388
|
+
let maxY = -Infinity
|
|
389
|
+
for (const candidate of boundsRows) {
|
|
390
|
+
const bounds = PcbDiagnosticFocusModel.#normalizeBounds(candidate)
|
|
391
|
+
if (!bounds) continue
|
|
392
|
+
minX = Math.min(minX, bounds.minX)
|
|
393
|
+
minY = Math.min(minY, bounds.minY)
|
|
394
|
+
maxX = Math.max(maxX, bounds.maxX)
|
|
395
|
+
maxY = Math.max(maxY, bounds.maxY)
|
|
396
|
+
}
|
|
397
|
+
if (!Number.isFinite(minX)) return null
|
|
367
398
|
return {
|
|
368
399
|
minX,
|
|
369
400
|
minY,
|