altium-toolkit 1.1.2 → 1.1.22
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/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
package/docs/api.md
CHANGED
|
@@ -111,6 +111,43 @@ helpers. `PcbStatisticsBuilder` emits board QA summaries used by `.PcbDoc`
|
|
|
111
111
|
models. `SchematicProjectParameterResolver` resolves dot-prefixed and
|
|
112
112
|
equals-prefixed schematic special strings for parser and SVG integrations.
|
|
113
113
|
|
|
114
|
+
## Library Exporters
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import {
|
|
118
|
+
SourceComponentClient,
|
|
119
|
+
SourceComponentBundleNormalizer,
|
|
120
|
+
SourceBundleExporter,
|
|
121
|
+
AltiumSchLibExporter,
|
|
122
|
+
AltiumPcbLibExporter,
|
|
123
|
+
AltiumLibraryBatchExporter
|
|
124
|
+
} from 'altium-toolkit/parser'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The exporter surface is local-first and host-controlled:
|
|
128
|
+
|
|
129
|
+
- `SourceComponentClient` performs component search, component fetch, model
|
|
130
|
+
asset fetch, retry, and response validation through an injected `fetcher`.
|
|
131
|
+
It does not use global `fetch` implicitly.
|
|
132
|
+
- `SourceComponentBundleNormalizer.normalize(raw)` converts provider-specific
|
|
133
|
+
component responses into a deterministic bundle with `symbol`, `footprint`,
|
|
134
|
+
`models`, `metadata`, `sourceJson`, and diagnostics fields.
|
|
135
|
+
- `SourceBundleExporter.export(bundle)` emits deterministic raw source bundle
|
|
136
|
+
entries: `manifest.json`, `source/source.json`, and optional `models/*`
|
|
137
|
+
assets.
|
|
138
|
+
- `AltiumSchLibExporter.export(bundles)` and
|
|
139
|
+
`AltiumPcbLibExporter.export(bundles)` write compact OLE-backed `.SchLib`
|
|
140
|
+
and `.PcbLib` byte arrays. The `.PcbLib` writer includes generated library
|
|
141
|
+
streams plus STEP/WRL model payload streams when the normalized bundle
|
|
142
|
+
contains model assets.
|
|
143
|
+
- `AltiumLibraryBatchExporter` orchestrates id lists, search-and-export,
|
|
144
|
+
per-component source/SchLib/PcbLib outputs, merged library outputs,
|
|
145
|
+
append/skip manifests, progress events, continue-on-error diagnostics, and
|
|
146
|
+
checkpoint state.
|
|
147
|
+
|
|
148
|
+
Hosts are responsible for choosing and configuring any outbound component
|
|
149
|
+
source. Tests use repo-owned fake responses only.
|
|
150
|
+
|
|
114
151
|
## Netlist Query
|
|
115
152
|
|
|
116
153
|
```js
|
package/docs/model-format.md
CHANGED
|
@@ -34,6 +34,24 @@ Circuit JSON array. `JSON.stringify(result)` serializes only the Circuit JSON
|
|
|
34
34
|
elements, including custom `altium_toolkit_*` sidecar elements; compatibility
|
|
35
35
|
fields are intentionally omitted from serialized JSON.
|
|
36
36
|
|
|
37
|
+
## Source Export Bundle
|
|
38
|
+
|
|
39
|
+
`SourceComponentBundleNormalizer` produces the exporter input contract used by
|
|
40
|
+
the source bundle, `.SchLib`, and `.PcbLib` writers:
|
|
41
|
+
|
|
42
|
+
- `id` and `name`: stable component identity
|
|
43
|
+
- `metadata`: provider metadata copied into deterministic plain-object form
|
|
44
|
+
- `symbol`: schematic symbol name, pins, primitives, and raw source object
|
|
45
|
+
- `footprint`: PCB footprint name, primitive families, and raw source object
|
|
46
|
+
- `models`: model id, file name, format, bytes/text, and optional source URL
|
|
47
|
+
- `sourceJson`: the original raw response retained for reproducible exports
|
|
48
|
+
- `diagnostics`: warnings for incomplete source data
|
|
49
|
+
|
|
50
|
+
`SourceBundleExporter.export()` serializes the original source response and a
|
|
51
|
+
manifest that lists included model assets. It does not fetch network resources;
|
|
52
|
+
callers provide already-normalized model bytes or use `SourceComponentClient`
|
|
53
|
+
before exporting.
|
|
54
|
+
|
|
37
55
|
## Renderer Compatibility Fields
|
|
38
56
|
|
|
39
57
|
For compatibility, `AltiumParser.parseArrayBuffer()` attaches the previous
|
|
@@ -1548,7 +1548,7 @@
|
|
|
1548
1548
|
"displayName",
|
|
1549
1549
|
"includeInBom",
|
|
1550
1550
|
"includeInNetlist",
|
|
1551
|
-
"
|
|
1551
|
+
"includeInPlacement"
|
|
1552
1552
|
],
|
|
1553
1553
|
"properties": {
|
|
1554
1554
|
"value": {
|
|
@@ -1566,7 +1566,7 @@
|
|
|
1566
1566
|
"includeInNetlist": {
|
|
1567
1567
|
"type": "boolean"
|
|
1568
1568
|
},
|
|
1569
|
-
"
|
|
1569
|
+
"includeInPlacement": {
|
|
1570
1570
|
"type": "boolean"
|
|
1571
1571
|
}
|
|
1572
1572
|
},
|
package/docs/testing.md
CHANGED
|
@@ -6,6 +6,11 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|
|
6
6
|
|
|
7
7
|
# Testing
|
|
8
8
|
|
|
9
|
+
Exporter tests use only synthetic component responses and generated OLE
|
|
10
|
+
streams. Do not add native customer files or provider-derived raw fixtures.
|
|
11
|
+
When exercising source lookup, inject a fake fetcher/client and assert emitted
|
|
12
|
+
entries, progress events, checkpoints, diagnostics, and OLE round trips.
|
|
13
|
+
|
|
9
14
|
Run the complete suite:
|
|
10
15
|
|
|
11
16
|
```bash
|
package/package.json
CHANGED
package/spec/library-scope.md
CHANGED
|
@@ -31,6 +31,9 @@ rendering primitives.
|
|
|
31
31
|
- Optional renderer CSS
|
|
32
32
|
- Versioned normalized model schema identifiers and machine-readable schema
|
|
33
33
|
contracts
|
|
34
|
+
- Source component client, source bundle exporter, generated `.SchLib` and
|
|
35
|
+
`.PcbLib` byte exporters, and batch export orchestration with progress,
|
|
36
|
+
checkpoint, append-skip, merged output, and retry-friendly client hooks
|
|
34
37
|
|
|
35
38
|
## Out Of Scope
|
|
36
39
|
|
|
@@ -40,6 +43,8 @@ rendering primitives.
|
|
|
40
43
|
- Three.js runtime, OrbitControls, canvas mounting, and picking
|
|
41
44
|
- STEP mesh loading and browser script injection
|
|
42
45
|
- Model ZIP export UI and download orchestration
|
|
46
|
+
- Choosing a hosted component-source provider for applications; hosts inject
|
|
47
|
+
fetch and endpoint configuration explicitly
|
|
43
48
|
- Server, deployment, and app metadata endpoints
|
|
44
49
|
- Native document authoring, round-trip writing, GUI automation, and compiled
|
|
45
50
|
multi-sheet project netlist generation
|
|
@@ -267,7 +267,7 @@ export class AltiumLayoutParser {
|
|
|
267
267
|
* @param {{ width: number, height: number, marginWidth: number, paperSize?: string }} sheet
|
|
268
268
|
* @param {{ fields: Record<string, string | string[]> }[]} textRecords
|
|
269
269
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
|
|
270
|
-
* @param {{ x: number, y: number }[]} texts
|
|
270
|
+
* @param {{ x: number, y: number, cornerX?: number, cornerY?: number }[]} texts
|
|
271
271
|
* @param {{ x: number, y: number }[]} components
|
|
272
272
|
* @param {{ x: number, y: number }[]} pins
|
|
273
273
|
* @param {{ x: number, y: number, width: number, height: number }[]} rectangles
|
|
@@ -303,12 +303,47 @@ export class AltiumLayoutParser {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
const margin = Math.max(Number(sheet?.marginWidth || 20), 20)
|
|
306
|
+
const nativeTemplateSheet =
|
|
307
|
+
AltiumLayoutParser.#resolveNativeStandardTemplateSheetSize(
|
|
308
|
+
sheet,
|
|
309
|
+
bounds,
|
|
310
|
+
margin
|
|
311
|
+
)
|
|
312
|
+
if (nativeTemplateSheet) {
|
|
313
|
+
return {
|
|
314
|
+
...sheet,
|
|
315
|
+
...nativeTemplateSheet
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
AltiumLayoutParser.#shouldPreserveNativeStandardTemplateSize(
|
|
321
|
+
sheet,
|
|
322
|
+
bounds,
|
|
323
|
+
margin
|
|
324
|
+
)
|
|
325
|
+
) {
|
|
326
|
+
return sheet
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
const footerBounds = AltiumLayoutParser.#collectSchematicFooterBounds(
|
|
307
330
|
textRecords,
|
|
308
331
|
Number(sheet?.width || 0)
|
|
309
332
|
)
|
|
310
|
-
const
|
|
311
|
-
|
|
333
|
+
const footerLineBounds =
|
|
334
|
+
AltiumLayoutParser.#collectSchematicFooterLineBounds(
|
|
335
|
+
lines,
|
|
336
|
+
sheet,
|
|
337
|
+
margin
|
|
338
|
+
)
|
|
339
|
+
const requiredWidthResult =
|
|
340
|
+
AltiumLayoutParser.#resolveSchematicRequiredWidth(
|
|
341
|
+
sheet,
|
|
342
|
+
Math.max(bounds.maxX, footerBounds?.maxX || 0),
|
|
343
|
+
footerLineBounds?.maxX || 0,
|
|
344
|
+
margin
|
|
345
|
+
)
|
|
346
|
+
const requiredWidth = requiredWidthResult.width
|
|
312
347
|
const requiredHeight =
|
|
313
348
|
Math.max(bounds.maxY, footerBounds?.maxY || 0) + margin * 2
|
|
314
349
|
|
|
@@ -322,10 +357,12 @@ export class AltiumLayoutParser {
|
|
|
322
357
|
return sheet
|
|
323
358
|
}
|
|
324
359
|
|
|
325
|
-
const standardSheet =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
360
|
+
const standardSheet = requiredWidthResult.usesNativeFrameEdge
|
|
361
|
+
? null
|
|
362
|
+
: AltiumLayoutParser.#resolveStandardSheetSize(
|
|
363
|
+
requiredWidth,
|
|
364
|
+
requiredHeight
|
|
365
|
+
)
|
|
329
366
|
|
|
330
367
|
if (standardSheet) {
|
|
331
368
|
return {
|
|
@@ -344,11 +381,12 @@ export class AltiumLayoutParser {
|
|
|
344
381
|
sheet.height,
|
|
345
382
|
requiredHeight
|
|
346
383
|
)
|
|
347
|
-
const resolvedStandardSheet =
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
384
|
+
const resolvedStandardSheet = requiredWidthResult.usesNativeFrameEdge
|
|
385
|
+
? null
|
|
386
|
+
: AltiumLayoutParser.#resolveStandardSheetSize(
|
|
387
|
+
resolvedWidth,
|
|
388
|
+
resolvedHeight
|
|
389
|
+
)
|
|
352
390
|
|
|
353
391
|
if (resolvedStandardSheet) {
|
|
354
392
|
return {
|
|
@@ -367,6 +405,126 @@ export class AltiumLayoutParser {
|
|
|
367
405
|
}
|
|
368
406
|
}
|
|
369
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Resolves the standard template page named by Altium standard sheet
|
|
410
|
+
* records, preserving landscape or portrait orientation from the stored
|
|
411
|
+
* custom dimensions.
|
|
412
|
+
* @param {Record<string, string | string[]> | undefined} fields Sheet fields.
|
|
413
|
+
* @param {number} fallbackWidth Stored custom width.
|
|
414
|
+
* @param {number} fallbackHeight Stored custom height.
|
|
415
|
+
* @returns {{ width: number, height: number, paperSize: string, sourceWidth?: number, sourceHeight?: number } | null}
|
|
416
|
+
*/
|
|
417
|
+
static resolveSchematicTemplatePageSize(
|
|
418
|
+
fields,
|
|
419
|
+
fallbackWidth,
|
|
420
|
+
fallbackHeight
|
|
421
|
+
) {
|
|
422
|
+
if (parseNumericField(fields, 'SheetStyle') !== 1) {
|
|
423
|
+
return null
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const templateFileName = getField(fields, 'TemplateFileName')
|
|
427
|
+
const match = String(templateFileName || '').match(
|
|
428
|
+
/(?:^|[^a-z0-9])A([0-5])(?:[^a-z0-9]|$)/iu
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if (!match) {
|
|
432
|
+
return null
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const paperSize = 'A' + match[1]
|
|
436
|
+
const portraitSheet = ISO_A_PORTRAIT_SHEETS.find(
|
|
437
|
+
(sheet) => sheet.label === paperSize
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if (!portraitSheet) {
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const normalizedFallbackWidth = Number(fallbackWidth || 0)
|
|
445
|
+
const normalizedFallbackHeight = Number(fallbackHeight || 0)
|
|
446
|
+
const portraitTemplate =
|
|
447
|
+
parseNumericField(fields, 'WorkspaceOrientation') === 1 ||
|
|
448
|
+
/portrait/iu.test(String(templateFileName || ''))
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
portraitTemplate &&
|
|
452
|
+
normalizedFallbackWidth > normalizedFallbackHeight &&
|
|
453
|
+
normalizedFallbackHeight > 0
|
|
454
|
+
) {
|
|
455
|
+
return {
|
|
456
|
+
width: normalizedFallbackHeight,
|
|
457
|
+
height: normalizedFallbackWidth,
|
|
458
|
+
sourceWidth: normalizedFallbackHeight,
|
|
459
|
+
sourceHeight: normalizedFallbackWidth,
|
|
460
|
+
paperSize
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const landscape = normalizedFallbackWidth >= normalizedFallbackHeight
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
width: landscape ? portraitSheet.height : portraitSheet.width,
|
|
468
|
+
height: landscape ? portraitSheet.width : portraitSheet.height,
|
|
469
|
+
paperSize
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Resolves the page width required by recovered schematic geometry.
|
|
475
|
+
* Native template linework can use the declared custom width as the inner
|
|
476
|
+
* frame edge; adding both margins would create an artificial right gutter.
|
|
477
|
+
* @param {{ width?: number, borderOn?: boolean, titleBlockOn?: boolean, sheetStyle?: number }} sheet
|
|
478
|
+
* @param {number} maxX
|
|
479
|
+
* @param {number} footerLineMaxX
|
|
480
|
+
* @param {number} margin
|
|
481
|
+
* @returns {{ width: number, usesNativeFrameEdge: boolean }}
|
|
482
|
+
*/
|
|
483
|
+
static #resolveSchematicRequiredWidth(sheet, maxX, footerLineMaxX, margin) {
|
|
484
|
+
const reachesNativeFrameEdge =
|
|
485
|
+
Number(sheet?.sheetStyle || 0) !== 1 &&
|
|
486
|
+
Boolean(sheet?.borderOn || sheet?.titleBlockOn) &&
|
|
487
|
+
footerLineMaxX > 0 &&
|
|
488
|
+
maxX <= footerLineMaxX + 0.01
|
|
489
|
+
const rightPadding = reachesNativeFrameEdge ? margin : margin * 2
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
width: maxX + rightPadding,
|
|
493
|
+
usesNativeFrameEdge: reachesNativeFrameEdge
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Collects owned lower-page linework that represents native title-block or
|
|
499
|
+
* footer chrome rather than schematic content.
|
|
500
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
|
|
501
|
+
* @param {{ width?: number }} sheet
|
|
502
|
+
* @param {number} margin
|
|
503
|
+
* @returns {{ maxX: number } | null}
|
|
504
|
+
*/
|
|
505
|
+
static #collectSchematicFooterLineBounds(lines, sheet, margin) {
|
|
506
|
+
const footerLimit = Math.max(margin * 6, 120)
|
|
507
|
+
const sheetWidth = Math.max(Number(sheet?.width || 0), 0)
|
|
508
|
+
const footerStartX = sheetWidth > 0 ? sheetWidth * 0.5 : 0
|
|
509
|
+
const coordinates = []
|
|
510
|
+
|
|
511
|
+
for (const line of lines || []) {
|
|
512
|
+
if (!line?.ownerIndex) continue
|
|
513
|
+
if (Math.max(line.y1, line.y2) > footerLimit) continue
|
|
514
|
+
if (Math.max(line.x1, line.x2) < footerStartX) continue
|
|
515
|
+
|
|
516
|
+
coordinates.push(line.x1, line.x2)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!coordinates.length) {
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
maxX: Math.max(...coordinates)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
370
528
|
/**
|
|
371
529
|
* Returns true when the parser should trust the authored custom sheet
|
|
372
530
|
* dimensions instead of shrinking the page to visible content bounds.
|
|
@@ -403,11 +561,108 @@ export class AltiumLayoutParser {
|
|
|
403
561
|
return false
|
|
404
562
|
}
|
|
405
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Returns true when a standard template has already resolved to its native
|
|
566
|
+
* template coordinate frame rather than a promoted ISO sheet envelope.
|
|
567
|
+
* @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, sheetStyle?: number }} sheet
|
|
568
|
+
* @param {{ maxX: number, maxY: number }} bounds
|
|
569
|
+
* @param {number} margin
|
|
570
|
+
* @returns {boolean}
|
|
571
|
+
*/
|
|
572
|
+
static #shouldPreserveNativeStandardTemplateSize(sheet, bounds, margin) {
|
|
573
|
+
const width = Number(sheet?.width || 0)
|
|
574
|
+
const height = Number(sheet?.height || 0)
|
|
575
|
+
const sourceWidth = Number(sheet?.sourceWidth || 0)
|
|
576
|
+
const sourceHeight = Number(sheet?.sourceHeight || 0)
|
|
577
|
+
|
|
578
|
+
if (
|
|
579
|
+
Number(sheet?.sheetStyle || 0) !== 1 ||
|
|
580
|
+
!sheet?.paperSize ||
|
|
581
|
+
width <= 0 ||
|
|
582
|
+
height <= 0 ||
|
|
583
|
+
width !== sourceWidth ||
|
|
584
|
+
height !== sourceHeight
|
|
585
|
+
) {
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return (
|
|
590
|
+
Number(bounds?.maxX || 0) <= width - margin + 0.01 &&
|
|
591
|
+
Number(bounds?.maxY || 0) <= height - margin + 0.01
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Derives a native standard-template frame from embedded template graphics
|
|
597
|
+
* when those graphics overrun the stored custom dimensions but fit inside
|
|
598
|
+
* a smaller-than-ISO template envelope.
|
|
599
|
+
* @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, sheetStyle?: number }} sheet
|
|
600
|
+
* @param {{ maxX: number, maxY: number }} bounds
|
|
601
|
+
* @param {number} margin
|
|
602
|
+
* @returns {{ width: number, height: number, sourceWidth: number, sourceHeight: number } | null}
|
|
603
|
+
*/
|
|
604
|
+
static #resolveNativeStandardTemplateSheetSize(sheet, bounds, margin) {
|
|
605
|
+
const width = Number(sheet?.width || 0)
|
|
606
|
+
const height = Number(sheet?.height || 0)
|
|
607
|
+
const sourceWidth = Number(sheet?.sourceWidth || 0)
|
|
608
|
+
const sourceHeight = Number(sheet?.sourceHeight || 0)
|
|
609
|
+
|
|
610
|
+
if (
|
|
611
|
+
Number(sheet?.sheetStyle || 0) !== 1 ||
|
|
612
|
+
!sheet?.paperSize ||
|
|
613
|
+
sourceWidth <= 0 ||
|
|
614
|
+
sourceHeight <= 0 ||
|
|
615
|
+
width <= sourceWidth ||
|
|
616
|
+
height <= sourceHeight
|
|
617
|
+
) {
|
|
618
|
+
return null
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const maxX = Number(bounds?.maxX || 0)
|
|
622
|
+
const maxY = Number(bounds?.maxY || 0)
|
|
623
|
+
if (maxX <= sourceWidth && maxY <= sourceHeight) {
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const nativeWidth = AltiumLayoutParser.#roundTemplateExtent(
|
|
628
|
+
maxX + margin
|
|
629
|
+
)
|
|
630
|
+
const nativeHeight = AltiumLayoutParser.#roundTemplateExtent(
|
|
631
|
+
maxY + margin
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if (
|
|
635
|
+
nativeWidth <= sourceWidth ||
|
|
636
|
+
nativeHeight <= sourceHeight ||
|
|
637
|
+
nativeWidth >= width ||
|
|
638
|
+
nativeHeight >= height
|
|
639
|
+
) {
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
width: nativeWidth,
|
|
645
|
+
height: nativeHeight,
|
|
646
|
+
sourceWidth: nativeWidth,
|
|
647
|
+
sourceHeight: nativeHeight
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Rounds a recovered template frame edge to the next schematic grid
|
|
653
|
+
* interval.
|
|
654
|
+
* @param {number} value Raw extent.
|
|
655
|
+
* @returns {number}
|
|
656
|
+
*/
|
|
657
|
+
static #roundTemplateExtent(value) {
|
|
658
|
+
return Math.ceil(Math.max(Number(value || 0), 0) / 10) * 10
|
|
659
|
+
}
|
|
660
|
+
|
|
406
661
|
/**
|
|
407
662
|
* Collects the visible coordinate envelope from recovered schematic
|
|
408
663
|
* primitives.
|
|
409
664
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
|
|
410
|
-
* @param {{ x: number, y: number }[]} texts
|
|
665
|
+
* @param {{ x: number, y: number, cornerX?: number, cornerY?: number }[]} texts
|
|
411
666
|
* @param {{ x: number, y: number }[]} components
|
|
412
667
|
* @param {{ x: number, y: number }[]} pins
|
|
413
668
|
* @param {{ x: number, y: number, width: number, height: number }[]} rectangles
|
|
@@ -434,6 +689,13 @@ export class AltiumLayoutParser {
|
|
|
434
689
|
|
|
435
690
|
for (const text of texts) {
|
|
436
691
|
coordinates.push([text.x, text.y])
|
|
692
|
+
|
|
693
|
+
if (
|
|
694
|
+
Number.isFinite(Number(text.cornerX)) &&
|
|
695
|
+
Number.isFinite(Number(text.cornerY))
|
|
696
|
+
) {
|
|
697
|
+
coordinates.push([Number(text.cornerX), Number(text.cornerY)])
|
|
698
|
+
}
|
|
437
699
|
}
|
|
438
700
|
|
|
439
701
|
for (const component of components) {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { AltiumPcbLibExporter } from './AltiumPcbLibExporter.mjs'
|
|
6
|
+
import { AltiumSchLibExporter } from './AltiumSchLibExporter.mjs'
|
|
7
|
+
import { SourceBundleExporter } from './SourceBundleExporter.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Orchestrates source lookup and local library export.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumLibraryBatchExporter {
|
|
13
|
+
#client
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ client?: { fetchComponentBundle?: Function, searchComponents?: Function } }} [options] Batch options.
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.#client = options.client || null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Searches components and exports the matching ids.
|
|
24
|
+
* @param {string} query Search query.
|
|
25
|
+
* @param {object} [options] Export options.
|
|
26
|
+
* @returns {Promise<object>}
|
|
27
|
+
*/
|
|
28
|
+
async searchAndExport(query, options = {}) {
|
|
29
|
+
const rows = await this.#requireClient().searchComponents(
|
|
30
|
+
query,
|
|
31
|
+
options
|
|
32
|
+
)
|
|
33
|
+
return this.exportIds(
|
|
34
|
+
rows.map((row) => row.id).filter(Boolean),
|
|
35
|
+
options
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Exports one list of provider ids.
|
|
41
|
+
* @param {string[]} ids Component ids.
|
|
42
|
+
* @param {{ appendManifest?: { completedIds?: string[] }, includeSourceBundle?: boolean, includeSchLib?: boolean, includePcbLib?: boolean, merged?: boolean, continueOnError?: boolean, onProgress?: Function }} [options] Export options.
|
|
43
|
+
* @returns {Promise<{ entries: object[], bundles: object[], diagnostics: object[], checkpoint: { completedIds: string[] } }>}
|
|
44
|
+
*/
|
|
45
|
+
async exportIds(ids, options = {}) {
|
|
46
|
+
const completedIds = [
|
|
47
|
+
...new Set(options.appendManifest?.completedIds || [])
|
|
48
|
+
]
|
|
49
|
+
const completedSet = new Set(completedIds)
|
|
50
|
+
const entries = []
|
|
51
|
+
const bundles = []
|
|
52
|
+
const diagnostics = []
|
|
53
|
+
|
|
54
|
+
for (const id of ids
|
|
55
|
+
.map((value) => String(value || ''))
|
|
56
|
+
.filter(Boolean)) {
|
|
57
|
+
if (completedSet.has(id)) {
|
|
58
|
+
AltiumLibraryBatchExporter.#emitProgress(options, {
|
|
59
|
+
id,
|
|
60
|
+
status: 'skipped'
|
|
61
|
+
})
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const bundle =
|
|
67
|
+
await this.#requireClient().fetchComponentBundle(id)
|
|
68
|
+
bundles.push(bundle)
|
|
69
|
+
entries.push(
|
|
70
|
+
...AltiumLibraryBatchExporter.#buildPerComponentEntries(
|
|
71
|
+
id,
|
|
72
|
+
bundle,
|
|
73
|
+
options
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
completedSet.add(id)
|
|
77
|
+
completedIds.push(id)
|
|
78
|
+
AltiumLibraryBatchExporter.#emitProgress(options, {
|
|
79
|
+
id,
|
|
80
|
+
status: 'exported'
|
|
81
|
+
})
|
|
82
|
+
} catch (error) {
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
id,
|
|
85
|
+
severity: 'error',
|
|
86
|
+
message: String(error?.message || error)
|
|
87
|
+
})
|
|
88
|
+
AltiumLibraryBatchExporter.#emitProgress(options, {
|
|
89
|
+
id,
|
|
90
|
+
status: 'failed'
|
|
91
|
+
})
|
|
92
|
+
if (!options.continueOnError) {
|
|
93
|
+
throw error
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.merged && bundles.length) {
|
|
99
|
+
entries.push(
|
|
100
|
+
...AltiumLibraryBatchExporter.#buildMergedEntries(bundles)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
entries,
|
|
106
|
+
bundles,
|
|
107
|
+
diagnostics,
|
|
108
|
+
checkpoint: { completedIds }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Builds per-component export entries.
|
|
114
|
+
* @param {string} id Component id.
|
|
115
|
+
* @param {object} bundle Normalized bundle.
|
|
116
|
+
* @param {object} options Export options.
|
|
117
|
+
* @returns {object[]}
|
|
118
|
+
*/
|
|
119
|
+
static #buildPerComponentEntries(id, bundle, options) {
|
|
120
|
+
const prefix = AltiumLibraryBatchExporter.#safePathSegment(id) + '/'
|
|
121
|
+
const entries = []
|
|
122
|
+
|
|
123
|
+
if (options.includeSourceBundle) {
|
|
124
|
+
entries.push(
|
|
125
|
+
...SourceBundleExporter.export(bundle).entries.map((entry) => ({
|
|
126
|
+
...entry,
|
|
127
|
+
path: prefix + entry.path
|
|
128
|
+
}))
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.includeSchLib) {
|
|
133
|
+
entries.push({
|
|
134
|
+
path: prefix + 'library.SchLib',
|
|
135
|
+
bytes: AltiumSchLibExporter.export([bundle]),
|
|
136
|
+
contentType: 'application/octet-stream'
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.includePcbLib) {
|
|
141
|
+
entries.push({
|
|
142
|
+
path: prefix + 'library.PcbLib',
|
|
143
|
+
bytes: AltiumPcbLibExporter.export([bundle]),
|
|
144
|
+
contentType: 'application/octet-stream'
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return entries
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds merged library output entries.
|
|
153
|
+
* @param {object[]} bundles Normalized bundles.
|
|
154
|
+
* @returns {object[]}
|
|
155
|
+
*/
|
|
156
|
+
static #buildMergedEntries(bundles) {
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
path: 'merged/library.SchLib',
|
|
160
|
+
bytes: AltiumSchLibExporter.export(bundles),
|
|
161
|
+
contentType: 'application/octet-stream'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
path: 'merged/library.PcbLib',
|
|
165
|
+
bytes: AltiumPcbLibExporter.export(bundles),
|
|
166
|
+
contentType: 'application/octet-stream'
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Emits progress.
|
|
173
|
+
* @param {object} options Export options.
|
|
174
|
+
* @param {object} event Progress event.
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*/
|
|
177
|
+
static #emitProgress(options, event) {
|
|
178
|
+
if (typeof options.onProgress === 'function') {
|
|
179
|
+
options.onProgress(event)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sanitizes one path segment.
|
|
185
|
+
* @param {string} value Raw value.
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
static #safePathSegment(value) {
|
|
189
|
+
return String(value || 'component').replace(
|
|
190
|
+
/[\\/:\u0000-\u001f]/gu,
|
|
191
|
+
'_'
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the configured client or throws.
|
|
197
|
+
* @returns {{ fetchComponentBundle?: Function, searchComponents?: Function }}
|
|
198
|
+
*/
|
|
199
|
+
#requireClient() {
|
|
200
|
+
if (!this.#client) {
|
|
201
|
+
throw new Error('AltiumLibraryBatchExporter client is required.')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.#client
|
|
205
|
+
}
|
|
206
|
+
}
|