altium-toolkit 1.1.41 → 1.2.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/README.md +113 -19
- package/docs/api.md +224 -18
- package/docs/capabilities.md +65 -0
- package/docs/migration/legacy-001.md +309 -0
- package/docs/migration/legacy-002.md +309 -0
- package/docs/migration/legacy-003.md +309 -0
- package/docs/migration/legacy-004.md +309 -0
- package/docs/migration/legacy-005.md +111 -0
- package/docs/migration.md +20 -0
- package/docs/model-format.md +28 -3
- package/docs/release-notes-v1.2.0.md +147 -0
- package/docs/testing.md +43 -1
- package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +1 -1
- package/examples/arduino-uno/example.mjs +1 -1
- package/examples/cli-utils.mjs +1 -1
- package/examples/corpus-smoke.mjs +1 -1
- package/examples/inspect-board.mjs +1 -1
- package/examples/library-catalog.mjs +1 -1
- package/examples/validate-library.mjs +1 -1
- package/package.json +20 -5
- package/spec/api-baseline-v1.1.41.json +1 -0
- package/spec/asset-baseline-v1.1.41.json +1 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +14 -4
- package/spec/native-source-manifest-v1.1.41.json +1 -0
- package/src/capabilities.mjs +4 -0
- package/src/convergence/AltiumCircuitJsonProjection.mjs +230 -0
- package/src/convergence/AltiumDocumentBuilder.mjs +167 -0
- package/src/convergence/AltiumExtensionResolver.mjs +98 -0
- package/src/convergence/AltiumProjectDocumentResolver.mjs +324 -0
- package/src/convergence/AltiumSchematicCoordinateProjection.mjs +130 -0
- package/src/convergence/AltiumWorkerClient.mjs +95 -0
- package/src/convergence/Parser.mjs +285 -0
- package/src/convergence/ParserInput.mjs +282 -0
- package/src/convergence/ProjectLoader.mjs +916 -0
- package/src/convergence/SchematicSvgRenderer.mjs +126 -0
- package/src/convergence/ToolkitCapabilities.mjs +49 -0
- package/src/core/circuit-json/CircuitJsonSchematicDocumentGraphicBuilder.mjs +785 -0
- package/src/core/circuit-json/CircuitJsonSchematicGraphicBuilder.mjs +909 -0
- package/src/core/circuit-json/CircuitJsonSchematicImageProjection.mjs +277 -0
- package/src/core/circuit-json/CircuitJsonSchematicStrokeStyle.mjs +47 -0
- package/src/extensions.mjs +11 -0
- package/src/index.mjs +20 -4
- package/src/interaction.mjs +7 -0
- package/src/legacy-netlist-query.mjs +11 -0
- package/src/legacy-parser.mjs +141 -0
- package/src/legacy-renderers.mjs +25 -0
- package/src/legacy-scene3d.mjs +10 -0
- package/src/manufacturing.mjs +4 -0
- package/src/parser.mjs +12 -138
- package/src/project.mjs +11 -0
- package/src/query.mjs +4 -0
- package/src/renderers.mjs +4 -22
- package/src/scene3d.mjs +5 -8
- package/src/simulation.mjs +4 -0
- package/src/styles/renderers.css +27 -0
- package/src/testing.mjs +8 -0
- package/src/workers/parser.worker.mjs +37 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { DocumentResult } from 'circuitjson-toolkit/parser'
|
|
5
|
+
|
|
6
|
+
import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
|
|
7
|
+
|
|
8
|
+
const SCHEMATIC_TITLE_BLOCK_FIELDS = [
|
|
9
|
+
'title',
|
|
10
|
+
'revision',
|
|
11
|
+
'documentNumber',
|
|
12
|
+
'sheetNumber',
|
|
13
|
+
'sheetTotal',
|
|
14
|
+
'date',
|
|
15
|
+
'drawnBy'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves project-owned schematic strings inside canonical documents.
|
|
20
|
+
*/
|
|
21
|
+
export class AltiumProjectDocumentResolver {
|
|
22
|
+
/**
|
|
23
|
+
* Returns canonical documents with referenced schematic strings resolved.
|
|
24
|
+
* @param {{ projectEntry?: { name: string, input: { data: string | ArrayBuffer | Uint8Array } } | null }} classified Classified project entries.
|
|
25
|
+
* @param {object[]} documents Parsed canonical documents.
|
|
26
|
+
* @param {string | string[]} extensionSelection Public extension selection.
|
|
27
|
+
* @returns {object[]} Original or resolved canonical documents.
|
|
28
|
+
*/
|
|
29
|
+
static resolve(classified, documents, extensionSelection) {
|
|
30
|
+
const context = AltiumProjectDocumentResolver.#context(
|
|
31
|
+
classified,
|
|
32
|
+
documents
|
|
33
|
+
)
|
|
34
|
+
const exposesProjectContext =
|
|
35
|
+
AltiumProjectDocumentResolver.#exposesProjectContext(
|
|
36
|
+
extensionSelection
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
let changed = false
|
|
40
|
+
const resolved = documents.map((document) => {
|
|
41
|
+
const next = context
|
|
42
|
+
? AltiumProjectDocumentResolver.#document(
|
|
43
|
+
document,
|
|
44
|
+
context,
|
|
45
|
+
exposesProjectContext
|
|
46
|
+
)
|
|
47
|
+
: AltiumProjectDocumentResolver.#withProjectContextExposure(
|
|
48
|
+
document,
|
|
49
|
+
exposesProjectContext
|
|
50
|
+
)
|
|
51
|
+
changed ||= next !== document
|
|
52
|
+
return next
|
|
53
|
+
})
|
|
54
|
+
return changed ? resolved : documents
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reads compact project facts from its canonical extension.
|
|
59
|
+
* @param {{ projectEntry?: { name: string, input: { data: string | ArrayBuffer | Uint8Array } } | null }} classified Classified project entries.
|
|
60
|
+
* @param {object[]} documents Parsed canonical documents.
|
|
61
|
+
* @returns {{ fileName: string, parameters: Record<string, any>, documents: string[] } | null} Project context.
|
|
62
|
+
*/
|
|
63
|
+
static #context(classified, documents) {
|
|
64
|
+
const entry = classified?.projectEntry
|
|
65
|
+
if (!entry) return null
|
|
66
|
+
const projectDocument = documents.find(
|
|
67
|
+
(document) =>
|
|
68
|
+
document?.source?.fileType === 'prjpcb' &&
|
|
69
|
+
document.source.fileName === entry.name
|
|
70
|
+
)
|
|
71
|
+
const projectContext =
|
|
72
|
+
projectDocument?.extensions?.altium?.projectContext
|
|
73
|
+
if (!projectContext) return null
|
|
74
|
+
return {
|
|
75
|
+
fileName: entry.name,
|
|
76
|
+
parameters: projectContext.parameters || {},
|
|
77
|
+
documents: Array.isArray(projectContext.documents)
|
|
78
|
+
? projectContext.documents
|
|
79
|
+
: []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves one schematic document without mutating its proven model.
|
|
85
|
+
* @param {object} document Canonical document.
|
|
86
|
+
* @param {{ fileName: string, parameters: Record<string, any>, documents: string[] }} context Project context.
|
|
87
|
+
* @param {boolean} exposesProjectContext Whether compact context remains public.
|
|
88
|
+
* @returns {object} Original or rebuilt canonical document.
|
|
89
|
+
*/
|
|
90
|
+
static #document(document, context, exposesProjectContext) {
|
|
91
|
+
if (String(document?.source?.fileType || '') !== 'schdoc') {
|
|
92
|
+
return AltiumProjectDocumentResolver.#withProjectContextExposure(
|
|
93
|
+
document,
|
|
94
|
+
exposesProjectContext
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
context.documents.length &&
|
|
99
|
+
!AltiumProjectDocumentResolver.#mentions(
|
|
100
|
+
context.documents,
|
|
101
|
+
document.source.fileName
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
return AltiumProjectDocumentResolver.#withProjectContextExposure(
|
|
105
|
+
document,
|
|
106
|
+
exposesProjectContext
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
const parameters = {
|
|
110
|
+
...context.parameters,
|
|
111
|
+
...AltiumProjectDocumentResolver.#currentValues(),
|
|
112
|
+
ProjectName: AltiumProjectDocumentResolver.#baseName(
|
|
113
|
+
context.fileName
|
|
114
|
+
),
|
|
115
|
+
DataSourceFileName: AltiumProjectDocumentResolver.#baseName(
|
|
116
|
+
context.fileName
|
|
117
|
+
),
|
|
118
|
+
DocumentName: AltiumProjectDocumentResolver.#baseName(
|
|
119
|
+
document.source.fileName
|
|
120
|
+
),
|
|
121
|
+
DocumentFullPathAndName: document.source.fileName
|
|
122
|
+
}
|
|
123
|
+
let resolutionCount = 0
|
|
124
|
+
const model = document.model.map((element) => {
|
|
125
|
+
if (element?.type !== 'schematic_text') return element
|
|
126
|
+
const resolved = SchematicProjectParameterResolver.resolveText(
|
|
127
|
+
element.text,
|
|
128
|
+
parameters
|
|
129
|
+
)
|
|
130
|
+
if (!resolved || resolved.resolvedText === element.text) {
|
|
131
|
+
return element
|
|
132
|
+
}
|
|
133
|
+
resolutionCount += 1
|
|
134
|
+
return { ...element, text: resolved.resolvedText }
|
|
135
|
+
})
|
|
136
|
+
const hidesProjectContext =
|
|
137
|
+
!exposesProjectContext &&
|
|
138
|
+
Boolean(document?.extensions?.altium?.projectContext)
|
|
139
|
+
const native = hidesProjectContext
|
|
140
|
+
? undefined
|
|
141
|
+
: document?.extensions?.altium?.native
|
|
142
|
+
const resolvedNative =
|
|
143
|
+
AltiumProjectDocumentResolver.#resolvedNativeDocument(
|
|
144
|
+
native,
|
|
145
|
+
parameters
|
|
146
|
+
)
|
|
147
|
+
const extensions = hidesProjectContext
|
|
148
|
+
? {}
|
|
149
|
+
: resolvedNative === native
|
|
150
|
+
? document.extensions
|
|
151
|
+
: {
|
|
152
|
+
altium: {
|
|
153
|
+
...document.extensions.altium,
|
|
154
|
+
native: resolvedNative
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
!resolutionCount &&
|
|
159
|
+
resolvedNative === native &&
|
|
160
|
+
extensions === document.extensions
|
|
161
|
+
) {
|
|
162
|
+
return document
|
|
163
|
+
}
|
|
164
|
+
return AltiumProjectDocumentResolver.#rebuild(document, {
|
|
165
|
+
model,
|
|
166
|
+
extensions,
|
|
167
|
+
statistics:
|
|
168
|
+
resolutionCount || resolvedNative !== native
|
|
169
|
+
? {
|
|
170
|
+
...document.statistics,
|
|
171
|
+
resolvedProjectParameterCount: resolutionCount
|
|
172
|
+
}
|
|
173
|
+
: document.statistics
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves retained native schematic text and title-block fields through the
|
|
179
|
+
* same project parameter set used by the canonical CircuitJSON model.
|
|
180
|
+
* @param {object | null | undefined} native Retained native renderer model.
|
|
181
|
+
* @param {Record<string, any>} parameters Resolved project parameters.
|
|
182
|
+
* @returns {object | null | undefined} Original or resolved native model.
|
|
183
|
+
*/
|
|
184
|
+
static #resolvedNativeDocument(native, parameters) {
|
|
185
|
+
if (!native?.schematic) return native
|
|
186
|
+
const resolved = SchematicProjectParameterResolver.applyToDocumentModel(
|
|
187
|
+
native,
|
|
188
|
+
parameters,
|
|
189
|
+
{ replaceText: true }
|
|
190
|
+
)
|
|
191
|
+
const sourceTexts = Array.isArray(native.schematic.texts)
|
|
192
|
+
? native.schematic.texts
|
|
193
|
+
: []
|
|
194
|
+
const resolvedTexts = Array.isArray(resolved.schematic.texts)
|
|
195
|
+
? resolved.schematic.texts
|
|
196
|
+
: []
|
|
197
|
+
const textChanged = sourceTexts.some(
|
|
198
|
+
(text, index) => text?.text !== resolvedTexts[index]?.text
|
|
199
|
+
)
|
|
200
|
+
const sourceTitleBlock = native.schematic.sheet?.titleBlock || {}
|
|
201
|
+
const resolvedTitleBlock = resolved.schematic.sheet?.titleBlock || {}
|
|
202
|
+
const titleBlockChanged = SCHEMATIC_TITLE_BLOCK_FIELDS.some(
|
|
203
|
+
(field) => sourceTitleBlock[field] !== resolvedTitleBlock[field]
|
|
204
|
+
)
|
|
205
|
+
if (!textChanged && !titleBlockChanged) return native
|
|
206
|
+
return { ...resolved, projectParameters: parameters }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Applies compact project-context exposure without rebuilding documents that
|
|
211
|
+
* already match the public selection.
|
|
212
|
+
* @param {object} document Canonical document.
|
|
213
|
+
* @param {boolean} exposesProjectContext Whether compact context remains public.
|
|
214
|
+
* @returns {object} Original or rebuilt project document.
|
|
215
|
+
*/
|
|
216
|
+
static #withProjectContextExposure(document, exposesProjectContext) {
|
|
217
|
+
if (
|
|
218
|
+
exposesProjectContext ||
|
|
219
|
+
!document?.extensions?.altium?.projectContext
|
|
220
|
+
) {
|
|
221
|
+
return document
|
|
222
|
+
}
|
|
223
|
+
return AltiumProjectDocumentResolver.#rebuild(document, {
|
|
224
|
+
extensions: {}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns true when public selection includes compact project context.
|
|
230
|
+
* @param {string | string[]} selection Extension selection.
|
|
231
|
+
* @returns {boolean} Whether project facts remain public.
|
|
232
|
+
*/
|
|
233
|
+
static #exposesProjectContext(selection) {
|
|
234
|
+
if (!Array.isArray(selection)) return selection !== 'none'
|
|
235
|
+
return (
|
|
236
|
+
selection.includes('altium.project-context') ||
|
|
237
|
+
selection.includes('altium.native-model')
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Rebuilds one canonical document through the validating shared boundary.
|
|
243
|
+
* @param {object} document Existing canonical document.
|
|
244
|
+
* @param {{ model?: object[], extensions?: object, statistics?: object }} fields Replacements.
|
|
245
|
+
* @returns {object} Proven immutable document.
|
|
246
|
+
*/
|
|
247
|
+
static #rebuild(document, fields) {
|
|
248
|
+
return DocumentResult.createValidated(
|
|
249
|
+
{
|
|
250
|
+
id: document.id,
|
|
251
|
+
model: fields.model || document.model,
|
|
252
|
+
source: document.source,
|
|
253
|
+
extensions:
|
|
254
|
+
fields.extensions === undefined
|
|
255
|
+
? document.extensions
|
|
256
|
+
: fields.extensions,
|
|
257
|
+
assets: document.assets,
|
|
258
|
+
diagnostics: document.diagnostics,
|
|
259
|
+
statistics: fields.statistics || document.statistics
|
|
260
|
+
},
|
|
261
|
+
AltiumProjectDocumentResolver.#runtime(document)
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Preserves an explicit source reference when the parser retained one.
|
|
267
|
+
* @param {object} document Canonical document.
|
|
268
|
+
* @returns {{ sourceReference?: object }} Runtime-only fields.
|
|
269
|
+
*/
|
|
270
|
+
static #runtime(document) {
|
|
271
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
272
|
+
document,
|
|
273
|
+
'sourceReference'
|
|
274
|
+
)
|
|
275
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
276
|
+
? { sourceReference: descriptor.value }
|
|
277
|
+
: {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds current-value special strings used by Altium templates.
|
|
282
|
+
* @returns {{ CurrentDate: string, CurrentTime: string }} Current values.
|
|
283
|
+
*/
|
|
284
|
+
static #currentValues() {
|
|
285
|
+
const now = new Date()
|
|
286
|
+
return {
|
|
287
|
+
CurrentDate: now.toLocaleDateString('en-US'),
|
|
288
|
+
CurrentTime: now.toLocaleTimeString('en-US')
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns the final normalized path segment.
|
|
294
|
+
* @param {unknown} value Path value.
|
|
295
|
+
* @returns {string} Basename.
|
|
296
|
+
*/
|
|
297
|
+
static #baseName(value) {
|
|
298
|
+
const path = String(value || '').replaceAll('\\', '/')
|
|
299
|
+
return path.split('/').pop() || path
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns true when project paths own one schematic source path.
|
|
304
|
+
* @param {string[]} projectPaths Normalized project document paths.
|
|
305
|
+
* @param {string} fileName Canonical source path.
|
|
306
|
+
* @returns {boolean} Whether the project references the schematic.
|
|
307
|
+
*/
|
|
308
|
+
static #mentions(projectPaths, fileName) {
|
|
309
|
+
const source = String(fileName || '').replaceAll('\\', '/')
|
|
310
|
+
const sourceBaseName = AltiumProjectDocumentResolver.#baseName(source)
|
|
311
|
+
return projectPaths.some((value) => {
|
|
312
|
+
const path = String(value || '').replaceAll('\\', '/')
|
|
313
|
+
return (
|
|
314
|
+
path === source ||
|
|
315
|
+
source.endsWith('/' + path) ||
|
|
316
|
+
path.endsWith('/' + source) ||
|
|
317
|
+
AltiumProjectDocumentResolver.#baseName(path) === sourceBaseName
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Object.freeze(AltiumProjectDocumentResolver.prototype)
|
|
324
|
+
Object.freeze(AltiumProjectDocumentResolver)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
const POINT_FIELDS = [
|
|
5
|
+
'anchor_position',
|
|
6
|
+
'center',
|
|
7
|
+
'end',
|
|
8
|
+
'mid',
|
|
9
|
+
'position',
|
|
10
|
+
'start'
|
|
11
|
+
]
|
|
12
|
+
const POINT_LIST_FIELDS = ['junctions', 'outline', 'points']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Converts Altium's bottom-origin schematic geometry to canonical SVG space.
|
|
16
|
+
*/
|
|
17
|
+
export class AltiumSchematicCoordinateProjection {
|
|
18
|
+
/**
|
|
19
|
+
* Reflects only schematic geometry through the authored sheet height.
|
|
20
|
+
* @param {object[]} model CircuitJSON model rows.
|
|
21
|
+
* @param {unknown} sheetHeight Native rendered sheet height.
|
|
22
|
+
* @returns {object[]} Projected model rows.
|
|
23
|
+
*/
|
|
24
|
+
static project(model, sheetHeight) {
|
|
25
|
+
const height = Number(sheetHeight)
|
|
26
|
+
if (!Number.isFinite(height) || height <= 0) return [...model]
|
|
27
|
+
return model.map((element) =>
|
|
28
|
+
String(element?.type || '').startsWith('schematic_') &&
|
|
29
|
+
element.type !== 'schematic_sheet'
|
|
30
|
+
? AltiumSchematicCoordinateProjection.#element(element, height)
|
|
31
|
+
: element
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reflects every common point-bearing field on one schematic row.
|
|
37
|
+
* @param {Record<string, any>} element CircuitJSON element.
|
|
38
|
+
* @param {number} height Sheet height.
|
|
39
|
+
* @returns {Record<string, any>} Reflected element.
|
|
40
|
+
*/
|
|
41
|
+
static #element(element, height) {
|
|
42
|
+
const projected = { ...element }
|
|
43
|
+
for (const field of POINT_FIELDS) {
|
|
44
|
+
if (element[field] !== undefined) {
|
|
45
|
+
projected[field] = AltiumSchematicCoordinateProjection.#point(
|
|
46
|
+
element[field],
|
|
47
|
+
height
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const field of POINT_LIST_FIELDS) {
|
|
52
|
+
if (Array.isArray(element[field])) {
|
|
53
|
+
projected[field] = element[field].map((point) =>
|
|
54
|
+
AltiumSchematicCoordinateProjection.#point(point, height)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const field of ['y', 'y1', 'y2']) {
|
|
59
|
+
const value = Number(element[field])
|
|
60
|
+
if (Number.isFinite(value)) projected[field] = height - value
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(element.edges)) {
|
|
63
|
+
projected.edges = element.edges.map((edge) => ({
|
|
64
|
+
...edge,
|
|
65
|
+
...(edge?.from
|
|
66
|
+
? {
|
|
67
|
+
from: AltiumSchematicCoordinateProjection.#point(
|
|
68
|
+
edge.from,
|
|
69
|
+
height
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
...(edge?.to
|
|
74
|
+
? {
|
|
75
|
+
to: AltiumSchematicCoordinateProjection.#point(
|
|
76
|
+
edge.to,
|
|
77
|
+
height
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
: {})
|
|
81
|
+
}))
|
|
82
|
+
}
|
|
83
|
+
if (element.type === 'schematic_arc') {
|
|
84
|
+
for (const field of ['start_angle_degrees', 'end_angle_degrees']) {
|
|
85
|
+
if (
|
|
86
|
+
Object.hasOwn(element, field) &&
|
|
87
|
+
Number.isFinite(Number(element[field]))
|
|
88
|
+
) {
|
|
89
|
+
projected[field] =
|
|
90
|
+
AltiumSchematicCoordinateProjection.#angle(
|
|
91
|
+
element[field]
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (element.direction === 'clockwise') {
|
|
96
|
+
projected.direction = 'counterclockwise'
|
|
97
|
+
} else if (element.direction === 'counterclockwise') {
|
|
98
|
+
projected.direction = 'clockwise'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return projected
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reflects an angle while keeping canonical zero positive.
|
|
106
|
+
* @param {unknown} value Angle candidate.
|
|
107
|
+
* @returns {number} Reflected angle.
|
|
108
|
+
*/
|
|
109
|
+
static #angle(value) {
|
|
110
|
+
const angle = Number(value || 0)
|
|
111
|
+
return angle ? -angle : 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reflects one point without dropping its relation metadata.
|
|
116
|
+
* @param {unknown} value Point candidate.
|
|
117
|
+
* @param {number} height Sheet height.
|
|
118
|
+
* @returns {unknown} Reflected point or the original value.
|
|
119
|
+
*/
|
|
120
|
+
static #point(value, height) {
|
|
121
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
122
|
+
return value
|
|
123
|
+
}
|
|
124
|
+
const y = Number(value.y)
|
|
125
|
+
return Number.isFinite(y) ? { ...value, y: height - y } : value
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Object.freeze(AltiumSchematicCoordinateProjection.prototype)
|
|
130
|
+
Object.freeze(AltiumSchematicCoordinateProjection)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { ParserWorkerClient } from 'circuitjson-toolkit/parser'
|
|
5
|
+
|
|
6
|
+
let client = null
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Owns the source-format worker client while reusing the shared protocol.
|
|
10
|
+
*/
|
|
11
|
+
export class AltiumWorkerClient {
|
|
12
|
+
/** @returns {boolean} Whether a browser-compatible Worker is available. */
|
|
13
|
+
static isAvailable() {
|
|
14
|
+
try {
|
|
15
|
+
return typeof globalThis.Worker === 'function'
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses one request through the shared worker protocol.
|
|
23
|
+
* @param {Record<string, any>} input Parser input.
|
|
24
|
+
* @param {Record<string, any>} options Common parser options.
|
|
25
|
+
* @returns {Promise<Record<string, any>>} Canonical document.
|
|
26
|
+
*/
|
|
27
|
+
static async parse(input, options) {
|
|
28
|
+
return await AltiumWorkerClient.#client().parse(input, options)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses through the worker while distinguishing local construction
|
|
33
|
+
* unavailability from parser, protocol, and runtime failures.
|
|
34
|
+
* @param {Record<string, any>} input Parser input.
|
|
35
|
+
* @param {Record<string, any>} options Common parser options.
|
|
36
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
37
|
+
*/
|
|
38
|
+
static async parseAttempt(input, options) {
|
|
39
|
+
return await AltiumWorkerClient.#client().parseAttempt(input, options)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Loads one project through the shared worker protocol.
|
|
44
|
+
* @param {Record<string, any>[]} entries Project entries.
|
|
45
|
+
* @param {Record<string, any>} options Common loader options.
|
|
46
|
+
* @returns {Promise<Record<string, any>>} Canonical project.
|
|
47
|
+
*/
|
|
48
|
+
static async loadProject(entries, options) {
|
|
49
|
+
return await AltiumWorkerClient.#client().loadProject(entries, options)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Loads through the worker while preserving operation failures.
|
|
54
|
+
* @param {Record<string, any>[]} entries Project entries.
|
|
55
|
+
* @param {Record<string, any>} options Common loader options.
|
|
56
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
57
|
+
*/
|
|
58
|
+
static async loadProjectAttempt(entries, options) {
|
|
59
|
+
return await AltiumWorkerClient.#client().loadProjectAttempt(
|
|
60
|
+
entries,
|
|
61
|
+
options
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Disposes the current source worker client. */
|
|
66
|
+
static dispose() {
|
|
67
|
+
client?.dispose()
|
|
68
|
+
client = null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the lazy source worker client.
|
|
73
|
+
* @returns {ParserWorkerClient} Shared-protocol client.
|
|
74
|
+
*/
|
|
75
|
+
static #client() {
|
|
76
|
+
if (!client) {
|
|
77
|
+
client = new ParserWorkerClient({
|
|
78
|
+
createWorker: () => {
|
|
79
|
+
const WorkerConstructor = globalThis.Worker
|
|
80
|
+
return Reflect.construct(WorkerConstructor, [
|
|
81
|
+
new URL(
|
|
82
|
+
'../workers/parser.worker.mjs',
|
|
83
|
+
import.meta.url
|
|
84
|
+
),
|
|
85
|
+
{ type: 'module' }
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
return client
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Object.freeze(AltiumWorkerClient.prototype)
|
|
95
|
+
Object.freeze(AltiumWorkerClient)
|