circuit-json-to-step 0.0.2 → 0.0.4

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 (43) hide show
  1. package/.github/workflows/bun-formatcheck.yml +26 -0
  2. package/.github/workflows/bun-pver-release.yml +77 -0
  3. package/.github/workflows/bun-test.yml +31 -0
  4. package/.github/workflows/bun-typecheck.yml +26 -0
  5. package/README.md +1 -3
  6. package/bunfig.toml +2 -2
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.js +465 -40
  9. package/lib/index.ts +33 -5
  10. package/lib/mesh-generation.ts +29 -1
  11. package/lib/step-model-merger/excluded-entity-types.ts +27 -0
  12. package/lib/step-model-merger/read-step-file.ts +23 -0
  13. package/lib/step-model-merger/types.ts +46 -0
  14. package/lib/step-model-merger/vector-utils.ts +74 -0
  15. package/lib/step-model-merger.ts +394 -0
  16. package/lib/step-text-utils.ts +6 -0
  17. package/package.json +8 -3
  18. package/test/basics/basics01/__snapshots__/basics01.snap.png +0 -0
  19. package/test/basics/basics01/basics01.test.ts +3 -2
  20. package/test/basics/basics02/__snapshots__/basics02.snap.png +0 -0
  21. package/test/basics/basics02/basics02.test.ts +3 -2
  22. package/test/basics/basics03/__snapshots__/basics03.snap.png +0 -0
  23. package/test/basics/basics03/basics03.test.ts +3 -2
  24. package/test/basics/basics04/__snapshots__/basics04.snap.png +0 -0
  25. package/test/basics/basics04/basics04.test.ts +3 -2
  26. package/test/basics/basics05/__snapshots__/basics05.snap.png +0 -0
  27. package/test/basics/basics05/basics05.json +40 -0
  28. package/test/basics/basics05/basics05.test.ts +55 -0
  29. package/test/fixtures/kicad-models/Panasonic_EVQPUJ_EVQPUA.step +3093 -0
  30. package/test/fixtures/kicad-models/R_0603_1608Metric.step +1049 -0
  31. package/test/fixtures/kicad-models/SW_Push_1P1T_NO_CK_KMR2.step +2916 -0
  32. package/test/fixtures/png-matcher.ts +173 -0
  33. package/test/fixtures/step-snapshot.ts +249 -0
  34. package/test/repros/kicad-step/__snapshots__/kicad-step-board.snap.png +0 -0
  35. package/test/repros/kicad-step/__snapshots__/resistor-fixture.snap.png +0 -0
  36. package/test/repros/kicad-step/__snapshots__/switch-fixture.snap.png +0 -0
  37. package/test/repros/kicad-step/kicad-step.json +163 -0
  38. package/test/repros/kicad-step/kicad-step.test.ts +211 -0
  39. package/test/repros/repro01/__snapshots__/repro01.snap.png +0 -0
  40. package/test/repros/repro01/repro01.test.ts +3 -2
  41. package/test/utils/load-step-files.ts +41 -0
  42. package/types/occt-import-js.d.ts +33 -0
  43. package/.claude/settings.local.json +0 -16
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // lib/index.ts
2
2
  import {
3
- Repository,
3
+ Repository as Repository2,
4
4
  ApplicationContext,
5
5
  ApplicationProtocolDefinition,
6
6
  ProductContext,
@@ -9,9 +9,9 @@ import {
9
9
  ProductDefinitionFormation,
10
10
  ProductDefinition,
11
11
  ProductDefinitionShape,
12
- Unknown,
13
- CartesianPoint as CartesianPoint2,
14
- Direction as Direction2,
12
+ Unknown as Unknown2,
13
+ CartesianPoint as CartesianPoint3,
14
+ Direction as Direction3,
15
15
  Axis2Placement3D as Axis2Placement3D2,
16
16
  Plane as Plane2,
17
17
  CylindricalSurface,
@@ -26,7 +26,7 @@ import {
26
26
  AdvancedFace as AdvancedFace2,
27
27
  Circle,
28
28
  ClosedShell as ClosedShell2,
29
- ManifoldSolidBrep as ManifoldSolidBrep2,
29
+ ManifoldSolidBrep as ManifoldSolidBrep3,
30
30
  ColourRgb,
31
31
  FillAreaStyleColour,
32
32
  FillAreaStyle,
@@ -233,11 +233,25 @@ async function generateComponentMeshes(options) {
233
233
  repo,
234
234
  circuitJson,
235
235
  boardThickness,
236
- includeExternalMeshes = false
236
+ includeExternalMeshes = false,
237
+ excludeCadComponentIds,
238
+ excludePcbComponentIds
237
239
  } = options;
238
240
  const solids = [];
239
241
  try {
240
- const filteredCircuitJson = circuitJson.filter((e) => e.type !== "pcb_board").map((e) => {
242
+ const filteredCircuitJson = circuitJson.filter((e) => {
243
+ if (e.type === "pcb_board") return false;
244
+ if (e.type === "cad_component" && e.cad_component_id && excludeCadComponentIds?.has(e.cad_component_id)) {
245
+ return false;
246
+ }
247
+ if (e.type === "pcb_component" && e.pcb_component_id && excludePcbComponentIds?.has(e.pcb_component_id)) {
248
+ return false;
249
+ }
250
+ if (e.type === "cad_component" && e.model_step_url) {
251
+ return false;
252
+ }
253
+ return true;
254
+ }).map((e) => {
241
255
  if (!includeExternalMeshes && e.type === "cad_component") {
242
256
  return {
243
257
  ...e,
@@ -298,9 +312,402 @@ async function generateComponentMeshes(options) {
298
312
  return solids;
299
313
  }
300
314
 
315
+ // lib/step-model-merger.ts
316
+ import {
317
+ CartesianPoint as CartesianPoint2,
318
+ Direction as Direction2,
319
+ ManifoldSolidBrep as ManifoldSolidBrep2,
320
+ Ref,
321
+ Unknown,
322
+ parseRepository
323
+ } from "stepts";
324
+ import { eid } from "stepts/lib/core/EntityId";
325
+
326
+ // lib/step-model-merger/excluded-entity-types.ts
327
+ var EXCLUDED_ENTITY_TYPES = /* @__PURE__ */ new Set([
328
+ "APPLICATION_CONTEXT",
329
+ "APPLICATION_PROTOCOL_DEFINITION",
330
+ "PRODUCT",
331
+ "PRODUCT_CONTEXT",
332
+ "PRODUCT_DEFINITION",
333
+ "PRODUCT_DEFINITION_FORMATION",
334
+ "PRODUCT_DEFINITION_CONTEXT",
335
+ "PRODUCT_DEFINITION_SHAPE",
336
+ "SHAPE_DEFINITION_REPRESENTATION",
337
+ "ADVANCED_BREP_SHAPE_REPRESENTATION",
338
+ "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
339
+ "PRESENTATION_STYLE_ASSIGNMENT",
340
+ "SURFACE_STYLE_USAGE",
341
+ "SURFACE_SIDE_STYLE",
342
+ "SURFACE_STYLE_FILL_AREA",
343
+ "FILL_AREA_STYLE",
344
+ "FILL_AREA_STYLE_COLOUR",
345
+ "COLOUR_RGB",
346
+ "STYLED_ITEM",
347
+ "CURVE_STYLE",
348
+ "DRAUGHTING_PRE_DEFINED_CURVE_FONT",
349
+ "PRODUCT_RELATED_PRODUCT_CATEGORY",
350
+ "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
351
+ "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
352
+ "ITEM_DEFINED_TRANSFORMATION"
353
+ ]);
354
+
355
+ // lib/step-model-merger/vector-utils.ts
356
+ function asVector3(value) {
357
+ return {
358
+ x: value?.x ?? 0,
359
+ y: value?.y ?? 0,
360
+ z: value?.z ?? 0
361
+ };
362
+ }
363
+ function toRadians(rotation) {
364
+ const factor = Math.PI / 180;
365
+ return {
366
+ x: rotation.x * factor,
367
+ y: rotation.y * factor,
368
+ z: rotation.z * factor
369
+ };
370
+ }
371
+ function transformPoint(point, rotation, translation) {
372
+ const rotated = rotateVector(point, rotation);
373
+ return [
374
+ rotated[0] + translation.x,
375
+ rotated[1] + translation.y,
376
+ rotated[2] + translation.z
377
+ ];
378
+ }
379
+ function transformDirection(vector, rotation) {
380
+ return rotateVector(vector, rotation);
381
+ }
382
+ function rotateVector(vector, rotation) {
383
+ let [x, y, z] = vector;
384
+ if (rotation.x !== 0) {
385
+ const cosX = Math.cos(rotation.x);
386
+ const sinX = Math.sin(rotation.x);
387
+ const y1 = y * cosX - z * sinX;
388
+ const z1 = y * sinX + z * cosX;
389
+ y = y1;
390
+ z = z1;
391
+ }
392
+ if (rotation.y !== 0) {
393
+ const cosY = Math.cos(rotation.y);
394
+ const sinY = Math.sin(rotation.y);
395
+ const x1 = x * cosY + z * sinY;
396
+ const z1 = -x * sinY + z * cosY;
397
+ x = x1;
398
+ z = z1;
399
+ }
400
+ if (rotation.z !== 0) {
401
+ const cosZ = Math.cos(rotation.z);
402
+ const sinZ = Math.sin(rotation.z);
403
+ const x1 = x * cosZ - y * sinZ;
404
+ const y1 = x * sinZ + y * cosZ;
405
+ x = x1;
406
+ y = y1;
407
+ }
408
+ return [x, y, z];
409
+ }
410
+
411
+ // lib/step-model-merger/read-step-file.ts
412
+ async function readStepFile(modelUrl) {
413
+ if (!/^https?:\/\//i.test(modelUrl)) {
414
+ throw new Error(
415
+ `Only HTTP(S) URLs are supported. For local files, read the file content and pass it directly. Received: ${modelUrl}`
416
+ );
417
+ }
418
+ const globalFetch = globalThis.fetch;
419
+ if (!globalFetch) {
420
+ throw new Error("fetch is not available in this environment");
421
+ }
422
+ const res = await globalFetch(modelUrl);
423
+ if (!res.ok) {
424
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
425
+ }
426
+ return await res.text();
427
+ }
428
+
429
+ // lib/step-model-merger.ts
430
+ async function mergeExternalStepModels(options) {
431
+ const { repo, circuitJson, boardThickness, fsMap } = options;
432
+ const cadComponents = circuitJson.filter(
433
+ (item) => item?.type === "cad_component" && typeof item.model_step_url === "string"
434
+ );
435
+ const pcbComponentMap = /* @__PURE__ */ new Map();
436
+ for (const item of circuitJson) {
437
+ if (item?.type === "pcb_component" && item.pcb_component_id) {
438
+ pcbComponentMap.set(item.pcb_component_id, item);
439
+ }
440
+ }
441
+ const solids = [];
442
+ const handledComponentIds = /* @__PURE__ */ new Set();
443
+ const handledPcbComponentIds = /* @__PURE__ */ new Set();
444
+ for (const component of cadComponents) {
445
+ const componentId = component.cad_component_id ?? "";
446
+ const stepUrl = component.model_step_url;
447
+ try {
448
+ const stepText = fsMap?.[stepUrl] ?? await readStepFile(stepUrl);
449
+ if (!stepText.trim()) {
450
+ throw new Error("STEP file is empty");
451
+ }
452
+ const pcbComponent = component.pcb_component_id ? pcbComponentMap.get(component.pcb_component_id) : void 0;
453
+ const layer = pcbComponent?.layer?.toLowerCase();
454
+ const transform = {
455
+ translation: asVector3(component.position),
456
+ rotation: asVector3(component.rotation)
457
+ };
458
+ const componentSolids = mergeSingleStepModel(repo, stepText, transform, {
459
+ layer,
460
+ boardThickness
461
+ });
462
+ if (componentSolids.length > 0) {
463
+ if (componentId) {
464
+ handledComponentIds.add(componentId);
465
+ }
466
+ const pcbComponentId = component.pcb_component_id;
467
+ if (pcbComponentId) {
468
+ handledPcbComponentIds.add(pcbComponentId);
469
+ }
470
+ }
471
+ solids.push(...componentSolids);
472
+ } catch (error) {
473
+ console.warn(`Failed to merge STEP model from ${stepUrl}:`, error);
474
+ }
475
+ }
476
+ return { solids, handledComponentIds, handledPcbComponentIds };
477
+ }
478
+ function mergeSingleStepModel(targetRepo, stepText, transform, placement) {
479
+ const sourceRepo = parseRepository(stepText);
480
+ let entries = sourceRepo.entries().map(([id, entity]) => [Number(id), entity]).filter(([, entity]) => !EXCLUDED_ENTITY_TYPES.has(entity.type));
481
+ entries = pruneInvalidEntries(entries);
482
+ adjustTransformForPlacement(entries, transform, placement);
483
+ applyTransform(entries, transform);
484
+ const idMapping = allocateIds(targetRepo, entries);
485
+ remapReferences(entries, idMapping);
486
+ for (const [oldId, entity] of entries) {
487
+ const mappedId = idMapping.get(oldId);
488
+ if (mappedId === void 0) continue;
489
+ targetRepo.set(eid(mappedId), entity);
490
+ }
491
+ const solids = [];
492
+ for (const [oldId, entity] of entries) {
493
+ if (entity instanceof ManifoldSolidBrep2) {
494
+ const mappedId = idMapping.get(oldId);
495
+ if (mappedId !== void 0) {
496
+ solids.push(new Ref(eid(mappedId)));
497
+ }
498
+ }
499
+ }
500
+ return solids;
501
+ }
502
+ function adjustTransformForPlacement(entries, transform, placement) {
503
+ if (!placement) return;
504
+ const points = [];
505
+ for (const [, entity] of entries) {
506
+ if (entity instanceof CartesianPoint2) {
507
+ points.push([entity.x, entity.y, entity.z]);
508
+ }
509
+ }
510
+ if (!points.length) return;
511
+ const rotationRadians = toRadians(transform.rotation);
512
+ let minX = Infinity;
513
+ let minY = Infinity;
514
+ let minZ = Infinity;
515
+ let maxX = -Infinity;
516
+ let maxY = -Infinity;
517
+ let maxZ = -Infinity;
518
+ for (const point of points) {
519
+ const [x, y, z] = rotateVector(point, rotationRadians);
520
+ if (x < minX) minX = x;
521
+ if (y < minY) minY = y;
522
+ if (z < minZ) minZ = z;
523
+ if (x > maxX) maxX = x;
524
+ if (y > maxY) maxY = y;
525
+ if (z > maxZ) maxZ = z;
526
+ }
527
+ if (!Number.isFinite(minX)) return;
528
+ const center = {
529
+ x: (minX + maxX) / 2,
530
+ y: (minY + maxY) / 2,
531
+ z: (minZ + maxZ) / 2
532
+ };
533
+ const normalizedLayer = placement.layer?.toLowerCase() === "bottom" ? "bottom" : "top";
534
+ const boardThickness = placement.boardThickness ?? 0;
535
+ const halfThickness = boardThickness / 2;
536
+ const targetX = transform.translation.x;
537
+ const targetY = transform.translation.y;
538
+ const targetZ = transform.translation.z;
539
+ transform.translation.x = targetX - center.x;
540
+ transform.translation.y = targetY - center.y;
541
+ if (boardThickness > 0) {
542
+ const offsetZ = targetZ - halfThickness;
543
+ if (normalizedLayer === "bottom") {
544
+ transform.translation.z = -maxZ + offsetZ;
545
+ transform.rotation.x = normalizeDegrees(transform.rotation.x + 180);
546
+ } else {
547
+ transform.translation.z = boardThickness - minZ + offsetZ;
548
+ }
549
+ } else {
550
+ transform.translation.z = targetZ - center.z;
551
+ }
552
+ }
553
+ function normalizeDegrees(value) {
554
+ const wrapped = value % 360;
555
+ return wrapped < 0 ? wrapped + 360 : wrapped;
556
+ }
557
+ function pruneInvalidEntries(entries) {
558
+ let remaining = entries.slice();
559
+ let remainingIds = new Set(remaining.map(([id]) => id));
560
+ let changed = true;
561
+ while (changed) {
562
+ changed = false;
563
+ const toRemove = /* @__PURE__ */ new Set();
564
+ for (const [entityId, entity] of remaining) {
565
+ const refs = collectReferencedIds(entity);
566
+ for (const refId of refs) {
567
+ if (!remainingIds.has(refId)) {
568
+ toRemove.add(entityId);
569
+ break;
570
+ }
571
+ }
572
+ }
573
+ if (toRemove.size > 0) {
574
+ changed = true;
575
+ remaining = remaining.filter(([id]) => !toRemove.has(id));
576
+ remainingIds = new Set(remaining.map(([id]) => id));
577
+ }
578
+ }
579
+ return remaining;
580
+ }
581
+ function collectReferencedIds(entity) {
582
+ const result = /* @__PURE__ */ new Set();
583
+ collectReferencedIdsRecursive(entity, result, /* @__PURE__ */ new Set());
584
+ return result;
585
+ }
586
+ function collectReferencedIdsRecursive(value, result, seen) {
587
+ if (!value) return;
588
+ if (value instanceof Ref) {
589
+ result.add(Number(value.id));
590
+ return;
591
+ }
592
+ if (value instanceof Unknown) {
593
+ for (const arg of value.args) {
594
+ arg.replace(/#(\d+)/g, (_, num) => {
595
+ result.add(Number(num));
596
+ return _;
597
+ });
598
+ }
599
+ return;
600
+ }
601
+ if (Array.isArray(value)) {
602
+ for (const item of value) {
603
+ collectReferencedIdsRecursive(item, result, seen);
604
+ }
605
+ return;
606
+ }
607
+ if (typeof value === "object") {
608
+ if (seen.has(value)) {
609
+ return;
610
+ }
611
+ seen.add(value);
612
+ for (const entry of Object.values(value)) {
613
+ collectReferencedIdsRecursive(entry, result, seen);
614
+ }
615
+ }
616
+ }
617
+ function applyTransform(entries, transform) {
618
+ const rotation = toRadians(transform.rotation);
619
+ for (const [, entity] of entries) {
620
+ if (entity instanceof CartesianPoint2) {
621
+ const [x, y, z] = transformPoint(
622
+ [entity.x, entity.y, entity.z],
623
+ rotation,
624
+ transform.translation
625
+ );
626
+ entity.x = x;
627
+ entity.y = y;
628
+ entity.z = z;
629
+ } else if (entity instanceof Direction2) {
630
+ const [dx, dy, dz] = transformDirection(
631
+ [entity.dx, entity.dy, entity.dz],
632
+ rotation
633
+ );
634
+ const length = Math.hypot(dx, dy, dz);
635
+ if (length > 0) {
636
+ entity.dx = dx / length;
637
+ entity.dy = dy / length;
638
+ entity.dz = dz / length;
639
+ }
640
+ }
641
+ }
642
+ }
643
+ function allocateIds(targetRepo, entries) {
644
+ let nextId = getNextEntityId(targetRepo);
645
+ const idMapping = /* @__PURE__ */ new Map();
646
+ for (const [oldId] of entries) {
647
+ idMapping.set(oldId, nextId);
648
+ nextId += 1;
649
+ }
650
+ return idMapping;
651
+ }
652
+ function getNextEntityId(repo) {
653
+ let maxId = 0;
654
+ for (const [id] of repo.entries()) {
655
+ const numericId = Number(id);
656
+ if (numericId > maxId) {
657
+ maxId = numericId;
658
+ }
659
+ }
660
+ return maxId + 1;
661
+ }
662
+ function remapReferences(entries, idMapping) {
663
+ for (const [, entity] of entries) {
664
+ remapValue(entity, idMapping, /* @__PURE__ */ new Set());
665
+ }
666
+ }
667
+ function remapValue(value, idMapping, seen) {
668
+ if (!value) return;
669
+ if (value instanceof Ref) {
670
+ const mapped = idMapping.get(Number(value.id));
671
+ if (mapped !== void 0) {
672
+ value.id = eid(mapped);
673
+ }
674
+ return;
675
+ }
676
+ if (value instanceof Unknown) {
677
+ value.args = value.args.map(
678
+ (arg) => arg.replace(/#(\d+)/g, (match, num) => {
679
+ const mapped = idMapping.get(Number(num));
680
+ return mapped !== void 0 ? `#${mapped}` : match;
681
+ })
682
+ );
683
+ return;
684
+ }
685
+ if (Array.isArray(value)) {
686
+ for (const item of value) {
687
+ remapValue(item, idMapping, seen);
688
+ }
689
+ return;
690
+ }
691
+ if (typeof value === "object") {
692
+ if (seen.has(value)) return;
693
+ seen.add(value);
694
+ for (const key of Object.keys(value)) {
695
+ remapValue(value[key], idMapping, seen);
696
+ }
697
+ }
698
+ }
699
+
700
+ // lib/step-text-utils.ts
701
+ function normalizeStepNumericExponents(stepText) {
702
+ return stepText.replace(
703
+ /(-?(?:\d+\.\d*|\.\d+|\d+))e([+-]?\d+)/g,
704
+ (_match, mantissa, exponent) => `${mantissa}E${exponent}`
705
+ );
706
+ }
707
+
301
708
  // lib/index.ts
302
709
  async function circuitJsonToStep(circuitJson, options = {}) {
303
- const repo = new Repository();
710
+ const repo = new Repository2();
304
711
  const pcbBoard = circuitJson.find((item) => item.type === "pcb_board");
305
712
  const holes = circuitJson.filter(
306
713
  (item) => item.type === "pcb_hole" || item.type === "pcb_plated_hole"
@@ -348,22 +755,22 @@ async function circuitJsonToStep(circuitJson, options = {}) {
348
755
  new ProductDefinitionShape("", "", productDef)
349
756
  );
350
757
  const lengthUnit = repo.add(
351
- new Unknown("", [
758
+ new Unknown2("", [
352
759
  "( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )"
353
760
  ])
354
761
  );
355
762
  const angleUnit = repo.add(
356
- new Unknown("", [
763
+ new Unknown2("", [
357
764
  "( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )"
358
765
  ])
359
766
  );
360
767
  const solidAngleUnit = repo.add(
361
- new Unknown("", [
768
+ new Unknown2("", [
362
769
  "( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )"
363
770
  ])
364
771
  );
365
772
  const uncertainty = repo.add(
366
- new Unknown("UNCERTAINTY_MEASURE_WITH_UNIT", [
773
+ new Unknown2("UNCERTAINTY_MEASURE_WITH_UNIT", [
367
774
  `LENGTH_MEASURE(1.E-07)`,
368
775
  `${lengthUnit}`,
369
776
  `'distance_accuracy_value'`,
@@ -371,7 +778,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
371
778
  ])
372
779
  );
373
780
  const geomContext = repo.add(
374
- new Unknown("", [
781
+ new Unknown2("", [
375
782
  `( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((${uncertainty})) GLOBAL_UNIT_ASSIGNED_CONTEXT((${lengthUnit},${angleUnit},${solidAngleUnit})) REPRESENTATION_CONTEXT('${productName}','3D') )`
376
783
  ])
377
784
  );
@@ -383,7 +790,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
383
790
  (point) => repo.add(
384
791
  new VertexPoint2(
385
792
  "",
386
- repo.add(new CartesianPoint2("", point.x, point.y, 0))
793
+ repo.add(new CartesianPoint3("", point.x, point.y, 0))
387
794
  )
388
795
  )
389
796
  );
@@ -391,7 +798,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
391
798
  (point) => repo.add(
392
799
  new VertexPoint2(
393
800
  "",
394
- repo.add(new CartesianPoint2("", point.x, point.y, boardThickness))
801
+ repo.add(new CartesianPoint3("", point.x, point.y, boardThickness))
395
802
  )
396
803
  )
397
804
  );
@@ -410,7 +817,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
410
817
  ];
411
818
  const vertices = corners.map(
412
819
  ([x, y, z]) => repo.add(
413
- new VertexPoint2("", repo.add(new CartesianPoint2("", x, y, z)))
820
+ new VertexPoint2("", repo.add(new CartesianPoint3("", x, y, z)))
414
821
  )
415
822
  );
416
823
  bottomVertices = [vertices[0], vertices[1], vertices[2], vertices[3]];
@@ -420,7 +827,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
420
827
  const p1 = v1.resolve(repo).pnt.resolve(repo);
421
828
  const p2 = v2.resolve(repo).pnt.resolve(repo);
422
829
  const dir = repo.add(
423
- new Direction2("", p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
830
+ new Direction3("", p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
424
831
  );
425
832
  const vec = repo.add(new Vector2("", dir, 1));
426
833
  const line = repo.add(new Line2("", v1.resolve(repo).pnt, vec));
@@ -442,14 +849,14 @@ async function circuitJsonToStep(circuitJson, options = {}) {
442
849
  for (let i = 0; i < bottomVertices.length; i++) {
443
850
  verticalEdges.push(createEdge(bottomVertices[i], topVertices[i]));
444
851
  }
445
- const origin = repo.add(new CartesianPoint2("", 0, 0, 0));
446
- const xDir = repo.add(new Direction2("", 1, 0, 0));
447
- const zDir = repo.add(new Direction2("", 0, 0, 1));
852
+ const origin = repo.add(new CartesianPoint3("", 0, 0, 0));
853
+ const xDir = repo.add(new Direction3("", 1, 0, 0));
854
+ const zDir = repo.add(new Direction3("", 0, 0, 1));
448
855
  const bottomFrame = repo.add(
449
856
  new Axis2Placement3D2(
450
857
  "",
451
858
  origin,
452
- repo.add(new Direction2("", 0, 0, -1)),
859
+ repo.add(new Direction3("", 0, 0, -1)),
453
860
  xDir
454
861
  )
455
862
  );
@@ -467,18 +874,18 @@ async function circuitJsonToStep(circuitJson, options = {}) {
467
874
  const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
468
875
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
469
876
  const radius = hole.hole_diameter / 2;
470
- const holeCenter = repo.add(new CartesianPoint2("", holeX, holeY, 0));
877
+ const holeCenter = repo.add(new CartesianPoint3("", holeX, holeY, 0));
471
878
  const holeVertex = repo.add(
472
879
  new VertexPoint2(
473
880
  "",
474
- repo.add(new CartesianPoint2("", holeX + radius, holeY, 0))
881
+ repo.add(new CartesianPoint3("", holeX + radius, holeY, 0))
475
882
  )
476
883
  );
477
884
  const holePlacement = repo.add(
478
885
  new Axis2Placement3D2(
479
886
  "",
480
887
  holeCenter,
481
- repo.add(new Direction2("", 0, 0, -1)),
888
+ repo.add(new Direction3("", 0, 0, -1)),
482
889
  xDir
483
890
  )
484
891
  );
@@ -503,7 +910,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
503
910
  true
504
911
  )
505
912
  );
506
- const topOrigin = repo.add(new CartesianPoint2("", 0, 0, boardThickness));
913
+ const topOrigin = repo.add(new CartesianPoint3("", 0, 0, boardThickness));
507
914
  const topFrame = repo.add(new Axis2Placement3D2("", topOrigin, zDir, xDir));
508
915
  const topPlane = repo.add(new Plane2("", topFrame));
509
916
  const topLoop = repo.add(
@@ -520,13 +927,13 @@ async function circuitJsonToStep(circuitJson, options = {}) {
520
927
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
521
928
  const radius = hole.hole_diameter / 2;
522
929
  const holeCenter = repo.add(
523
- new CartesianPoint2("", holeX, holeY, boardThickness)
930
+ new CartesianPoint3("", holeX, holeY, boardThickness)
524
931
  );
525
932
  const holeVertex = repo.add(
526
933
  new VertexPoint2(
527
934
  "",
528
935
  repo.add(
529
- new CartesianPoint2("", holeX + radius, holeY, boardThickness)
936
+ new CartesianPoint3("", holeX + radius, holeY, boardThickness)
530
937
  )
531
938
  )
532
939
  );
@@ -563,8 +970,8 @@ async function circuitJsonToStep(circuitJson, options = {}) {
563
970
  y: bottomV2.y - bottomV1.y,
564
971
  z: 0
565
972
  };
566
- const normalDir = repo.add(new Direction2("", edgeDir.y, -edgeDir.x, 0));
567
- const refDir = repo.add(new Direction2("", edgeDir.x, edgeDir.y, 0));
973
+ const normalDir = repo.add(new Direction3("", edgeDir.y, -edgeDir.x, 0));
974
+ const refDir = repo.add(new Direction3("", edgeDir.x, edgeDir.y, 0));
568
975
  const sideFrame = repo.add(
569
976
  new Axis2Placement3D2("", bottomV1Pnt, normalDir, refDir)
570
977
  );
@@ -594,18 +1001,18 @@ async function circuitJsonToStep(circuitJson, options = {}) {
594
1001
  const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
595
1002
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
596
1003
  const radius = hole.hole_diameter / 2;
597
- const bottomHoleCenter = repo.add(new CartesianPoint2("", holeX, holeY, 0));
1004
+ const bottomHoleCenter = repo.add(new CartesianPoint3("", holeX, holeY, 0));
598
1005
  const bottomHoleVertex = repo.add(
599
1006
  new VertexPoint2(
600
1007
  "",
601
- repo.add(new CartesianPoint2("", holeX + radius, holeY, 0))
1008
+ repo.add(new CartesianPoint3("", holeX + radius, holeY, 0))
602
1009
  )
603
1010
  );
604
1011
  const bottomHolePlacement = repo.add(
605
1012
  new Axis2Placement3D2(
606
1013
  "",
607
1014
  bottomHoleCenter,
608
- repo.add(new Direction2("", 0, 0, -1)),
1015
+ repo.add(new Direction3("", 0, 0, -1)),
609
1016
  xDir
610
1017
  )
611
1018
  );
@@ -622,13 +1029,13 @@ async function circuitJsonToStep(circuitJson, options = {}) {
622
1029
  )
623
1030
  );
624
1031
  const topHoleCenter = repo.add(
625
- new CartesianPoint2("", holeX, holeY, boardThickness)
1032
+ new CartesianPoint3("", holeX, holeY, boardThickness)
626
1033
  );
627
1034
  const topHoleVertex = repo.add(
628
1035
  new VertexPoint2(
629
1036
  "",
630
1037
  repo.add(
631
- new CartesianPoint2("", holeX + radius, holeY, boardThickness)
1038
+ new CartesianPoint3("", holeX + radius, holeY, boardThickness)
632
1039
  )
633
1040
  )
634
1041
  );
@@ -664,20 +1071,37 @@ async function circuitJsonToStep(circuitJson, options = {}) {
664
1071
  }
665
1072
  const allFaces = [bottomFace, topFace, ...sideFaces, ...holeCylindricalFaces];
666
1073
  const shell = repo.add(new ClosedShell2("", allFaces));
667
- const solid = repo.add(new ManifoldSolidBrep2(productName, shell));
1074
+ const solid = repo.add(new ManifoldSolidBrep3(productName, shell));
668
1075
  const allSolids = [solid];
1076
+ let handledComponentIds = /* @__PURE__ */ new Set();
1077
+ let handledPcbComponentIds = /* @__PURE__ */ new Set();
1078
+ if (options.includeComponents && options.includeExternalMeshes) {
1079
+ const mergeResult = await mergeExternalStepModels({
1080
+ repo,
1081
+ circuitJson,
1082
+ boardThickness,
1083
+ fsMap: options.fsMap
1084
+ });
1085
+ handledComponentIds = mergeResult.handledComponentIds;
1086
+ handledPcbComponentIds = mergeResult.handledPcbComponentIds;
1087
+ allSolids.push(...mergeResult.solids);
1088
+ }
669
1089
  if (options.includeComponents) {
670
1090
  const componentSolids = await generateComponentMeshes({
671
1091
  repo,
672
1092
  circuitJson,
673
1093
  boardThickness,
674
- includeExternalMeshes: options.includeExternalMeshes
1094
+ includeExternalMeshes: options.includeExternalMeshes,
1095
+ excludeCadComponentIds: handledComponentIds,
1096
+ excludePcbComponentIds: handledPcbComponentIds
675
1097
  });
676
1098
  allSolids.push(...componentSolids);
677
1099
  }
678
1100
  const styledItems = [];
679
- for (const solidRef of allSolids) {
680
- const color = repo.add(new ColourRgb("", 0.2, 0.6, 0.2));
1101
+ allSolids.forEach((solidRef, index) => {
1102
+ const isBoard = index === 0;
1103
+ const [r, g, b] = isBoard ? [0.2, 0.6, 0.2] : [0.75, 0.75, 0.75];
1104
+ const color = repo.add(new ColourRgb("", r, g, b));
681
1105
  const fillColor = repo.add(new FillAreaStyleColour("", color));
682
1106
  const fillStyle = repo.add(new FillAreaStyle("", [fillColor]));
683
1107
  const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle));
@@ -686,7 +1110,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
686
1110
  const presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]));
687
1111
  const styledItem = repo.add(new StyledItem("", [presStyle], solidRef));
688
1112
  styledItems.push(styledItem);
689
- }
1113
+ });
690
1114
  repo.add(
691
1115
  new MechanicalDesignGeometricPresentationRepresentation(
692
1116
  "",
@@ -698,7 +1122,8 @@ async function circuitJsonToStep(circuitJson, options = {}) {
698
1122
  new AdvancedBrepShapeRepresentation(productName, allSolids, geomContext)
699
1123
  );
700
1124
  repo.add(new ShapeDefinitionRepresentation(productDefShape, shapeRep));
701
- return repo.toPartFile({ name: productName });
1125
+ const stepText = repo.toPartFile({ name: productName });
1126
+ return normalizeStepNumericExponents(stepText);
702
1127
  }
703
1128
  export {
704
1129
  circuitJsonToStep