circuitjson-toolkit 1.0.10 → 1.0.17

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 (40) hide show
  1. package/AGENTS.md +20 -3
  2. package/docs/model-format.md +15 -0
  3. package/package.json +3 -2
  4. package/src/core/CircuitJsonBomBuilder.mjs +22 -25
  5. package/src/core/CircuitJsonElementValidator.mjs +233 -16
  6. package/src/core/CircuitJsonIndexer.mjs +510 -5
  7. package/src/core/CircuitJsonManufacturingBuilder.mjs +488 -16
  8. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  9. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  10. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  11. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  12. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  13. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  14. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  15. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  16. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  17. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  18. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  19. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  20. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  21. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  22. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  23. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  24. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  25. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +227 -5
  26. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  27. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  28. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  29. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  30. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  31. package/src/index.mjs +2 -0
  32. package/src/renderers.mjs +29 -0
  33. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  34. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  35. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  36. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  37. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  38. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  39. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  40. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -0,0 +1,233 @@
1
+ const COMPONENT_ICON_BY_TYPE = {
2
+ battery: 'battery',
3
+ capacitor: 'capacitor',
4
+ chip: 'chip',
5
+ connector: 'connector',
6
+ crystal: 'crystal',
7
+ current_source: 'source',
8
+ diode: 'diode',
9
+ fiducial: 'fiducial',
10
+ fuse: 'fuse',
11
+ ground: 'ground',
12
+ inductor: 'inductor',
13
+ interconnect: 'connector',
14
+ led: 'led',
15
+ mosfet: 'transistor',
16
+ op_amp: 'op-amp',
17
+ pin_header: 'connector',
18
+ potentiometer: 'potentiometer',
19
+ push_button: 'switch',
20
+ resistor: 'resistor',
21
+ resonator: 'crystal',
22
+ switch: 'switch',
23
+ test_point: 'test-point',
24
+ transistor: 'transistor',
25
+ voltage_probe: 'probe',
26
+ voltage_source: 'source'
27
+ }
28
+
29
+ /**
30
+ * Normalizes generic source-level CircuitJSON metadata for stable consumers.
31
+ */
32
+ export class CircuitJsonSourceMetadata {
33
+ /**
34
+ * Builds a safe source net name from display-oriented text.
35
+ * @param {unknown} value Source net label.
36
+ * @param {{ fallback?: string, usedNames?: Set<string> }} [options]
37
+ * @returns {string}
38
+ */
39
+ static normalizeSourceNetName(value, options = {}) {
40
+ const fallback = String(options.fallback || 'net').trim() || 'net'
41
+ let name = String(value || '').trim() || fallback
42
+ name = name
43
+ .replaceAll('+', '_P')
44
+ .replaceAll('-', '_')
45
+ .replace(/[^A-Za-z0-9_]+/gu, '_')
46
+ .replace(/_+/gu, '_')
47
+ .replace(/_+$/u, '')
48
+ if (!name) {
49
+ name = fallback.replace(/[^A-Za-z0-9_]+/gu, '_') || 'net'
50
+ }
51
+ if (/^[0-9]/u.test(name)) {
52
+ name = 'net_' + name
53
+ }
54
+ return CircuitJsonSourceMetadata.#dedupeName(name, options.usedNames)
55
+ }
56
+
57
+ /**
58
+ * Builds a stable source port name from a pad or pin label.
59
+ * @param {unknown} value Source port label.
60
+ * @param {{ fallback?: string, usedNames?: Set<string> }} [options]
61
+ * @returns {string}
62
+ */
63
+ static normalizeSourcePortName(value, options = {}) {
64
+ const text = String(value ?? '').trim()
65
+ const fallback = String(options.fallback || 'pin').trim() || 'pin'
66
+ const name = /^[0-9]+$/u.test(text) ? 'pin' + Number(text) : text
67
+ return CircuitJsonSourceMetadata.#dedupeName(
68
+ name || fallback,
69
+ options.usedNames
70
+ )
71
+ }
72
+
73
+ /**
74
+ * Builds stable semantic metadata for one source component.
75
+ * @param {object} component Source component row.
76
+ * @returns {{ sourceFtype: string, componentType: string, componentIcon: string, supplierPartNumber: string, supplierPartNumbers: Record<string, string> }}
77
+ */
78
+ static normalizeSourceComponent(component) {
79
+ const sourceFtype =
80
+ CircuitJsonSourceMetadata.#sourceFtype(component) ||
81
+ CircuitJsonSourceMetadata.#inferSourceFtype(component)
82
+ const componentType = sourceFtype
83
+ .replace(/^simple_/u, '')
84
+ .replaceAll('_', '-')
85
+ const supplierPartNumbers =
86
+ CircuitJsonSourceMetadata.#supplierPartNumbers(component)
87
+ return {
88
+ sourceFtype,
89
+ componentType,
90
+ componentIcon:
91
+ COMPONENT_ICON_BY_TYPE[componentType.replaceAll('-', '_')] ||
92
+ componentType ||
93
+ 'component',
94
+ supplierPartNumber: String(
95
+ Object.values(supplierPartNumbers).find(Boolean) || ''
96
+ ).trim(),
97
+ supplierPartNumbers
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Returns a unique name against an optional used-name set.
103
+ * @param {string} name Base name.
104
+ * @param {Set<string> | undefined} usedNames Existing names.
105
+ * @returns {string}
106
+ */
107
+ static #dedupeName(name, usedNames) {
108
+ if (!(usedNames instanceof Set) || !usedNames.has(name)) return name
109
+ let index = 2
110
+ let candidate = name + '_' + index
111
+ while (usedNames.has(candidate)) {
112
+ index += 1
113
+ candidate = name + '_' + index
114
+ }
115
+ return candidate
116
+ }
117
+
118
+ /**
119
+ * Resolves an explicit source component function type.
120
+ * @param {object} component Source component row.
121
+ * @returns {string}
122
+ */
123
+ static #sourceFtype(component) {
124
+ return String(component?.ftype || component?.sourceFtype || '').trim()
125
+ }
126
+
127
+ /**
128
+ * Infers a source component function type from generic fields.
129
+ * @param {object} component Source component row.
130
+ * @returns {string}
131
+ */
132
+ static #inferSourceFtype(component) {
133
+ const reference = String(
134
+ component?.name ||
135
+ component?.reference ||
136
+ component?.designator ||
137
+ ''
138
+ ).trim()
139
+ const text = [
140
+ reference,
141
+ component?.footprint,
142
+ component?.package,
143
+ component?.package_name,
144
+ component?.value,
145
+ component?.description
146
+ ]
147
+ .map((value) => String(value || '').toLowerCase())
148
+ .filter(Boolean)
149
+ .join(' ')
150
+
151
+ if (text.includes('led')) return 'simple_led'
152
+ if (/^tp[0-9A-Z_-]*/iu.test(reference) || text.includes('test point')) {
153
+ return 'simple_test_point'
154
+ }
155
+ if (/^fid[0-9A-Z_-]*/iu.test(reference) || text.includes('fiducial')) {
156
+ return 'simple_fiducial'
157
+ }
158
+ if (/^sw[0-9A-Z_-]*/iu.test(reference) || text.includes('switch')) {
159
+ return 'simple_switch'
160
+ }
161
+ if (/^r[0-9A-Z_-]*/iu.test(reference) || component?.resistance) {
162
+ return 'simple_resistor'
163
+ }
164
+ if (/^c[0-9A-Z_-]*/iu.test(reference) || component?.capacitance) {
165
+ return 'simple_capacitor'
166
+ }
167
+ if (/^l[0-9A-Z_-]*/iu.test(reference) || component?.inductance) {
168
+ return 'simple_inductor'
169
+ }
170
+ if (text.includes('mosfet')) return 'simple_mosfet'
171
+ if (/^q[0-9A-Z_-]*/iu.test(reference) || text.includes('transistor')) {
172
+ return 'simple_transistor'
173
+ }
174
+ if (/^d[0-9A-Z_-]*/iu.test(reference)) return 'simple_diode'
175
+ if (/^(u|ic)[0-9A-Z_-]*/iu.test(reference) || text.includes('chip')) {
176
+ return 'simple_chip'
177
+ }
178
+ return ''
179
+ }
180
+
181
+ /**
182
+ * Normalizes supplier part fields into a stable keyed object.
183
+ * @param {object} component Source component row.
184
+ * @returns {Record<string, string>}
185
+ */
186
+ static #supplierPartNumbers(component) {
187
+ const numbers = {}
188
+ for (const field of ['supplier_part_numbers', 'supplierPartNumbers']) {
189
+ const value = component?.[field]
190
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
191
+ continue
192
+ }
193
+ for (const [key, entry] of Object.entries(value)) {
194
+ const normalized = String(entry || '').trim()
195
+ if (normalized) numbers[String(key || 'supplier')] = normalized
196
+ }
197
+ }
198
+ CircuitJsonSourceMetadata.#addSupplierNumber(
199
+ numbers,
200
+ 'supplier',
201
+ component?.supplier_part_number ?? component?.supplierPartNumber
202
+ )
203
+ CircuitJsonSourceMetadata.#addSupplierNumber(
204
+ numbers,
205
+ 'distributor',
206
+ component?.distributor_part_number ??
207
+ component?.distributorPartNumber
208
+ )
209
+ CircuitJsonSourceMetadata.#addSupplierNumber(
210
+ numbers,
211
+ 'assembly',
212
+ component?.assembly_part_number ?? component?.assemblyPartNumber
213
+ )
214
+ CircuitJsonSourceMetadata.#addSupplierNumber(
215
+ numbers,
216
+ 'catalog',
217
+ component?.catalog_part_number ?? component?.catalogPartNumber
218
+ )
219
+ return numbers
220
+ }
221
+
222
+ /**
223
+ * Adds one supplier part number when present.
224
+ * @param {Record<string, string>} numbers Supplier number map.
225
+ * @param {string} key Supplier key.
226
+ * @param {unknown} value Supplier part value.
227
+ * @returns {void}
228
+ */
229
+ static #addSupplierNumber(numbers, key, value) {
230
+ const normalized = String(value || '').trim()
231
+ if (normalized && !numbers[key]) numbers[key] = normalized
232
+ }
233
+ }
@@ -8,7 +8,9 @@ const PCB_RENDERED_TYPES = new Set([
8
8
  'pcb_copper_text',
9
9
  'pcb_courtyard',
10
10
  'pcb_courtyard_circle',
11
+ 'pcb_courtyard_line',
11
12
  'pcb_courtyard_outline',
13
+ 'pcb_courtyard_path',
12
14
  'pcb_courtyard_pill',
13
15
  'pcb_courtyard_polygon',
14
16
  'pcb_courtyard_rect',
@@ -78,6 +80,81 @@ const ROUTING_DSN_TYPES = new Set([
78
80
  'source_net'
79
81
  ])
80
82
 
83
+ const VARIANT_ROWS_BY_SET = {
84
+ sourceComponentFtypes: {
85
+ type: 'source_component',
86
+ group: 'ftype',
87
+ capability: 'bom',
88
+ status: 'grouped'
89
+ },
90
+ pcbBoardShapes: {
91
+ type: 'pcb_board',
92
+ group: 'shape',
93
+ capability: 'pcb',
94
+ status: 'rendered'
95
+ },
96
+ pcbSmtPadShapes: {
97
+ type: 'pcb_smtpad',
98
+ group: 'shape',
99
+ capability: 'pcb',
100
+ status: 'rendered'
101
+ },
102
+ pcbHoleShapes: {
103
+ type: 'pcb_hole',
104
+ group: 'hole_shape',
105
+ capability: 'pcb',
106
+ status: 'rendered'
107
+ },
108
+ pcbPlatedHoleShapes: {
109
+ type: 'pcb_plated_hole',
110
+ group: 'shape',
111
+ capability: 'pcb',
112
+ status: 'rendered'
113
+ },
114
+ pcbPlatedHoleHoleShapes: {
115
+ type: 'pcb_plated_hole',
116
+ group: 'hole_shape',
117
+ capability: 'pcb',
118
+ status: 'rendered'
119
+ },
120
+ pcbSolderPasteShapes: {
121
+ type: 'pcb_solder_paste',
122
+ group: 'shape',
123
+ capability: 'pcb',
124
+ status: 'rendered'
125
+ },
126
+ pcbCutoutShapes: {
127
+ type: 'pcb_cutout',
128
+ group: 'shape',
129
+ capability: 'pcb',
130
+ status: 'rendered'
131
+ },
132
+ pcbCopperPourShapes: {
133
+ type: 'pcb_copper_pour',
134
+ group: 'shape',
135
+ capability: 'pcb',
136
+ status: 'rendered'
137
+ },
138
+ simulationSourceKinds: {
139
+ type: 'simulation_voltage_source',
140
+ group: 'kind',
141
+ capability: 'simulation',
142
+ status: 'summarized'
143
+ },
144
+ simulationWaveShapes: {
145
+ type: 'simulation_voltage_source',
146
+ group: 'wave_shape',
147
+ capability: 'simulation',
148
+ status: 'summarized'
149
+ },
150
+ simulationExperimentMethods: {
151
+ type: 'simulation_experiment',
152
+ group: 'spice_options.method',
153
+ capability: 'simulation',
154
+ status: 'summarized'
155
+ }
156
+ }
157
+
81
158
  /**
82
159
  * Builds document-level support coverage reports from known element metadata.
83
160
  */
@@ -85,7 +162,7 @@ export class CircuitJsonSupportMatrixBuilder {
85
162
  /**
86
163
  * Builds a support matrix for the known schema snapshot and present rows.
87
164
  * @param {object[]} [circuitJson] Parsed element array.
88
- * @returns {{ sourceFormat: string, totals: object, rows: object[], gaps: object[] }}
165
+ * @returns {{ sourceFormat: string, totals: object, rows: object[], variantRows: object[], gaps: object[] }}
89
166
  */
90
167
  static build(circuitJson = []) {
91
168
  const elements = Array.isArray(circuitJson) ? circuitJson : []
@@ -101,11 +178,18 @@ export class CircuitJsonSupportMatrixBuilder {
101
178
  presentTypes.has(type)
102
179
  )
103
180
  )
181
+ const variantRows =
182
+ CircuitJsonSupportMatrixBuilder.#variantRows(elements)
104
183
 
105
184
  return {
106
185
  sourceFormat: 'circuitjson',
107
- totals: CircuitJsonSupportMatrixBuilder.#totals(rows, presentTypes),
186
+ totals: CircuitJsonSupportMatrixBuilder.#totals(
187
+ rows,
188
+ presentTypes,
189
+ variantRows
190
+ ),
108
191
  rows,
192
+ variantRows,
109
193
  gaps: rows.flatMap((row) =>
110
194
  CircuitJsonSupportMatrixBuilder.#gaps(row)
111
195
  )
@@ -142,6 +226,7 @@ export class CircuitJsonSupportMatrixBuilder {
142
226
  diagnostics: CircuitJsonSupportMatrixBuilder.#diagnostics(type),
143
227
  schematic: SCHEMATIC_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
144
228
  pcb: PCB_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
229
+ scene3d: type === 'cad_component' ? 'external-model' : 'none',
145
230
  bom: type === 'source_component' ? 'grouped' : 'none',
146
231
  manufacturing: CircuitJsonSupportMatrixBuilder.#manufacturing(type),
147
232
  simulation: type.startsWith('simulation_') ? 'preserved' : 'none'
@@ -182,6 +267,7 @@ export class CircuitJsonSupportMatrixBuilder {
182
267
  if (
183
268
  capabilities.pcb === 'rendered' ||
184
269
  capabilities.schematic === 'rendered' ||
270
+ capabilities.scene3d === 'external-model' ||
185
271
  capabilities.diagnostics === 'normalized'
186
272
  ) {
187
273
  return capabilities.manufacturing === 'routing-dsn'
@@ -222,13 +308,145 @@ export class CircuitJsonSupportMatrixBuilder {
222
308
  return []
223
309
  }
224
310
 
311
+ /**
312
+ * Builds variant coverage rows from active schema metadata.
313
+ * @param {object[]} elements Parsed element rows.
314
+ * @returns {object[]}
315
+ */
316
+ static #variantRows(elements) {
317
+ const schema = CircuitJsonElementValidator.variantSets()
318
+ const present =
319
+ CircuitJsonSupportMatrixBuilder.#presentVariants(elements)
320
+ return Object.entries(schema).flatMap(([setName, values]) => {
321
+ const definition = VARIANT_ROWS_BY_SET[setName]
322
+ if (!definition) return []
323
+ return values.map((value) =>
324
+ CircuitJsonSupportMatrixBuilder.#variantRow(
325
+ setName,
326
+ definition,
327
+ value,
328
+ present
329
+ )
330
+ )
331
+ })
332
+ }
333
+
334
+ /**
335
+ * Builds one variant coverage row.
336
+ * @param {string} setName Variant set name.
337
+ * @param {object} definition Row definition.
338
+ * @param {string} value Variant value.
339
+ * @param {Map<string, Set<string>>} present Present variant values.
340
+ * @returns {object}
341
+ */
342
+ static #variantRow(setName, definition, value, present) {
343
+ const isPresent = Boolean(present.get(setName)?.has(value))
344
+ return {
345
+ set: setName,
346
+ type: definition.type,
347
+ group: definition.group,
348
+ value,
349
+ present: isPresent,
350
+ capability: definition.capability,
351
+ status: definition.status,
352
+ note: CircuitJsonSupportMatrixBuilder.#variantNote(
353
+ definition,
354
+ value,
355
+ isPresent
356
+ )
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Builds a short human-readable variant support note.
362
+ * @param {object} definition Variant row definition.
363
+ * @param {string} value Variant value.
364
+ * @param {boolean} present Whether the variant is present.
365
+ * @returns {string}
366
+ */
367
+ static #variantNote(definition, value, present) {
368
+ const label =
369
+ definition.type + '.' + definition.group + ' ' + value + ' is '
370
+ if (!present) return label + 'known but not present.'
371
+ if (definition.status === 'rendered') return label + 'rendered.'
372
+ if (definition.status === 'grouped') {
373
+ return label + 'used for BOM grouping.'
374
+ }
375
+ if (definition.status === 'summarized') {
376
+ return label + 'summarized in simulation setup.'
377
+ }
378
+ return label + 'preserved as metadata.'
379
+ }
380
+
381
+ /**
382
+ * Extracts variant values present in the current document.
383
+ * @param {object[]} elements Parsed element rows.
384
+ * @returns {Map<string, Set<string>>}
385
+ */
386
+ static #presentVariants(elements) {
387
+ const present = new Map()
388
+ const add = (setName, value) => {
389
+ const text = String(value || '').trim()
390
+ if (!text) return
391
+ if (!present.has(setName)) present.set(setName, new Set())
392
+ present.get(setName).add(text)
393
+ }
394
+
395
+ for (const element of elements) {
396
+ if (element?.type === 'source_component') {
397
+ add('sourceComponentFtypes', element.ftype)
398
+ }
399
+ if (element?.type === 'pcb_board') {
400
+ add('pcbBoardShapes', element.shape || 'rect')
401
+ }
402
+ if (element?.type === 'pcb_smtpad') {
403
+ add('pcbSmtPadShapes', element.shape)
404
+ }
405
+ if (element?.type === 'pcb_hole') {
406
+ add('pcbHoleShapes', element.hole_shape || element.shape)
407
+ }
408
+ if (element?.type === 'pcb_plated_hole') {
409
+ add('pcbPlatedHoleShapes', element.shape || 'circle')
410
+ add('pcbPlatedHoleHoleShapes', element.hole_shape)
411
+ }
412
+ if (element?.type === 'pcb_solder_paste') {
413
+ add('pcbSolderPasteShapes', element.shape)
414
+ }
415
+ if (element?.type === 'pcb_cutout') {
416
+ add('pcbCutoutShapes', element.shape || 'rect')
417
+ }
418
+ if (element?.type === 'pcb_copper_pour') {
419
+ add('pcbCopperPourShapes', element.shape || 'polygon')
420
+ }
421
+ if (
422
+ element?.type === 'simulation_voltage_source' ||
423
+ element?.type === 'simulation_current_source'
424
+ ) {
425
+ add(
426
+ 'simulationSourceKinds',
427
+ element.source_type || element.sourceType || element.kind
428
+ )
429
+ add('simulationWaveShapes', element.wave_shape)
430
+ }
431
+ if (element?.type === 'simulation_experiment') {
432
+ add(
433
+ 'simulationExperimentMethods',
434
+ element.spice_options?.method
435
+ )
436
+ }
437
+ }
438
+
439
+ return present
440
+ }
441
+
225
442
  /**
226
443
  * Builds aggregate matrix counts.
227
444
  * @param {object[]} rows Matrix rows.
228
445
  * @param {Set<string>} presentTypes Present element types.
446
+ * @param {object[]} variantRows Variant coverage rows.
229
447
  * @returns {object}
230
448
  */
231
- static #totals(rows, presentTypes) {
449
+ static #totals(rows, presentTypes, variantRows) {
232
450
  return {
233
451
  knownElementTypes: rows.length,
234
452
  presentElementTypes: rows.filter((row) => row.present).length,
@@ -236,7 +454,8 @@ export class CircuitJsonSupportMatrixBuilder {
236
454
  (row) =>
237
455
  row.present &&
238
456
  (row.capabilities.pcb === 'rendered' ||
239
- row.capabilities.schematic === 'rendered')
457
+ row.capabilities.schematic === 'rendered' ||
458
+ row.capabilities.scene3d === 'external-model')
240
459
  ).length,
241
460
  diagnosticElementTypes: rows.filter(
242
461
  (row) =>
@@ -244,7 +463,10 @@ export class CircuitJsonSupportMatrixBuilder {
244
463
  ).length,
245
464
  unknownPresentElementTypes: [...presentTypes].filter(
246
465
  (type) => !rows.some((row) => row.type === type)
247
- ).length
466
+ ).length,
467
+ knownVariantValues: variantRows.length,
468
+ presentVariantValues: variantRows.filter((row) => row.present)
469
+ .length
248
470
  }
249
471
  }
250
472