@ifc-lite/export 1.17.2 → 1.18.0

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.
@@ -2,10 +2,10 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { EntityExtractor, generateHeader, getAttributeNames, serializeValue, ref, } from '@ifc-lite/parser';
5
- import { PropertyValueType, QuantityType } from '@ifc-lite/data';
6
5
  import { generateIfcGuid } from '@ifc-lite/encoding';
7
6
  import { collectReferencedEntityIds, getVisibleEntityIds, collectStyleEntities } from './reference-collector.js';
8
7
  import { convertStepLine, needsConversion } from './schema-converter.js';
8
+ import { escapeStepString, toStepReal, quantityTypeToIfcType, serializePropertyValue, serializeAttributeValue, serializeStepArgs, serializeStepValue, splitTopLevelArgs, splitTopLevelStepArguments, } from './step-serialization.js';
9
9
  /**
10
10
  * IFC STEP file exporter
11
11
  */
@@ -17,8 +17,11 @@ export class StepExporter {
17
17
  constructor(dataStore, mutationView) {
18
18
  this.dataStore = dataStore;
19
19
  this.mutationView = mutationView || null;
20
- // Start new IDs after the highest existing ID
21
- this.nextExpressId = this.findMaxExpressId() + 1;
20
+ const maxExisting = this.findMaxExpressId();
21
+ const overlayWatermark = typeof mutationView?.peekNextExpressId === 'function'
22
+ ? mutationView.peekNextExpressId() - 1
23
+ : 0;
24
+ this.nextExpressId = Math.max(maxExisting, overlayWatermark) + 1;
22
25
  this.entityExtractor = dataStore.source ? new EntityExtractor(dataStore.source) : null;
23
26
  }
24
27
  /**
@@ -279,12 +282,12 @@ export class StepExporter {
279
282
  const crs = gm.projectedCRS;
280
283
  const crsId = this.nextExpressId++;
281
284
  // IfcProjectedCRS(Name, Description, GeodeticDatum, VerticalDatum, MapProjection, MapZone, MapUnit)
282
- const name = crs.name ? `'${this.escapeStepString(String(crs.name))}'` : '$';
283
- const desc = crs.description ? `'${this.escapeStepString(String(crs.description))}'` : '$';
284
- const datum = crs.geodeticDatum ? `'${this.escapeStepString(String(crs.geodeticDatum))}'` : '$';
285
- const vDatum = crs.verticalDatum ? `'${this.escapeStepString(String(crs.verticalDatum))}'` : '$';
286
- const proj = crs.mapProjection ? `'${this.escapeStepString(String(crs.mapProjection))}'` : '$';
287
- const zone = crs.mapZone ? `'${this.escapeStepString(String(crs.mapZone))}'` : '$';
285
+ const name = crs.name ? `'${escapeStepString(String(crs.name))}'` : '$';
286
+ const desc = crs.description ? `'${escapeStepString(String(crs.description))}'` : '$';
287
+ const datum = crs.geodeticDatum ? `'${escapeStepString(String(crs.geodeticDatum))}'` : '$';
288
+ const vDatum = crs.verticalDatum ? `'${escapeStepString(String(crs.verticalDatum))}'` : '$';
289
+ const proj = crs.mapProjection ? `'${escapeStepString(String(crs.mapProjection))}'` : '$';
290
+ const zone = crs.mapZone ? `'${escapeStepString(String(crs.mapZone))}'` : '$';
288
291
  const mapUnitRef = crs.mapUnit
289
292
  ? `#${this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines)}`
290
293
  : '$';
@@ -295,12 +298,12 @@ export class StepExporter {
295
298
  if (contextId) {
296
299
  const mc = gm.mapConversion || {};
297
300
  const mcId = this.nextExpressId++;
298
- const eastings = this.toStepReal(Number(mc.eastings) || 0);
299
- const northings = this.toStepReal(Number(mc.northings) || 0);
300
- const height = this.toStepReal(Number(mc.orthogonalHeight) || 0);
301
- const abscissa = mc.xAxisAbscissa !== undefined ? this.toStepReal(Number(mc.xAxisAbscissa)) : '$';
302
- const ordinate = mc.xAxisOrdinate !== undefined ? this.toStepReal(Number(mc.xAxisOrdinate)) : '$';
303
- const scale = mc.scale !== undefined ? this.toStepReal(Number(mc.scale)) : '$';
301
+ const eastings = toStepReal(Number(mc.eastings) || 0);
302
+ const northings = toStepReal(Number(mc.northings) || 0);
303
+ const height = toStepReal(Number(mc.orthogonalHeight) || 0);
304
+ const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
305
+ const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
306
+ const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
304
307
  // IfcMapConversion(SourceCRS, TargetCRS, Eastings, Northings, OrthogonalHeight, XAxisAbscissa, XAxisOrdinate, Scale)
305
308
  newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${crsId},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
306
309
  newEntityCount++;
@@ -315,12 +318,12 @@ export class StepExporter {
315
318
  if (contextId) {
316
319
  const mc = gm.mapConversion;
317
320
  const mcId = this.nextExpressId++;
318
- const eastings = this.toStepReal(Number(mc.eastings) || 0);
319
- const northings = this.toStepReal(Number(mc.northings) || 0);
320
- const height = this.toStepReal(Number(mc.orthogonalHeight) || 0);
321
- const abscissa = mc.xAxisAbscissa !== undefined ? this.toStepReal(Number(mc.xAxisAbscissa)) : '$';
322
- const ordinate = mc.xAxisOrdinate !== undefined ? this.toStepReal(Number(mc.xAxisOrdinate)) : '$';
323
- const scale = mc.scale !== undefined ? this.toStepReal(Number(mc.scale)) : '$';
321
+ const eastings = toStepReal(Number(mc.eastings) || 0);
322
+ const northings = toStepReal(Number(mc.northings) || 0);
323
+ const height = toStepReal(Number(mc.orthogonalHeight) || 0);
324
+ const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
325
+ const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
326
+ const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
324
327
  newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${existingCrsIds[0]},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
325
328
  newEntityCount++;
326
329
  }
@@ -329,8 +332,18 @@ export class StepExporter {
329
332
  }
330
333
  }
331
334
  }
332
- // If delta only, only export modified entities
333
- if (options.deltaOnly && modifiedEntities.size === 0) {
335
+ // If delta only, only export modified entities. Overlay-created entities
336
+ // also count without this, `createEntity()`-only edits would silently
337
+ // drop out of delta exports.
338
+ const overlayNewEntityCount = (this.mutationView
339
+ && options.applyMutations !== false
340
+ && typeof this.mutationView.getNewEntities === 'function') ? this.mutationView.getNewEntities().length : 0;
341
+ // Georef-only deltas (newGeorefLines populated but no entity changes) must
342
+ // still produce a non-empty DATA section.
343
+ if (options.deltaOnly
344
+ && modifiedEntities.size === 0
345
+ && overlayNewEntityCount === 0
346
+ && newGeorefLines.length === 0) {
334
347
  const emptyContent = new TextEncoder().encode(header + 'DATA;\nENDSEC;\nEND-ISO-10303-21;\n');
335
348
  return {
336
349
  content: emptyContent,
@@ -357,7 +370,16 @@ export class StepExporter {
357
370
  const decoder = new TextDecoder();
358
371
  const source = this.dataStore.source;
359
372
  // Extract existing entities from source
373
+ const overlayActive = !!this.mutationView && (options.applyMutations !== false);
360
374
  for (const [expressId, entityRef] of this.dataStore.entityIndex.byId) {
375
+ // Skip entities deleted via the overlay (only when mutations are applied)
376
+ if (overlayActive && typeof this.mutationView.isDeleted === 'function' && this.mutationView.isDeleted(expressId)) {
377
+ continue;
378
+ }
379
+ // Skip overlay-only entities — emitted by the new-entities pass below
380
+ if (entityRef.byteLength === 0 || entityRef.byteOffset < 0) {
381
+ continue;
382
+ }
361
383
  // Skip entities outside the visible closure
362
384
  if (allowedEntityIds !== null && !allowedEntityIds.has(expressId)) {
363
385
  continue;
@@ -378,9 +400,19 @@ export class StepExporter {
378
400
  }
379
401
  // Get original entity text
380
402
  const entityText = decoder.decode(source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
381
- const nextEntityText = modifiedAttributes.has(expressId)
403
+ let nextEntityText = modifiedAttributes.has(expressId)
382
404
  ? this.applyAttributeMutations(entityText, entityType, modifiedAttributes.get(expressId))
383
405
  : entityText;
406
+ const positional = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
407
+ ? this.mutationView.getPositionalMutationsForEntity(expressId)
408
+ : null;
409
+ if (positional && positional.size > 0) {
410
+ nextEntityText = this.applyPositionalMutations(nextEntityText, positional);
411
+ if (!modifiedEntities.has(expressId)) {
412
+ modifiedEntities.add(expressId);
413
+ modifiedEntityCount++;
414
+ }
415
+ }
384
416
  // Apply schema conversion if exporting to a different schema version
385
417
  if (converting) {
386
418
  const converted = convertStepLine(nextEntityText, sourceSchema, schema);
@@ -429,6 +461,39 @@ export class StepExporter {
429
461
  for (const line of newGeorefLines) {
430
462
  entities.push(line);
431
463
  }
464
+ // Add overlay-created entities (store.addEntity / mutationView.createEntity).
465
+ // Apply the same filters as the source-iteration pass so newly-created
466
+ // beams/slabs don't smuggle their geometry helpers (IfcCartesianPoint,
467
+ // IfcExtrudedAreaSolid, etc.) past `includeGeometry:false` /
468
+ // `exportPropertiesOnly()` modes.
469
+ if (this.mutationView
470
+ && (options.applyMutations !== false)
471
+ && typeof this.mutationView.getNewEntities === 'function') {
472
+ for (const entity of this.mutationView.getNewEntities()) {
473
+ // STEP requires UPPERCASE entity type tokens. `NewEntity.type` is
474
+ // stored in canonical PascalCase per the public API contract; the
475
+ // upper-case happens here at the file-format boundary.
476
+ const upperType = entity.type.toUpperCase();
477
+ if (options.includeGeometry === false && this.isGeometryEntity(upperType)) {
478
+ continue;
479
+ }
480
+ if (allowedEntityIds !== null && !allowedEntityIds.has(entity.expressId)) {
481
+ continue;
482
+ }
483
+ const line = `#${entity.expressId}=${upperType}(${serializeStepArgs(entity.attributes)});`;
484
+ if (converting) {
485
+ const converted = convertStepLine(line, sourceSchema, schema);
486
+ if (converted !== null) {
487
+ entities.push(converted);
488
+ newEntityCount++;
489
+ }
490
+ }
491
+ else {
492
+ entities.push(line);
493
+ newEntityCount++;
494
+ }
495
+ }
496
+ }
432
497
  // Assemble final file as Uint8Array chunks to avoid V8 string length limit
433
498
  const content = assembleStepBytes(header, entities);
434
499
  return {
@@ -486,11 +551,11 @@ export class StepExporter {
486
551
  for (const prop of pset.properties) {
487
552
  const propId = this.nextExpressId++;
488
553
  count++;
489
- const valueStr = this.serializePropertyValue(prop.value, prop.type);
554
+ const valueStr = serializePropertyValue(prop.value, prop.type);
490
555
  const unitId = prop.unit ? this.findUnitId(prop.unit) : null;
491
556
  const unitStr = unitId !== null ? ref(unitId) : null;
492
557
  // #ID=IFCPROPERTYSINGLEVALUE('Name',$,Value,Unit);
493
- const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${this.escapeStepString(prop.name)}',$,${valueStr},${unitStr ? serializeValue(unitStr) : '$'});`;
558
+ const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${escapeStepString(prop.name)}',$,${valueStr},${unitStr ? serializeValue(unitStr) : '$'});`;
494
559
  lines.push(line);
495
560
  propertyIds.push(propId);
496
561
  }
@@ -500,7 +565,7 @@ export class StepExporter {
500
565
  const propRefs = propertyIds.map(id => `#${id}`).join(',');
501
566
  const globalId = this.generateGlobalId();
502
567
  // #ID=IFCPROPERTYSET('GlobalId',$,'Name',$,(#props));
503
- const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',$,'${this.escapeStepString(pset.name)}',$,(${propRefs}));`;
568
+ const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',$,'${escapeStepString(pset.name)}',$,(${propRefs}));`;
504
569
  lines.push(psetLine);
505
570
  if (typeOwnedPsetNames?.has(pset.name)) {
506
571
  generatedTypeOwnedPsetIds.set(pset.name, psetId);
@@ -528,10 +593,10 @@ export class StepExporter {
528
593
  for (const q of qset.quantities) {
529
594
  const qId = this.nextExpressId++;
530
595
  count++;
531
- const ifcType = this.quantityTypeToIfcType(q.type);
596
+ const ifcType = quantityTypeToIfcType(q.type);
532
597
  // #ID=IFCQUANTITYLENGTH('Name',$,$,Value,$);
533
- const val = this.toStepReal(q.value);
534
- const line = `#${qId}=${ifcType}('${this.escapeStepString(q.name)}',$,$,${val},$);`;
598
+ const val = toStepReal(q.value);
599
+ const line = `#${qId}=${ifcType}('${escapeStepString(q.name)}',$,$,${val},$);`;
535
600
  lines.push(line);
536
601
  quantityIds.push(qId);
537
602
  }
@@ -541,7 +606,7 @@ export class StepExporter {
541
606
  const quantRefs = quantityIds.map(id => `#${id}`).join(',');
542
607
  const globalId = this.generateGlobalId();
543
608
  // #ID=IFCELEMENTQUANTITY('GlobalId',$,'Name',$,$,(#quants));
544
- const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',$,'${this.escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
609
+ const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',$,'${escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
545
610
  lines.push(qsetLine);
546
611
  // Create IfcRelDefinesByProperties to link qset to entity
547
612
  const relId = this.nextExpressId++;
@@ -552,60 +617,6 @@ export class StepExporter {
552
617
  }
553
618
  return { lines, count };
554
619
  }
555
- /**
556
- * Map QuantityType to IFC STEP entity type
557
- */
558
- quantityTypeToIfcType(type) {
559
- switch (type) {
560
- case QuantityType.Length: return 'IFCQUANTITYLENGTH';
561
- case QuantityType.Area: return 'IFCQUANTITYAREA';
562
- case QuantityType.Volume: return 'IFCQUANTITYVOLUME';
563
- case QuantityType.Count: return 'IFCQUANTITYCOUNT';
564
- case QuantityType.Weight: return 'IFCQUANTITYWEIGHT';
565
- case QuantityType.Time: return 'IFCQUANTITYTIME';
566
- default: return 'IFCQUANTITYCOUNT';
567
- }
568
- }
569
- /**
570
- * Serialize a property value to STEP format
571
- */
572
- serializePropertyValue(value, type) {
573
- if (value === null || value === undefined) {
574
- return '$';
575
- }
576
- switch (type) {
577
- case PropertyValueType.String:
578
- case PropertyValueType.Label:
579
- case PropertyValueType.Text:
580
- return `IFCLABEL('${this.escapeStepString(String(value))}')`;
581
- case PropertyValueType.Identifier:
582
- return `IFCIDENTIFIER('${this.escapeStepString(String(value))}')`;
583
- case PropertyValueType.Real:
584
- const num = Number(value);
585
- if (!Number.isFinite(num))
586
- return '$';
587
- return `IFCREAL(${num.toString().includes('.') ? num : num + '.'})`;
588
- case PropertyValueType.Integer:
589
- return `IFCINTEGER(${Math.round(Number(value))})`;
590
- case PropertyValueType.Boolean:
591
- case PropertyValueType.Logical:
592
- if (value === true)
593
- return `IFCBOOLEAN(.T.)`;
594
- if (value === false)
595
- return `IFCBOOLEAN(.F.)`;
596
- return `IFCLOGICAL(.U.)`;
597
- case PropertyValueType.Enum:
598
- return `.${String(value).toUpperCase()}.`;
599
- case PropertyValueType.List:
600
- if (Array.isArray(value)) {
601
- const items = value.map(v => this.serializePropertyValue(v, PropertyValueType.String));
602
- return `(${items.join(',')})`;
603
- }
604
- return '$';
605
- default:
606
- return `IFCLABEL('${this.escapeStepString(String(value))}')`;
607
- }
608
- }
609
620
  /**
610
621
  * Rewrite root IFC attributes directly on the original STEP entity line.
611
622
  */
@@ -619,13 +630,13 @@ export class StepExporter {
619
630
  if (attrNames.length === 0) {
620
631
  return entityText;
621
632
  }
622
- const args = this.splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
633
+ const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
623
634
  let changed = false;
624
635
  for (const [attrName, value] of attributeMutations) {
625
636
  const index = attrNames.indexOf(attrName);
626
637
  if (index < 0 || index >= args.length)
627
638
  continue;
628
- args[index] = this.serializeAttributeValue(value, args[index]);
639
+ args[index] = serializeAttributeValue(value, args[index]);
629
640
  changed = true;
630
641
  }
631
642
  if (!changed) {
@@ -633,77 +644,28 @@ export class StepExporter {
633
644
  }
634
645
  return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
635
646
  }
636
- serializeAttributeValue(value, currentToken) {
637
- const trimmed = value.trim();
638
- const current = currentToken.trim();
639
- if (value === '')
640
- return '$';
641
- if (trimmed === '$' || trimmed === '*')
642
- return trimmed;
643
- if (/^#\d+$/.test(trimmed))
644
- return trimmed;
645
- if (/^\.[A-Z0-9_]+\.$/i.test(current) || /^\.[A-Z0-9_]+\.$/i.test(trimmed)) {
646
- return `.${trimmed.replace(/^\./, '').replace(/\.$/, '').toUpperCase()}.`;
647
- }
648
- if (/^(?:\.T\.|\.F\.|\.U\.)$/i.test(current)) {
649
- const normalized = trimmed.toLowerCase();
650
- if (normalized === 'true' || normalized === '.t.')
651
- return '.T.';
652
- if (normalized === 'false' || normalized === '.f.')
653
- return '.F.';
654
- return '.U.';
655
- }
656
- if (/^-?\d+(?:\.\d+)?(?:E[+-]?\d+)?$/i.test(trimmed) && /^-?\d/.test(current)) {
657
- const numberValue = Number(trimmed);
658
- if (!Number.isFinite(numberValue))
659
- return '$';
660
- return current.includes('.') || /E/i.test(current)
661
- ? this.toStepReal(numberValue)
662
- : String(numberValue);
663
- }
664
- return serializeValue(value);
665
- }
666
- splitTopLevelArgs(text) {
667
- const parts = [];
668
- let current = '';
669
- let depth = 0;
670
- let inString = false;
671
- for (let i = 0; i < text.length; i++) {
672
- const char = text[i];
673
- current += char;
674
- if (inString) {
675
- if (char === '\'') {
676
- if (text[i + 1] === '\'') {
677
- current += text[i + 1];
678
- i++;
679
- }
680
- else {
681
- inString = false;
682
- }
683
- }
684
- continue;
685
- }
686
- if (char === '\'') {
687
- inString = true;
688
- continue;
689
- }
690
- if (char === '(') {
691
- depth++;
692
- continue;
693
- }
694
- if (char === ')') {
695
- depth--;
647
+ /**
648
+ * Apply positional STEP argument overrides to an entity line.
649
+ * Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
650
+ * have no symbolic names. Indexes that fall outside the existing arg list
651
+ * are silently ignored.
652
+ */
653
+ applyPositionalMutations(entityText, positionals) {
654
+ const openParen = entityText.indexOf('(');
655
+ const closeParen = entityText.lastIndexOf(');');
656
+ if (openParen < 0 || closeParen < openParen)
657
+ return entityText;
658
+ const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
659
+ let changed = false;
660
+ for (const [index, value] of positionals) {
661
+ if (index < 0 || index >= args.length)
696
662
  continue;
697
- }
698
- if (char === ',' && depth === 0) {
699
- parts.push(current.slice(0, -1).trim());
700
- current = '';
701
- }
702
- }
703
- if (current.trim() || text.endsWith(',')) {
704
- parts.push(current.trim());
663
+ args[index] = serializeStepValue(value);
664
+ changed = true;
705
665
  }
706
- return parts;
666
+ if (!changed)
667
+ return entityText;
668
+ return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
707
669
  }
708
670
  resolveMapUnitReference(unitName, newGeorefLines) {
709
671
  const normalized = this.normalizeMapUnitName(unitName);
@@ -725,7 +687,7 @@ export class StepExporter {
725
687
  const name = normalized === 'US SURVEY FOOT' ? 'US SURVEY FOOT' : 'FOOT';
726
688
  newGeorefLines.push(`#${dimId}=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);`);
727
689
  newGeorefLines.push(`#${siUnitId}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);`);
728
- newGeorefLines.push(`#${measureId}=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(${this.toStepReal(factor)}),#${siUnitId});`);
690
+ newGeorefLines.push(`#${measureId}=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(${toStepReal(factor)}),#${siUnitId});`);
729
691
  newGeorefLines.push(`#${convUnitId}=IFCCONVERSIONBASEDUNIT(#${dimId},.LENGTHUNIT.,'${name}',#${measureId});`);
730
692
  return convUnitId;
731
693
  }
@@ -808,24 +770,6 @@ export class StepExporter {
808
770
  }
809
771
  return first3dContext ?? contextIds[0] ?? null;
810
772
  }
811
- /**
812
- * Convert a number to a valid STEP REAL literal.
813
- * Handles NaN/Infinity (→ 0.) and ensures a decimal point is present.
814
- */
815
- toStepReal(v) {
816
- if (!Number.isFinite(v))
817
- return '0.';
818
- const s = v.toString();
819
- return s.includes('.') ? s : s + '.';
820
- }
821
- /**
822
- * Escape a string for STEP format
823
- */
824
- escapeStepString(str) {
825
- return str
826
- .replace(/\\/g, '\\\\')
827
- .replace(/'/g, "''");
828
- }
829
773
  /**
830
774
  * Generate a new IFC GlobalId (22 character base64)
831
775
  */
@@ -1040,48 +984,12 @@ export class StepExporter {
1040
984
  if (!match)
1041
985
  return null;
1042
986
  const [, prefix, attrsText, suffix] = match;
1043
- const attrs = this.splitTopLevelStepArguments(attrsText);
987
+ const attrs = splitTopLevelStepArguments(attrsText);
1044
988
  if (attrIndex >= attrs.length)
1045
989
  return null;
1046
990
  attrs[attrIndex] = replacement;
1047
991
  return `${prefix}${attrs.join(',')}${suffix}`;
1048
992
  }
1049
- /**
1050
- * Split a STEP argument list on top-level commas while preserving nested syntax.
1051
- */
1052
- splitTopLevelStepArguments(input) {
1053
- const parts = [];
1054
- let current = '';
1055
- let depth = 0;
1056
- let inString = false;
1057
- for (let i = 0; i < input.length; i++) {
1058
- const char = input[i];
1059
- if (char === "'") {
1060
- current += char;
1061
- if (inString && i + 1 < input.length && input[i + 1] === "'") {
1062
- current += input[i + 1];
1063
- i++;
1064
- continue;
1065
- }
1066
- inString = !inString;
1067
- continue;
1068
- }
1069
- if (!inString) {
1070
- if (char === '(')
1071
- depth++;
1072
- else if (char === ')')
1073
- depth--;
1074
- else if (char === ',' && depth === 0) {
1075
- parts.push(current);
1076
- current = '';
1077
- continue;
1078
- }
1079
- }
1080
- current += char;
1081
- }
1082
- parts.push(current);
1083
- return parts;
1084
- }
1085
993
  }
1086
994
  /**
1087
995
  * Quick export function for simple use cases.