circuitjson-toolkit 1.0.17 → 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/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,368 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from './context/CircuitJsonDocumentContext.mjs'
|
|
2
|
+
import { PcbPrimitivePreparation } from './context/PcbPrimitivePreparation.mjs'
|
|
3
|
+
import { CanonicalInteractionOptions } from './interaction/CanonicalInteractionOptions.mjs'
|
|
4
|
+
import { PcbBoundsSelectionModel } from './PcbBoundsSelectionModel.mjs'
|
|
5
|
+
import { PcbCandidateSelectionModel } from './PcbCandidateSelectionModel.mjs'
|
|
6
|
+
import { PcbDiagnosticFocusModel } from './PcbDiagnosticFocusModel.mjs'
|
|
7
|
+
import { PcbInteractionPrimitiveModel } from './PcbInteractionPrimitiveModel.mjs'
|
|
8
|
+
|
|
9
|
+
const PRIMITIVE_CACHE = ['pcb', 'interaction-primitives-v1']
|
|
10
|
+
const SPATIAL_CACHE = ['interaction', 'pcb-spatial-v2']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reuses a broad-phase index while retaining exact CircuitJSON hit predicates.
|
|
14
|
+
*/
|
|
15
|
+
export class PcbInteractionIndex {
|
|
16
|
+
#candidateQueries = 0
|
|
17
|
+
#context
|
|
18
|
+
#defaults
|
|
19
|
+
#diagnosticFocus = null
|
|
20
|
+
#exactTests = 0
|
|
21
|
+
#groups
|
|
22
|
+
#model
|
|
23
|
+
#primitivesByRecordId
|
|
24
|
+
#spatial
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates one bound reusable interaction index.
|
|
28
|
+
* @param {unknown} document DocumentResult, CircuitJSON model, or context.
|
|
29
|
+
* @param {Record<string, any>} [options] Reusable visibility defaults.
|
|
30
|
+
* @returns {PcbInteractionIndex} Bound interaction index.
|
|
31
|
+
*/
|
|
32
|
+
static create(document, options = {}) {
|
|
33
|
+
const defaults = CanonicalInteractionOptions.normalize(options)
|
|
34
|
+
const context = CircuitJsonDocumentContext.prepare(document)
|
|
35
|
+
const model = PcbPrimitivePreparation.prepareInteraction(context)
|
|
36
|
+
const preparedRecords = PcbInteractionIndex.#spatialRecords(model)
|
|
37
|
+
const spatial = PcbPrimitivePreparation.prepareSpatial(
|
|
38
|
+
context,
|
|
39
|
+
model,
|
|
40
|
+
() => preparedRecords.records
|
|
41
|
+
)
|
|
42
|
+
return new PcbInteractionIndex(
|
|
43
|
+
context,
|
|
44
|
+
model,
|
|
45
|
+
spatial,
|
|
46
|
+
preparedRecords.primitivesByRecordId,
|
|
47
|
+
defaults
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates one prepared interaction service instance.
|
|
53
|
+
* @param {CircuitJsonDocumentContext} context Prepared context.
|
|
54
|
+
* @param {Record<string, any>} model Primitive model.
|
|
55
|
+
* @param {PcbSpatialIndex} spatial Spatial index.
|
|
56
|
+
* @param {Map<string, object>} primitivesByRecordId Stable primitive lookup.
|
|
57
|
+
* @param {Record<string, any>} defaults Normalized defaults.
|
|
58
|
+
*/
|
|
59
|
+
constructor(context, model, spatial, primitivesByRecordId, defaults) {
|
|
60
|
+
this.#context = context
|
|
61
|
+
this.#model = model
|
|
62
|
+
this.#spatial = spatial
|
|
63
|
+
this.#primitivesByRecordId = primitivesByRecordId
|
|
64
|
+
this.#defaults = defaults
|
|
65
|
+
this.#groups = model.groups || []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns ordered canonical hit records for one board-space point.
|
|
70
|
+
* @param {{ x: number, y: number }} point CircuitJSON millimeter point.
|
|
71
|
+
* @param {Record<string, any>} [options] Per-query visibility options.
|
|
72
|
+
* @returns {object[]} Clone-safe ordered hit records.
|
|
73
|
+
*/
|
|
74
|
+
hitTest(point, options = {}) {
|
|
75
|
+
const normalizedPoint = CanonicalInteractionOptions.point(point)
|
|
76
|
+
const normalized = CanonicalInteractionOptions.normalize(
|
|
77
|
+
options,
|
|
78
|
+
this.#defaults
|
|
79
|
+
)
|
|
80
|
+
const records = this.#spatial.search(
|
|
81
|
+
PcbInteractionIndex.#candidateBounds(
|
|
82
|
+
normalizedPoint,
|
|
83
|
+
normalized.tolerance
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
this.#candidateQueries += 1
|
|
87
|
+
this.#exactTests += records.length
|
|
88
|
+
const exactRecords = records.map((record) => ({
|
|
89
|
+
recordId: record.id,
|
|
90
|
+
primitive: this.#primitivesByRecordId.get(record.id)
|
|
91
|
+
}))
|
|
92
|
+
return PcbInteractionPrimitiveModel.hitTestRecords(
|
|
93
|
+
exactRecords,
|
|
94
|
+
normalizedPoint,
|
|
95
|
+
normalized,
|
|
96
|
+
this.#groups
|
|
97
|
+
).map((hit) =>
|
|
98
|
+
PcbInteractionIndex.#hitRecord(
|
|
99
|
+
hit,
|
|
100
|
+
this.#primitivesByRecordId.get(hit.recordId)
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the highest-priority hit or null.
|
|
107
|
+
* @param {{ x: number, y: number }} point CircuitJSON millimeter point.
|
|
108
|
+
* @param {Record<string, any>} [options] Per-query visibility options.
|
|
109
|
+
* @returns {object | null} First hit.
|
|
110
|
+
*/
|
|
111
|
+
pick(point, options = {}) {
|
|
112
|
+
return this.hitTest(point, options)[0] || null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolves canonical area selection from prepared primitive bounds.
|
|
117
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Board-space selection bounds.
|
|
118
|
+
* @param {Record<string, any>} [options] Per-selection visibility options.
|
|
119
|
+
* @returns {{ bounds: object | null, point: object | null, candidates: object[], selectedCandidate: object | null, componentKeys: string[], netNames: string[] }} Clone-safe selection result.
|
|
120
|
+
*/
|
|
121
|
+
selectBounds(bounds, options = {}) {
|
|
122
|
+
const normalized = CanonicalInteractionOptions.normalize(
|
|
123
|
+
options,
|
|
124
|
+
this.#defaults
|
|
125
|
+
)
|
|
126
|
+
const normalizedBounds = PcbBoundsSelectionModel.normalizeBounds(bounds)
|
|
127
|
+
if (!normalizedBounds) {
|
|
128
|
+
return PcbBoundsSelectionModel.resolvePrimitives(
|
|
129
|
+
[],
|
|
130
|
+
bounds,
|
|
131
|
+
normalized
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
const primitives = this.#spatial
|
|
135
|
+
.search(normalizedBounds)
|
|
136
|
+
.map((record) => this.#primitivesByRecordId.get(record.id))
|
|
137
|
+
return PcbBoundsSelectionModel.resolvePrimitives(
|
|
138
|
+
primitives,
|
|
139
|
+
normalizedBounds,
|
|
140
|
+
normalized
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Alias for canonical area-selection terminology used by source toolkits.
|
|
146
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Board-space selection bounds.
|
|
147
|
+
* @param {Record<string, any>} [options] Per-selection visibility options.
|
|
148
|
+
* @returns {{ bounds: object | null, point: object | null, candidates: object[], selectedCandidate: object | null, componentKeys: string[], netNames: string[] }} Clone-safe selection result.
|
|
149
|
+
*/
|
|
150
|
+
selectArea(bounds, options = {}) {
|
|
151
|
+
return this.selectBounds(bounds, options)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolves click/hover candidates and the legacy selected-candidate state.
|
|
156
|
+
* @param {{ x: number, y: number }} point CircuitJSON millimeter point.
|
|
157
|
+
* @param {Record<string, any>} [options] Per-query visibility options.
|
|
158
|
+
* @returns {{ point: { x: number, y: number }, candidates: object[], componentCandidate: object | null, netCandidate: object | null, selectedCandidate: object | null }} Clone-safe selection state.
|
|
159
|
+
*/
|
|
160
|
+
selectionAt(point, options = {}) {
|
|
161
|
+
const normalizedPoint = CanonicalInteractionOptions.point(point)
|
|
162
|
+
const candidates = this.hitTest(normalizedPoint, options)
|
|
163
|
+
return {
|
|
164
|
+
point: normalizedPoint,
|
|
165
|
+
candidates,
|
|
166
|
+
componentCandidate:
|
|
167
|
+
PcbCandidateSelectionModel.componentCandidate(candidates),
|
|
168
|
+
netCandidate: PcbCandidateSelectionModel.netCandidate(candidates),
|
|
169
|
+
selectedCandidate:
|
|
170
|
+
PcbCandidateSelectionModel.selectedCandidate(candidates)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Snaps a board-space point to the nearest prepared primitive anchor.
|
|
176
|
+
* @param {{ x: number, y: number }} point CircuitJSON millimeter point.
|
|
177
|
+
* @param {{ tolerance?: number }} [options] Snap tolerance options.
|
|
178
|
+
* @returns {{ snapped: boolean, point: { x: number, y: number } }} Clone-safe snap result.
|
|
179
|
+
*/
|
|
180
|
+
snap(point, options = {}) {
|
|
181
|
+
const normalizedPoint = CanonicalInteractionOptions.point(point)
|
|
182
|
+
const normalized = CanonicalInteractionOptions.normalize(options, {
|
|
183
|
+
...this.#defaults,
|
|
184
|
+
tolerance: 0
|
|
185
|
+
})
|
|
186
|
+
return PcbInteractionPrimitiveModel.resolveSnapPointFromModel(
|
|
187
|
+
this.#model,
|
|
188
|
+
normalizedPoint,
|
|
189
|
+
{ tolerance: normalized.tolerance }
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns physical and virtual layer rows from the lazy complete model.
|
|
195
|
+
* @returns {{ physicalLayers: object[], virtualLayers: object[] }} Clone-safe layers.
|
|
196
|
+
*/
|
|
197
|
+
resolveLayers() {
|
|
198
|
+
const model = PcbPrimitivePreparation.prepareComplete(this.#context)
|
|
199
|
+
return structuredClone({
|
|
200
|
+
physicalLayers: model.layers,
|
|
201
|
+
virtualLayers: model.virtualLayers || []
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolves one diagnostic id to its legacy board focus row.
|
|
207
|
+
* @param {string} diagnosticId Stable diagnostic id.
|
|
208
|
+
* @returns {object | null} Clone-safe diagnostic focus row.
|
|
209
|
+
*/
|
|
210
|
+
resolveDiagnosticFocus(diagnosticId) {
|
|
211
|
+
if (typeof diagnosticId !== 'string') {
|
|
212
|
+
throw new TypeError('Diagnostic focus ids must be strings.')
|
|
213
|
+
}
|
|
214
|
+
if (!this.#diagnosticFocus) {
|
|
215
|
+
const model = PcbPrimitivePreparation.prepareComplete(this.#context)
|
|
216
|
+
this.#diagnosticFocus = PcbDiagnosticFocusModel.buildPrepared(
|
|
217
|
+
this.#context.model,
|
|
218
|
+
model
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
const focus = this.#diagnosticFocus.get(diagnosticId) || null
|
|
222
|
+
return focus ? structuredClone(focus) : null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Returns clone-safe construction and query counters.
|
|
227
|
+
* @returns {Record<string, number>} Interaction statistics.
|
|
228
|
+
*/
|
|
229
|
+
get statistics() {
|
|
230
|
+
const statistics = this.#context.statistics
|
|
231
|
+
return {
|
|
232
|
+
validationPasses: statistics.validationPasses,
|
|
233
|
+
primitiveBuilds:
|
|
234
|
+
statistics.derivedBuilds[PRIMITIVE_CACHE.join(':')] || 0,
|
|
235
|
+
spatialIndexBuilds:
|
|
236
|
+
statistics.derivedBuilds[SPATIAL_CACHE.join(':')] || 0,
|
|
237
|
+
completePrimitiveBuilds:
|
|
238
|
+
statistics.derivedBuilds['render:pcb-primitives-v1'] || 0,
|
|
239
|
+
candidateQueries: this.#candidateQueries,
|
|
240
|
+
exactTests: this.#exactTests
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Builds stable plain records for the broad phase.
|
|
246
|
+
* @param {Record<string, any>} model Primitive model.
|
|
247
|
+
* @returns {{ records: object[], primitivesByRecordId: Map<string, object> }} Spatial records and exact primitive lookup.
|
|
248
|
+
*/
|
|
249
|
+
static #spatialRecords(model) {
|
|
250
|
+
const records = []
|
|
251
|
+
const primitivesByRecordId = new Map()
|
|
252
|
+
for (const [ordinal, primitive] of (model.primitives || []).entries()) {
|
|
253
|
+
const bounds =
|
|
254
|
+
PcbInteractionPrimitiveModel.interactionBounds(primitive)
|
|
255
|
+
if (!bounds) continue
|
|
256
|
+
const id = `pcb-primitive:${ordinal}`
|
|
257
|
+
records.push({
|
|
258
|
+
id,
|
|
259
|
+
bounds
|
|
260
|
+
})
|
|
261
|
+
primitivesByRecordId.set(id, primitive)
|
|
262
|
+
}
|
|
263
|
+
return { records, primitivesByRecordId }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Expands point queries enough to cover exact local-axis tolerance after
|
|
268
|
+
* arbitrary two-dimensional rotation plus floating-point boundary drift.
|
|
269
|
+
* @param {{ x: number, y: number }} point Normalized query point.
|
|
270
|
+
* @param {number} tolerance Exact narrow-phase tolerance.
|
|
271
|
+
* @returns {number} Conservative broad-phase tolerance.
|
|
272
|
+
*/
|
|
273
|
+
static #candidateTolerance(point, tolerance) {
|
|
274
|
+
const magnitude = Math.max(
|
|
275
|
+
1,
|
|
276
|
+
Math.abs(point.x),
|
|
277
|
+
Math.abs(point.y),
|
|
278
|
+
tolerance
|
|
279
|
+
)
|
|
280
|
+
return tolerance * Math.SQRT2 + Number.EPSILON * magnitude * 16
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Builds a finite query rectangle without passing the conservative
|
|
285
|
+
* expansion through the public spatial-index tolerance limit.
|
|
286
|
+
* @param {{ x: number, y: number }} point Normalized query point.
|
|
287
|
+
* @param {number} tolerance Exact narrow-phase tolerance.
|
|
288
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }} Conservative finite query bounds.
|
|
289
|
+
*/
|
|
290
|
+
static #candidateBounds(point, tolerance) {
|
|
291
|
+
const expansion = PcbInteractionIndex.#candidateTolerance(
|
|
292
|
+
point,
|
|
293
|
+
tolerance
|
|
294
|
+
)
|
|
295
|
+
return {
|
|
296
|
+
minX: Math.max(-Number.MAX_VALUE, point.x - expansion),
|
|
297
|
+
minY: Math.max(-Number.MAX_VALUE, point.y - expansion),
|
|
298
|
+
maxX: Math.min(Number.MAX_VALUE, point.x + expansion),
|
|
299
|
+
maxY: Math.min(Number.MAX_VALUE, point.y + expansion)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Converts one legacy exact hit to the common clone-safe shape.
|
|
305
|
+
* @param {object} hit Legacy exact hit.
|
|
306
|
+
* @param {object | undefined} primitive Matching primitive.
|
|
307
|
+
* @returns {object} Canonical hit record.
|
|
308
|
+
*/
|
|
309
|
+
static #hitRecord(hit, primitive) {
|
|
310
|
+
const source = primitive?.source || primitive?.sourceRoute || {}
|
|
311
|
+
const elementType = String(source.type || '')
|
|
312
|
+
const elementId = PcbInteractionIndex.#elementId(
|
|
313
|
+
source,
|
|
314
|
+
elementType,
|
|
315
|
+
primitive
|
|
316
|
+
)
|
|
317
|
+
const bounds = PcbInteractionPrimitiveModel.interactionBounds(
|
|
318
|
+
primitive
|
|
319
|
+
) || {
|
|
320
|
+
minX: 0,
|
|
321
|
+
minY: 0,
|
|
322
|
+
maxX: 0,
|
|
323
|
+
maxY: 0
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
elementId,
|
|
327
|
+
primitiveId: String(primitive?.id || hit.id || ''),
|
|
328
|
+
kind: String(primitive?.kind || hit.kind || ''),
|
|
329
|
+
side: ['top', 'bottom'].includes(primitive?.side)
|
|
330
|
+
? primitive.side
|
|
331
|
+
: null,
|
|
332
|
+
layerId: String(primitive?.layer || hit.layer || ''),
|
|
333
|
+
bounds: { ...bounds },
|
|
334
|
+
distance: Number(hit.distance),
|
|
335
|
+
componentId: String(
|
|
336
|
+
primitive?.componentId || hit.componentId || ''
|
|
337
|
+
),
|
|
338
|
+
componentKey: String(
|
|
339
|
+
primitive?.componentKey || hit.componentKey || ''
|
|
340
|
+
),
|
|
341
|
+
netName: String(primitive?.netName || hit.netName || ''),
|
|
342
|
+
groupIds: [...(primitive?.groupIds || hit.groupIds || [])],
|
|
343
|
+
source: {
|
|
344
|
+
format: 'circuitjson',
|
|
345
|
+
elementId,
|
|
346
|
+
elementType
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Resolves the stable owning CircuitJSON element id.
|
|
353
|
+
* @param {object} source Source element.
|
|
354
|
+
* @param {string} elementType Source type.
|
|
355
|
+
* @param {object | undefined} primitive Primitive.
|
|
356
|
+
* @returns {string} Stable element id.
|
|
357
|
+
*/
|
|
358
|
+
static #elementId(source, elementType, primitive) {
|
|
359
|
+
const direct = source[elementType + '_id']
|
|
360
|
+
if (typeof direct === 'string' && direct) return direct
|
|
361
|
+
for (const [key, value] of Object.entries(source)) {
|
|
362
|
+
if (key.endsWith('_id') && typeof value === 'string' && value) {
|
|
363
|
+
return value
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return String(primitive?.id || '')
|
|
367
|
+
}
|
|
368
|
+
}
|