altium-toolkit 1.4.6 → 1.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -10,6 +10,7 @@ import { PcbSvgRenderer as LegacyPcbSvgRenderer } from '../ui/PcbSvgRenderer.mjs
10
10
  export class PcbSvgRenderer {
11
11
  static #SUBSURFACE_GROUP = '<g class="pcb-copper pcb-copper--subsurface">'
12
12
  static #SURFACE_GROUP = '<g class="pcb-copper pcb-copper--surface">'
13
+ static #FOOTPRINT_GROUP = '<g class="pcb-footprints">'
13
14
 
14
15
  /**
15
16
  * Renders one native Altium PCB document as SVG markup.
@@ -23,7 +24,7 @@ export class PcbSvgRenderer {
23
24
  documentModel,
24
25
  options
25
26
  )
26
- if (subsurfacePadIndexes.length === 0) return markup
27
+ if (subsurfacePadIndexes.size === 0) return markup
27
28
 
28
29
  return PcbSvgRenderer.#movePadsToSubsurfaceGroup(
29
30
  markup,
@@ -46,85 +47,125 @@ export class PcbSvgRenderer {
46
47
  * Layer-only exports do not use composite surface/subsurface grouping.
47
48
  * @param {Record<string, any>} documentModel Native renderer document.
48
49
  * @param {Record<string, any>} options Historical renderer options.
49
- * @returns {number[]} Stable pad indexes.
50
+ * @returns {Set<number>} Stable pad indexes.
50
51
  */
51
52
  static #subsurfacePadIndexes(documentModel, options) {
52
- if (options?.layerView) return []
53
+ if (options?.layerView) return new Set()
53
54
 
54
- return (documentModel?.pcb?.pads || [])
55
- .map((pad, index) =>
56
- pad?.copperRenderGroup === 'subsurface' ? index : -1
57
- )
58
- .filter((index) => index >= 0)
55
+ const indexes = new Set()
56
+ for (const [index, pad] of (documentModel?.pcb?.pads || []).entries()) {
57
+ if (pad?.copperRenderGroup === 'subsurface') {
58
+ indexes.add(index)
59
+ }
60
+ }
61
+ return indexes
59
62
  }
60
63
 
61
64
  /**
62
65
  * Moves contextual pad groups into the same SVG group as contextual traces
63
- * so the browser composites both primitives with one shared opacity.
66
+ * with one forward scan and one final markup reconstruction.
64
67
  * @param {string} markup Historical renderer markup.
65
- * @param {number[]} padIndexes Stable pad indexes to relocate.
68
+ * @param {Set<number>} padIndexes Stable pad indexes to relocate.
66
69
  * @returns {string} Copper-group-aware markup.
67
70
  */
68
71
  static #movePadsToSubsurfaceGroup(markup, padIndexes) {
69
72
  const subsurfaceStart = markup.indexOf(PcbSvgRenderer.#SUBSURFACE_GROUP)
70
73
  const surfaceStart = markup.indexOf(PcbSvgRenderer.#SURFACE_GROUP)
71
- if (subsurfaceStart < 0 || surfaceStart <= subsurfaceStart) {
74
+ const surfaceEnd = markup.indexOf(
75
+ PcbSvgRenderer.#FOOTPRINT_GROUP,
76
+ surfaceStart
77
+ )
78
+ if (
79
+ subsurfaceStart < 0 ||
80
+ surfaceStart <= subsurfaceStart ||
81
+ surfaceEnd <= surfaceStart
82
+ ) {
72
83
  return markup
73
84
  }
74
85
 
75
86
  const subsurfaceClose = markup.lastIndexOf('</g>', surfaceStart)
76
87
  if (subsurfaceClose < subsurfaceStart) return markup
77
88
 
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
89
+ const partition = PcbSvgRenderer.#partitionSurfacePads(
90
+ markup,
91
+ surfaceStart,
92
+ surfaceEnd,
93
+ padIndexes
98
94
  )
95
+ if (!partition.subsurfacePadMarkup) return markup
96
+
99
97
  return (
100
- remainingMarkup.slice(0, updatedSubsurfaceClose) +
101
- relocatedPads.join('') +
102
- remainingMarkup.slice(updatedSubsurfaceClose)
98
+ markup.slice(0, subsurfaceClose) +
99
+ partition.subsurfacePadMarkup +
100
+ markup.slice(subsurfaceClose, surfaceStart) +
101
+ partition.surfaceMarkup +
102
+ markup.slice(surfaceEnd)
103
103
  )
104
104
  }
105
105
 
106
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.
107
+ * Partitions selected top-level pad groups from one surface-group span.
108
+ * @param {string} markup Complete historical renderer markup.
109
+ * @param {number} start Surface-group start offset.
110
+ * @param {number} end Surface-group end offset.
111
+ * @param {Set<number>} padIndexes Stable pad indexes to relocate.
112
+ * @returns {{ surfaceMarkup: string, subsurfacePadMarkup: string }}
114
113
  */
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
114
+ static #partitionSurfacePads(markup, start, end, padIndexes) {
115
+ const surfaceFragments = []
116
+ const subsurfacePadFragments = []
117
+ const padGroupPattern =
118
+ /<g class="pcb-pad\b[^"]*"[^>]*data-element-key="pcb-pad-(\d+)"[^>]*>/gu
119
+ let cursor = start
120
+ padGroupPattern.lastIndex = start
119
121
 
120
- const groupStart = markup.lastIndexOf('<g', keyStart)
121
- const groupEndStart = markup.indexOf('</g>', keyStart)
122
- if (groupStart < minimumStart || groupEndStart < 0) return null
122
+ for (
123
+ let match = padGroupPattern.exec(markup);
124
+ match && match.index < end;
125
+ match = padGroupPattern.exec(markup)
126
+ ) {
127
+ const groupEnd = PcbSvgRenderer.#groupEnd(
128
+ markup,
129
+ padGroupPattern.lastIndex,
130
+ end
131
+ )
132
+ if (groupEnd < 0) break
133
+
134
+ if (padIndexes.has(Number(match[1]))) {
135
+ surfaceFragments.push(markup.slice(cursor, match.index))
136
+ subsurfacePadFragments.push(markup.slice(match.index, groupEnd))
137
+ cursor = groupEnd
138
+ }
139
+ padGroupPattern.lastIndex = groupEnd
140
+ }
123
141
 
124
- const groupEnd = groupEndStart + '</g>'.length
142
+ surfaceFragments.push(markup.slice(cursor, end))
125
143
  return {
126
- markup: markup.slice(0, groupStart) + markup.slice(groupEnd),
127
- padMarkup: markup.slice(groupStart, groupEnd)
144
+ surfaceMarkup: surfaceFragments.join(''),
145
+ subsurfacePadMarkup: subsurfacePadFragments.join('')
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Finds the matching close for one renderer-owned pad group.
151
+ * @param {string} markup Complete historical renderer markup.
152
+ * @param {number} contentStart Offset immediately after the pad open tag.
153
+ * @param {number} limit Exclusive end of the surface-group span.
154
+ * @returns {number} Offset immediately after the matching close, or -1.
155
+ */
156
+ static #groupEnd(markup, contentStart, limit) {
157
+ const groupTagPattern = /<g\b[^>]*>|<\/g>/gu
158
+ let depth = 1
159
+ groupTagPattern.lastIndex = contentStart
160
+
161
+ for (
162
+ let match = groupTagPattern.exec(markup);
163
+ match && match.index < limit;
164
+ match = groupTagPattern.exec(markup)
165
+ ) {
166
+ depth += match[0] === '</g>' ? -1 : 1
167
+ if (depth === 0) return groupTagPattern.lastIndex
128
168
  }
169
+ return -1
129
170
  }
130
171
  }