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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
2
|
import { CircuitJsonPcbPrimitiveBuilder } from './CircuitJsonPcbPrimitiveBuilder.mjs'
|
|
3
|
+
import { CircuitJsonDocumentContext } from './context/CircuitJsonDocumentContext.mjs'
|
|
4
|
+
import { PcbPrimitivePreparation } from './context/PcbPrimitivePreparation.mjs'
|
|
5
|
+
import { PcbInteractionBounds } from './interaction/PcbInteractionBounds.mjs'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Builds renderer-neutral PCB primitives for element-array board documents.
|
|
@@ -11,13 +14,17 @@ export class PcbInteractionPrimitiveModel {
|
|
|
11
14
|
* @returns {{ bounds: object, layers: object[], virtualLayers: object[], components: object[], nets: object[], primitives: object[], anchors: object[], diagnostics: object[], airwires: object[], traceLengths: object[], groups: object[], anchorOffsets: object[] }}
|
|
12
15
|
*/
|
|
13
16
|
static build(documentModel) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const context = PcbInteractionPrimitiveModel.#context(documentModel)
|
|
18
|
+
if (context) return PcbPrimitivePreparation.prepareComplete(context)
|
|
19
|
+
|
|
20
|
+
const elements = CircuitJsonDocument.normalizeModel(
|
|
21
|
+
CircuitJsonPcbPrimitiveBuilder.elements(documentModel)
|
|
22
|
+
)
|
|
23
|
+
try {
|
|
24
|
+
return CircuitJsonPcbPrimitiveBuilder.build(elements)
|
|
25
|
+
} catch {
|
|
17
26
|
return PcbInteractionPrimitiveModel.#emptyModel()
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
return CircuitJsonPcbPrimitiveBuilder.build(documentModel)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -26,13 +33,7 @@ export class PcbInteractionPrimitiveModel {
|
|
|
26
33
|
* @returns {{ physicalLayers: object[], virtualLayers: object[] }}
|
|
27
34
|
*/
|
|
28
35
|
static resolveLayerGroups(documentModel) {
|
|
29
|
-
|
|
30
|
-
!PcbInteractionPrimitiveModel.#isElementArrayDocument(documentModel)
|
|
31
|
-
) {
|
|
32
|
-
return { physicalLayers: [], virtualLayers: [] }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const model = CircuitJsonPcbPrimitiveBuilder.build(documentModel)
|
|
36
|
+
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
36
37
|
return {
|
|
37
38
|
physicalLayers: model.layers,
|
|
38
39
|
virtualLayers: model.virtualLayers || []
|
|
@@ -47,6 +48,21 @@ export class PcbInteractionPrimitiveModel {
|
|
|
47
48
|
* @returns {{ snapped: boolean, point: { x: number, y: number } }}
|
|
48
49
|
*/
|
|
49
50
|
static resolveSnapPoint(documentModel, point, options = {}) {
|
|
51
|
+
return PcbInteractionPrimitiveModel.resolveSnapPointFromModel(
|
|
52
|
+
PcbInteractionPrimitiveModel.build(documentModel),
|
|
53
|
+
point,
|
|
54
|
+
options
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Snaps against anchors from one already prepared primitive model.
|
|
60
|
+
* @param {{ anchors?: object[] }} model Prepared primitive model.
|
|
61
|
+
* @param {{ x?: unknown, y?: unknown }} point Board-space point.
|
|
62
|
+
* @param {{ tolerance?: number }} [options] Snap options.
|
|
63
|
+
* @returns {{ snapped: boolean, point: { x: number, y: number } }}
|
|
64
|
+
*/
|
|
65
|
+
static resolveSnapPointFromModel(model, point, options = {}) {
|
|
50
66
|
const normalizedPoint = PcbInteractionPrimitiveModel.#point(point)
|
|
51
67
|
if (!normalizedPoint) return { snapped: false, point: { x: 0, y: 0 } }
|
|
52
68
|
|
|
@@ -57,8 +73,7 @@ export class PcbInteractionPrimitiveModel {
|
|
|
57
73
|
let bestPoint = null
|
|
58
74
|
let bestDistanceSq = Infinity
|
|
59
75
|
|
|
60
|
-
for (const anchor of
|
|
61
|
-
.anchors) {
|
|
76
|
+
for (const anchor of model?.anchors || []) {
|
|
62
77
|
const distanceSq = PcbInteractionPrimitiveModel.#distanceSquared(
|
|
63
78
|
normalizedPoint,
|
|
64
79
|
anchor.point
|
|
@@ -84,21 +99,66 @@ export class PcbInteractionPrimitiveModel {
|
|
|
84
99
|
* @returns {object[]}
|
|
85
100
|
*/
|
|
86
101
|
static hitTest(documentModel, point, options = {}) {
|
|
102
|
+
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
103
|
+
return PcbInteractionPrimitiveModel.hitTestPrimitives(
|
|
104
|
+
model.primitives,
|
|
105
|
+
point,
|
|
106
|
+
options,
|
|
107
|
+
model.groups
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Applies the legacy exact narrow phase to prepared primitive candidates.
|
|
113
|
+
* @param {object[]} primitives Prepared primitive candidates.
|
|
114
|
+
* @param {{ x?: unknown, y?: unknown }} point Board-space point.
|
|
115
|
+
* @param {{ side?: 'top' | 'bottom', hiddenLayers?: string[], hiddenObjects?: string[], tolerance?: number }} [options] Hit-test options.
|
|
116
|
+
* @param {object[]} [groups] Prepared group rows.
|
|
117
|
+
* @returns {object[]} Prioritized hit candidates.
|
|
118
|
+
*/
|
|
119
|
+
static hitTestPrimitives(primitives, point, options = {}, groups = []) {
|
|
120
|
+
return PcbInteractionPrimitiveModel.hitTestRecords(
|
|
121
|
+
(primitives || []).map((primitive, index) => ({
|
|
122
|
+
recordId: String(index),
|
|
123
|
+
primitive
|
|
124
|
+
})),
|
|
125
|
+
point,
|
|
126
|
+
options,
|
|
127
|
+
groups
|
|
128
|
+
).map(({ recordId: _recordId, ...hit }) => hit)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Applies the exact narrow phase while preserving a caller-owned stable
|
|
133
|
+
* record identity independently from non-unique primitive ids.
|
|
134
|
+
* @param {{ recordId: string, primitive: object }[]} records Prepared primitive records.
|
|
135
|
+
* @param {{ x?: unknown, y?: unknown }} point Board-space point.
|
|
136
|
+
* @param {{ side?: 'top' | 'bottom', hiddenLayers?: string[], hiddenObjects?: string[], tolerance?: number }} [options] Hit-test options.
|
|
137
|
+
* @param {object[]} [groups] Prepared group rows.
|
|
138
|
+
* @returns {object[]} Prioritized hits carrying their stable record ids.
|
|
139
|
+
*/
|
|
140
|
+
static hitTestRecords(records, point, options = {}, groups = []) {
|
|
87
141
|
const normalizedPoint = PcbInteractionPrimitiveModel.#point(point)
|
|
88
142
|
if (!normalizedPoint) return []
|
|
89
|
-
|
|
143
|
+
const preparedOptions =
|
|
144
|
+
PcbInteractionPrimitiveModel.#visibilityOptions(options)
|
|
90
145
|
const tolerance = PcbInteractionPrimitiveModel.#number(
|
|
91
146
|
options.tolerance,
|
|
92
147
|
0.2
|
|
93
148
|
)
|
|
94
|
-
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
95
149
|
const groupsById = new Map(
|
|
96
|
-
(
|
|
150
|
+
(groups || []).map((group) => [String(group.id || ''), group])
|
|
97
151
|
)
|
|
98
152
|
const hits = []
|
|
99
153
|
|
|
100
|
-
for (const
|
|
101
|
-
|
|
154
|
+
for (const record of records || []) {
|
|
155
|
+
const primitive = record.primitive
|
|
156
|
+
if (
|
|
157
|
+
!PcbInteractionPrimitiveModel.#isVisible(
|
|
158
|
+
primitive,
|
|
159
|
+
preparedOptions
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
102
162
|
continue
|
|
103
163
|
}
|
|
104
164
|
const distance = PcbInteractionPrimitiveModel.#hitDistance(
|
|
@@ -109,6 +169,7 @@ export class PcbInteractionPrimitiveModel {
|
|
|
109
169
|
if (distance === null) continue
|
|
110
170
|
|
|
111
171
|
hits.push({
|
|
172
|
+
recordId: String(record.recordId),
|
|
112
173
|
...PcbInteractionPrimitiveModel.#candidate(
|
|
113
174
|
primitive,
|
|
114
175
|
groupsById
|
|
@@ -126,14 +187,53 @@ export class PcbInteractionPrimitiveModel {
|
|
|
126
187
|
}
|
|
127
188
|
|
|
128
189
|
/**
|
|
129
|
-
*
|
|
130
|
-
* @param {object
|
|
131
|
-
* @returns {
|
|
190
|
+
* Resolves conservative exact-geometry bounds for spatial indexing.
|
|
191
|
+
* @param {object} primitive Prepared PCB primitive.
|
|
192
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null} Interaction bounds.
|
|
132
193
|
*/
|
|
133
|
-
static
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
194
|
+
static interactionBounds(primitive) {
|
|
195
|
+
return PcbInteractionBounds.resolve(primitive)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolves one canonical context without exposing validation failures from
|
|
200
|
+
* this legacy tolerant facade.
|
|
201
|
+
* @param {unknown} documentModel Document result, model, or context.
|
|
202
|
+
* @returns {CircuitJsonDocumentContext | null} Prepared context.
|
|
203
|
+
*/
|
|
204
|
+
static #context(documentModel) {
|
|
205
|
+
try {
|
|
206
|
+
if (documentModel instanceof CircuitJsonDocumentContext) {
|
|
207
|
+
return documentModel
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
documentModel &&
|
|
211
|
+
typeof documentModel === 'object' &&
|
|
212
|
+
!Array.isArray(documentModel)
|
|
213
|
+
) {
|
|
214
|
+
const schema = Object.getOwnPropertyDescriptor(
|
|
215
|
+
documentModel,
|
|
216
|
+
'schema'
|
|
217
|
+
)
|
|
218
|
+
const model = Object.getOwnPropertyDescriptor(
|
|
219
|
+
documentModel,
|
|
220
|
+
'model'
|
|
221
|
+
)
|
|
222
|
+
if (
|
|
223
|
+
schema &&
|
|
224
|
+
Object.hasOwn(schema, 'value') &&
|
|
225
|
+
schema.value === 'ecad-toolkit.document.v1' &&
|
|
226
|
+
model &&
|
|
227
|
+
Object.hasOwn(model, 'value') &&
|
|
228
|
+
Array.isArray(model.value)
|
|
229
|
+
) {
|
|
230
|
+
return CircuitJsonDocumentContext.prepare(documentModel)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null
|
|
234
|
+
} catch {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
137
237
|
}
|
|
138
238
|
|
|
139
239
|
/**
|
|
@@ -164,39 +264,59 @@ export class PcbInteractionPrimitiveModel {
|
|
|
164
264
|
* @returns {boolean}
|
|
165
265
|
*/
|
|
166
266
|
static #isVisible(primitive, options) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
if (primitive.layer && hiddenLayers.has(String(primitive.layer))) {
|
|
267
|
+
if (
|
|
268
|
+
primitive.layer &&
|
|
269
|
+
options.hiddenLayers.has(String(primitive.layer))
|
|
270
|
+
) {
|
|
173
271
|
return false
|
|
174
272
|
}
|
|
175
273
|
|
|
176
|
-
const hiddenObjects = new Set(
|
|
177
|
-
(Array.isArray(options.hiddenObjects) ? options.hiddenObjects : [])
|
|
178
|
-
.map(String)
|
|
179
|
-
.filter(Boolean)
|
|
180
|
-
)
|
|
181
274
|
if (
|
|
182
|
-
hiddenObjects.has(
|
|
275
|
+
options.hiddenObjects.has(
|
|
183
276
|
PcbInteractionPrimitiveModel.#componentSideObjectKey(primitive)
|
|
184
277
|
)
|
|
185
278
|
) {
|
|
186
279
|
return false
|
|
187
280
|
}
|
|
188
281
|
if (
|
|
189
|
-
hiddenObjects.has(
|
|
282
|
+
options.hiddenObjects.has(
|
|
190
283
|
PcbInteractionPrimitiveModel.#objectKey(primitive)
|
|
191
284
|
)
|
|
192
285
|
) {
|
|
193
286
|
return false
|
|
194
287
|
}
|
|
195
288
|
|
|
196
|
-
const side =
|
|
289
|
+
const side = options.side
|
|
197
290
|
return !side || !primitive.side || primitive.side === side
|
|
198
291
|
}
|
|
199
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Builds visibility lookup sets once per hit-test call.
|
|
295
|
+
* @param {object} options Raw legacy hit options.
|
|
296
|
+
* @returns {{ side: string, hiddenLayers: Set<string>, hiddenObjects: Set<string> }} Prepared options.
|
|
297
|
+
*/
|
|
298
|
+
static #visibilityOptions(options) {
|
|
299
|
+
return {
|
|
300
|
+
side: String(options.side || ''),
|
|
301
|
+
hiddenLayers: new Set(
|
|
302
|
+
(Array.isArray(options.hiddenLayers)
|
|
303
|
+
? options.hiddenLayers
|
|
304
|
+
: []
|
|
305
|
+
)
|
|
306
|
+
.map(String)
|
|
307
|
+
.filter(Boolean)
|
|
308
|
+
),
|
|
309
|
+
hiddenObjects: new Set(
|
|
310
|
+
(Array.isArray(options.hiddenObjects)
|
|
311
|
+
? options.hiddenObjects
|
|
312
|
+
: []
|
|
313
|
+
)
|
|
314
|
+
.map(String)
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
200
320
|
/**
|
|
201
321
|
* Resolves component-side object keys for component-backed primitives.
|
|
202
322
|
* @param {object} primitive Primitive row.
|
|
@@ -268,27 +388,40 @@ export class PcbInteractionPrimitiveModel {
|
|
|
268
388
|
return distance <= width / 2 + tolerance ? distance : null
|
|
269
389
|
}
|
|
270
390
|
if (primitive.kind === 'via') {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
tolerance
|
|
276
|
-
)
|
|
277
|
-
? 0
|
|
278
|
-
: null
|
|
279
|
-
}
|
|
280
|
-
const distance = Math.sqrt(
|
|
281
|
-
PcbInteractionPrimitiveModel.#distanceSquared(point, primitive)
|
|
391
|
+
const hit = PcbInteractionPrimitiveModel.#shapeHit(
|
|
392
|
+
primitive,
|
|
393
|
+
point,
|
|
394
|
+
tolerance
|
|
282
395
|
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
396
|
+
if (hit === null) return null
|
|
397
|
+
return String(primitive.shape || 'circle') === 'circle'
|
|
398
|
+
? Math.sqrt(
|
|
399
|
+
PcbInteractionPrimitiveModel.#distanceSquared(
|
|
400
|
+
point,
|
|
401
|
+
primitive
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
: hit
|
|
286
405
|
}
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
primitive
|
|
406
|
+
if (primitive.kind === 'pad') {
|
|
407
|
+
return PcbInteractionPrimitiveModel.#shapeHit(
|
|
408
|
+
primitive,
|
|
409
|
+
point,
|
|
410
|
+
tolerance
|
|
290
411
|
)
|
|
412
|
+
}
|
|
413
|
+
if (
|
|
414
|
+
['zone', 'keepout', 'cutout', 'courtyard'].includes(primitive.kind)
|
|
291
415
|
) {
|
|
416
|
+
const points =
|
|
417
|
+
PcbInteractionPrimitiveModel.#primitivePoints(primitive)
|
|
418
|
+
if (points.length >= 3) {
|
|
419
|
+
return PcbInteractionPrimitiveModel.#polygonHit(
|
|
420
|
+
points,
|
|
421
|
+
point,
|
|
422
|
+
tolerance
|
|
423
|
+
)
|
|
424
|
+
}
|
|
292
425
|
return PcbInteractionPrimitiveModel.#inside(
|
|
293
426
|
point,
|
|
294
427
|
primitive.bounds,
|
|
@@ -298,15 +431,213 @@ export class PcbInteractionPrimitiveModel {
|
|
|
298
431
|
: null
|
|
299
432
|
}
|
|
300
433
|
if (primitive.kind === 'board') {
|
|
301
|
-
|
|
434
|
+
const points =
|
|
435
|
+
PcbInteractionPrimitiveModel.#primitivePoints(primitive)
|
|
436
|
+
const inside =
|
|
437
|
+
points.length >= 3
|
|
438
|
+
? PcbInteractionPrimitiveModel.#polygonHit(
|
|
439
|
+
points,
|
|
440
|
+
point,
|
|
441
|
+
0
|
|
442
|
+
) !== null
|
|
443
|
+
: PcbInteractionPrimitiveModel.#inside(
|
|
444
|
+
point,
|
|
445
|
+
primitive.bounds,
|
|
446
|
+
0
|
|
447
|
+
)
|
|
448
|
+
return inside ? Number.MAX_SAFE_INTEGER : null
|
|
449
|
+
}
|
|
450
|
+
return null
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Applies exact common pad and via shape predicates.
|
|
455
|
+
* @param {object} primitive Shape primitive.
|
|
456
|
+
* @param {{ x: number, y: number }} point Board point.
|
|
457
|
+
* @param {number} tolerance Hit tolerance.
|
|
458
|
+
* @returns {number | null} Hit distance or null.
|
|
459
|
+
*/
|
|
460
|
+
static #shapeHit(primitive, point, tolerance) {
|
|
461
|
+
const points = PcbInteractionPrimitiveModel.#primitivePoints(primitive)
|
|
462
|
+
if (points.length >= 3) {
|
|
463
|
+
return PcbInteractionPrimitiveModel.#polygonHit(
|
|
464
|
+
points,
|
|
302
465
|
point,
|
|
303
|
-
|
|
466
|
+
tolerance
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
const center = PcbInteractionPrimitiveModel.#point(primitive)
|
|
470
|
+
if (!center) return null
|
|
471
|
+
const width = Math.max(
|
|
472
|
+
PcbInteractionPrimitiveModel.#number(
|
|
473
|
+
primitive.width ?? primitive.diameter,
|
|
304
474
|
0
|
|
475
|
+
),
|
|
476
|
+
0
|
|
477
|
+
)
|
|
478
|
+
const height = Math.max(
|
|
479
|
+
PcbInteractionPrimitiveModel.#number(
|
|
480
|
+
primitive.height ?? primitive.diameter ?? primitive.width,
|
|
481
|
+
width
|
|
482
|
+
),
|
|
483
|
+
0
|
|
484
|
+
)
|
|
485
|
+
if (!width || !height) return null
|
|
486
|
+
const local = PcbInteractionPrimitiveModel.#rotatePoint(
|
|
487
|
+
point,
|
|
488
|
+
center,
|
|
489
|
+
-PcbInteractionPrimitiveModel.#number(primitive.rotation, 0)
|
|
490
|
+
)
|
|
491
|
+
const shape = String(primitive.shape || 'circle').toLowerCase()
|
|
492
|
+
if (['circle', 'oval_circle'].includes(shape)) {
|
|
493
|
+
const x = (local.x - center.x) / (width / 2 + tolerance)
|
|
494
|
+
const y = (local.y - center.y) / (height / 2 + tolerance)
|
|
495
|
+
return x * x + y * y <= 1 ? 0 : null
|
|
496
|
+
}
|
|
497
|
+
if (['pill', 'oval', 'rotated_pill', 'capsule'].includes(shape)) {
|
|
498
|
+
return PcbInteractionPrimitiveModel.#capsuleHit(
|
|
499
|
+
local,
|
|
500
|
+
center,
|
|
501
|
+
width,
|
|
502
|
+
height,
|
|
503
|
+
tolerance
|
|
305
504
|
)
|
|
306
|
-
? Number.MAX_SAFE_INTEGER
|
|
307
|
-
: null
|
|
308
505
|
}
|
|
309
|
-
|
|
506
|
+
if (['rounded_rect', 'roundrect'].includes(shape)) {
|
|
507
|
+
return PcbInteractionPrimitiveModel.#roundedRectHit(
|
|
508
|
+
local,
|
|
509
|
+
center,
|
|
510
|
+
width,
|
|
511
|
+
height,
|
|
512
|
+
PcbInteractionPrimitiveModel.#number(primitive.radius, 0),
|
|
513
|
+
tolerance
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
return Math.abs(local.x - center.x) <= width / 2 + tolerance &&
|
|
517
|
+
Math.abs(local.y - center.y) <= height / 2 + tolerance
|
|
518
|
+
? 0
|
|
519
|
+
: null
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Tests a point against a horizontal or vertical capsule.
|
|
524
|
+
* @param {{ x: number, y: number }} point Local point.
|
|
525
|
+
* @param {{ x: number, y: number }} center Shape center.
|
|
526
|
+
* @param {number} width Shape width.
|
|
527
|
+
* @param {number} height Shape height.
|
|
528
|
+
* @param {number} tolerance Hit tolerance.
|
|
529
|
+
* @returns {number | null} Hit distance or null.
|
|
530
|
+
*/
|
|
531
|
+
static #capsuleHit(point, center, width, height, tolerance) {
|
|
532
|
+
const radius = Math.min(width, height) / 2
|
|
533
|
+
const horizontal = width >= height
|
|
534
|
+
const halfSegment = Math.abs(width - height) / 2
|
|
535
|
+
const start = horizontal
|
|
536
|
+
? { x: center.x - halfSegment, y: center.y }
|
|
537
|
+
: { x: center.x, y: center.y - halfSegment }
|
|
538
|
+
const end = horizontal
|
|
539
|
+
? { x: center.x + halfSegment, y: center.y }
|
|
540
|
+
: { x: center.x, y: center.y + halfSegment }
|
|
541
|
+
return PcbInteractionPrimitiveModel.#distanceToSegment(
|
|
542
|
+
point,
|
|
543
|
+
start,
|
|
544
|
+
end
|
|
545
|
+
) <=
|
|
546
|
+
radius + tolerance
|
|
547
|
+
? 0
|
|
548
|
+
: null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Tests one rounded rectangle in local coordinates.
|
|
553
|
+
* @param {{ x: number, y: number }} point Local point.
|
|
554
|
+
* @param {{ x: number, y: number }} center Shape center.
|
|
555
|
+
* @param {number} width Shape width.
|
|
556
|
+
* @param {number} height Shape height.
|
|
557
|
+
* @param {number} radius Requested corner radius.
|
|
558
|
+
* @param {number} tolerance Hit tolerance.
|
|
559
|
+
* @returns {number | null} Hit distance or null.
|
|
560
|
+
*/
|
|
561
|
+
static #roundedRectHit(point, center, width, height, radius, tolerance) {
|
|
562
|
+
const corner = Math.max(0, Math.min(radius, width / 2, height / 2))
|
|
563
|
+
const x = Math.max(
|
|
564
|
+
Math.abs(point.x - center.x) - (width / 2 - corner),
|
|
565
|
+
0
|
|
566
|
+
)
|
|
567
|
+
const y = Math.max(
|
|
568
|
+
Math.abs(point.y - center.y) - (height / 2 - corner),
|
|
569
|
+
0
|
|
570
|
+
)
|
|
571
|
+
return Math.hypot(x, y) <= corner + tolerance ? 0 : null
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Tests a point against a polygon with edge tolerance.
|
|
576
|
+
* @param {{ x: number, y: number }[]} points Polygon points.
|
|
577
|
+
* @param {{ x: number, y: number }} point Board point.
|
|
578
|
+
* @param {number} tolerance Hit tolerance.
|
|
579
|
+
* @returns {number | null} Hit distance or null.
|
|
580
|
+
*/
|
|
581
|
+
static #polygonHit(points, point, tolerance) {
|
|
582
|
+
let inside = false
|
|
583
|
+
let distance = Infinity
|
|
584
|
+
for (
|
|
585
|
+
let index = 0, previous = points.length - 1;
|
|
586
|
+
index < points.length;
|
|
587
|
+
previous = index, index += 1
|
|
588
|
+
) {
|
|
589
|
+
const start = points[previous]
|
|
590
|
+
const end = points[index]
|
|
591
|
+
distance = Math.min(
|
|
592
|
+
distance,
|
|
593
|
+
PcbInteractionPrimitiveModel.#distanceToSegment(
|
|
594
|
+
point,
|
|
595
|
+
start,
|
|
596
|
+
end
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
if (
|
|
600
|
+
start.y > point.y !== end.y > point.y &&
|
|
601
|
+
point.x <
|
|
602
|
+
((end.x - start.x) * (point.y - start.y)) /
|
|
603
|
+
(end.y - start.y) +
|
|
604
|
+
start.x
|
|
605
|
+
) {
|
|
606
|
+
inside = !inside
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return inside || distance <= tolerance ? 0 : null
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Resolves valid polygon points from one primitive.
|
|
614
|
+
* @param {object} primitive PCB primitive.
|
|
615
|
+
* @returns {{ x: number, y: number }[]} Points.
|
|
616
|
+
*/
|
|
617
|
+
static #primitivePoints(primitive) {
|
|
618
|
+
return (Array.isArray(primitive?.points) ? primitive.points : [])
|
|
619
|
+
.map(PcbInteractionPrimitiveModel.#point)
|
|
620
|
+
.filter(Boolean)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Rotates a point around one center.
|
|
625
|
+
* @param {{ x: number, y: number }} point Point.
|
|
626
|
+
* @param {{ x: number, y: number }} center Center.
|
|
627
|
+
* @param {number} degrees Counter-clockwise degrees.
|
|
628
|
+
* @returns {{ x: number, y: number }} Rotated point.
|
|
629
|
+
*/
|
|
630
|
+
static #rotatePoint(point, center, degrees) {
|
|
631
|
+
if (!degrees) return point
|
|
632
|
+
const radians = (degrees * Math.PI) / 180
|
|
633
|
+
const cos = Math.cos(radians)
|
|
634
|
+
const sin = Math.sin(radians)
|
|
635
|
+
const x = point.x - center.x
|
|
636
|
+
const y = point.y - center.y
|
|
637
|
+
return {
|
|
638
|
+
x: center.x + x * cos - y * sin,
|
|
639
|
+
y: center.y + x * sin + y * cos
|
|
640
|
+
}
|
|
310
641
|
}
|
|
311
642
|
|
|
312
643
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BinaryDataSnapshot } from './context/BinaryDataSnapshot.mjs'
|
|
2
|
+
import { ToolkitAsset } from './contracts/ToolkitAsset.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Owns direct asynchronous project payloads before host callbacks run.
|
|
6
|
+
*/
|
|
7
|
+
export class ProjectAsyncInputOwner {
|
|
8
|
+
/**
|
|
9
|
+
* Owns candidate bytes and prepares companion assets once.
|
|
10
|
+
* @param {{ entries: object[] }} classified Classified entries.
|
|
11
|
+
* @param {'none' | 'metadata' | 'full'} assetMode Asset decode mode.
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
static own(classified, assetMode) {
|
|
15
|
+
const snapshots = new WeakMap()
|
|
16
|
+
for (const entry of classified.entries) {
|
|
17
|
+
if (entry.name.toLowerCase().endsWith('.json')) {
|
|
18
|
+
entry.input.data = ProjectAsyncInputOwner.#binary(
|
|
19
|
+
entry.input.data,
|
|
20
|
+
snapshots
|
|
21
|
+
)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
if (assetMode === 'none') continue
|
|
25
|
+
entry.companionAsset = ProjectAsyncInputOwner.#companionAsset(
|
|
26
|
+
entry,
|
|
27
|
+
assetMode
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Copies one binary value once while leaving immutable text unchanged.
|
|
34
|
+
* @param {unknown} value Entry data.
|
|
35
|
+
* @param {WeakMap<object, unknown>} snapshots Request-local binary copies.
|
|
36
|
+
* @returns {unknown} Owned data.
|
|
37
|
+
*/
|
|
38
|
+
static #binary(value, snapshots) {
|
|
39
|
+
if (typeof value === 'string') return value
|
|
40
|
+
if (value && typeof value === 'object' && snapshots.has(value)) {
|
|
41
|
+
return snapshots.get(value)
|
|
42
|
+
}
|
|
43
|
+
const owned = BinaryDataSnapshot.clone(value)
|
|
44
|
+
if (value && typeof value === 'object') snapshots.set(value, owned)
|
|
45
|
+
return owned
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prepares one companion asset as the final canonical payload snapshot.
|
|
50
|
+
* @param {object} entry Classified companion entry.
|
|
51
|
+
* @param {'metadata' | 'full'} mode Asset decode mode.
|
|
52
|
+
* @returns {Record<string, any>} Prepared companion asset.
|
|
53
|
+
*/
|
|
54
|
+
static #companionAsset(entry, mode) {
|
|
55
|
+
return ToolkitAsset.prepare(
|
|
56
|
+
{
|
|
57
|
+
kind: 'companion',
|
|
58
|
+
name: entry.name,
|
|
59
|
+
mediaType: 'application/octet-stream',
|
|
60
|
+
byteLength: entry.byteLength,
|
|
61
|
+
data: entry.input.data,
|
|
62
|
+
source: { entryName: entry.name }
|
|
63
|
+
},
|
|
64
|
+
{ mode }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Object.freeze(ProjectAsyncInputOwner.prototype)
|
|
70
|
+
Object.freeze(ProjectAsyncInputOwner)
|