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,4 +1,5 @@
|
|
|
1
1
|
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonLegacyNormalizer } from './context/CircuitJsonLegacyNormalizer.mjs'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Builds PCB primitive element indexes with a tolerant local fallback.
|
|
@@ -13,10 +14,25 @@ export class CircuitJsonPcbPrimitiveIndex {
|
|
|
13
14
|
try {
|
|
14
15
|
return CircuitJsonIndexer.index(elements)
|
|
15
16
|
} catch (error) {
|
|
16
|
-
|
|
17
|
+
const normalized = CircuitJsonLegacyNormalizer.normalize(elements)
|
|
18
|
+
if (normalized === elements) {
|
|
19
|
+
if (CircuitJsonPcbPrimitiveIndex.#isUnknownTypeError(error)) {
|
|
20
|
+
return CircuitJsonPcbPrimitiveIndex.#localIndex(elements)
|
|
21
|
+
}
|
|
17
22
|
throw error
|
|
18
23
|
}
|
|
19
|
-
|
|
24
|
+
try {
|
|
25
|
+
return CircuitJsonIndexer.index(normalized)
|
|
26
|
+
} catch (normalizedError) {
|
|
27
|
+
if (
|
|
28
|
+
CircuitJsonPcbPrimitiveIndex.#isUnknownTypeError(
|
|
29
|
+
normalizedError
|
|
30
|
+
)
|
|
31
|
+
) {
|
|
32
|
+
return CircuitJsonPcbPrimitiveIndex.#localIndex(normalized)
|
|
33
|
+
}
|
|
34
|
+
throw normalizedError
|
|
35
|
+
}
|
|
20
36
|
}
|
|
21
37
|
}
|
|
22
38
|
|
|
@@ -355,9 +355,15 @@ export class CircuitJsonPcbPrimitiveOverlays {
|
|
|
355
355
|
pcb_trace_ids:
|
|
356
356
|
diagnostic?.pcbTraceIds || diagnostic?.pcb_trace_ids,
|
|
357
357
|
pcb_smtpad_id:
|
|
358
|
-
diagnostic?.pcbSmtpadId ||
|
|
358
|
+
diagnostic?.pcbSmtpadId ||
|
|
359
|
+
diagnostic?.pcb_smtpad_id ||
|
|
360
|
+
diagnostic?.pcbPadId ||
|
|
361
|
+
diagnostic?.pcb_pad_id,
|
|
359
362
|
pcb_smtpad_ids:
|
|
360
|
-
diagnostic?.pcbSmtpadIds ||
|
|
363
|
+
diagnostic?.pcbSmtpadIds ||
|
|
364
|
+
diagnostic?.pcb_smtpad_ids ||
|
|
365
|
+
diagnostic?.pcbPadIds ||
|
|
366
|
+
diagnostic?.pcb_pad_ids,
|
|
361
367
|
pcb_via_id: diagnostic?.pcbViaId || diagnostic?.pcb_via_id,
|
|
362
368
|
pcb_via_ids: diagnostic?.pcbViaIds || diagnostic?.pcb_via_ids,
|
|
363
369
|
pcb_plated_hole_id:
|
|
@@ -646,7 +652,12 @@ export class CircuitJsonPcbPrimitiveOverlays {
|
|
|
646
652
|
['pcb_trace_id', [element?.pcb_trace_id, element?.pcb_trace_ids]],
|
|
647
653
|
[
|
|
648
654
|
'pcb_smtpad_id',
|
|
649
|
-
[
|
|
655
|
+
[
|
|
656
|
+
element?.pcb_smtpad_id,
|
|
657
|
+
element?.pcb_smtpad_ids,
|
|
658
|
+
element?.pcb_pad_id,
|
|
659
|
+
element?.pcb_pad_ids
|
|
660
|
+
]
|
|
650
661
|
],
|
|
651
662
|
['pcb_via_id', [element?.pcb_via_id, element?.pcb_via_ids]],
|
|
652
663
|
[
|
|
@@ -762,12 +773,18 @@ export class CircuitJsonPcbPrimitiveOverlays {
|
|
|
762
773
|
* @returns {object | null}
|
|
763
774
|
*/
|
|
764
775
|
static #mergeBounds(rows) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
776
|
+
let minX = Infinity
|
|
777
|
+
let minY = Infinity
|
|
778
|
+
let maxX = -Infinity
|
|
779
|
+
let maxY = -Infinity
|
|
780
|
+
for (const bounds of rows) {
|
|
781
|
+
if (!bounds) continue
|
|
782
|
+
minX = Math.min(minX, bounds.minX)
|
|
783
|
+
minY = Math.min(minY, bounds.minY)
|
|
784
|
+
maxX = Math.max(maxX, bounds.maxX)
|
|
785
|
+
maxY = Math.max(maxY, bounds.maxY)
|
|
786
|
+
}
|
|
787
|
+
if (!Number.isFinite(minX)) return null
|
|
771
788
|
return {
|
|
772
789
|
minX: CircuitJsonPcbPrimitiveOverlays.#round(minX),
|
|
773
790
|
minY: CircuitJsonPcbPrimitiveOverlays.#round(minY),
|
|
@@ -325,12 +325,13 @@ export class CircuitJsonPcbZonePrimitiveBuilder {
|
|
|
325
325
|
* @returns {object[]}
|
|
326
326
|
*/
|
|
327
327
|
static #brepShapes(element) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
element
|
|
333
|
-
|
|
328
|
+
if (Array.isArray(element?.brep_shapes) && element.brep_shapes.length) {
|
|
329
|
+
return element.brep_shapes
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(element?.brepShapes) && element.brepShapes.length) {
|
|
332
|
+
return element.brepShapes
|
|
333
|
+
}
|
|
334
|
+
return [element?.brep_shape, element?.brepShape].filter(Boolean)
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
/**
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { CIRCUIT_JSON_UPSTREAM_DEFAULT_ID_FIELDS } from './CircuitJsonUpstreamSchema.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reports upstream defaults intentionally left unmaterialized by raw parsing.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonSerializedInputAudit {
|
|
7
|
+
/**
|
|
8
|
+
* Audits missing random upstream identity defaults without mutating input.
|
|
9
|
+
* @param {unknown} model Serialized CircuitJSON model candidate.
|
|
10
|
+
* @param {string} [source] Source identity for diagnostics.
|
|
11
|
+
* @returns {{ diagnostics: object[], statistics: Record<string, number> }} Audit rows.
|
|
12
|
+
*/
|
|
13
|
+
static inspect(model, source = '') {
|
|
14
|
+
if (!Array.isArray(model)) {
|
|
15
|
+
return { diagnostics: [], statistics: {} }
|
|
16
|
+
}
|
|
17
|
+
let length
|
|
18
|
+
try {
|
|
19
|
+
length = Object.getOwnPropertyDescriptor(model, 'length')?.value
|
|
20
|
+
} catch {
|
|
21
|
+
return { diagnostics: [], statistics: {} }
|
|
22
|
+
}
|
|
23
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
24
|
+
return { diagnostics: [], statistics: {} }
|
|
25
|
+
}
|
|
26
|
+
const diagnostics = []
|
|
27
|
+
for (let index = 0; index < length; index += 1) {
|
|
28
|
+
const element = CircuitJsonSerializedInputAudit.#dataField(
|
|
29
|
+
model,
|
|
30
|
+
String(index)
|
|
31
|
+
)
|
|
32
|
+
const type = CircuitJsonSerializedInputAudit.#dataField(
|
|
33
|
+
element,
|
|
34
|
+
'type'
|
|
35
|
+
)
|
|
36
|
+
if (typeof type !== 'string') continue
|
|
37
|
+
const idField = CIRCUIT_JSON_UPSTREAM_DEFAULT_ID_FIELDS[type]
|
|
38
|
+
if (!idField) continue
|
|
39
|
+
const identity = CircuitJsonSerializedInputAudit.#dataField(
|
|
40
|
+
element,
|
|
41
|
+
idField
|
|
42
|
+
)
|
|
43
|
+
if (identity !== undefined) continue
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
code: 'CIRCUITJSON_UPSTREAM_DEFAULT_ID_OMITTED',
|
|
46
|
+
severity: 'warning',
|
|
47
|
+
message:
|
|
48
|
+
`Serialized CircuitJSON omitted ${idField}; ` +
|
|
49
|
+
'the random upstream default was not materialized.',
|
|
50
|
+
source,
|
|
51
|
+
location: { elementIndex: index },
|
|
52
|
+
details: {
|
|
53
|
+
elementType: type,
|
|
54
|
+
idField,
|
|
55
|
+
materialized: false
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
diagnostics,
|
|
61
|
+
statistics: diagnostics.length
|
|
62
|
+
? { upstreamDefaultIdentityOmissions: diagnostics.length }
|
|
63
|
+
: {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads one own data field without invoking caller accessors.
|
|
69
|
+
* @param {unknown} owner Field owner.
|
|
70
|
+
* @param {string} key Field name.
|
|
71
|
+
* @returns {unknown} Data value or undefined.
|
|
72
|
+
*/
|
|
73
|
+
static #dataField(owner, key) {
|
|
74
|
+
if (!owner || typeof owner !== 'object') return undefined
|
|
75
|
+
try {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, key)
|
|
77
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
78
|
+
? descriptor.value
|
|
79
|
+
: undefined
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Object.freeze(CircuitJsonSerializedInputAudit.prototype)
|
|
87
|
+
Object.freeze(CircuitJsonSerializedInputAudit)
|
|
@@ -185,7 +185,11 @@ export class CircuitJsonSourceMetadata {
|
|
|
185
185
|
*/
|
|
186
186
|
static #supplierPartNumbers(component) {
|
|
187
187
|
const numbers = {}
|
|
188
|
-
for (const field of [
|
|
188
|
+
for (const field of [
|
|
189
|
+
'supplier_part_numbers',
|
|
190
|
+
'supplierPartNumbers',
|
|
191
|
+
'legacy_supplier_part_numbers'
|
|
192
|
+
]) {
|
|
189
193
|
const value = component?.[field]
|
|
190
194
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
191
195
|
continue
|
|
@@ -56,12 +56,14 @@ const SCHEMATIC_RENDERED_TYPES = new Set([
|
|
|
56
56
|
'schematic_component',
|
|
57
57
|
'schematic_debug_object',
|
|
58
58
|
'schematic_group',
|
|
59
|
+
'schematic_image',
|
|
59
60
|
'schematic_line',
|
|
60
61
|
'schematic_net_label',
|
|
61
62
|
'schematic_path',
|
|
62
63
|
'schematic_port',
|
|
63
64
|
'schematic_rect',
|
|
64
65
|
'schematic_sheet',
|
|
66
|
+
'schematic_sheet_symbol',
|
|
65
67
|
'schematic_symbol',
|
|
66
68
|
'schematic_table',
|
|
67
69
|
'schematic_table_cell',
|
|
@@ -171,7 +173,7 @@ export class CircuitJsonSupportMatrixBuilder {
|
|
|
171
173
|
.map((element) => String(element?.type || ''))
|
|
172
174
|
.filter(Boolean)
|
|
173
175
|
)
|
|
174
|
-
const rows = CircuitJsonElementValidator.
|
|
176
|
+
const rows = CircuitJsonElementValidator.canonicalElementTypes().map(
|
|
175
177
|
(type) =>
|
|
176
178
|
CircuitJsonSupportMatrixBuilder.#row(
|
|
177
179
|
type,
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const TOOLKIT_ELEMENT_TYPES = new Set([
|
|
2
|
+
'schematic_image',
|
|
3
|
+
'schematic_sheet_symbol'
|
|
4
|
+
])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates canonical toolkit element types that are newer than the pinned
|
|
8
|
+
* upstream CircuitJSON union.
|
|
9
|
+
*/
|
|
10
|
+
export class CircuitJsonToolkitElementSchema {
|
|
11
|
+
/**
|
|
12
|
+
* Returns the toolkit-owned canonical element types.
|
|
13
|
+
* @returns {string[]} Stable element type names.
|
|
14
|
+
*/
|
|
15
|
+
static elementTypes() {
|
|
16
|
+
return [...TOOLKIT_ELEMENT_TYPES]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns whether a type is owned by the canonical toolkit overlay.
|
|
21
|
+
* @param {unknown} type Element type.
|
|
22
|
+
* @returns {boolean} Whether the type is recognized.
|
|
23
|
+
*/
|
|
24
|
+
static has(type) {
|
|
25
|
+
return typeof type === 'string' && TOOLKIT_ELEMENT_TYPES.has(type)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates one toolkit-owned canonical element.
|
|
30
|
+
* @param {Record<string, any>} value Element value.
|
|
31
|
+
* @param {string} type Element type.
|
|
32
|
+
* @param {string} [location] Human-readable location suffix.
|
|
33
|
+
* @returns {string[]} Empty on success or one validation error.
|
|
34
|
+
*/
|
|
35
|
+
static validate(value, type, location = '') {
|
|
36
|
+
const valid =
|
|
37
|
+
(type === 'schematic_image' &&
|
|
38
|
+
CircuitJsonToolkitElementSchema.#schematicImage(value)) ||
|
|
39
|
+
(type === 'schematic_sheet_symbol' &&
|
|
40
|
+
CircuitJsonToolkitElementSchema.#schematicSheetSymbol(value))
|
|
41
|
+
return valid
|
|
42
|
+
? []
|
|
43
|
+
: [
|
|
44
|
+
`CircuitJSON element ${type}${location} does not match the canonical toolkit schema.`
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validates an asset-backed schematic image.
|
|
50
|
+
* @param {Record<string, any>} value Image element.
|
|
51
|
+
* @returns {boolean} Whether the complete image contract matches.
|
|
52
|
+
*/
|
|
53
|
+
static #schematicImage(value) {
|
|
54
|
+
if (
|
|
55
|
+
value.type !== 'schematic_image' ||
|
|
56
|
+
!CircuitJsonToolkitElementSchema.#requiredString(
|
|
57
|
+
value.schematic_image_id
|
|
58
|
+
) ||
|
|
59
|
+
!CircuitJsonToolkitElementSchema.#requiredString(value.asset_id) ||
|
|
60
|
+
!CircuitJsonToolkitElementSchema.#point(value.center) ||
|
|
61
|
+
!CircuitJsonToolkitElementSchema.#size(value.size)
|
|
62
|
+
) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
for (const field of [
|
|
66
|
+
'schematic_sheet_id',
|
|
67
|
+
'schematic_component_id',
|
|
68
|
+
'subcircuit_id',
|
|
69
|
+
'source_path',
|
|
70
|
+
'source_name'
|
|
71
|
+
]) {
|
|
72
|
+
if (
|
|
73
|
+
value[field] !== undefined &&
|
|
74
|
+
typeof value[field] !== 'string'
|
|
75
|
+
) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
value.rotation !== undefined &&
|
|
81
|
+
!CircuitJsonToolkitElementSchema.#number(value.rotation)
|
|
82
|
+
) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
value.opacity !== undefined &&
|
|
87
|
+
(!CircuitJsonToolkitElementSchema.#number(value.opacity) ||
|
|
88
|
+
value.opacity < 0 ||
|
|
89
|
+
value.opacity > 1)
|
|
90
|
+
) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
value.preserve_aspect_ratio !== undefined &&
|
|
95
|
+
typeof value.preserve_aspect_ratio !== 'boolean'
|
|
96
|
+
) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
return (
|
|
100
|
+
value.render_order === undefined ||
|
|
101
|
+
Number.isSafeInteger(value.render_order)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validates one hierarchical child-sheet symbol.
|
|
107
|
+
* @param {Record<string, any>} value Sheet-symbol element.
|
|
108
|
+
* @returns {boolean} Whether the complete symbol contract matches.
|
|
109
|
+
*/
|
|
110
|
+
static #schematicSheetSymbol(value) {
|
|
111
|
+
if (
|
|
112
|
+
value.type !== 'schematic_sheet_symbol' ||
|
|
113
|
+
!CircuitJsonToolkitElementSchema.#requiredString(
|
|
114
|
+
value.schematic_sheet_symbol_id
|
|
115
|
+
) ||
|
|
116
|
+
typeof value.name !== 'string' ||
|
|
117
|
+
!CircuitJsonToolkitElementSchema.#point(value.center) ||
|
|
118
|
+
!CircuitJsonToolkitElementSchema.#positive(value.width) ||
|
|
119
|
+
!CircuitJsonToolkitElementSchema.#positive(value.height)
|
|
120
|
+
) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
for (const field of [
|
|
124
|
+
'schematic_sheet_id',
|
|
125
|
+
'schematic_group_id',
|
|
126
|
+
'subcircuit_id',
|
|
127
|
+
'source_file_name',
|
|
128
|
+
'color',
|
|
129
|
+
'fill_color'
|
|
130
|
+
]) {
|
|
131
|
+
if (
|
|
132
|
+
value[field] !== undefined &&
|
|
133
|
+
typeof value[field] !== 'string'
|
|
134
|
+
) {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const field of ['is_dashed', 'is_filled']) {
|
|
139
|
+
if (
|
|
140
|
+
value[field] !== undefined &&
|
|
141
|
+
typeof value[field] !== 'boolean'
|
|
142
|
+
) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
value.stroke_width !== undefined &&
|
|
148
|
+
(!CircuitJsonToolkitElementSchema.#number(value.stroke_width) ||
|
|
149
|
+
value.stroke_width < 0)
|
|
150
|
+
) {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
return (
|
|
154
|
+
value.render_order === undefined ||
|
|
155
|
+
Number.isSafeInteger(value.render_order)
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validates a required non-empty string.
|
|
161
|
+
* @param {unknown} value Candidate.
|
|
162
|
+
* @returns {boolean} Whether the value is valid.
|
|
163
|
+
*/
|
|
164
|
+
static #requiredString(value) {
|
|
165
|
+
return typeof value === 'string' && value.length > 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validates a finite number.
|
|
170
|
+
* @param {unknown} value Candidate.
|
|
171
|
+
* @returns {boolean} Whether the value is valid.
|
|
172
|
+
*/
|
|
173
|
+
static #number(value) {
|
|
174
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validates a positive finite number.
|
|
179
|
+
* @param {unknown} value Candidate.
|
|
180
|
+
* @returns {boolean} Whether the value is valid.
|
|
181
|
+
*/
|
|
182
|
+
static #positive(value) {
|
|
183
|
+
return CircuitJsonToolkitElementSchema.#number(value) && value > 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validates one finite point.
|
|
188
|
+
* @param {unknown} value Candidate.
|
|
189
|
+
* @returns {boolean} Whether the point is valid.
|
|
190
|
+
*/
|
|
191
|
+
static #point(value) {
|
|
192
|
+
return Boolean(
|
|
193
|
+
value &&
|
|
194
|
+
typeof value === 'object' &&
|
|
195
|
+
!Array.isArray(value) &&
|
|
196
|
+
CircuitJsonToolkitElementSchema.#number(value.x) &&
|
|
197
|
+
CircuitJsonToolkitElementSchema.#number(value.y)
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validates one positive image size.
|
|
203
|
+
* @param {unknown} value Candidate.
|
|
204
|
+
* @returns {boolean} Whether the size is valid.
|
|
205
|
+
*/
|
|
206
|
+
static #size(value) {
|
|
207
|
+
return Boolean(
|
|
208
|
+
value &&
|
|
209
|
+
typeof value === 'object' &&
|
|
210
|
+
!Array.isArray(value) &&
|
|
211
|
+
CircuitJsonToolkitElementSchema.#positive(value.width) &&
|
|
212
|
+
CircuitJsonToolkitElementSchema.#positive(value.height)
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Object.freeze(CircuitJsonToolkitElementSchema.prototype)
|
|
218
|
+
Object.freeze(CircuitJsonToolkitElementSchema)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const LENGTH_FACTORS_TO_MM = new Map([
|
|
2
|
+
['mm', 1],
|
|
3
|
+
['millimeter', 1],
|
|
4
|
+
['millimeters', 1],
|
|
5
|
+
['cm', 10],
|
|
6
|
+
['centimeter', 10],
|
|
7
|
+
['centimeters', 10],
|
|
8
|
+
['m', 1000],
|
|
9
|
+
['meter', 1000],
|
|
10
|
+
['meters', 1000],
|
|
11
|
+
['in', 25.4],
|
|
12
|
+
['inch', 25.4],
|
|
13
|
+
['inches', 25.4],
|
|
14
|
+
['mil', 0.0254],
|
|
15
|
+
['mils', 0.0254],
|
|
16
|
+
['um', 0.001],
|
|
17
|
+
['micrometer', 0.001],
|
|
18
|
+
['micrometers', 0.001]
|
|
19
|
+
])
|
|
20
|
+
const ANGLE_FACTORS_TO_DEG = new Map([
|
|
21
|
+
['deg', 1],
|
|
22
|
+
['degree', 1],
|
|
23
|
+
['degrees', 1],
|
|
24
|
+
['rad', 180 / Math.PI],
|
|
25
|
+
['radian', 180 / Math.PI],
|
|
26
|
+
['radians', 180 / Math.PI]
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rounds unit conversions to stable precision.
|
|
31
|
+
* @param {number} value Numeric value.
|
|
32
|
+
* @returns {number} Rounded value.
|
|
33
|
+
*/
|
|
34
|
+
export function round(value) {
|
|
35
|
+
return Math.round(value * 1_000_000) / 1_000_000
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parses one numeric value with an optional unit suffix.
|
|
40
|
+
* @param {unknown} value Value candidate.
|
|
41
|
+
* @param {Map<string, number>} unitFactors Unit factor lookup.
|
|
42
|
+
* @returns {number | null} Parsed value or null.
|
|
43
|
+
*/
|
|
44
|
+
function parseUnitValue(value, unitFactors) {
|
|
45
|
+
if (typeof value === 'number') {
|
|
46
|
+
return Number.isFinite(value) ? round(value) : null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const text = String(value ?? '').trim()
|
|
50
|
+
if (!text) return null
|
|
51
|
+
const match = text.match(
|
|
52
|
+
/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]+)?$/iu
|
|
53
|
+
)
|
|
54
|
+
if (!match) return null
|
|
55
|
+
|
|
56
|
+
const number = Number(match[1])
|
|
57
|
+
if (!Number.isFinite(number)) return null
|
|
58
|
+
const unit = String(match[2] || '').toLowerCase()
|
|
59
|
+
const factor = unit ? unitFactors.get(unit) : 1
|
|
60
|
+
return Number.isFinite(factor) ? round(number * factor) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parses an optional millimeter length.
|
|
65
|
+
* @param {unknown} value Length candidate.
|
|
66
|
+
* @returns {number | null} Parsed length or null.
|
|
67
|
+
*/
|
|
68
|
+
export function optionalLength(value) {
|
|
69
|
+
return parseUnitValue(value, LENGTH_FACTORS_TO_MM)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses an optional degree angle.
|
|
74
|
+
* @param {unknown} value Angle candidate.
|
|
75
|
+
* @returns {number | null} Parsed angle or null.
|
|
76
|
+
*/
|
|
77
|
+
export function optionalAngle(value) {
|
|
78
|
+
return parseUnitValue(value, ANGLE_FACTORS_TO_DEG)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parses an optional point.
|
|
83
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
84
|
+
* @returns {{ x: number, y: number } | null} Parsed point or null.
|
|
85
|
+
*/
|
|
86
|
+
export function optionalPoint(point) {
|
|
87
|
+
const x = optionalLength(point?.x)
|
|
88
|
+
const y = optionalLength(point?.y)
|
|
89
|
+
return x === null || y === null ? null : { x, y }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parses an optional size.
|
|
94
|
+
* @param {{ width?: unknown, height?: unknown } | null | undefined} size Size.
|
|
95
|
+
* @returns {{ width: number, height: number } | null} Parsed size or null.
|
|
96
|
+
*/
|
|
97
|
+
export function optionalSize(size) {
|
|
98
|
+
const width = optionalLength(size?.width)
|
|
99
|
+
const height = optionalLength(size?.height)
|
|
100
|
+
return width === null || height === null ? null : { width, height }
|
|
101
|
+
}
|