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,89 @@
|
|
|
1
|
+
import { ToolkitError } from './ToolkitError.mjs'
|
|
2
|
+
|
|
3
|
+
const STAGES = ['detect', 'decode', 'project', 'validate', 'complete']
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates ordered shared progress records.
|
|
7
|
+
*/
|
|
8
|
+
export class ToolkitProgress {
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes one progress update and validates it against the previous row.
|
|
11
|
+
* @param {Record<string, any>} fields Progress fields.
|
|
12
|
+
* @param {Record<string, any> | null} [previous] Previous progress row.
|
|
13
|
+
* @returns {Record<string, any>} Clone-safe progress row.
|
|
14
|
+
*/
|
|
15
|
+
static create(fields = {}, previous = null) {
|
|
16
|
+
const stage = String(fields.stage || '')
|
|
17
|
+
const stageIndex = STAGES.indexOf(stage)
|
|
18
|
+
if (stageIndex < 0) {
|
|
19
|
+
throw ToolkitProgress.#error(
|
|
20
|
+
'ERR_PROGRESS_STAGE',
|
|
21
|
+
`Unknown progress stage: ${stage || '(empty)'}`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
if (previous?.stage === 'complete') {
|
|
25
|
+
throw ToolkitProgress.#error(
|
|
26
|
+
'ERR_PROGRESS_TERMINAL',
|
|
27
|
+
'Progress cannot continue after complete.'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
const previousIndex = previous ? STAGES.indexOf(previous.stage) : -1
|
|
31
|
+
if (previous && (previousIndex < 0 || stageIndex < previousIndex)) {
|
|
32
|
+
throw ToolkitProgress.#error(
|
|
33
|
+
'ERR_PROGRESS_ORDER',
|
|
34
|
+
`Progress cannot move from ${previous.stage} to ${stage}.`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const completed = ToolkitProgress.#count(fields.completed, null)
|
|
39
|
+
const total = ToolkitProgress.#count(fields.total, null)
|
|
40
|
+
if (completed !== null && total !== null && completed > total) {
|
|
41
|
+
throw ToolkitProgress.#error(
|
|
42
|
+
'ERR_PROGRESS_COUNT',
|
|
43
|
+
'Progress completed count exceeds total.'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = { stage }
|
|
48
|
+
if (fields.detail != null && String(fields.detail)) {
|
|
49
|
+
result.detail = String(fields.detail)
|
|
50
|
+
}
|
|
51
|
+
if (fields.completed !== undefined) result.completed = completed
|
|
52
|
+
if (fields.total !== undefined) result.total = total
|
|
53
|
+
if (fields.message != null && String(fields.message)) {
|
|
54
|
+
result.message = String(fields.message)
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalizes a nullable non-negative count.
|
|
61
|
+
* @param {unknown} value Count candidate.
|
|
62
|
+
* @param {number | null} fallback Fallback value.
|
|
63
|
+
* @returns {number | null} Normalized count.
|
|
64
|
+
*/
|
|
65
|
+
static #count(value, fallback) {
|
|
66
|
+
if (value == null) return fallback
|
|
67
|
+
const count = Number(value)
|
|
68
|
+
if (!Number.isFinite(count) || count < 0) {
|
|
69
|
+
throw ToolkitProgress.#error(
|
|
70
|
+
'ERR_PROGRESS_COUNT',
|
|
71
|
+
'Progress counts must be finite and non-negative.'
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return count
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a progress validation error.
|
|
79
|
+
* @param {string} code Error code.
|
|
80
|
+
* @param {string} message Error message.
|
|
81
|
+
* @returns {ToolkitError} Typed error.
|
|
82
|
+
*/
|
|
83
|
+
static #error(code, message) {
|
|
84
|
+
return new ToolkitError(message, {
|
|
85
|
+
code,
|
|
86
|
+
category: 'runtime'
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { ToolkitError } from '../contracts/ToolkitError.mjs'
|
|
2
|
+
|
|
3
|
+
const SIDES = new Set(['top', 'bottom'])
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes safe reusable PCB interaction options.
|
|
7
|
+
*/
|
|
8
|
+
export class CanonicalInteractionOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes a safe finite CircuitJSON millimeter point.
|
|
11
|
+
* @param {unknown} point Point candidate.
|
|
12
|
+
* @returns {{ x: number, y: number }} Normalized point.
|
|
13
|
+
*/
|
|
14
|
+
static point(point) {
|
|
15
|
+
if (!point || typeof point !== 'object') {
|
|
16
|
+
throw CanonicalInteractionOptions.error(
|
|
17
|
+
'Interaction point must be a plain finite x/y object.'
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
let prototype
|
|
21
|
+
let descriptors
|
|
22
|
+
try {
|
|
23
|
+
if (Array.isArray(point)) throw new TypeError('array')
|
|
24
|
+
prototype = Object.getPrototypeOf(point)
|
|
25
|
+
descriptors = Object.getOwnPropertyDescriptors(point)
|
|
26
|
+
} catch {
|
|
27
|
+
throw CanonicalInteractionOptions.error(
|
|
28
|
+
'Interaction point could not be inspected safely.'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
const x = CanonicalInteractionOptions.#dataValue(descriptors.x)
|
|
32
|
+
const y = CanonicalInteractionOptions.#dataValue(descriptors.y)
|
|
33
|
+
if (
|
|
34
|
+
(prototype !== Object.prototype && prototype !== null) ||
|
|
35
|
+
typeof x !== 'number' ||
|
|
36
|
+
typeof y !== 'number' ||
|
|
37
|
+
!Number.isFinite(x) ||
|
|
38
|
+
!Number.isFinite(y)
|
|
39
|
+
) {
|
|
40
|
+
throw CanonicalInteractionOptions.error(
|
|
41
|
+
'Interaction point must be a plain finite x/y object.'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return { x, y }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes one option record over existing defaults.
|
|
49
|
+
* @param {unknown} options Option candidate.
|
|
50
|
+
* @param {Record<string, any>} [defaults] Existing normalized defaults.
|
|
51
|
+
* @returns {{ side: 'top' | 'bottom' | null, tolerance: number, hiddenLayers: string[], hiddenObjects: string[] }} Normalized options.
|
|
52
|
+
*/
|
|
53
|
+
static normalize(options = {}, defaults = {}) {
|
|
54
|
+
const record = CanonicalInteractionOptions.#record(options)
|
|
55
|
+
return {
|
|
56
|
+
side: CanonicalInteractionOptions.#side(
|
|
57
|
+
record.side,
|
|
58
|
+
defaults.side ?? null
|
|
59
|
+
),
|
|
60
|
+
tolerance: CanonicalInteractionOptions.#tolerance(
|
|
61
|
+
record.tolerance,
|
|
62
|
+
defaults.tolerance ?? 0.2
|
|
63
|
+
),
|
|
64
|
+
hiddenLayers: CanonicalInteractionOptions.#strings(
|
|
65
|
+
record.hiddenLayers,
|
|
66
|
+
defaults.hiddenLayers || [],
|
|
67
|
+
'hiddenLayers'
|
|
68
|
+
),
|
|
69
|
+
hiddenObjects: CanonicalInteractionOptions.#strings(
|
|
70
|
+
record.hiddenObjects,
|
|
71
|
+
defaults.hiddenObjects || [],
|
|
72
|
+
'hiddenObjects'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads supported own data fields without invoking accessors.
|
|
79
|
+
* @param {unknown} value Option candidate.
|
|
80
|
+
* @returns {Record<string, any>} Safe option record.
|
|
81
|
+
*/
|
|
82
|
+
static #record(value) {
|
|
83
|
+
if (!value || typeof value !== 'object') {
|
|
84
|
+
throw CanonicalInteractionOptions.error(
|
|
85
|
+
'Interaction options must be a plain object.'
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
let prototype
|
|
89
|
+
let descriptors
|
|
90
|
+
try {
|
|
91
|
+
if (Array.isArray(value)) throw new TypeError('array')
|
|
92
|
+
prototype = Object.getPrototypeOf(value)
|
|
93
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
94
|
+
} catch {
|
|
95
|
+
throw CanonicalInteractionOptions.error(
|
|
96
|
+
'Interaction options could not be inspected safely.'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
100
|
+
throw CanonicalInteractionOptions.error(
|
|
101
|
+
'Interaction options must be a plain object.'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
const allowed = new Set([
|
|
105
|
+
'hiddenLayers',
|
|
106
|
+
'hiddenObjects',
|
|
107
|
+
'side',
|
|
108
|
+
'tolerance'
|
|
109
|
+
])
|
|
110
|
+
const result = {}
|
|
111
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
112
|
+
const descriptor = descriptors[key]
|
|
113
|
+
if (
|
|
114
|
+
typeof key !== 'string' ||
|
|
115
|
+
!allowed.has(key) ||
|
|
116
|
+
descriptor.enumerable !== true ||
|
|
117
|
+
descriptor.get ||
|
|
118
|
+
descriptor.set
|
|
119
|
+
) {
|
|
120
|
+
throw CanonicalInteractionOptions.error(
|
|
121
|
+
'Interaction options contain an unsupported field.'
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
result[key] = descriptor.value
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalizes one optional board side.
|
|
131
|
+
* @param {unknown} value Side candidate.
|
|
132
|
+
* @param {'top' | 'bottom' | null} fallback Fallback side.
|
|
133
|
+
* @returns {'top' | 'bottom' | null} Normalized side.
|
|
134
|
+
*/
|
|
135
|
+
static #side(value, fallback) {
|
|
136
|
+
const side = value === undefined ? fallback : value
|
|
137
|
+
if (side !== null && !SIDES.has(side)) {
|
|
138
|
+
throw CanonicalInteractionOptions.error(
|
|
139
|
+
'Interaction side must be top or bottom.'
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return side
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Normalizes one nonnegative interaction tolerance.
|
|
147
|
+
* @param {unknown} value Tolerance candidate.
|
|
148
|
+
* @param {number} fallback Fallback tolerance.
|
|
149
|
+
* @returns {number} Normalized tolerance.
|
|
150
|
+
*/
|
|
151
|
+
static #tolerance(value, fallback) {
|
|
152
|
+
const tolerance = value === undefined ? fallback : value
|
|
153
|
+
if (
|
|
154
|
+
typeof tolerance !== 'number' ||
|
|
155
|
+
!Number.isFinite(tolerance) ||
|
|
156
|
+
tolerance < 0 ||
|
|
157
|
+
tolerance > 1000000
|
|
158
|
+
) {
|
|
159
|
+
throw CanonicalInteractionOptions.error(
|
|
160
|
+
'Interaction tolerance must be a bounded nonnegative number.'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
return tolerance
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Normalizes one unique string-list option through data descriptors.
|
|
168
|
+
* @param {unknown} value Array candidate.
|
|
169
|
+
* @param {string[]} fallback Fallback values.
|
|
170
|
+
* @param {string} name Option name.
|
|
171
|
+
* @returns {string[]} Normalized strings.
|
|
172
|
+
*/
|
|
173
|
+
static #strings(value, fallback, name) {
|
|
174
|
+
if (value === undefined) return [...fallback]
|
|
175
|
+
let descriptors
|
|
176
|
+
try {
|
|
177
|
+
if (
|
|
178
|
+
!Array.isArray(value) ||
|
|
179
|
+
Object.getPrototypeOf(value) !== Array.prototype
|
|
180
|
+
) {
|
|
181
|
+
throw new TypeError('array')
|
|
182
|
+
}
|
|
183
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
184
|
+
} catch {
|
|
185
|
+
throw CanonicalInteractionOptions.error(
|
|
186
|
+
`Interaction ${name} must be a plain string array.`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
const length = descriptors.length?.value
|
|
190
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 4096) {
|
|
191
|
+
throw CanonicalInteractionOptions.error(
|
|
192
|
+
`Interaction ${name} must be a bounded string array.`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
const result = []
|
|
196
|
+
for (let index = 0; index < length; index += 1) {
|
|
197
|
+
const descriptor = descriptors[String(index)]
|
|
198
|
+
if (
|
|
199
|
+
!descriptor ||
|
|
200
|
+
descriptor.get ||
|
|
201
|
+
descriptor.set ||
|
|
202
|
+
typeof descriptor.value !== 'string' ||
|
|
203
|
+
!descriptor.value.trim()
|
|
204
|
+
) {
|
|
205
|
+
throw CanonicalInteractionOptions.error(
|
|
206
|
+
`Interaction ${name} must contain non-empty strings.`
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
result.push(descriptor.value.trim())
|
|
210
|
+
}
|
|
211
|
+
const allowed = new Set([
|
|
212
|
+
'length',
|
|
213
|
+
...Array.from({ length }, (_entry, index) => String(index))
|
|
214
|
+
])
|
|
215
|
+
if (Reflect.ownKeys(descriptors).some((key) => !allowed.has(key))) {
|
|
216
|
+
throw CanonicalInteractionOptions.error(
|
|
217
|
+
`Interaction ${name} must be a plain string array.`
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
return [...new Set(result)]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns a descriptor's own data value without invoking accessors.
|
|
225
|
+
* @param {PropertyDescriptor | undefined} descriptor Field descriptor.
|
|
226
|
+
* @returns {unknown} Data value or undefined.
|
|
227
|
+
*/
|
|
228
|
+
static #dataValue(descriptor) {
|
|
229
|
+
return descriptor && !descriptor.get && !descriptor.set
|
|
230
|
+
? descriptor.value
|
|
231
|
+
: undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates one typed interaction-option error.
|
|
236
|
+
* @param {string} message Failure message.
|
|
237
|
+
* @returns {ToolkitError} Typed error.
|
|
238
|
+
*/
|
|
239
|
+
static error(message) {
|
|
240
|
+
return new ToolkitError(message, {
|
|
241
|
+
code: 'ERR_INTERACTION_OPTIONS',
|
|
242
|
+
category: 'validation',
|
|
243
|
+
format: 'circuitjson'
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves conservative broad-phase bounds from every exact geometry signal.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbInteractionBounds {
|
|
5
|
+
/**
|
|
6
|
+
* Unions stored, point-list, segment-stroke, and rotated shape bounds.
|
|
7
|
+
* @param {object} primitive Prepared PCB primitive.
|
|
8
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null} Conservative bounds.
|
|
9
|
+
*/
|
|
10
|
+
static resolve(primitive) {
|
|
11
|
+
const stored = PcbInteractionBounds.#bounds(primitive?.bounds)
|
|
12
|
+
const points = (
|
|
13
|
+
Array.isArray(primitive?.points) ? primitive.points : []
|
|
14
|
+
)
|
|
15
|
+
.map(PcbInteractionBounds.#point)
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
const pointBounds = points.length
|
|
18
|
+
? PcbInteractionBounds.#pointBounds(points)
|
|
19
|
+
: null
|
|
20
|
+
const segment = PcbInteractionBounds.#segmentBounds(primitive)
|
|
21
|
+
const shape = PcbInteractionBounds.#rotatedShapeBounds(primitive)
|
|
22
|
+
const signals = [stored, pointBounds, segment, shape].filter(Boolean)
|
|
23
|
+
return signals.length ? PcbInteractionBounds.#merge(signals) : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves stroke-expanded explicit segment bounds.
|
|
28
|
+
* @param {object} primitive Primitive candidate.
|
|
29
|
+
* @returns {object | null} Segment bounds.
|
|
30
|
+
*/
|
|
31
|
+
static #segmentBounds(primitive) {
|
|
32
|
+
const start = PcbInteractionBounds.#point({
|
|
33
|
+
x: primitive?.x1,
|
|
34
|
+
y: primitive?.y1
|
|
35
|
+
})
|
|
36
|
+
const end = PcbInteractionBounds.#point({
|
|
37
|
+
x: primitive?.x2,
|
|
38
|
+
y: primitive?.y2
|
|
39
|
+
})
|
|
40
|
+
if (!start || !end) return null
|
|
41
|
+
const halfWidth = Math.max(
|
|
42
|
+
PcbInteractionBounds.#number(primitive?.width, 0) / 2,
|
|
43
|
+
0
|
|
44
|
+
)
|
|
45
|
+
return {
|
|
46
|
+
minX: Math.min(start.x, end.x) - halfWidth,
|
|
47
|
+
minY: Math.min(start.y, end.y) - halfWidth,
|
|
48
|
+
maxX: Math.max(start.x, end.x) + halfWidth,
|
|
49
|
+
maxY: Math.max(start.y, end.y) + halfWidth
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves an axis-aligned envelope for one rotated center-size shape.
|
|
55
|
+
* @param {object} primitive Primitive candidate.
|
|
56
|
+
* @returns {object | null} Rotated shape bounds.
|
|
57
|
+
*/
|
|
58
|
+
static #rotatedShapeBounds(primitive) {
|
|
59
|
+
const center = PcbInteractionBounds.#point(primitive)
|
|
60
|
+
const width = PcbInteractionBounds.#finite(
|
|
61
|
+
primitive?.width ?? primitive?.diameter
|
|
62
|
+
)
|
|
63
|
+
const height = PcbInteractionBounds.#finite(
|
|
64
|
+
primitive?.height ?? primitive?.diameter ?? primitive?.width
|
|
65
|
+
)
|
|
66
|
+
if (!center || width === null || height === null) return null
|
|
67
|
+
const radians =
|
|
68
|
+
(PcbInteractionBounds.#number(primitive?.rotation, 0) * Math.PI) /
|
|
69
|
+
180
|
|
70
|
+
const cos = Math.abs(Math.cos(radians))
|
|
71
|
+
const sin = Math.abs(Math.sin(radians))
|
|
72
|
+
const halfWidth = (Math.abs(width) * cos + Math.abs(height) * sin) / 2
|
|
73
|
+
const halfHeight = (Math.abs(width) * sin + Math.abs(height) * cos) / 2
|
|
74
|
+
return {
|
|
75
|
+
minX: center.x - halfWidth,
|
|
76
|
+
minY: center.y - halfHeight,
|
|
77
|
+
maxX: center.x + halfWidth,
|
|
78
|
+
maxY: center.y + halfHeight
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves minimal bounds for normalized points.
|
|
84
|
+
* @param {{ x: number, y: number }[]} points Normalized points.
|
|
85
|
+
* @returns {object} Point bounds.
|
|
86
|
+
*/
|
|
87
|
+
static #pointBounds(points) {
|
|
88
|
+
return points.reduce(
|
|
89
|
+
(bounds, point) => ({
|
|
90
|
+
minX: Math.min(bounds.minX, point.x),
|
|
91
|
+
minY: Math.min(bounds.minY, point.y),
|
|
92
|
+
maxX: Math.max(bounds.maxX, point.x),
|
|
93
|
+
maxY: Math.max(bounds.maxY, point.y)
|
|
94
|
+
}),
|
|
95
|
+
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unions normalized bounds rows.
|
|
101
|
+
* @param {object[]} rows Bounds rows.
|
|
102
|
+
* @returns {object} Merged bounds.
|
|
103
|
+
*/
|
|
104
|
+
static #merge(rows) {
|
|
105
|
+
return rows.reduce(
|
|
106
|
+
(bounds, row) => ({
|
|
107
|
+
minX: Math.min(bounds.minX, row.minX),
|
|
108
|
+
minY: Math.min(bounds.minY, row.minY),
|
|
109
|
+
maxX: Math.max(bounds.maxX, row.maxX),
|
|
110
|
+
maxY: Math.max(bounds.maxY, row.maxY)
|
|
111
|
+
}),
|
|
112
|
+
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalizes a point candidate.
|
|
118
|
+
* @param {unknown} value Point candidate.
|
|
119
|
+
* @returns {{ x: number, y: number } | null} Point or null.
|
|
120
|
+
*/
|
|
121
|
+
static #point(value) {
|
|
122
|
+
const x = PcbInteractionBounds.#finite(value?.x)
|
|
123
|
+
const y = PcbInteractionBounds.#finite(value?.y)
|
|
124
|
+
return x === null || y === null ? null : { x, y }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalizes ordered rectangle bounds.
|
|
129
|
+
* @param {unknown} value Bounds candidate.
|
|
130
|
+
* @returns {object | null} Bounds or null.
|
|
131
|
+
*/
|
|
132
|
+
static #bounds(value) {
|
|
133
|
+
const minX = PcbInteractionBounds.#finite(value?.minX)
|
|
134
|
+
const minY = PcbInteractionBounds.#finite(value?.minY)
|
|
135
|
+
const maxX = PcbInteractionBounds.#finite(value?.maxX)
|
|
136
|
+
const maxY = PcbInteractionBounds.#finite(value?.maxY)
|
|
137
|
+
return minX !== null &&
|
|
138
|
+
minY !== null &&
|
|
139
|
+
maxX !== null &&
|
|
140
|
+
maxY !== null &&
|
|
141
|
+
minX <= maxX &&
|
|
142
|
+
minY <= maxY
|
|
143
|
+
? { minX, minY, maxX, maxY }
|
|
144
|
+
: null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Converts one value to a finite number or null.
|
|
149
|
+
* @param {unknown} value Number candidate.
|
|
150
|
+
* @returns {number | null} Finite number or null.
|
|
151
|
+
*/
|
|
152
|
+
static #finite(value) {
|
|
153
|
+
if (value === undefined || value === null || value === '') return null
|
|
154
|
+
const number = Number(value)
|
|
155
|
+
return Number.isFinite(number) ? number : null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Converts one value to a finite number with fallback.
|
|
160
|
+
* @param {unknown} value Number candidate.
|
|
161
|
+
* @param {number} fallback Fallback number.
|
|
162
|
+
* @returns {number} Finite number.
|
|
163
|
+
*/
|
|
164
|
+
static #number(value, fallback) {
|
|
165
|
+
return PcbInteractionBounds.#finite(value) ?? fallback
|
|
166
|
+
}
|
|
167
|
+
}
|