altium-toolkit 1.4.4 → 1.4.6
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/release-notes-v1.4.6.md +23 -0
- package/package.json +1 -1
- package/spec/2026-08-21-anisotropic-round-smt-pad-projection-design.md +84 -0
- package/spec/2026-08-21-anisotropic-round-smt-pad-projection-plan.md +91 -0
- package/src/convergence/AltiumCircuitJsonProjection.mjs +87 -4
- package/src/convergence/PcbSideResolvedRenderModel.mjs +6 -2
- package/src/convergence/PcbSvgRenderer.mjs +130 -0
- package/src/extensions.mjs +1 -0
- package/src/styles/altium-renderers.css +4 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# altium-toolkit 1.4.6
|
|
2
|
+
|
|
3
|
+
Version 1.4.6 preserves anisotropic ROUND/CIRCLE SMT pad dimensions when
|
|
4
|
+
converting Altium renderer data to Circuit JSON.
|
|
5
|
+
|
|
6
|
+
## Anisotropic ROUND/CIRCLE SMT projection
|
|
7
|
+
|
|
8
|
+
- Unequal positive dimensions become dimension-preserving `pill` geometry, or
|
|
9
|
+
`rotated_pill` when the authored rotation is meaningful.
|
|
10
|
+
- Zero-hole SMT filtering and stable source ordering keep through-hole and
|
|
11
|
+
unrelated pads unchanged.
|
|
12
|
+
- Equal-diameter ROUND/CIRCLE pads retain their existing circle geometry.
|
|
13
|
+
|
|
14
|
+
## Verification
|
|
15
|
+
|
|
16
|
+
- Public `Parser.parse()` regression coverage exercises schematic and PCB-only models,
|
|
17
|
+
mixed SMT/through-hole order, rotation tolerance, and metadata preservation.
|
|
18
|
+
- The complete test suite, performance check, feature-preservation check,
|
|
19
|
+
formatting check, and npm publish dry run passed for this release metadata.
|
|
20
|
+
|
|
21
|
+
## Contributor
|
|
22
|
+
|
|
23
|
+
Thanks to Ahmed Alshaybani for the original anisotropic pad projection work.
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Anisotropic Round SMT Pad Projection Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Preserve the authored X/Y geometry of every Altium surface-mount pad whose
|
|
6
|
+
native shape is `ROUND` or `CIRCLE` and whose two positive dimensions differ.
|
|
7
|
+
The canonical CircuitJSON model must expose these pads as `pill` or
|
|
8
|
+
`rotated_pill` records instead of expanding them into overlapping circles.
|
|
9
|
+
|
|
10
|
+
## Current Behavior and Root Cause
|
|
11
|
+
|
|
12
|
+
The historical CircuitJSON adapter classifies all Altium `ROUND` pads as
|
|
13
|
+
circles and assigns a radius derived from the larger dimension. That behavior
|
|
14
|
+
is part of the provenance-pinned 1.1.41 implementation and must remain
|
|
15
|
+
unchanged. The public convergence layer currently forwards the lossy SMT pad
|
|
16
|
+
records without restoring the native dimensions.
|
|
17
|
+
|
|
18
|
+
## Chosen Approach
|
|
19
|
+
|
|
20
|
+
Apply a source-neutral correction in `AltiumCircuitJsonProjection` after the
|
|
21
|
+
historical adapter has produced its immutable rows. Pair canonical
|
|
22
|
+
`pcb_smtpad` rows with native surface-mount pads in their deterministic source
|
|
23
|
+
order. For native round pads with unequal positive X/Y dimensions, replace only
|
|
24
|
+
the canonical geometry fields:
|
|
25
|
+
|
|
26
|
+
- use `pill` when rotation is effectively zero;
|
|
27
|
+
- use `rotated_pill` and `ccw_rotation` otherwise;
|
|
28
|
+
- retain width and height in millimetres;
|
|
29
|
+
- set the end radius to half the shorter dimension;
|
|
30
|
+
- preserve identifiers, ownership, layer, port, net, position, and metadata.
|
|
31
|
+
|
|
32
|
+
Use the same small rotation tolerance as the existing canonical adapter so
|
|
33
|
+
floating-point noise does not create a rotated shape. Equal round pads and all
|
|
34
|
+
other pad shapes remain byte-for-byte unchanged.
|
|
35
|
+
|
|
36
|
+
## Alternatives Considered
|
|
37
|
+
|
|
38
|
+
1. Modify the historical adapter. This is smaller locally but violates the
|
|
39
|
+
frozen native source contract and its feature-preservation gates.
|
|
40
|
+
2. Add an ECAD Forge renderer workaround. This would leave canonical data
|
|
41
|
+
incorrect for every other consumer and duplicate library behavior in the
|
|
42
|
+
host app.
|
|
43
|
+
3. Correct the convergence projection. This keeps the historical extension
|
|
44
|
+
stable while fixing the public canonical contract for all consumers and is
|
|
45
|
+
therefore the selected approach.
|
|
46
|
+
|
|
47
|
+
## Testing
|
|
48
|
+
|
|
49
|
+
Add repo-owned synthetic tests without committing any supplied native board:
|
|
50
|
+
|
|
51
|
+
- verify the public `Parser.parse()` result for unrotated and rotated
|
|
52
|
+
anisotropic round SMT pads;
|
|
53
|
+
- verify equal round pads stay circles;
|
|
54
|
+
- verify a through-hole pad interleaved between SMT pads does not disturb the
|
|
55
|
+
native-to-canonical pairing;
|
|
56
|
+
- verify near-zero rotation uses an ordinary pill;
|
|
57
|
+
- run the complete library test, format, performance, package dry-run, and
|
|
58
|
+
feature-preservation gates;
|
|
59
|
+
- verify the public parser against the existing local NodeMCU demo as
|
|
60
|
+
non-committed evidence that all 16 affected SMT pads retain their geometry.
|
|
61
|
+
|
|
62
|
+
The new test file credits both the original contributor and the project
|
|
63
|
+
copyright holder and remains GPL-3.0-or-later.
|
|
64
|
+
|
|
65
|
+
## Release and Deployment
|
|
66
|
+
|
|
67
|
+
Release the fix as the next patch of `altium-toolkit`, preserving the original
|
|
68
|
+
pull-request contributor in commit attribution and release notes. Publish the
|
|
69
|
+
GitHub release and npm package, then verify the registry version and `latest`
|
|
70
|
+
tag.
|
|
71
|
+
|
|
72
|
+
Update ECAD Forge through npm to the latest published versions of all five
|
|
73
|
+
toolkit dependencies, bump the app patch version, synchronize structured data,
|
|
74
|
+
run the full test, structured-data, static-build, and formatting gates, commit
|
|
75
|
+
and push `main`, create the GitHub release, watch the deployment workflow to a
|
|
76
|
+
successful conclusion, and verify the production app version and corrected pad
|
|
77
|
+
geometry.
|
|
78
|
+
|
|
79
|
+
## Non-Goals
|
|
80
|
+
|
|
81
|
+
Anisotropic through-hole copper and slot-hole representation is a related but
|
|
82
|
+
separate canonical-model issue because it requires preserving both the outer
|
|
83
|
+
copper aperture and drill geometry. This release does not silently change that
|
|
84
|
+
contract.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Anisotropic ROUND SMT Pad Projection Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **Execution note:** Follow the `superpowers:executing-plans` skill and verify each checkpoint before continuing.
|
|
4
|
+
|
|
5
|
+
**Goal:** Preserve anisotropic Altium ROUND/CIRCLE SMT pad dimensions by projecting them to Circuit JSON pill geometry, release the fix in `altium-toolkit`, and deploy ECAD Forge with current releases of all toolkit dependencies.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Keep the vendored parser adapter unchanged. Add a convergence-layer projection after native schematic projection, pairing adapted pads with canonical renderer pads by stable order. Only zero-hole ROUND/CIRCLE pads with unequal positive dimensions are reclassified. Use a small angular epsilon so floating-point noise near zero does not turn a horizontal pill into a rotated pill.
|
|
8
|
+
|
|
9
|
+
**Tech stack:** Node.js ESM, Node test runner, npm, GitHub CLI, GitHub Actions.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: Add a public-parser regression test
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
|
|
17
|
+
- Create: `tests/canonical-pcb-round-pad-projection.test.mjs`
|
|
18
|
+
- Reference: `src/Parser.mjs`
|
|
19
|
+
- Reference: `src/AltiumParser.mjs`
|
|
20
|
+
|
|
21
|
+
1. Add a synthetic renderer-model fixture by temporarily replacing `AltiumParser.parseArrayBufferToRendererModel`.
|
|
22
|
+
2. Exercise `Parser.parse()` rather than the convergence helper directly.
|
|
23
|
+
3. Include, in mixed renderer order:
|
|
24
|
+
- an unequal ROUND SMT pad at zero rotation;
|
|
25
|
+
- a through-hole ROUND pad between SMT pads;
|
|
26
|
+
- an unequal ROUND SMT pad with near-zero rotation;
|
|
27
|
+
- an unequal ROUND SMT pad at 90 degrees;
|
|
28
|
+
- an equal-diameter ROUND SMT pad.
|
|
29
|
+
4. Assert the public Circuit JSON output contains `pill`, `pill`, `rotated_pill`, and `circle` respectively, with both anisotropic dimensions preserved.
|
|
30
|
+
5. Run the focused test and confirm it fails because the unequal pads are still circles.
|
|
31
|
+
6. Commit the failing regression test with contributor attribution.
|
|
32
|
+
|
|
33
|
+
## Task 2: Implement the convergence projection
|
|
34
|
+
|
|
35
|
+
**Files:**
|
|
36
|
+
|
|
37
|
+
- Modify: `src/convergence/AltiumCircuitJsonProjection.mjs`
|
|
38
|
+
- Test: `tests/canonical-pcb-round-pad-projection.test.mjs`
|
|
39
|
+
|
|
40
|
+
1. Add a private projection method that filters native renderer pads to SMT pads and pairs them with canonical SMT pads in stable order.
|
|
41
|
+
2. For renderer `ROUND`/`CIRCLE` pads with zero hole, positive unequal X/Y sizes, update the canonical pad shape:
|
|
42
|
+
- `pill` when absolute rotation is at most `1e-6` degrees;
|
|
43
|
+
- `rotated_pill` otherwise.
|
|
44
|
+
3. Set `radius = min(width, height) / 2`, preserve `width` and `height`, and leave IDs, layers, positions, rotations, and metadata untouched.
|
|
45
|
+
4. Call the projection after the existing native schematic projection and before model construction.
|
|
46
|
+
5. Run the focused test and confirm it passes.
|
|
47
|
+
6. Run `npm test`, `npm run check:format`, `npm run test:performance`, and `npm run test:features`.
|
|
48
|
+
7. Parse the noncommitted NodeMCU sample on main and on the fixed tree; verify all 16 previously circular unequal ROUND SMT pads become dimension-preserving pills without changing the total SMT-pad count.
|
|
49
|
+
8. Commit the implementation with `Co-authored-by: Ahmed Alshaybani`.
|
|
50
|
+
|
|
51
|
+
## Task 3: Review and release `altium-toolkit`
|
|
52
|
+
|
|
53
|
+
**Files:**
|
|
54
|
+
|
|
55
|
+
- Modify: `package.json`
|
|
56
|
+
- Modify: `package-lock.json`
|
|
57
|
+
- Create: `docs/release-notes-v1.4.6.md`
|
|
58
|
+
|
|
59
|
+
1. Request an independent code review of the full change from the original main revision.
|
|
60
|
+
2. Address all valid findings and rerun affected tests.
|
|
61
|
+
3. Bump the package patch version to `1.4.6` without creating an automatic tag.
|
|
62
|
+
4. Add concise release notes describing the generalized ROUND/CIRCLE SMT projection, public-parser regression coverage, and contributor credit.
|
|
63
|
+
5. Run fresh release gates: `npm test`, `npm run check:format`, `npm run test:performance`, `npm run test:features`, and `npm publish --dry-run`.
|
|
64
|
+
6. Commit the release metadata and push `main`.
|
|
65
|
+
7. Create and verify GitHub release `v1.4.6`.
|
|
66
|
+
8. Publish `altium-toolkit@1.4.6` to npm through web authentication and verify the registry version and `latest` dist-tag.
|
|
67
|
+
9. Comment on PR #1 with the adapted release outcome and contributor credit.
|
|
68
|
+
|
|
69
|
+
## Task 4: Update and verify ECAD Forge
|
|
70
|
+
|
|
71
|
+
**Files:**
|
|
72
|
+
|
|
73
|
+
- Modify: `package.json`
|
|
74
|
+
- Modify: `package-lock.json`
|
|
75
|
+
- Modify generated structured-data HTML under `src/` if the sync command changes it
|
|
76
|
+
|
|
77
|
+
1. Query npm for the latest versions of `gerber-toolkit`, `circuitjson-toolkit`, `altium-toolkit`, `kicad-toolkit`, and `pcb-scene3d-viewer`.
|
|
78
|
+
2. Install all five current releases explicitly, ensuring `altium-toolkit@1.4.6` is selected.
|
|
79
|
+
3. Bump the ECAD Forge patch version to `1.13.22` without creating an automatic tag.
|
|
80
|
+
4. Run `npm run sync:structured-data` and inspect the generated changes.
|
|
81
|
+
5. Run `npm test`, `npm run check:structured-data`, `npm run build:static`, and the repository format check if present.
|
|
82
|
+
6. Commit only the intended dependency, version, and generated structured-data changes.
|
|
83
|
+
|
|
84
|
+
## Task 5: Release, deploy, and verify ECAD Forge
|
|
85
|
+
|
|
86
|
+
1. Push ECAD Forge `main`.
|
|
87
|
+
2. Create and verify GitHub release `v1.13.22` with concise release notes.
|
|
88
|
+
3. Resolve the `Deploy to FTP (main)` workflow run associated with the pushed commit and watch it to a successful conclusion.
|
|
89
|
+
4. Verify the production site responds successfully and reports the new app version.
|
|
90
|
+
5. Load an Altium PCB through the production app and confirm the affected anisotropic ROUND SMT pads render as elongated pads rather than circles.
|
|
91
|
+
6. Confirm both repositories are clean and summarize exact commits, releases, npm versions, test results, deployment conclusion, and production evidence.
|
|
@@ -25,11 +25,21 @@ export class AltiumCircuitJsonProjection {
|
|
|
25
25
|
*/
|
|
26
26
|
static project(adapted, native) {
|
|
27
27
|
const nativeSchematic = native?.schematic
|
|
28
|
-
if (!nativeSchematic)
|
|
28
|
+
if (!nativeSchematic) {
|
|
29
|
+
return AltiumCircuitJsonProjection.#projectPcbSmtPadShapes(
|
|
30
|
+
adapted,
|
|
31
|
+
native?.pcb
|
|
32
|
+
)
|
|
33
|
+
}
|
|
29
34
|
const schematic =
|
|
30
35
|
AltiumCircuitJsonProjection.#schematicWithSourceTypes(
|
|
31
36
|
nativeSchematic
|
|
32
37
|
)
|
|
38
|
+
const elementsWithProjectedPadShapes =
|
|
39
|
+
AltiumCircuitJsonProjection.#projectPcbSmtPadShapes(
|
|
40
|
+
adapted,
|
|
41
|
+
native?.pcb
|
|
42
|
+
)
|
|
33
43
|
|
|
34
44
|
const sourceFormat = Primitives.sourceFormat(native)
|
|
35
45
|
const idScope = Primitives.idScope(native, sourceFormat)
|
|
@@ -53,18 +63,18 @@ export class AltiumCircuitJsonProjection {
|
|
|
53
63
|
])
|
|
54
64
|
)
|
|
55
65
|
const legacySchematicSourceTraceIds = new Set(
|
|
56
|
-
|
|
66
|
+
elementsWithProjectedPadShapes
|
|
57
67
|
.filter((element) => element?.type === 'schematic_trace')
|
|
58
68
|
.map((element) => String(element.source_trace_id || ''))
|
|
59
69
|
.filter(Boolean)
|
|
60
70
|
)
|
|
61
71
|
const protectedPcbSourceTraceIds = new Set(
|
|
62
|
-
|
|
72
|
+
elementsWithProjectedPadShapes
|
|
63
73
|
.filter((element) => element?.type === 'pcb_trace')
|
|
64
74
|
.map((element) => String(element.source_trace_id || ''))
|
|
65
75
|
.filter(Boolean)
|
|
66
76
|
)
|
|
67
|
-
const model =
|
|
77
|
+
const model = elementsWithProjectedPadShapes
|
|
68
78
|
.filter((element) =>
|
|
69
79
|
AltiumCircuitJsonProjection.#preservesAdaptedElement(
|
|
70
80
|
element,
|
|
@@ -109,6 +119,79 @@ export class AltiumCircuitJsonProjection {
|
|
|
109
119
|
)
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Restores native dimensions for unequal round SMT pads after the frozen
|
|
124
|
+
* historical adapter has classified them as circles.
|
|
125
|
+
* @param {object[]} adapted Historical adapter output.
|
|
126
|
+
* @param {Record<string, any> | undefined} pcb Native PCB model.
|
|
127
|
+
* @returns {object[]} Canonical rows with projected SMT pad shapes.
|
|
128
|
+
*/
|
|
129
|
+
static #projectPcbSmtPadShapes(adapted, pcb) {
|
|
130
|
+
const nativeSmtPads = Primitives.array(pcb?.pads).filter(
|
|
131
|
+
(pad) => !Primitives.isThroughHolePad(pad)
|
|
132
|
+
)
|
|
133
|
+
let smtPadIndex = 0
|
|
134
|
+
|
|
135
|
+
return adapted.map((element) => {
|
|
136
|
+
if (element?.type !== 'pcb_smtpad') return element
|
|
137
|
+
|
|
138
|
+
const nativePad = nativeSmtPads[smtPadIndex]
|
|
139
|
+
smtPadIndex += 1
|
|
140
|
+
if (
|
|
141
|
+
!AltiumCircuitJsonProjection.#isAnisotropicRoundSmtPad(
|
|
142
|
+
nativePad
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return element
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const width = Primitives.milNumber(
|
|
149
|
+
nativePad.sizeTopX || nativePad.sizeX || nativePad.width,
|
|
150
|
+
0
|
|
151
|
+
)
|
|
152
|
+
const height = Primitives.milNumber(
|
|
153
|
+
nativePad.sizeTopY || nativePad.sizeY || nativePad.height,
|
|
154
|
+
0
|
|
155
|
+
)
|
|
156
|
+
const rotation =
|
|
157
|
+
Primitives.number(
|
|
158
|
+
nativePad.rotation || nativePad.holeRotation,
|
|
159
|
+
0
|
|
160
|
+
) || 0
|
|
161
|
+
const hasRotation = Math.abs(rotation) > 0.000001
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...element,
|
|
165
|
+
shape: hasRotation ? 'rotated_pill' : 'pill',
|
|
166
|
+
width,
|
|
167
|
+
height,
|
|
168
|
+
radius: Primitives.round(Math.min(width, height) / 2),
|
|
169
|
+
...(hasRotation ? { ccw_rotation: rotation } : {})
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Returns whether a native SMT pad requires a dimension-preserving pill.
|
|
176
|
+
* @param {Record<string, any> | undefined} pad Native PCB pad.
|
|
177
|
+
* @returns {boolean} Whether the native pad is an unequal round SMT pad.
|
|
178
|
+
*/
|
|
179
|
+
static #isAnisotropicRoundSmtPad(pad) {
|
|
180
|
+
const shape = String(
|
|
181
|
+
pad?.shapeTopName || pad?.shapeName || pad?.shape || ''
|
|
182
|
+
)
|
|
183
|
+
.trim()
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
if (shape !== 'round' && shape !== 'circle') return false
|
|
186
|
+
|
|
187
|
+
const width =
|
|
188
|
+
Primitives.number(pad?.sizeTopX ?? pad?.sizeX ?? pad?.width, 0) || 0
|
|
189
|
+
const height =
|
|
190
|
+
Primitives.number(pad?.sizeTopY ?? pad?.sizeY ?? pad?.height, 0) ||
|
|
191
|
+
0
|
|
192
|
+
return width > 0 && height > 0 && width !== height
|
|
193
|
+
}
|
|
194
|
+
|
|
112
195
|
/**
|
|
113
196
|
* Classifies native Altium record-27 wire segments at the convergence
|
|
114
197
|
* boundary while preserving explicit source-neutral classifications.
|
|
@@ -123,10 +123,14 @@ export class PcbSideResolvedRenderModel {
|
|
|
123
123
|
const layerId = PcbSideResolvedRenderModel.#effectivePadLayerId(pad)
|
|
124
124
|
const apertureSide =
|
|
125
125
|
layerId === 1 ? 'front' : layerId === 32 ? 'back' : side
|
|
126
|
-
|
|
126
|
+
const projectedPad = {
|
|
127
|
+
...pad,
|
|
128
|
+
copperRenderGroup: apertureSide === side ? 'surface' : 'subsurface'
|
|
129
|
+
}
|
|
130
|
+
if (apertureSide !== 'back') return projectedPad
|
|
127
131
|
|
|
128
132
|
return {
|
|
129
|
-
...
|
|
133
|
+
...projectedPad,
|
|
130
134
|
sizeTopX: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
131
135
|
pad.sizeBottomX,
|
|
132
136
|
pad.sizeMidX,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { PcbSvgRenderer as LegacyPcbSvgRenderer } from '../ui/PcbSvgRenderer.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders native Altium PCB models through the preserved historical renderer
|
|
8
|
+
* while applying convergence-owned copper grouping semantics.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbSvgRenderer {
|
|
11
|
+
static #SUBSURFACE_GROUP = '<g class="pcb-copper pcb-copper--subsurface">'
|
|
12
|
+
static #SURFACE_GROUP = '<g class="pcb-copper pcb-copper--surface">'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders one native Altium PCB document as SVG markup.
|
|
16
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
17
|
+
* @param {Record<string, any>} [options] Historical renderer options.
|
|
18
|
+
* @returns {string} Rendered SVG panel markup.
|
|
19
|
+
*/
|
|
20
|
+
static render(documentModel, options = {}) {
|
|
21
|
+
const markup = LegacyPcbSvgRenderer.render(documentModel, options)
|
|
22
|
+
const subsurfacePadIndexes = PcbSvgRenderer.#subsurfacePadIndexes(
|
|
23
|
+
documentModel,
|
|
24
|
+
options
|
|
25
|
+
)
|
|
26
|
+
if (subsurfacePadIndexes.length === 0) return markup
|
|
27
|
+
|
|
28
|
+
return PcbSvgRenderer.#movePadsToSubsurfaceGroup(
|
|
29
|
+
markup,
|
|
30
|
+
subsurfacePadIndexes
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Renders one deterministic SVG entry per physical or primitive layer.
|
|
36
|
+
* Layer exports intentionally preserve the historical layer-only output.
|
|
37
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
38
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string, role: string, svg: string }[]}
|
|
39
|
+
*/
|
|
40
|
+
static renderLayerSvgs(documentModel) {
|
|
41
|
+
return LegacyPcbSvgRenderer.renderLayerSvgs(documentModel)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Finds pad indexes whose copper must share the contextual copper group.
|
|
46
|
+
* Layer-only exports do not use composite surface/subsurface grouping.
|
|
47
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
48
|
+
* @param {Record<string, any>} options Historical renderer options.
|
|
49
|
+
* @returns {number[]} Stable pad indexes.
|
|
50
|
+
*/
|
|
51
|
+
static #subsurfacePadIndexes(documentModel, options) {
|
|
52
|
+
if (options?.layerView) return []
|
|
53
|
+
|
|
54
|
+
return (documentModel?.pcb?.pads || [])
|
|
55
|
+
.map((pad, index) =>
|
|
56
|
+
pad?.copperRenderGroup === 'subsurface' ? index : -1
|
|
57
|
+
)
|
|
58
|
+
.filter((index) => index >= 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Moves contextual pad groups into the same SVG group as contextual traces
|
|
63
|
+
* so the browser composites both primitives with one shared opacity.
|
|
64
|
+
* @param {string} markup Historical renderer markup.
|
|
65
|
+
* @param {number[]} padIndexes Stable pad indexes to relocate.
|
|
66
|
+
* @returns {string} Copper-group-aware markup.
|
|
67
|
+
*/
|
|
68
|
+
static #movePadsToSubsurfaceGroup(markup, padIndexes) {
|
|
69
|
+
const subsurfaceStart = markup.indexOf(PcbSvgRenderer.#SUBSURFACE_GROUP)
|
|
70
|
+
const surfaceStart = markup.indexOf(PcbSvgRenderer.#SURFACE_GROUP)
|
|
71
|
+
if (subsurfaceStart < 0 || surfaceStart <= subsurfaceStart) {
|
|
72
|
+
return markup
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const subsurfaceClose = markup.lastIndexOf('</g>', surfaceStart)
|
|
76
|
+
if (subsurfaceClose < subsurfaceStart) return markup
|
|
77
|
+
|
|
78
|
+
let remainingMarkup = markup
|
|
79
|
+
const relocatedPads = []
|
|
80
|
+
for (const padIndex of padIndexes) {
|
|
81
|
+
const result = PcbSvgRenderer.#extractPadGroup(
|
|
82
|
+
remainingMarkup,
|
|
83
|
+
padIndex,
|
|
84
|
+
surfaceStart
|
|
85
|
+
)
|
|
86
|
+
if (!result) continue
|
|
87
|
+
remainingMarkup = result.markup
|
|
88
|
+
relocatedPads.push(result.padMarkup)
|
|
89
|
+
}
|
|
90
|
+
if (relocatedPads.length === 0) return markup
|
|
91
|
+
|
|
92
|
+
const updatedSurfaceStart = remainingMarkup.indexOf(
|
|
93
|
+
PcbSvgRenderer.#SURFACE_GROUP
|
|
94
|
+
)
|
|
95
|
+
const updatedSubsurfaceClose = remainingMarkup.lastIndexOf(
|
|
96
|
+
'</g>',
|
|
97
|
+
updatedSurfaceStart
|
|
98
|
+
)
|
|
99
|
+
return (
|
|
100
|
+
remainingMarkup.slice(0, updatedSubsurfaceClose) +
|
|
101
|
+
relocatedPads.join('') +
|
|
102
|
+
remainingMarkup.slice(updatedSubsurfaceClose)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extracts one top-level pad group from the historical surface group.
|
|
108
|
+
* Pad groups contain only leaf SVG shapes, so their first closing group is
|
|
109
|
+
* also the matching closing tag.
|
|
110
|
+
* @param {string} markup Current renderer markup.
|
|
111
|
+
* @param {number} padIndex Stable pad index.
|
|
112
|
+
* @param {number} minimumStart Earliest valid surface-group position.
|
|
113
|
+
* @returns {{ markup: string, padMarkup: string } | null} Extraction result.
|
|
114
|
+
*/
|
|
115
|
+
static #extractPadGroup(markup, padIndex, minimumStart) {
|
|
116
|
+
const elementKey = 'data-element-key="pcb-pad-' + padIndex + '"'
|
|
117
|
+
const keyStart = markup.indexOf(elementKey, minimumStart)
|
|
118
|
+
if (keyStart < 0) return null
|
|
119
|
+
|
|
120
|
+
const groupStart = markup.lastIndexOf('<g', keyStart)
|
|
121
|
+
const groupEndStart = markup.indexOf('</g>', keyStart)
|
|
122
|
+
if (groupStart < minimumStart || groupEndStart < 0) return null
|
|
123
|
+
|
|
124
|
+
const groupEnd = groupEndStart + '</g>'.length
|
|
125
|
+
return {
|
|
126
|
+
markup: markup.slice(0, groupStart) + markup.slice(groupEnd),
|
|
127
|
+
padMarkup: markup.slice(groupStart, groupEnd)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/extensions.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export * from 'circuitjson-toolkit/extensions'
|
|
5
5
|
|
|
6
6
|
export { AltiumExtensionResolver } from './convergence/AltiumExtensionResolver.mjs'
|
|
7
|
+
export { PcbSvgRenderer } from './convergence/PcbSvgRenderer.mjs'
|
|
7
8
|
export { SchematicSvgRenderer } from './convergence/SchematicSvgRenderer.mjs'
|
|
8
9
|
export * from './legacy-parser.mjs'
|
|
9
10
|
export * from './legacy-netlist-query.mjs'
|