circuit-json-to-step 0.0.2 → 0.0.3

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 (41) 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.js +470 -40
  8. package/lib/index.ts +26 -5
  9. package/lib/mesh-generation.ts +29 -1
  10. package/lib/step-model-merger/excluded-entity-types.ts +27 -0
  11. package/lib/step-model-merger/read-step-file.ts +29 -0
  12. package/lib/step-model-merger/types.ts +40 -0
  13. package/lib/step-model-merger/vector-utils.ts +74 -0
  14. package/lib/step-model-merger.ts +394 -0
  15. package/lib/step-text-utils.ts +6 -0
  16. package/package.json +8 -3
  17. package/test/basics/basics01/__snapshots__/basics01.snap.png +0 -0
  18. package/test/basics/basics01/basics01.test.ts +3 -2
  19. package/test/basics/basics02/__snapshots__/basics02.snap.png +0 -0
  20. package/test/basics/basics02/basics02.test.ts +3 -2
  21. package/test/basics/basics03/__snapshots__/basics03.snap.png +0 -0
  22. package/test/basics/basics03/basics03.test.ts +3 -2
  23. package/test/basics/basics04/__snapshots__/basics04.snap.png +0 -0
  24. package/test/basics/basics04/basics04.test.ts +3 -2
  25. package/test/basics/basics05/__snapshots__/basics05.snap.png +0 -0
  26. package/test/basics/basics05/basics05.json +40 -0
  27. package/test/basics/basics05/basics05.test.ts +55 -0
  28. package/test/fixtures/kicad-models/Panasonic_EVQPUJ_EVQPUA.step +3093 -0
  29. package/test/fixtures/kicad-models/R_0603_1608Metric.step +1049 -0
  30. package/test/fixtures/kicad-models/SW_Push_1P1T_NO_CK_KMR2.step +2916 -0
  31. package/test/fixtures/png-matcher.ts +173 -0
  32. package/test/fixtures/step-snapshot.ts +249 -0
  33. package/test/repros/kicad-step/__snapshots__/kicad-step-board.snap.png +0 -0
  34. package/test/repros/kicad-step/__snapshots__/resistor-fixture.snap.png +0 -0
  35. package/test/repros/kicad-step/__snapshots__/switch-fixture.snap.png +0 -0
  36. package/test/repros/kicad-step/kicad-step.json +163 -0
  37. package/test/repros/kicad-step/kicad-step.test.ts +208 -0
  38. package/test/repros/repro01/__snapshots__/repro01.snap.png +0 -0
  39. package/test/repros/repro01/repro01.test.ts +3 -2
  40. package/types/occt-import-js.d.ts +33 -0
  41. 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,408 @@ 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
+ import { promises as fs } from "fs";
413
+ import path from "path";
414
+ import { fileURLToPath } from "url";
415
+ async function readStepFile(modelUrl) {
416
+ if (/^https?:\/\//i.test(modelUrl)) {
417
+ const globalFetch = globalThis.fetch;
418
+ if (!globalFetch) {
419
+ throw new Error("fetch is not available in this environment");
420
+ }
421
+ const res = await globalFetch(modelUrl);
422
+ if (!res.ok) {
423
+ throw new Error(`HTTP ${res.status} ${res.statusText}`);
424
+ }
425
+ return await res.text();
426
+ }
427
+ if (modelUrl.startsWith("file://")) {
428
+ const filePath = fileURLToPath(modelUrl);
429
+ return await fs.readFile(filePath, "utf8");
430
+ }
431
+ const resolvedPath = path.isAbsolute(modelUrl) ? modelUrl : path.resolve(process.cwd(), modelUrl);
432
+ return await fs.readFile(resolvedPath, "utf8");
433
+ }
434
+
435
+ // lib/step-model-merger.ts
436
+ async function mergeExternalStepModels(options) {
437
+ const { repo, circuitJson, boardThickness } = options;
438
+ const cadComponents = circuitJson.filter(
439
+ (item) => item?.type === "cad_component" && typeof item.model_step_url === "string"
440
+ );
441
+ const pcbComponentMap = /* @__PURE__ */ new Map();
442
+ for (const item of circuitJson) {
443
+ if (item?.type === "pcb_component" && item.pcb_component_id) {
444
+ pcbComponentMap.set(item.pcb_component_id, item);
445
+ }
446
+ }
447
+ const solids = [];
448
+ const handledComponentIds = /* @__PURE__ */ new Set();
449
+ const handledPcbComponentIds = /* @__PURE__ */ new Set();
450
+ for (const component of cadComponents) {
451
+ const componentId = component.cad_component_id ?? "";
452
+ const stepUrl = component.model_step_url;
453
+ try {
454
+ const stepText = await readStepFile(stepUrl);
455
+ if (!stepText.trim()) {
456
+ throw new Error("STEP file is empty");
457
+ }
458
+ const pcbComponent = component.pcb_component_id ? pcbComponentMap.get(component.pcb_component_id) : void 0;
459
+ const layer = pcbComponent?.layer?.toLowerCase();
460
+ const transform = {
461
+ translation: asVector3(component.position),
462
+ rotation: asVector3(component.rotation)
463
+ };
464
+ const componentSolids = mergeSingleStepModel(repo, stepText, transform, {
465
+ layer,
466
+ boardThickness
467
+ });
468
+ if (componentSolids.length > 0) {
469
+ if (componentId) {
470
+ handledComponentIds.add(componentId);
471
+ }
472
+ const pcbComponentId = component.pcb_component_id;
473
+ if (pcbComponentId) {
474
+ handledPcbComponentIds.add(pcbComponentId);
475
+ }
476
+ }
477
+ solids.push(...componentSolids);
478
+ } catch (error) {
479
+ console.warn(`Failed to merge STEP model from ${stepUrl}:`, error);
480
+ }
481
+ }
482
+ return { solids, handledComponentIds, handledPcbComponentIds };
483
+ }
484
+ function mergeSingleStepModel(targetRepo, stepText, transform, placement) {
485
+ const sourceRepo = parseRepository(stepText);
486
+ let entries = sourceRepo.entries().map(([id, entity]) => [Number(id), entity]).filter(([, entity]) => !EXCLUDED_ENTITY_TYPES.has(entity.type));
487
+ entries = pruneInvalidEntries(entries);
488
+ adjustTransformForPlacement(entries, transform, placement);
489
+ applyTransform(entries, transform);
490
+ const idMapping = allocateIds(targetRepo, entries);
491
+ remapReferences(entries, idMapping);
492
+ for (const [oldId, entity] of entries) {
493
+ const mappedId = idMapping.get(oldId);
494
+ if (mappedId === void 0) continue;
495
+ targetRepo.set(eid(mappedId), entity);
496
+ }
497
+ const solids = [];
498
+ for (const [oldId, entity] of entries) {
499
+ if (entity instanceof ManifoldSolidBrep2) {
500
+ const mappedId = idMapping.get(oldId);
501
+ if (mappedId !== void 0) {
502
+ solids.push(new Ref(eid(mappedId)));
503
+ }
504
+ }
505
+ }
506
+ return solids;
507
+ }
508
+ function adjustTransformForPlacement(entries, transform, placement) {
509
+ if (!placement) return;
510
+ const points = [];
511
+ for (const [, entity] of entries) {
512
+ if (entity instanceof CartesianPoint2) {
513
+ points.push([entity.x, entity.y, entity.z]);
514
+ }
515
+ }
516
+ if (!points.length) return;
517
+ const rotationRadians = toRadians(transform.rotation);
518
+ let minX = Infinity;
519
+ let minY = Infinity;
520
+ let minZ = Infinity;
521
+ let maxX = -Infinity;
522
+ let maxY = -Infinity;
523
+ let maxZ = -Infinity;
524
+ for (const point of points) {
525
+ const [x, y, z] = rotateVector(point, rotationRadians);
526
+ if (x < minX) minX = x;
527
+ if (y < minY) minY = y;
528
+ if (z < minZ) minZ = z;
529
+ if (x > maxX) maxX = x;
530
+ if (y > maxY) maxY = y;
531
+ if (z > maxZ) maxZ = z;
532
+ }
533
+ if (!Number.isFinite(minX)) return;
534
+ const center = {
535
+ x: (minX + maxX) / 2,
536
+ y: (minY + maxY) / 2,
537
+ z: (minZ + maxZ) / 2
538
+ };
539
+ const normalizedLayer = placement.layer?.toLowerCase() === "bottom" ? "bottom" : "top";
540
+ const boardThickness = placement.boardThickness ?? 0;
541
+ const halfThickness = boardThickness / 2;
542
+ const targetX = transform.translation.x;
543
+ const targetY = transform.translation.y;
544
+ const targetZ = transform.translation.z;
545
+ transform.translation.x = targetX - center.x;
546
+ transform.translation.y = targetY - center.y;
547
+ if (boardThickness > 0) {
548
+ const offsetZ = targetZ - halfThickness;
549
+ if (normalizedLayer === "bottom") {
550
+ transform.translation.z = -maxZ + offsetZ;
551
+ transform.rotation.x = normalizeDegrees(transform.rotation.x + 180);
552
+ } else {
553
+ transform.translation.z = boardThickness - minZ + offsetZ;
554
+ }
555
+ } else {
556
+ transform.translation.z = targetZ - center.z;
557
+ }
558
+ }
559
+ function normalizeDegrees(value) {
560
+ const wrapped = value % 360;
561
+ return wrapped < 0 ? wrapped + 360 : wrapped;
562
+ }
563
+ function pruneInvalidEntries(entries) {
564
+ let remaining = entries.slice();
565
+ let remainingIds = new Set(remaining.map(([id]) => id));
566
+ let changed = true;
567
+ while (changed) {
568
+ changed = false;
569
+ const toRemove = /* @__PURE__ */ new Set();
570
+ for (const [entityId, entity] of remaining) {
571
+ const refs = collectReferencedIds(entity);
572
+ for (const refId of refs) {
573
+ if (!remainingIds.has(refId)) {
574
+ toRemove.add(entityId);
575
+ break;
576
+ }
577
+ }
578
+ }
579
+ if (toRemove.size > 0) {
580
+ changed = true;
581
+ remaining = remaining.filter(([id]) => !toRemove.has(id));
582
+ remainingIds = new Set(remaining.map(([id]) => id));
583
+ }
584
+ }
585
+ return remaining;
586
+ }
587
+ function collectReferencedIds(entity) {
588
+ const result = /* @__PURE__ */ new Set();
589
+ collectReferencedIdsRecursive(entity, result, /* @__PURE__ */ new Set());
590
+ return result;
591
+ }
592
+ function collectReferencedIdsRecursive(value, result, seen) {
593
+ if (!value) return;
594
+ if (value instanceof Ref) {
595
+ result.add(Number(value.id));
596
+ return;
597
+ }
598
+ if (value instanceof Unknown) {
599
+ for (const arg of value.args) {
600
+ arg.replace(/#(\d+)/g, (_, num) => {
601
+ result.add(Number(num));
602
+ return _;
603
+ });
604
+ }
605
+ return;
606
+ }
607
+ if (Array.isArray(value)) {
608
+ for (const item of value) {
609
+ collectReferencedIdsRecursive(item, result, seen);
610
+ }
611
+ return;
612
+ }
613
+ if (typeof value === "object") {
614
+ if (seen.has(value)) {
615
+ return;
616
+ }
617
+ seen.add(value);
618
+ for (const entry of Object.values(value)) {
619
+ collectReferencedIdsRecursive(entry, result, seen);
620
+ }
621
+ }
622
+ }
623
+ function applyTransform(entries, transform) {
624
+ const rotation = toRadians(transform.rotation);
625
+ for (const [, entity] of entries) {
626
+ if (entity instanceof CartesianPoint2) {
627
+ const [x, y, z] = transformPoint(
628
+ [entity.x, entity.y, entity.z],
629
+ rotation,
630
+ transform.translation
631
+ );
632
+ entity.x = x;
633
+ entity.y = y;
634
+ entity.z = z;
635
+ } else if (entity instanceof Direction2) {
636
+ const [dx, dy, dz] = transformDirection(
637
+ [entity.dx, entity.dy, entity.dz],
638
+ rotation
639
+ );
640
+ const length = Math.hypot(dx, dy, dz);
641
+ if (length > 0) {
642
+ entity.dx = dx / length;
643
+ entity.dy = dy / length;
644
+ entity.dz = dz / length;
645
+ }
646
+ }
647
+ }
648
+ }
649
+ function allocateIds(targetRepo, entries) {
650
+ let nextId = getNextEntityId(targetRepo);
651
+ const idMapping = /* @__PURE__ */ new Map();
652
+ for (const [oldId] of entries) {
653
+ idMapping.set(oldId, nextId);
654
+ nextId += 1;
655
+ }
656
+ return idMapping;
657
+ }
658
+ function getNextEntityId(repo) {
659
+ let maxId = 0;
660
+ for (const [id] of repo.entries()) {
661
+ const numericId = Number(id);
662
+ if (numericId > maxId) {
663
+ maxId = numericId;
664
+ }
665
+ }
666
+ return maxId + 1;
667
+ }
668
+ function remapReferences(entries, idMapping) {
669
+ for (const [, entity] of entries) {
670
+ remapValue(entity, idMapping, /* @__PURE__ */ new Set());
671
+ }
672
+ }
673
+ function remapValue(value, idMapping, seen) {
674
+ if (!value) return;
675
+ if (value instanceof Ref) {
676
+ const mapped = idMapping.get(Number(value.id));
677
+ if (mapped !== void 0) {
678
+ value.id = eid(mapped);
679
+ }
680
+ return;
681
+ }
682
+ if (value instanceof Unknown) {
683
+ value.args = value.args.map(
684
+ (arg) => arg.replace(/#(\d+)/g, (match, num) => {
685
+ const mapped = idMapping.get(Number(num));
686
+ return mapped !== void 0 ? `#${mapped}` : match;
687
+ })
688
+ );
689
+ return;
690
+ }
691
+ if (Array.isArray(value)) {
692
+ for (const item of value) {
693
+ remapValue(item, idMapping, seen);
694
+ }
695
+ return;
696
+ }
697
+ if (typeof value === "object") {
698
+ if (seen.has(value)) return;
699
+ seen.add(value);
700
+ for (const key of Object.keys(value)) {
701
+ remapValue(value[key], idMapping, seen);
702
+ }
703
+ }
704
+ }
705
+
706
+ // lib/step-text-utils.ts
707
+ function normalizeStepNumericExponents(stepText) {
708
+ return stepText.replace(
709
+ /(-?(?:\d+\.\d*|\.\d+|\d+))e([+-]?\d+)/g,
710
+ (_match, mantissa, exponent) => `${mantissa}E${exponent}`
711
+ );
712
+ }
713
+
301
714
  // lib/index.ts
302
715
  async function circuitJsonToStep(circuitJson, options = {}) {
303
- const repo = new Repository();
716
+ const repo = new Repository2();
304
717
  const pcbBoard = circuitJson.find((item) => item.type === "pcb_board");
305
718
  const holes = circuitJson.filter(
306
719
  (item) => item.type === "pcb_hole" || item.type === "pcb_plated_hole"
@@ -348,22 +761,22 @@ async function circuitJsonToStep(circuitJson, options = {}) {
348
761
  new ProductDefinitionShape("", "", productDef)
349
762
  );
350
763
  const lengthUnit = repo.add(
351
- new Unknown("", [
764
+ new Unknown2("", [
352
765
  "( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )"
353
766
  ])
354
767
  );
355
768
  const angleUnit = repo.add(
356
- new Unknown("", [
769
+ new Unknown2("", [
357
770
  "( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )"
358
771
  ])
359
772
  );
360
773
  const solidAngleUnit = repo.add(
361
- new Unknown("", [
774
+ new Unknown2("", [
362
775
  "( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )"
363
776
  ])
364
777
  );
365
778
  const uncertainty = repo.add(
366
- new Unknown("UNCERTAINTY_MEASURE_WITH_UNIT", [
779
+ new Unknown2("UNCERTAINTY_MEASURE_WITH_UNIT", [
367
780
  `LENGTH_MEASURE(1.E-07)`,
368
781
  `${lengthUnit}`,
369
782
  `'distance_accuracy_value'`,
@@ -371,7 +784,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
371
784
  ])
372
785
  );
373
786
  const geomContext = repo.add(
374
- new Unknown("", [
787
+ new Unknown2("", [
375
788
  `( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((${uncertainty})) GLOBAL_UNIT_ASSIGNED_CONTEXT((${lengthUnit},${angleUnit},${solidAngleUnit})) REPRESENTATION_CONTEXT('${productName}','3D') )`
376
789
  ])
377
790
  );
@@ -383,7 +796,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
383
796
  (point) => repo.add(
384
797
  new VertexPoint2(
385
798
  "",
386
- repo.add(new CartesianPoint2("", point.x, point.y, 0))
799
+ repo.add(new CartesianPoint3("", point.x, point.y, 0))
387
800
  )
388
801
  )
389
802
  );
@@ -391,7 +804,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
391
804
  (point) => repo.add(
392
805
  new VertexPoint2(
393
806
  "",
394
- repo.add(new CartesianPoint2("", point.x, point.y, boardThickness))
807
+ repo.add(new CartesianPoint3("", point.x, point.y, boardThickness))
395
808
  )
396
809
  )
397
810
  );
@@ -410,7 +823,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
410
823
  ];
411
824
  const vertices = corners.map(
412
825
  ([x, y, z]) => repo.add(
413
- new VertexPoint2("", repo.add(new CartesianPoint2("", x, y, z)))
826
+ new VertexPoint2("", repo.add(new CartesianPoint3("", x, y, z)))
414
827
  )
415
828
  );
416
829
  bottomVertices = [vertices[0], vertices[1], vertices[2], vertices[3]];
@@ -420,7 +833,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
420
833
  const p1 = v1.resolve(repo).pnt.resolve(repo);
421
834
  const p2 = v2.resolve(repo).pnt.resolve(repo);
422
835
  const dir = repo.add(
423
- new Direction2("", p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
836
+ new Direction3("", p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
424
837
  );
425
838
  const vec = repo.add(new Vector2("", dir, 1));
426
839
  const line = repo.add(new Line2("", v1.resolve(repo).pnt, vec));
@@ -442,14 +855,14 @@ async function circuitJsonToStep(circuitJson, options = {}) {
442
855
  for (let i = 0; i < bottomVertices.length; i++) {
443
856
  verticalEdges.push(createEdge(bottomVertices[i], topVertices[i]));
444
857
  }
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));
858
+ const origin = repo.add(new CartesianPoint3("", 0, 0, 0));
859
+ const xDir = repo.add(new Direction3("", 1, 0, 0));
860
+ const zDir = repo.add(new Direction3("", 0, 0, 1));
448
861
  const bottomFrame = repo.add(
449
862
  new Axis2Placement3D2(
450
863
  "",
451
864
  origin,
452
- repo.add(new Direction2("", 0, 0, -1)),
865
+ repo.add(new Direction3("", 0, 0, -1)),
453
866
  xDir
454
867
  )
455
868
  );
@@ -467,18 +880,18 @@ async function circuitJsonToStep(circuitJson, options = {}) {
467
880
  const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
468
881
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
469
882
  const radius = hole.hole_diameter / 2;
470
- const holeCenter = repo.add(new CartesianPoint2("", holeX, holeY, 0));
883
+ const holeCenter = repo.add(new CartesianPoint3("", holeX, holeY, 0));
471
884
  const holeVertex = repo.add(
472
885
  new VertexPoint2(
473
886
  "",
474
- repo.add(new CartesianPoint2("", holeX + radius, holeY, 0))
887
+ repo.add(new CartesianPoint3("", holeX + radius, holeY, 0))
475
888
  )
476
889
  );
477
890
  const holePlacement = repo.add(
478
891
  new Axis2Placement3D2(
479
892
  "",
480
893
  holeCenter,
481
- repo.add(new Direction2("", 0, 0, -1)),
894
+ repo.add(new Direction3("", 0, 0, -1)),
482
895
  xDir
483
896
  )
484
897
  );
@@ -503,7 +916,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
503
916
  true
504
917
  )
505
918
  );
506
- const topOrigin = repo.add(new CartesianPoint2("", 0, 0, boardThickness));
919
+ const topOrigin = repo.add(new CartesianPoint3("", 0, 0, boardThickness));
507
920
  const topFrame = repo.add(new Axis2Placement3D2("", topOrigin, zDir, xDir));
508
921
  const topPlane = repo.add(new Plane2("", topFrame));
509
922
  const topLoop = repo.add(
@@ -520,13 +933,13 @@ async function circuitJsonToStep(circuitJson, options = {}) {
520
933
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
521
934
  const radius = hole.hole_diameter / 2;
522
935
  const holeCenter = repo.add(
523
- new CartesianPoint2("", holeX, holeY, boardThickness)
936
+ new CartesianPoint3("", holeX, holeY, boardThickness)
524
937
  );
525
938
  const holeVertex = repo.add(
526
939
  new VertexPoint2(
527
940
  "",
528
941
  repo.add(
529
- new CartesianPoint2("", holeX + radius, holeY, boardThickness)
942
+ new CartesianPoint3("", holeX + radius, holeY, boardThickness)
530
943
  )
531
944
  )
532
945
  );
@@ -563,8 +976,8 @@ async function circuitJsonToStep(circuitJson, options = {}) {
563
976
  y: bottomV2.y - bottomV1.y,
564
977
  z: 0
565
978
  };
566
- const normalDir = repo.add(new Direction2("", edgeDir.y, -edgeDir.x, 0));
567
- const refDir = repo.add(new Direction2("", edgeDir.x, edgeDir.y, 0));
979
+ const normalDir = repo.add(new Direction3("", edgeDir.y, -edgeDir.x, 0));
980
+ const refDir = repo.add(new Direction3("", edgeDir.x, edgeDir.y, 0));
568
981
  const sideFrame = repo.add(
569
982
  new Axis2Placement3D2("", bottomV1Pnt, normalDir, refDir)
570
983
  );
@@ -594,18 +1007,18 @@ async function circuitJsonToStep(circuitJson, options = {}) {
594
1007
  const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
595
1008
  const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
596
1009
  const radius = hole.hole_diameter / 2;
597
- const bottomHoleCenter = repo.add(new CartesianPoint2("", holeX, holeY, 0));
1010
+ const bottomHoleCenter = repo.add(new CartesianPoint3("", holeX, holeY, 0));
598
1011
  const bottomHoleVertex = repo.add(
599
1012
  new VertexPoint2(
600
1013
  "",
601
- repo.add(new CartesianPoint2("", holeX + radius, holeY, 0))
1014
+ repo.add(new CartesianPoint3("", holeX + radius, holeY, 0))
602
1015
  )
603
1016
  );
604
1017
  const bottomHolePlacement = repo.add(
605
1018
  new Axis2Placement3D2(
606
1019
  "",
607
1020
  bottomHoleCenter,
608
- repo.add(new Direction2("", 0, 0, -1)),
1021
+ repo.add(new Direction3("", 0, 0, -1)),
609
1022
  xDir
610
1023
  )
611
1024
  );
@@ -622,13 +1035,13 @@ async function circuitJsonToStep(circuitJson, options = {}) {
622
1035
  )
623
1036
  );
624
1037
  const topHoleCenter = repo.add(
625
- new CartesianPoint2("", holeX, holeY, boardThickness)
1038
+ new CartesianPoint3("", holeX, holeY, boardThickness)
626
1039
  );
627
1040
  const topHoleVertex = repo.add(
628
1041
  new VertexPoint2(
629
1042
  "",
630
1043
  repo.add(
631
- new CartesianPoint2("", holeX + radius, holeY, boardThickness)
1044
+ new CartesianPoint3("", holeX + radius, holeY, boardThickness)
632
1045
  )
633
1046
  )
634
1047
  );
@@ -664,20 +1077,36 @@ async function circuitJsonToStep(circuitJson, options = {}) {
664
1077
  }
665
1078
  const allFaces = [bottomFace, topFace, ...sideFaces, ...holeCylindricalFaces];
666
1079
  const shell = repo.add(new ClosedShell2("", allFaces));
667
- const solid = repo.add(new ManifoldSolidBrep2(productName, shell));
1080
+ const solid = repo.add(new ManifoldSolidBrep3(productName, shell));
668
1081
  const allSolids = [solid];
1082
+ let handledComponentIds = /* @__PURE__ */ new Set();
1083
+ let handledPcbComponentIds = /* @__PURE__ */ new Set();
1084
+ if (options.includeComponents && options.includeExternalMeshes) {
1085
+ const mergeResult = await mergeExternalStepModels({
1086
+ repo,
1087
+ circuitJson,
1088
+ boardThickness
1089
+ });
1090
+ handledComponentIds = mergeResult.handledComponentIds;
1091
+ handledPcbComponentIds = mergeResult.handledPcbComponentIds;
1092
+ allSolids.push(...mergeResult.solids);
1093
+ }
669
1094
  if (options.includeComponents) {
670
1095
  const componentSolids = await generateComponentMeshes({
671
1096
  repo,
672
1097
  circuitJson,
673
1098
  boardThickness,
674
- includeExternalMeshes: options.includeExternalMeshes
1099
+ includeExternalMeshes: options.includeExternalMeshes,
1100
+ excludeCadComponentIds: handledComponentIds,
1101
+ excludePcbComponentIds: handledPcbComponentIds
675
1102
  });
676
1103
  allSolids.push(...componentSolids);
677
1104
  }
678
1105
  const styledItems = [];
679
- for (const solidRef of allSolids) {
680
- const color = repo.add(new ColourRgb("", 0.2, 0.6, 0.2));
1106
+ allSolids.forEach((solidRef, index) => {
1107
+ const isBoard = index === 0;
1108
+ const [r, g, b] = isBoard ? [0.2, 0.6, 0.2] : [0.75, 0.75, 0.75];
1109
+ const color = repo.add(new ColourRgb("", r, g, b));
681
1110
  const fillColor = repo.add(new FillAreaStyleColour("", color));
682
1111
  const fillStyle = repo.add(new FillAreaStyle("", [fillColor]));
683
1112
  const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle));
@@ -686,7 +1115,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
686
1115
  const presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]));
687
1116
  const styledItem = repo.add(new StyledItem("", [presStyle], solidRef));
688
1117
  styledItems.push(styledItem);
689
- }
1118
+ });
690
1119
  repo.add(
691
1120
  new MechanicalDesignGeometricPresentationRepresentation(
692
1121
  "",
@@ -698,7 +1127,8 @@ async function circuitJsonToStep(circuitJson, options = {}) {
698
1127
  new AdvancedBrepShapeRepresentation(productName, allSolids, geomContext)
699
1128
  );
700
1129
  repo.add(new ShapeDefinitionRepresentation(productDefShape, shapeRep));
701
- return repo.toPartFile({ name: productName });
1130
+ const stepText = repo.toPartFile({ name: productName });
1131
+ return normalizeStepNumericExponents(stepText);
702
1132
  }
703
1133
  export {
704
1134
  circuitJsonToStep