altium-toolkit 1.1.26 → 1.1.31

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.1.26",
3
+ "version": "1.1.31",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -52,6 +52,11 @@ import { SchematicBindingProvenanceParser } from './SchematicBindingProvenancePa
52
52
  import { SchematicConnectivityQaBuilder } from './SchematicConnectivityQaBuilder.mjs'
53
53
  import { SchematicQaReportBuilder } from './SchematicQaReportBuilder.mjs'
54
54
  import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
55
+ import { AltiumSchematicArcAngleNormalizer } from './AltiumSchematicArcAngleNormalizer.mjs'
56
+ import { AltiumSchematicFreeGraphicStrokeNormalizer } from './AltiumSchematicFreeGraphicStrokeNormalizer.mjs'
57
+ import { AltiumSchematicHiddenDesignatorResolver } from './AltiumSchematicHiddenDesignatorResolver.mjs'
58
+ import { AltiumSchematicPackedImageResolver } from './AltiumSchematicPackedImageResolver.mjs'
59
+ import { AltiumSchematicSheetBoundsNormalizer } from './AltiumSchematicSheetBoundsNormalizer.mjs'
55
60
  import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
56
61
  const {
57
62
  countMatchingKeys,
@@ -856,7 +861,7 @@ export class AltiumParser {
856
861
  )
857
862
  }
858
863
 
859
- return NormalizedModelSchema.attach({
864
+ const documentModel = NormalizedModelSchema.attach({
860
865
  kind: 'schematic',
861
866
  fileType: 'SchDoc',
862
867
  fileName,
@@ -935,6 +940,34 @@ export class AltiumParser {
935
940
  },
936
941
  bom
937
942
  })
943
+
944
+ return AltiumParser.#prepareSchematicDocument(
945
+ documentModel,
946
+ arrayBuffer
947
+ )
948
+ }
949
+
950
+ /**
951
+ * Applies universal schematic post-processing after the parser has built
952
+ * the normalized renderer model.
953
+ * @param {object} documentModel Parsed schematic document model.
954
+ * @param {ArrayBuffer} arrayBuffer Source file buffer.
955
+ * @returns {object}
956
+ */
957
+ static #prepareSchematicDocument(documentModel, arrayBuffer) {
958
+ return AltiumSchematicPackedImageResolver.hydrate(
959
+ AltiumSchematicFreeGraphicStrokeNormalizer.normalize(
960
+ AltiumSchematicSheetBoundsNormalizer.normalize(
961
+ AltiumSchematicArcAngleNormalizer.normalize(
962
+ AltiumSchematicHiddenDesignatorResolver.annotate(
963
+ documentModel,
964
+ arrayBuffer
965
+ )
966
+ )
967
+ )
968
+ ),
969
+ arrayBuffer
970
+ )
938
971
  }
939
972
 
940
973
  /**
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Normalizes Altium schematic arc angles before SVG rendering.
3
+ */
4
+ export class AltiumSchematicArcAngleNormalizer {
5
+ static #FULL_CIRCLE_TOLERANCE = 0.001
6
+
7
+ /**
8
+ * Rewrites non-full arcs to the shortest authored sweep.
9
+ * @param {object} documentModel Parsed document model.
10
+ * @returns {object}
11
+ */
12
+ static normalize(documentModel) {
13
+ const arcs = documentModel?.schematic?.arcs
14
+ if (!Array.isArray(arcs)) {
15
+ return documentModel
16
+ }
17
+
18
+ for (const arc of arcs) {
19
+ AltiumSchematicArcAngleNormalizer.#normalizeArc(arc)
20
+ }
21
+
22
+ return documentModel
23
+ }
24
+
25
+ /**
26
+ * Normalizes one arc's end angle while preserving full-circle arcs.
27
+ * @param {object} arc Arc primitive.
28
+ * @returns {void}
29
+ */
30
+ static #normalizeArc(arc) {
31
+ const startAngle = Number(arc?.startAngle)
32
+ const endAngle = Number(arc?.endAngle)
33
+ if (!Number.isFinite(startAngle) || !Number.isFinite(endAngle)) {
34
+ return
35
+ }
36
+
37
+ const delta =
38
+ AltiumSchematicArcAngleNormalizer.#normalizeSingleTurnDelta(
39
+ endAngle - startAngle
40
+ )
41
+ if (AltiumSchematicArcAngleNormalizer.#isFullCircleDelta(delta)) {
42
+ return
43
+ }
44
+
45
+ if (delta > 180) {
46
+ arc.endAngle = endAngle - 360
47
+ } else if (delta < -180) {
48
+ arc.endAngle = endAngle + 360
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Keeps a delta inside one signed turn.
54
+ * @param {number} delta Source angle delta.
55
+ * @returns {number}
56
+ */
57
+ static #normalizeSingleTurnDelta(delta) {
58
+ let normalized = delta
59
+
60
+ while (normalized <= -360) {
61
+ normalized += 360
62
+ }
63
+
64
+ while (normalized > 360) {
65
+ normalized -= 360
66
+ }
67
+
68
+ return normalized
69
+ }
70
+
71
+ /**
72
+ * Returns true when one normalized delta describes a full circle.
73
+ * @param {number} delta Normalized angle delta.
74
+ * @returns {boolean}
75
+ */
76
+ static #isFullCircleDelta(delta) {
77
+ return (
78
+ Math.abs(Math.abs(delta) - 360) <=
79
+ AltiumSchematicArcAngleNormalizer.#FULL_CIRCLE_TOLERANCE
80
+ )
81
+ }
82
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Normalizes ownerless Altium drawing primitive strokes to match app rendering.
3
+ */
4
+ export class AltiumSchematicFreeGraphicStrokeNormalizer {
5
+ static #SOURCE_THIN_STROKE = 1
6
+
7
+ static #APP_THIN_STROKE = 0.85
8
+
9
+ /**
10
+ * Applies app-side stroke normalization to free schematic graphics.
11
+ * @param {object} documentModel Parsed document model.
12
+ * @returns {object}
13
+ */
14
+ static normalize(documentModel) {
15
+ const schematic = documentModel?.schematic
16
+ if (!schematic || typeof schematic !== 'object') {
17
+ return documentModel
18
+ }
19
+
20
+ AltiumSchematicFreeGraphicStrokeNormalizer.#normalizeLines(
21
+ schematic.lines
22
+ )
23
+ for (const family of [
24
+ schematic.arcs,
25
+ schematic.beziers,
26
+ schematic.pies,
27
+ schematic.polygons,
28
+ schematic.rectangles,
29
+ schematic.roundedRectangles,
30
+ schematic.ellipses
31
+ ]) {
32
+ AltiumSchematicFreeGraphicStrokeNormalizer.#normalizeFreeGraphics(
33
+ family
34
+ )
35
+ }
36
+
37
+ return documentModel
38
+ }
39
+
40
+ /**
41
+ * Normalizes free drawing line strokes while leaving wires and buses alone.
42
+ * @param {object[] | undefined} lines Parsed line primitives.
43
+ * @returns {void}
44
+ */
45
+ static #normalizeLines(lines) {
46
+ if (!Array.isArray(lines)) {
47
+ return
48
+ }
49
+
50
+ for (const line of lines) {
51
+ if (
52
+ line?.recordType === '6' &&
53
+ !line.ownerIndex &&
54
+ line.isBus !== true
55
+ ) {
56
+ AltiumSchematicFreeGraphicStrokeNormalizer.#normalizeStroke(
57
+ line,
58
+ 'width'
59
+ )
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Normalizes ownerless drawing primitive strokes.
66
+ * @param {object[] | undefined} primitives Primitive family.
67
+ * @returns {void}
68
+ */
69
+ static #normalizeFreeGraphics(primitives) {
70
+ if (!Array.isArray(primitives)) {
71
+ return
72
+ }
73
+
74
+ for (const primitive of primitives) {
75
+ if (primitive?.ownerIndex) {
76
+ continue
77
+ }
78
+
79
+ AltiumSchematicFreeGraphicStrokeNormalizer.#normalizeStroke(
80
+ primitive,
81
+ 'width'
82
+ )
83
+ AltiumSchematicFreeGraphicStrokeNormalizer.#normalizeStroke(
84
+ primitive,
85
+ 'lineWidth'
86
+ )
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Rewrites one source thin-stroke value to the app-normalized width.
92
+ * @param {object} primitive Primitive object.
93
+ * @param {string} key Stroke key.
94
+ * @returns {void}
95
+ */
96
+ static #normalizeStroke(primitive, key) {
97
+ if (
98
+ Number(primitive?.[key]) ===
99
+ AltiumSchematicFreeGraphicStrokeNormalizer.#SOURCE_THIN_STROKE
100
+ ) {
101
+ primitive[key] =
102
+ AltiumSchematicFreeGraphicStrokeNormalizer.#APP_THIN_STROKE
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,191 @@
1
+ import { AsciiRecordParser } from './AsciiRecordParser.mjs'
2
+
3
+ /**
4
+ * Marks Altium schematic components whose native designator labels are hidden.
5
+ */
6
+ export class AltiumSchematicHiddenDesignatorResolver {
7
+ /**
8
+ * Annotates parsed components with schematic designator visibility metadata.
9
+ * @param {object} documentModel Parsed document model.
10
+ * @param {ArrayBuffer} arrayBuffer Source document bytes.
11
+ * @returns {object}
12
+ */
13
+ static annotate(documentModel, arrayBuffer) {
14
+ if (!documentModel?.schematic?.components?.length || !arrayBuffer) {
15
+ return documentModel
16
+ }
17
+
18
+ const hiddenDesignatorIds =
19
+ AltiumSchematicHiddenDesignatorResolver.#hiddenDesignatorUniqueIds(
20
+ arrayBuffer
21
+ )
22
+
23
+ if (!hiddenDesignatorIds.size) {
24
+ return documentModel
25
+ }
26
+
27
+ for (const component of documentModel.schematic.components) {
28
+ if (hiddenDesignatorIds.has(String(component.uniqueId || ''))) {
29
+ component.schematicDesignatorVisible = false
30
+ }
31
+ }
32
+
33
+ return documentModel
34
+ }
35
+
36
+ /**
37
+ * Finds component IDs whose owner designator record is explicitly hidden.
38
+ * @param {ArrayBuffer} arrayBuffer Source document bytes.
39
+ * @returns {Set<string>}
40
+ */
41
+ static #hiddenDesignatorUniqueIds(arrayBuffer) {
42
+ let records = []
43
+ try {
44
+ records = AsciiRecordParser.parse(arrayBuffer)
45
+ } catch {
46
+ return new Set()
47
+ }
48
+
49
+ const hiddenUniqueIds = new Set()
50
+
51
+ for (let index = 0; index < records.length; index += 1) {
52
+ const record = records[index]
53
+ if (
54
+ !AltiumSchematicHiddenDesignatorResolver.#isComponentRecord(
55
+ record
56
+ )
57
+ ) {
58
+ continue
59
+ }
60
+
61
+ const uniqueId =
62
+ AltiumSchematicHiddenDesignatorResolver.#field(
63
+ record.fields,
64
+ 'UniqueID'
65
+ ) ||
66
+ AltiumSchematicHiddenDesignatorResolver.#field(
67
+ record.fields,
68
+ 'UniqueId'
69
+ )
70
+ if (!uniqueId) {
71
+ continue
72
+ }
73
+
74
+ const visibility =
75
+ AltiumSchematicHiddenDesignatorResolver.#ownerDesignatorVisibility(
76
+ records,
77
+ index + 1
78
+ )
79
+ if (
80
+ visibility.hasHiddenDesignator &&
81
+ !visibility.hasVisibleDesignator
82
+ ) {
83
+ hiddenUniqueIds.add(uniqueId)
84
+ }
85
+ }
86
+
87
+ return hiddenUniqueIds
88
+ }
89
+
90
+ /**
91
+ * Resolves designator visibility in the records following one component.
92
+ * @param {{ fields: Record<string, string | string[]> }[]} records Parsed records.
93
+ * @param {number} startIndex First record after the component.
94
+ * @returns {{ hasHiddenDesignator: boolean, hasVisibleDesignator: boolean }}
95
+ */
96
+ static #ownerDesignatorVisibility(records, startIndex) {
97
+ let hasHiddenDesignator = false
98
+ let hasVisibleDesignator = false
99
+
100
+ for (let index = startIndex; index < records.length; index += 1) {
101
+ const record = records[index]
102
+ if (
103
+ AltiumSchematicHiddenDesignatorResolver.#isComponentRecord(
104
+ record
105
+ )
106
+ ) {
107
+ break
108
+ }
109
+
110
+ if (
111
+ !AltiumSchematicHiddenDesignatorResolver.#isDesignatorRecord(
112
+ record
113
+ )
114
+ ) {
115
+ continue
116
+ }
117
+
118
+ if (
119
+ AltiumSchematicHiddenDesignatorResolver.#isHiddenRecord(record)
120
+ ) {
121
+ hasHiddenDesignator = true
122
+ continue
123
+ }
124
+
125
+ hasVisibleDesignator = true
126
+ }
127
+
128
+ return { hasHiddenDesignator, hasVisibleDesignator }
129
+ }
130
+
131
+ /**
132
+ * Returns true when one raw record is a component placement.
133
+ * @param {{ fields?: Record<string, string | string[]> }} record Raw record.
134
+ * @returns {boolean}
135
+ */
136
+ static #isComponentRecord(record) {
137
+ return (
138
+ AltiumSchematicHiddenDesignatorResolver.#field(
139
+ record?.fields,
140
+ 'RECORD'
141
+ ) === '1'
142
+ )
143
+ }
144
+
145
+ /**
146
+ * Returns true when one raw record is a component designator label.
147
+ * @param {{ fields?: Record<string, string | string[]> }} record Raw record.
148
+ * @returns {boolean}
149
+ */
150
+ static #isDesignatorRecord(record) {
151
+ return (
152
+ AltiumSchematicHiddenDesignatorResolver.#field(
153
+ record?.fields,
154
+ 'Name'
155
+ )
156
+ .trim()
157
+ .toLowerCase() === 'designator'
158
+ )
159
+ }
160
+
161
+ /**
162
+ * Returns true when one raw record is explicitly hidden.
163
+ * @param {{ fields?: Record<string, string | string[]> }} record Raw record.
164
+ * @returns {boolean}
165
+ */
166
+ static #isHiddenRecord(record) {
167
+ return (
168
+ AltiumSchematicHiddenDesignatorResolver.#field(
169
+ record?.fields,
170
+ 'IsHidden'
171
+ )
172
+ .trim()
173
+ .toUpperCase() === 'T'
174
+ )
175
+ }
176
+
177
+ /**
178
+ * Reads one string field from parser records.
179
+ * @param {Record<string, string | string[]> | undefined} fields Field map.
180
+ * @param {string} key Field key.
181
+ * @returns {string}
182
+ */
183
+ static #field(fields, key) {
184
+ const value = fields?.[key]
185
+ if (Array.isArray(value)) {
186
+ return String(value[0] || '')
187
+ }
188
+
189
+ return String(value || '')
190
+ }
191
+ }