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.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. 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
- documentModel,
36
+ primitives,
21
37
  normalizedBounds,
22
- options
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 | object[]} documentModel Parsed PCB document model.
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(documentModel, bounds, options) {
99
- return PcbInteractionPrimitiveModel.build(
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 = String(options?.side || '')
129
+ const side = options.side
116
130
  if (side && primitive.side && primitive.side !== side) return false
117
131
 
118
- const hiddenLayers = new Set(
119
- (Array.isArray(options?.hiddenLayers) ? options.hiddenLayers : [])
120
- .map(String)
121
- .filter(Boolean)
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
- const hiddenObjects = new Set(
128
- (Array.isArray(options?.hiddenObjects) ? options.hiddenObjects : [])
129
- .map(String)
130
- .filter(Boolean)
139
+ return !options.hiddenObjects.has(
140
+ PcbBoundsSelectionModel.#objectKey(primitive)
131
141
  )
132
- return !hiddenObjects.has(PcbBoundsSelectionModel.#objectKey(primitive))
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
- const model = PcbInteractionPrimitiveModel.build(documentModel)
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
- [element?.pcb_smtpad_id, element?.pcb_smtpad_ids]
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
- const rows = boundsRows
359
- .map((bounds) => PcbDiagnosticFocusModel.#normalizeBounds(bounds))
360
- .filter(Boolean)
361
- if (!rows.length) return null
362
-
363
- const minX = Math.min(...rows.map((bounds) => bounds.minX))
364
- const minY = Math.min(...rows.map((bounds) => bounds.minY))
365
- const maxX = Math.max(...rows.map((bounds) => bounds.maxX))
366
- const maxY = Math.max(...rows.map((bounds) => bounds.maxY))
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,