altium-toolkit 1.0.8 → 1.0.10

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.
Files changed (102) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,312 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ const { getField, parseNumericField } = ParserUtils
8
+ const COMPONENT_OWNER_AXIS_THRESHOLD = 160
9
+
10
+ /**
11
+ * Resolves native owner-linked text records for schematic components.
12
+ */
13
+ export class SchematicComponentOwnerTextResolver {
14
+ /**
15
+ * Resolves raw text records belonging to one schematic component.
16
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} componentRecord Component placement record.
17
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Indexed schematic records.
18
+ * @param {Map<string, { fields: Record<string, string | string[]> }[]>} relatedTexts Text records grouped by native OwnerIndex.
19
+ * @returns {{ fields: Record<string, string | string[]> }[]}
20
+ */
21
+ static resolveOwnerTexts(componentRecord, records, relatedTexts) {
22
+ const ownerIndexes =
23
+ SchematicComponentOwnerTextResolver.#resolveOwnerIndexes(
24
+ componentRecord,
25
+ records
26
+ )
27
+
28
+ for (const ownerIndex of ownerIndexes) {
29
+ const ownerTexts = relatedTexts.get(ownerIndex) || []
30
+ if (
31
+ SchematicComponentOwnerTextResolver.#hasComponentLabelText(
32
+ ownerTexts
33
+ )
34
+ ) {
35
+ return ownerTexts
36
+ }
37
+ }
38
+
39
+ return []
40
+ }
41
+
42
+ /**
43
+ * Returns true when a schematic primitive participates in owner display
44
+ * mode selection.
45
+ * @param {Record<string, string | string[]>} fields
46
+ * @returns {boolean}
47
+ */
48
+ static isDisplayModeSelectablePrimitive(fields) {
49
+ const recordType = getField(fields, 'RECORD')
50
+
51
+ return (
52
+ recordType === '2' ||
53
+ recordType === '6' ||
54
+ recordType === '11' ||
55
+ recordType === '12' ||
56
+ recordType === '13' ||
57
+ recordType === '27' ||
58
+ (SchematicComponentOwnerTextResolver.#hasCoordinatePair(
59
+ fields,
60
+ 'Location'
61
+ ) &&
62
+ SchematicComponentOwnerTextResolver.#hasCoordinatePair(
63
+ fields,
64
+ 'Corner'
65
+ ))
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Resolves candidate owner indexes for one schematic component record.
71
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} componentRecord Component placement record.
72
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Indexed schematic records.
73
+ * @returns {string[]}
74
+ */
75
+ static #resolveOwnerIndexes(componentRecord, records) {
76
+ return SchematicComponentOwnerTextResolver.#dedupeStrings([
77
+ SchematicComponentOwnerTextResolver.#inferFollowingOwnerIndex(
78
+ componentRecord,
79
+ records
80
+ ),
81
+ ...SchematicComponentOwnerTextResolver.#legacyOwnerIndexes(
82
+ componentRecord
83
+ )
84
+ ])
85
+ }
86
+
87
+ /**
88
+ * Infers the owner id used by the display primitives following a component.
89
+ * @param {{ recordIndex?: number }} componentRecord Component placement record.
90
+ * @param {{ fields: Record<string, string | string[]> }[]} records Indexed schematic records.
91
+ * @returns {string}
92
+ */
93
+ static #inferFollowingOwnerIndex(componentRecord, records) {
94
+ if (!Number.isInteger(componentRecord?.recordIndex)) {
95
+ return ''
96
+ }
97
+
98
+ let labelOwnerIndex = ''
99
+
100
+ for (
101
+ let index = componentRecord.recordIndex + 1;
102
+ index < records.length;
103
+ index += 1
104
+ ) {
105
+ const fields = records[index]?.fields
106
+ const recordType = getField(fields, 'RECORD')
107
+ if (recordType === '1' || recordType === '45') {
108
+ break
109
+ }
110
+
111
+ const ownerIndex = getField(fields, 'OwnerIndex')
112
+ if (!ownerIndex) {
113
+ continue
114
+ }
115
+
116
+ if (
117
+ SchematicComponentOwnerTextResolver.#isDisplayOwnerRecord(
118
+ fields
119
+ ) &&
120
+ SchematicComponentOwnerTextResolver.#isNearComponent(
121
+ fields,
122
+ componentRecord
123
+ )
124
+ ) {
125
+ return ownerIndex
126
+ }
127
+
128
+ if (
129
+ !labelOwnerIndex &&
130
+ SchematicComponentOwnerTextResolver.#isComponentLabelRecord(
131
+ fields
132
+ ) &&
133
+ SchematicComponentOwnerTextResolver.#isNearComponent(
134
+ fields,
135
+ componentRecord
136
+ )
137
+ ) {
138
+ labelOwnerIndex = ownerIndex
139
+ }
140
+ }
141
+
142
+ return labelOwnerIndex
143
+ }
144
+
145
+ /**
146
+ * Resolves legacy owner keys used by older printable schematic records.
147
+ * @param {{ fields: Record<string, string | string[]> }} componentRecord Component placement record.
148
+ * @returns {string[]}
149
+ */
150
+ static #legacyOwnerIndexes(componentRecord) {
151
+ const indexInSheet = parseNumericField(
152
+ componentRecord?.fields,
153
+ 'IndexInSheet'
154
+ )
155
+ const ownerIndex = getField(componentRecord?.fields, 'OwnerIndex')
156
+ const keys = []
157
+
158
+ if (indexInSheet !== null) {
159
+ keys.push(String(indexInSheet + 1), String(indexInSheet))
160
+ }
161
+
162
+ keys.push(ownerIndex)
163
+
164
+ return keys
165
+ }
166
+
167
+ /**
168
+ * Returns true when one record can identify a component display owner.
169
+ * @param {Record<string, string | string[]>} fields Raw record fields.
170
+ * @returns {boolean}
171
+ */
172
+ static #isDisplayOwnerRecord(fields) {
173
+ const ownerPartId = getField(fields, 'OwnerPartId')
174
+
175
+ return (
176
+ ownerPartId !== '-1' &&
177
+ SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
178
+ fields
179
+ )
180
+ )
181
+ }
182
+
183
+ /**
184
+ * Returns true when a related text group contains a component label.
185
+ * @param {{ fields: Record<string, string | string[]> }[]} records Text records.
186
+ * @returns {boolean}
187
+ */
188
+ static #hasComponentLabelText(records) {
189
+ return records.some((record) =>
190
+ SchematicComponentOwnerTextResolver.#isComponentLabelRecord(
191
+ record.fields
192
+ )
193
+ )
194
+ }
195
+
196
+ /**
197
+ * Returns true when one text record is a component designator or comment.
198
+ * @param {Record<string, string | string[]>} fields Raw record fields.
199
+ * @returns {boolean}
200
+ */
201
+ static #isComponentLabelRecord(fields) {
202
+ const name = getField(fields, 'Name').trim().toLowerCase()
203
+
204
+ return name === 'designator' || name === 'comment' || name === 'value'
205
+ }
206
+
207
+ /**
208
+ * Returns true when one owner record is plausibly part of a component's
209
+ * display group.
210
+ * @param {Record<string, string | string[]>} fields Raw record fields.
211
+ * @param {{ fields: Record<string, string | string[]> }} componentRecord Component record.
212
+ * @returns {boolean}
213
+ */
214
+ static #isNearComponent(fields, componentRecord) {
215
+ const componentX = parseNumericField(
216
+ componentRecord?.fields,
217
+ 'Location.X'
218
+ )
219
+ const componentY = parseNumericField(
220
+ componentRecord?.fields,
221
+ 'Location.Y'
222
+ )
223
+ const bounds = SchematicComponentOwnerTextResolver.#recordBounds(fields)
224
+
225
+ if (componentX === null || componentY === null || !bounds) {
226
+ return true
227
+ }
228
+
229
+ return (
230
+ SchematicComponentOwnerTextResolver.#axisDistance(
231
+ componentX,
232
+ bounds.minX,
233
+ bounds.maxX
234
+ ) <= COMPONENT_OWNER_AXIS_THRESHOLD &&
235
+ SchematicComponentOwnerTextResolver.#axisDistance(
236
+ componentY,
237
+ bounds.minY,
238
+ bounds.maxY
239
+ ) <= COMPONENT_OWNER_AXIS_THRESHOLD
240
+ )
241
+ }
242
+
243
+ /**
244
+ * Resolves a loose coordinate envelope for one schematic record.
245
+ * @param {Record<string, string | string[]>} fields Raw record fields.
246
+ * @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
247
+ */
248
+ static #recordBounds(fields) {
249
+ const points = [
250
+ {
251
+ x: parseNumericField(fields, 'Location.X'),
252
+ y: parseNumericField(fields, 'Location.Y')
253
+ },
254
+ {
255
+ x: parseNumericField(fields, 'Corner.X'),
256
+ y: parseNumericField(fields, 'Corner.Y')
257
+ }
258
+ ].filter((point) => point.x !== null && point.y !== null)
259
+
260
+ if (!points.length) {
261
+ return null
262
+ }
263
+
264
+ return {
265
+ minX: Math.min(...points.map((point) => point.x)),
266
+ maxX: Math.max(...points.map((point) => point.x)),
267
+ minY: Math.min(...points.map((point) => point.y)),
268
+ maxY: Math.max(...points.map((point) => point.y))
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Measures how far a coordinate is from an inclusive axis interval.
274
+ * @param {number} value Coordinate value.
275
+ * @param {number} min Axis interval minimum.
276
+ * @param {number} max Axis interval maximum.
277
+ * @returns {number}
278
+ */
279
+ static #axisDistance(value, min, max) {
280
+ if (value < min) return min - value
281
+ if (value > max) return value - max
282
+ return 0
283
+ }
284
+
285
+ /**
286
+ * Removes duplicate and empty string values while preserving order.
287
+ * @param {string[]} values Candidate values.
288
+ * @returns {string[]}
289
+ */
290
+ static #dedupeStrings(values) {
291
+ return [
292
+ ...new Set(
293
+ values
294
+ .map((value) => String(value || '').trim())
295
+ .filter(Boolean)
296
+ )
297
+ ]
298
+ }
299
+
300
+ /**
301
+ * Returns true when both X and Y exist for a point prefix.
302
+ * @param {Record<string, string | string[]>} fields
303
+ * @param {string} prefix
304
+ * @returns {boolean}
305
+ */
306
+ static #hasCoordinatePair(fields, prefix) {
307
+ return (
308
+ parseNumericField(fields, prefix + '.X') !== null &&
309
+ parseNumericField(fields, prefix + '.Y') !== null
310
+ )
311
+ }
312
+ }
@@ -15,19 +15,30 @@ export class SchematicComponentTextResolver {
15
15
  * @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
16
16
  * @param {{ x: number, y: number, text: string, name: string }[]} texts
17
17
  * @param {{ x: number, y: number, libReference: string }} component
18
- * @returns {string}
18
+ * @returns {string | null}
19
19
  */
20
20
  static resolveDesignator(ownerTexts, texts, component) {
21
- const ownerDesignator = SchematicComponentTextResolver.#findRelatedText(
22
- ownerTexts,
23
- 'Designator'
24
- )
21
+ const ownerDesignator =
22
+ SchematicComponentTextResolver.#findRelatedTextRecord(
23
+ ownerTexts,
24
+ 'Designator'
25
+ )
25
26
  if (
27
+ ownerDesignator.found &&
26
28
  SchematicComponentTextResolver.#isResolvedComponentText(
27
- ownerDesignator
29
+ ownerDesignator.text
30
+ )
31
+ ) {
32
+ return ownerDesignator.text
33
+ }
34
+
35
+ if (
36
+ ownerDesignator.found &&
37
+ SchematicComponentTextResolver.#isExplicitEmptyText(
38
+ ownerDesignator.text
28
39
  )
29
40
  ) {
30
- return ownerDesignator
41
+ return ''
31
42
  }
32
43
 
33
44
  return SchematicComponentTextResolver.#findNearbyComponentDesignator(
@@ -45,15 +56,24 @@ export class SchematicComponentTextResolver {
45
56
  */
46
57
  static resolveValue(ownerTexts, texts, component) {
47
58
  const ownerValue =
48
- SchematicComponentTextResolver.#findRelatedText(
59
+ SchematicComponentTextResolver.#findFirstRelatedTextRecord(
49
60
  ownerTexts,
50
- 'Comment'
51
- ) ||
52
- SchematicComponentTextResolver.#findRelatedText(ownerTexts, 'VALUE')
61
+ ['Comment', 'VALUE']
62
+ )
63
+ if (
64
+ ownerValue.found &&
65
+ SchematicComponentTextResolver.#isResolvedComponentText(
66
+ ownerValue.text
67
+ )
68
+ ) {
69
+ return ownerValue.text
70
+ }
71
+
53
72
  if (
54
- SchematicComponentTextResolver.#isResolvedComponentText(ownerValue)
73
+ ownerValue.found &&
74
+ SchematicComponentTextResolver.#isExplicitEmptyText(ownerValue.text)
55
75
  ) {
56
- return ownerValue
76
+ return ''
57
77
  }
58
78
 
59
79
  return (
@@ -65,7 +85,9 @@ export class SchematicComponentTextResolver {
65
85
  SchematicComponentTextResolver.#inferComponentValueHint(
66
86
  component.libReference
67
87
  )
68
- ) || ownerValue
88
+ ) ||
89
+ ownerValue.text ||
90
+ ''
69
91
  )
70
92
  }
71
93
 
@@ -73,22 +95,44 @@ export class SchematicComponentTextResolver {
73
95
  * Finds a related text value by name.
74
96
  * @param {{ fields: Record<string, string | string[]> }[]} records
75
97
  * @param {string} logicalName
76
- * @returns {string}
98
+ * @returns {{ found: boolean, text: string }}
77
99
  */
78
- static #findRelatedText(records, logicalName) {
100
+ static #findRelatedTextRecord(records, logicalName) {
79
101
  const match = records.find(
80
102
  (record) =>
81
103
  getField(record.fields, 'Name').toLowerCase() ===
82
104
  logicalName.toLowerCase()
83
105
  )
84
- return match ? getDisplayText(match.fields) : ''
106
+ return match
107
+ ? { found: true, text: getDisplayText(match.fields) }
108
+ : { found: false, text: '' }
109
+ }
110
+
111
+ /**
112
+ * Finds the first related text value by logical name.
113
+ * @param {{ fields: Record<string, string | string[]> }[]} records
114
+ * @param {string[]} logicalNames Logical text names.
115
+ * @returns {{ found: boolean, text: string }}
116
+ */
117
+ static #findFirstRelatedTextRecord(records, logicalNames) {
118
+ for (const logicalName of logicalNames) {
119
+ const match = SchematicComponentTextResolver.#findRelatedTextRecord(
120
+ records,
121
+ logicalName
122
+ )
123
+ if (match.found) {
124
+ return match
125
+ }
126
+ }
127
+
128
+ return { found: false, text: '' }
85
129
  }
86
130
 
87
131
  /**
88
132
  * Finds the closest nearby designator text for one component.
89
133
  * @param {{ x: number, y: number, text: string, name: string }[]} texts
90
134
  * @param {{ x: number, y: number, libReference: string }} component
91
- * @returns {string}
135
+ * @returns {string | null}
92
136
  */
93
137
  static #findNearbyComponentDesignator(texts, component) {
94
138
  const expectedPrefix =
@@ -128,7 +172,7 @@ export class SchematicComponentTextResolver {
128
172
  }))
129
173
  .sort((left, right) => left.score - right.score)
130
174
 
131
- return rankedCandidates[0]?.text || ''
175
+ return rankedCandidates[0]?.text || null
132
176
  }
133
177
 
134
178
  /**
@@ -268,6 +312,15 @@ export class SchematicComponentTextResolver {
268
312
  )
269
313
  }
270
314
 
315
+ /**
316
+ * Returns true when owner-linked text intentionally contains no value.
317
+ * @param {string} value Text value.
318
+ * @returns {boolean}
319
+ */
320
+ static #isExplicitEmptyText(value) {
321
+ return String(value ?? '') === ''
322
+ }
323
+
271
324
  /**
272
325
  * Infers the visible designator prefix from a library reference.
273
326
  * @param {string} libReference