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,304 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from '../core/context/CircuitJsonDocumentContext.mjs'
|
|
2
|
+
import { CanonicalBomRows } from '../core/rendering/CanonicalBomRows.mjs'
|
|
3
|
+
import { CanonicalBomOrder } from '../core/rendering/CanonicalBomOrder.mjs'
|
|
4
|
+
import { CanonicalRenderOptions } from '../core/rendering/CanonicalRenderOptions.mjs'
|
|
5
|
+
import { SafeXmlText } from './SafeXmlText.mjs'
|
|
6
|
+
|
|
7
|
+
const PREPARED_BOM_ROWS = new WeakMap()
|
|
8
|
+
const PREPARED_BOM_TABLES = new WeakMap()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders canonical grouped BOM data as deterministic HTML.
|
|
12
|
+
*/
|
|
13
|
+
export class BomTableRenderer {
|
|
14
|
+
/**
|
|
15
|
+
* Renders a DocumentInput or existing grouped BOM rows.
|
|
16
|
+
* @param {unknown} document DocumentInput or grouped BOM rows.
|
|
17
|
+
* @param {Record<string, any>} [options] Canonical render options.
|
|
18
|
+
* @returns {string} Deterministic BOM HTML.
|
|
19
|
+
*/
|
|
20
|
+
static render(document, options = {}) {
|
|
21
|
+
const normalized = CanonicalRenderOptions.normalize(options)
|
|
22
|
+
CanonicalRenderOptions.requireCanonicalFidelity(normalized.fidelity)
|
|
23
|
+
const legacyRows = BomTableRenderer.#legacyRows(document)
|
|
24
|
+
if (legacyRows) {
|
|
25
|
+
return BomTableRenderer.#renderRows(legacyRows)
|
|
26
|
+
}
|
|
27
|
+
const context = CircuitJsonDocumentContext.prepare(document)
|
|
28
|
+
const rows = context.getOrCreateDerived('render', 'bom-rows-v1', () => {
|
|
29
|
+
const built = CanonicalBomRows.build(context.model)
|
|
30
|
+
BomTableRenderer.#freeze(built)
|
|
31
|
+
PREPARED_BOM_ROWS.set(built, context)
|
|
32
|
+
return built
|
|
33
|
+
})
|
|
34
|
+
if (!Array.isArray(rows) || PREPARED_BOM_ROWS.get(rows) !== context) {
|
|
35
|
+
throw CanonicalRenderOptions.error(
|
|
36
|
+
'BOM rendering encountered a derived-row cache collision.'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
const table = context.getOrCreateDerived(
|
|
40
|
+
'render',
|
|
41
|
+
'bom-table-v1',
|
|
42
|
+
() => {
|
|
43
|
+
const entry = Object.freeze({
|
|
44
|
+
html: BomTableRenderer.#renderRows(rows)
|
|
45
|
+
})
|
|
46
|
+
PREPARED_BOM_TABLES.set(entry, context)
|
|
47
|
+
return entry
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
if (
|
|
51
|
+
!table ||
|
|
52
|
+
typeof table !== 'object' ||
|
|
53
|
+
PREPARED_BOM_TABLES.get(table) !== context
|
|
54
|
+
) {
|
|
55
|
+
throw CanonicalRenderOptions.error(
|
|
56
|
+
'BOM rendering encountered a derived-table cache collision.'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return table.html
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Copies a legacy grouped-row input through safe own data descriptors.
|
|
64
|
+
* @param {unknown} value Input candidate.
|
|
65
|
+
* @returns {object[] | null} Safe grouped rows or null for DocumentInput.
|
|
66
|
+
*/
|
|
67
|
+
static #legacyRows(value) {
|
|
68
|
+
const entries = BomTableRenderer.#arrayEntries(value)
|
|
69
|
+
if (!entries?.length) return null
|
|
70
|
+
const descriptors = entries.map((row) =>
|
|
71
|
+
BomTableRenderer.#rowDescriptors(row)
|
|
72
|
+
)
|
|
73
|
+
if (descriptors.some((row) => row?.type)) return null
|
|
74
|
+
if (descriptors.some((row) => !row)) return null
|
|
75
|
+
return descriptors
|
|
76
|
+
.map((row) => BomTableRenderer.#copyRow(row))
|
|
77
|
+
.sort(CanonicalBomOrder.compareRows)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Copies top-level array entries through own data descriptors.
|
|
82
|
+
* @param {unknown} value Array candidate.
|
|
83
|
+
* @returns {unknown[] | null} Safe entries or null.
|
|
84
|
+
*/
|
|
85
|
+
static #arrayEntries(value) {
|
|
86
|
+
let isArray
|
|
87
|
+
let descriptors
|
|
88
|
+
try {
|
|
89
|
+
isArray = Array.isArray(value)
|
|
90
|
+
if (!isArray || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
const length = descriptors.length?.value
|
|
98
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 1000000) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
const result = []
|
|
102
|
+
for (let index = 0; index < length; index += 1) {
|
|
103
|
+
const descriptor = descriptors[String(index)]
|
|
104
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
105
|
+
throw CanonicalRenderOptions.error(
|
|
106
|
+
'BOM input arrays must contain only plain data entries.'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
result.push(descriptor.value)
|
|
110
|
+
}
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Inspects one possible legacy row without reading its fields.
|
|
116
|
+
* @param {unknown} row Row candidate.
|
|
117
|
+
* @returns {Record<string, PropertyDescriptor> | null} Descriptors or null.
|
|
118
|
+
*/
|
|
119
|
+
static #rowDescriptors(row) {
|
|
120
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return null
|
|
121
|
+
try {
|
|
122
|
+
const prototype = Object.getPrototypeOf(row)
|
|
123
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
124
|
+
return null
|
|
125
|
+
return Object.getOwnPropertyDescriptors(row)
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Copies and validates one legacy grouped BOM row.
|
|
133
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Row descriptors.
|
|
134
|
+
* @returns {object} Safe grouped row.
|
|
135
|
+
*/
|
|
136
|
+
static #copyRow(descriptors) {
|
|
137
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
138
|
+
if (descriptor.get || descriptor.set) {
|
|
139
|
+
throw CanonicalRenderOptions.error(
|
|
140
|
+
'BOM rows must contain only plain data fields.'
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const designators = BomTableRenderer.#stringArray(
|
|
145
|
+
descriptors.designators?.value
|
|
146
|
+
)
|
|
147
|
+
if (!designators?.length) {
|
|
148
|
+
throw CanonicalRenderOptions.error(
|
|
149
|
+
'BOM rows require a non-empty designators array.'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
const quantity = descriptors.quantity?.value
|
|
153
|
+
if (
|
|
154
|
+
quantity !== undefined &&
|
|
155
|
+
(!Number.isSafeInteger(quantity) || quantity < 0)
|
|
156
|
+
) {
|
|
157
|
+
throw CanonicalRenderOptions.error(
|
|
158
|
+
'BOM row quantity must be a nonnegative integer.'
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
designators: [...new Set(designators)].sort(
|
|
163
|
+
CanonicalBomOrder.compareDesignators
|
|
164
|
+
),
|
|
165
|
+
quantity: quantity ?? designators.length,
|
|
166
|
+
value: BomTableRenderer.#scalar(descriptors.value?.value),
|
|
167
|
+
pattern: BomTableRenderer.#scalar(descriptors.pattern?.value),
|
|
168
|
+
source: BomTableRenderer.#scalar(descriptors.source?.value)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Copies a dense plain string array without indexed property reads.
|
|
174
|
+
* @param {unknown} value Array candidate.
|
|
175
|
+
* @returns {string[] | null} Safe string array.
|
|
176
|
+
*/
|
|
177
|
+
static #stringArray(value) {
|
|
178
|
+
let descriptors
|
|
179
|
+
try {
|
|
180
|
+
if (!Array.isArray(value)) return null
|
|
181
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) return null
|
|
182
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
183
|
+
} catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
const length = descriptors.length?.value
|
|
187
|
+
if (!Number.isSafeInteger(length) || length < 1 || length > 4096) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
const result = []
|
|
191
|
+
for (let index = 0; index < length; index += 1) {
|
|
192
|
+
const descriptor = descriptors[String(index)]
|
|
193
|
+
if (
|
|
194
|
+
!descriptor ||
|
|
195
|
+
descriptor.get ||
|
|
196
|
+
descriptor.set ||
|
|
197
|
+
typeof descriptor.value !== 'string' ||
|
|
198
|
+
!descriptor.value.trim()
|
|
199
|
+
) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
result.push(descriptor.value.trim())
|
|
203
|
+
}
|
|
204
|
+
return result
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Normalizes an optional primitive BOM display value.
|
|
209
|
+
* @param {unknown} value Value candidate.
|
|
210
|
+
* @returns {string | number | boolean} Safe scalar.
|
|
211
|
+
*/
|
|
212
|
+
static #scalar(value) {
|
|
213
|
+
if (value === undefined || value === null) return ''
|
|
214
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) {
|
|
215
|
+
throw CanonicalRenderOptions.error(
|
|
216
|
+
'BOM row display fields must be scalar values.'
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
220
|
+
throw CanonicalRenderOptions.error(
|
|
221
|
+
'BOM row display fields must be finite.'
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
return value
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Iteratively freezes renderer-owned BOM rows without recursion limits.
|
|
229
|
+
* @param {unknown} value Renderer-owned value.
|
|
230
|
+
* @returns {unknown} Frozen value.
|
|
231
|
+
*/
|
|
232
|
+
static #freeze(value) {
|
|
233
|
+
const pending = [value]
|
|
234
|
+
const visited = new WeakSet()
|
|
235
|
+
while (pending.length) {
|
|
236
|
+
const current = pending.pop()
|
|
237
|
+
if (
|
|
238
|
+
!current ||
|
|
239
|
+
typeof current !== 'object' ||
|
|
240
|
+
visited.has(current)
|
|
241
|
+
) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
visited.add(current)
|
|
245
|
+
for (const descriptor of Object.values(
|
|
246
|
+
Object.getOwnPropertyDescriptors(current)
|
|
247
|
+
)) {
|
|
248
|
+
if ('value' in descriptor) pending.push(descriptor.value)
|
|
249
|
+
}
|
|
250
|
+
Object.freeze(current)
|
|
251
|
+
}
|
|
252
|
+
return value
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Renders normalized grouped rows.
|
|
257
|
+
* @param {object[]} rows Grouped BOM rows.
|
|
258
|
+
* @returns {string} Deterministic BOM HTML.
|
|
259
|
+
*/
|
|
260
|
+
static #renderRows(rows) {
|
|
261
|
+
if (!rows.length) {
|
|
262
|
+
return '<section class="bom-empty">No BOM rows recovered.</section>'
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
'<table class="bom-table"><thead><tr>' +
|
|
266
|
+
'<th>Designators</th><th>Qty</th><th>Value</th><th>Pattern</th><th>Source</th>' +
|
|
267
|
+
'</tr></thead><tbody>' +
|
|
268
|
+
rows.map((row) => BomTableRenderer.#renderRow(row)).join('') +
|
|
269
|
+
'</tbody></table>'
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Renders one escaped BOM row.
|
|
275
|
+
* @param {object} row Grouped BOM row.
|
|
276
|
+
* @returns {string} Table row HTML.
|
|
277
|
+
*/
|
|
278
|
+
static #renderRow(row) {
|
|
279
|
+
return (
|
|
280
|
+
'<tr><td>' +
|
|
281
|
+
BomTableRenderer.#escape((row.designators || []).join(', ')) +
|
|
282
|
+
'</td><td>' +
|
|
283
|
+
BomTableRenderer.#escape(
|
|
284
|
+
row.quantity ?? row.designators?.length ?? 0
|
|
285
|
+
) +
|
|
286
|
+
'</td><td>' +
|
|
287
|
+
BomTableRenderer.#escape(row.value ?? '') +
|
|
288
|
+
'</td><td>' +
|
|
289
|
+
BomTableRenderer.#escape(row.pattern ?? '') +
|
|
290
|
+
'</td><td>' +
|
|
291
|
+
BomTableRenderer.#escape(row.source ?? '') +
|
|
292
|
+
'</td></tr>'
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Escapes HTML text.
|
|
298
|
+
* @param {unknown} value Raw value.
|
|
299
|
+
* @returns {string} Escaped text.
|
|
300
|
+
*/
|
|
301
|
+
static #escape(value) {
|
|
302
|
+
return SafeXmlText.escape(value)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SafeXmlText } from './SafeXmlText.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializes every prepared PCB board substrate in stable primitive order.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonPcbBoardSvgRenderer {
|
|
7
|
+
/**
|
|
8
|
+
* Renders all board primitives in one prepared primitive model.
|
|
9
|
+
* @param {{ primitives?: object[] }} model Primitive model.
|
|
10
|
+
* @returns {string} Board substrate markup.
|
|
11
|
+
*/
|
|
12
|
+
static render(model) {
|
|
13
|
+
const markup = []
|
|
14
|
+
for (const board of model.primitives || []) {
|
|
15
|
+
if (board.kind !== 'board' || !board.bounds) continue
|
|
16
|
+
markup.push(CircuitJsonPcbBoardSvgRenderer.#renderBoard(board))
|
|
17
|
+
}
|
|
18
|
+
return markup.join('')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders one polygonal or rectangular board primitive.
|
|
23
|
+
* @param {object} board Board primitive.
|
|
24
|
+
* @returns {string} Board markup.
|
|
25
|
+
*/
|
|
26
|
+
static #renderBoard(board) {
|
|
27
|
+
if (Array.isArray(board.points) && board.points.length >= 3) {
|
|
28
|
+
return (
|
|
29
|
+
'<polygon class="pcb-board" data-layer="board" points="' +
|
|
30
|
+
SafeXmlText.escape(
|
|
31
|
+
CircuitJsonPcbBoardSvgRenderer.#points(board.points)
|
|
32
|
+
) +
|
|
33
|
+
'"></polygon>'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
'<rect class="pcb-board" x="' +
|
|
38
|
+
CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.minX) +
|
|
39
|
+
'" y="' +
|
|
40
|
+
CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.minY) +
|
|
41
|
+
'" width="' +
|
|
42
|
+
CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.width) +
|
|
43
|
+
'" height="' +
|
|
44
|
+
CircuitJsonPcbBoardSvgRenderer.#number(board.bounds.height) +
|
|
45
|
+
'" rx="' +
|
|
46
|
+
CircuitJsonPcbBoardSvgRenderer.#number(
|
|
47
|
+
Math.min(board.bounds.width, board.bounds.height) * 0.018
|
|
48
|
+
) +
|
|
49
|
+
'" data-layer="board"></rect>'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Formats polygon points for an SVG attribute.
|
|
55
|
+
* @param {{ x: number, y: number }[]} points Polygon points.
|
|
56
|
+
* @returns {string} SVG point list.
|
|
57
|
+
*/
|
|
58
|
+
static #points(points) {
|
|
59
|
+
return points
|
|
60
|
+
.map(
|
|
61
|
+
(point) =>
|
|
62
|
+
CircuitJsonPcbBoardSvgRenderer.#number(point.x) +
|
|
63
|
+
',' +
|
|
64
|
+
CircuitJsonPcbBoardSvgRenderer.#number(point.y)
|
|
65
|
+
)
|
|
66
|
+
.join(' ')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Formats one finite SVG number.
|
|
71
|
+
* @param {unknown} value Number candidate.
|
|
72
|
+
* @returns {string} SVG number.
|
|
73
|
+
*/
|
|
74
|
+
static #number(value) {
|
|
75
|
+
const number = Number(value)
|
|
76
|
+
return Number.isFinite(number)
|
|
77
|
+
? Number(number.toFixed(6)).toString()
|
|
78
|
+
: '0'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SafeXmlText } from './SafeXmlText.mjs'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Renders shared SVG attributes for CircuitJSON PCB primitives.
|
|
3
5
|
*/
|
|
@@ -119,10 +121,6 @@ export class CircuitJsonPcbPrimitiveAttributeRenderer {
|
|
|
119
121
|
* @returns {string}
|
|
120
122
|
*/
|
|
121
123
|
static #escapeHtml(value) {
|
|
122
|
-
return
|
|
123
|
-
.replaceAll('&', '&')
|
|
124
|
-
.replaceAll('<', '<')
|
|
125
|
-
.replaceAll('>', '>')
|
|
126
|
-
.replaceAll('"', '"')
|
|
124
|
+
return SafeXmlText.escape(value)
|
|
127
125
|
}
|
|
128
126
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { PcbInteractionPrimitiveModel } from '../core/PcbInteractionPrimitiveModel.mjs'
|
|
2
|
+
import { PcbRenderPlan } from '../core/rendering/PcbRenderPlan.mjs'
|
|
3
|
+
import { CanonicalSvgDocument } from '../core/rendering/CanonicalSvgDocument.mjs'
|
|
4
|
+
import { CircuitJsonPcbBoardSvgRenderer } from './CircuitJsonPcbBoardSvgRenderer.mjs'
|
|
2
5
|
import { CircuitJsonPcbPrimitiveAttributeRenderer } from './CircuitJsonPcbPrimitiveAttributeRenderer.mjs'
|
|
3
6
|
import { CircuitJsonPcbViaSvgRenderer } from './CircuitJsonPcbViaSvgRenderer.mjs'
|
|
7
|
+
import { SafeXmlText } from './SafeXmlText.mjs'
|
|
4
8
|
/**
|
|
5
9
|
* Renders standards-shaped PCB element arrays into app-compatible SVG.
|
|
6
10
|
*/
|
|
@@ -14,6 +18,54 @@ export class CircuitJsonPcbSvgRenderer {
|
|
|
14
18
|
static render(documentModel, options = {}) {
|
|
15
19
|
const side = options.side === 'bottom' ? 'bottom' : 'top'
|
|
16
20
|
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
21
|
+
return CircuitJsonPcbSvgRenderer.#renderModel(model, side)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renders multiple board sides from one prepared primitive model.
|
|
26
|
+
* @param {object | object[]} documentModel Parsed document model.
|
|
27
|
+
* @param {unknown[]} [sides] Requested board sides.
|
|
28
|
+
* @returns {string[]} SVG markup in requested order.
|
|
29
|
+
*/
|
|
30
|
+
static renderSides(documentModel, sides = ['top', 'bottom']) {
|
|
31
|
+
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
32
|
+
const requested = Array.isArray(sides) ? sides : [sides]
|
|
33
|
+
return requested.map((side) =>
|
|
34
|
+
CircuitJsonPcbSvgRenderer.#renderModel(
|
|
35
|
+
model,
|
|
36
|
+
side === 'bottom' ? 'bottom' : 'top'
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Renders one prepared PCB plan without rebuilding primitives.
|
|
43
|
+
* @param {Record<string, any>} plan Prepared PCB render plan.
|
|
44
|
+
* @param {{ layerIds?: string[] | null }} [options] Prepared-plan options.
|
|
45
|
+
* @returns {string} SVG markup.
|
|
46
|
+
*/
|
|
47
|
+
static renderPlan(plan, options = {}) {
|
|
48
|
+
PcbRenderPlan.requirePrepared(plan)
|
|
49
|
+
const layerIds =
|
|
50
|
+
options.layerIds === undefined
|
|
51
|
+
? plan.selectedLayerIds
|
|
52
|
+
: options.layerIds
|
|
53
|
+
return CanonicalSvgDocument.decorate(
|
|
54
|
+
CircuitJsonPcbSvgRenderer.#renderModel(
|
|
55
|
+
PcbRenderPlan.modelForLayers(plan, layerIds),
|
|
56
|
+
plan.side
|
|
57
|
+
),
|
|
58
|
+
plan.svg
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wraps one prepared primitive model in the stable SVG document.
|
|
64
|
+
* @param {Record<string, any>} model Prepared primitive model.
|
|
65
|
+
* @param {'top' | 'bottom'} side Active side.
|
|
66
|
+
* @returns {string} SVG markup.
|
|
67
|
+
*/
|
|
68
|
+
static #renderModel(model, side) {
|
|
17
69
|
const viewBox = CircuitJsonPcbSvgRenderer.#viewBox(model.bounds)
|
|
18
70
|
return (
|
|
19
71
|
'<svg class="pcb-svg pcb-svg--app-palette pcb-svg--circuitjson pcb-svg--' +
|
|
@@ -21,48 +73,13 @@ export class CircuitJsonPcbSvgRenderer {
|
|
|
21
73
|
'" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="' +
|
|
22
74
|
CircuitJsonPcbSvgRenderer.#formatViewBox(viewBox) +
|
|
23
75
|
'">' +
|
|
24
|
-
|
|
76
|
+
CircuitJsonPcbBoardSvgRenderer.render(model) +
|
|
25
77
|
CircuitJsonPcbSvgRenderer.#renderCopper(model, side) +
|
|
26
78
|
CircuitJsonPcbSvgRenderer.#renderComponentLabels(model) +
|
|
27
79
|
CircuitJsonPcbSvgRenderer.#renderOverlays(model, side) +
|
|
28
80
|
'</svg>'
|
|
29
81
|
)
|
|
30
82
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Renders the board substrate.
|
|
33
|
-
* @param {{ primitives: object[] }} model Primitive model.
|
|
34
|
-
* @returns {string}
|
|
35
|
-
*/
|
|
36
|
-
static #renderBoard(model) {
|
|
37
|
-
const board = model.primitives.find(
|
|
38
|
-
(primitive) => primitive.kind === 'board'
|
|
39
|
-
)
|
|
40
|
-
if (!board?.bounds) return ''
|
|
41
|
-
if (Array.isArray(board.points) && board.points.length >= 3) {
|
|
42
|
-
return (
|
|
43
|
-
'<polygon class="pcb-board" data-layer="board" points="' +
|
|
44
|
-
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
45
|
-
CircuitJsonPcbSvgRenderer.#pointsAttribute(board.points)
|
|
46
|
-
) +
|
|
47
|
-
'"></polygon>'
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
return (
|
|
51
|
-
'<rect class="pcb-board" x="' +
|
|
52
|
-
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minX) +
|
|
53
|
-
'" y="' +
|
|
54
|
-
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minY) +
|
|
55
|
-
'" width="' +
|
|
56
|
-
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.width) +
|
|
57
|
-
'" height="' +
|
|
58
|
-
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.height) +
|
|
59
|
-
'" rx="' +
|
|
60
|
-
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
61
|
-
Math.min(board.bounds.width, board.bounds.height) * 0.018
|
|
62
|
-
) +
|
|
63
|
-
'" data-layer="board"></rect>'
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
83
|
/**
|
|
67
84
|
* Renders copper and drilled primitives.
|
|
68
85
|
* @param {{ primitives: object[] }} model Primitive model.
|
|
@@ -755,9 +772,16 @@ export class CircuitJsonPcbSvgRenderer {
|
|
|
755
772
|
if (primitiveSide) return primitiveSide === side
|
|
756
773
|
|
|
757
774
|
const layer = String(primitive.layer || '').toLowerCase()
|
|
758
|
-
if (side === 'bottom')
|
|
775
|
+
if (side === 'bottom') {
|
|
776
|
+
return layer === 'bottom' || layer.startsWith('bottom_')
|
|
777
|
+
}
|
|
759
778
|
|
|
760
|
-
return
|
|
779
|
+
return (
|
|
780
|
+
layer === 'top' ||
|
|
781
|
+
layer.startsWith('top_') ||
|
|
782
|
+
!layer ||
|
|
783
|
+
layer === 'board'
|
|
784
|
+
)
|
|
761
785
|
}
|
|
762
786
|
|
|
763
787
|
/**
|
|
@@ -955,10 +979,6 @@ export class CircuitJsonPcbSvgRenderer {
|
|
|
955
979
|
* @returns {string}
|
|
956
980
|
*/
|
|
957
981
|
static #escapeHtml(value) {
|
|
958
|
-
return
|
|
959
|
-
.replaceAll('&', '&')
|
|
960
|
-
.replaceAll('<', '<')
|
|
961
|
-
.replaceAll('>', '>')
|
|
962
|
-
.replaceAll('"', '"')
|
|
982
|
+
return SafeXmlText.escape(value)
|
|
963
983
|
}
|
|
964
984
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SafeXmlText } from './SafeXmlText.mjs'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Renders via-like drilled primitives as SVG shapes.
|
|
3
5
|
*/
|
|
@@ -159,10 +161,6 @@ export class CircuitJsonPcbViaSvgRenderer {
|
|
|
159
161
|
* @returns {string}
|
|
160
162
|
*/
|
|
161
163
|
static #escapeHtml(value) {
|
|
162
|
-
return
|
|
163
|
-
.replaceAll('&', '&')
|
|
164
|
-
.replaceAll('"', '"')
|
|
165
|
-
.replaceAll('<', '<')
|
|
166
|
-
.replaceAll('>', '>')
|
|
164
|
+
return SafeXmlText.escape(value)
|
|
167
165
|
}
|
|
168
166
|
}
|