circuit-json-to-step 0.0.27 → 0.0.29

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.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  Unknown as Unknown2,
13
13
  CartesianPoint as CartesianPoint4,
14
14
  Direction as Direction4,
15
- Axis2Placement3D as Axis2Placement3D3,
15
+ Axis2Placement3D as Axis2Placement3D4,
16
16
  Plane as Plane3,
17
17
  CylindricalSurface as CylindricalSurface2,
18
18
  VertexPoint as VertexPoint3,
@@ -28,7 +28,8 @@ import {
28
28
  ClosedShell as ClosedShell2,
29
29
  ManifoldSolidBrep as ManifoldSolidBrep4,
30
30
  MechanicalDesignGeometricPresentationRepresentation,
31
- AdvancedBrepShapeRepresentation,
31
+ AdvancedBrepShapeRepresentation as AdvancedBrepShapeRepresentation2,
32
+ ShapeRepresentation,
32
33
  ShapeDefinitionRepresentation
33
34
  } from "stepts";
34
35
 
@@ -373,12 +374,16 @@ async function generateComponentMeshes(options) {
373
374
 
374
375
  // lib/step-model-merger.ts
375
376
  import {
377
+ AdvancedBrepShapeRepresentation,
378
+ Axis2Placement3D as Axis2Placement3D2,
376
379
  CartesianPoint as CartesianPoint2,
377
380
  Direction as Direction2,
381
+ Entity,
378
382
  ManifoldSolidBrep as ManifoldSolidBrep3,
379
383
  Ref,
380
384
  Unknown,
381
- parseRepository
385
+ parseRepository,
386
+ stepStr
382
387
  } from "stepts";
383
388
  import { eid } from "stepts";
384
389
 
@@ -416,14 +421,6 @@ function toRadians(rotation) {
416
421
  z: rotation.z * factor
417
422
  };
418
423
  }
419
- function transformPoint(point, rotation, translation) {
420
- const rotated = rotateVector(point, rotation);
421
- return [
422
- rotated[0] + translation.x,
423
- rotated[1] + translation.y,
424
- rotated[2] + translation.z
425
- ];
426
- }
427
424
  function transformDirection(vector, rotation) {
428
425
  return rotateVector(vector, rotation);
429
426
  }
@@ -489,13 +486,19 @@ async function mergeExternalStepModels(options) {
489
486
  const solids = [];
490
487
  const handledComponentIds = /* @__PURE__ */ new Set();
491
488
  const handledPcbComponentIds = /* @__PURE__ */ new Set();
489
+ const importedModels = /* @__PURE__ */ new Map();
492
490
  for (const component of cadComponents) {
493
491
  const componentId = component.cad_component_id ?? "";
494
492
  const stepUrl = component.model_step_url;
495
493
  try {
496
- const stepText = fsMap?.[stepUrl] ?? await readStepFile(stepUrl);
497
- if (!stepText.trim()) {
498
- throw new Error("STEP file is empty");
494
+ let importedModel = importedModels.get(stepUrl);
495
+ if (!importedModel) {
496
+ const stepText = fsMap?.[stepUrl] ?? await readStepFile(stepUrl);
497
+ if (!stepText.trim()) {
498
+ throw new Error("STEP file is empty");
499
+ }
500
+ importedModel = importStepModelOnce(repo, stepText, stepUrl);
501
+ importedModels.set(stepUrl, importedModel);
499
502
  }
500
503
  const pcbComponent = component.pcb_component_id ? pcbComponentMap.get(component.pcb_component_id) : void 0;
501
504
  const layer = pcbComponent?.layer?.toLowerCase();
@@ -503,9 +506,14 @@ async function mergeExternalStepModels(options) {
503
506
  translation: asVector3(component.position),
504
507
  rotation: asVector3(component.rotation)
505
508
  };
506
- const componentSolids = mergeSingleStepModel(repo, stepText, transform, {
507
- layer,
508
- boardThickness
509
+ const componentSolids = createMappedStepModelInstance({
510
+ repo,
511
+ importedModel,
512
+ transform,
513
+ placement: {
514
+ layer,
515
+ boardThickness
516
+ }
509
517
  });
510
518
  if (componentSolids.length > 0) {
511
519
  if (componentId) {
@@ -523,12 +531,38 @@ async function mergeExternalStepModels(options) {
523
531
  }
524
532
  return { solids, handledComponentIds, handledPcbComponentIds };
525
533
  }
526
- function mergeSingleStepModel(targetRepo, stepText, transform, placement) {
534
+ var RepresentationMap = class extends Entity {
535
+ constructor(mappingOrigin, mappedRepresentation) {
536
+ super();
537
+ this.mappingOrigin = mappingOrigin;
538
+ this.mappedRepresentation = mappedRepresentation;
539
+ }
540
+ mappingOrigin;
541
+ mappedRepresentation;
542
+ type = "REPRESENTATION_MAP";
543
+ toStep() {
544
+ return `REPRESENTATION_MAP(${this.mappingOrigin},${this.mappedRepresentation})`;
545
+ }
546
+ };
547
+ var MappedItem = class extends Entity {
548
+ constructor(name, mappingSource, mappingTarget) {
549
+ super();
550
+ this.name = name;
551
+ this.mappingSource = mappingSource;
552
+ this.mappingTarget = mappingTarget;
553
+ }
554
+ name;
555
+ mappingSource;
556
+ mappingTarget;
557
+ type = "MAPPED_ITEM";
558
+ toStep() {
559
+ return `MAPPED_ITEM(${stepStr(this.name)},${this.mappingSource},${this.mappingTarget})`;
560
+ }
561
+ };
562
+ function importStepModelOnce(targetRepo, stepText, modelName) {
527
563
  const sourceRepo = parseRepository(stepText);
528
564
  let entries = sourceRepo.entries().map(([id, entity]) => [Number(id), entity]).filter(([, entity]) => !EXCLUDED_ENTITY_TYPES.has(entity.type));
529
565
  entries = pruneInvalidEntries(entries);
530
- adjustTransformForPlacement(entries, transform, placement);
531
- applyTransform(entries, transform);
532
566
  const idMapping = allocateIds(targetRepo, entries);
533
567
  remapReferences(entries, idMapping);
534
568
  for (const [oldId, entity] of entries) {
@@ -545,7 +579,65 @@ function mergeSingleStepModel(targetRepo, stepText, transform, placement) {
545
579
  }
546
580
  }
547
581
  }
548
- return solids;
582
+ const originPoint = targetRepo.add(new CartesianPoint2("", 0, 0, 0));
583
+ const zDirection = targetRepo.add(new Direction2("", 0, 0, 1));
584
+ const xDirection = targetRepo.add(new Direction2("", 1, 0, 0));
585
+ const mappingOrigin = targetRepo.add(
586
+ new Axis2Placement3D2("", originPoint, zDirection, xDirection)
587
+ );
588
+ const representation = targetRepo.add(
589
+ new AdvancedBrepShapeRepresentation(
590
+ modelName,
591
+ solids,
592
+ getGeomContext(targetRepo)
593
+ )
594
+ );
595
+ const representationMap = targetRepo.add(
596
+ new RepresentationMap(mappingOrigin, representation)
597
+ );
598
+ return { entries, representationMap };
599
+ }
600
+ function createMappedStepModelInstance({
601
+ repo,
602
+ importedModel,
603
+ transform,
604
+ placement
605
+ }) {
606
+ adjustTransformForPlacement(importedModel.entries, transform, placement);
607
+ const placementTarget = createPlacementTarget(repo, transform);
608
+ const mappedItem = repo.add(
609
+ new MappedItem("", importedModel.representationMap, placementTarget)
610
+ );
611
+ return [mappedItem];
612
+ }
613
+ function createPlacementTarget(repo, transform) {
614
+ const rotation = toRadians(transform.rotation);
615
+ const [refX, refY, refZ] = transformDirection([1, 0, 0], rotation);
616
+ const [axisX, axisY, axisZ] = transformDirection([0, 0, 1], rotation);
617
+ const axis = repo.add(new Direction2("", axisX, axisY, axisZ));
618
+ const refDirection = repo.add(new Direction2("", refX, refY, refZ));
619
+ const origin = repo.add(
620
+ new CartesianPoint2(
621
+ "",
622
+ transform.translation.x,
623
+ transform.translation.y,
624
+ transform.translation.z
625
+ )
626
+ );
627
+ return repo.add(new Axis2Placement3D2("", origin, axis, refDirection));
628
+ }
629
+ function getGeomContext(repo) {
630
+ for (const [id, entity] of repo.entries()) {
631
+ if (entity.type === "GEOMETRIC_REPRESENTATION_CONTEXT") {
632
+ return new Ref(id);
633
+ }
634
+ if (entity instanceof Unknown && entity.args.some(
635
+ (arg) => arg.includes("GEOMETRIC_REPRESENTATION_CONTEXT")
636
+ )) {
637
+ return new Ref(id);
638
+ }
639
+ }
640
+ throw new Error("GEOMETRIC_REPRESENTATION_CONTEXT is missing");
549
641
  }
550
642
  function adjustTransformForPlacement(entries, transform, placement) {
551
643
  if (!placement) return;
@@ -667,32 +759,6 @@ function collectReferencedIdsRecursive(value, result, seen) {
667
759
  }
668
760
  }
669
761
  }
670
- function applyTransform(entries, transform) {
671
- const rotation = toRadians(transform.rotation);
672
- for (const [, entity] of entries) {
673
- if (entity instanceof CartesianPoint2) {
674
- const [x, y, z] = transformPoint(
675
- [entity.x, entity.y, entity.z],
676
- rotation,
677
- transform.translation
678
- );
679
- entity.x = x;
680
- entity.y = y;
681
- entity.z = z;
682
- } else if (entity instanceof Direction2) {
683
- const [dx, dy, dz] = transformDirection(
684
- [entity.dx, entity.dy, entity.dz],
685
- rotation
686
- );
687
- const length = Math.hypot(dx, dy, dz);
688
- if (length > 0) {
689
- entity.dx = dx / length;
690
- entity.dy = dy / length;
691
- entity.dz = dz / length;
692
- }
693
- }
694
- }
695
- }
696
762
  function allocateIds(targetRepo, entries) {
697
763
  let nextId = getNextEntityId(targetRepo);
698
764
  const idMapping = /* @__PURE__ */ new Map();
@@ -762,7 +828,7 @@ function normalizeStepNumericExponents(stepText) {
762
828
  var package_default = {
763
829
  name: "circuit-json-to-step",
764
830
  main: "dist/index.js",
765
- version: "0.0.23",
831
+ version: "0.0.28",
766
832
  type: "module",
767
833
  scripts: {
768
834
  "pull-reference": `git clone https://github.com/tscircuit/circuit-json.git && find circuit-json/tests -name '*.test.ts' -exec bash -c 'mv "$0" "\${0%.test.ts}.ts"' {} \\; && git clone https://github.com/tscircuit/stepts.git && find stepts/tests -name '*.test.ts' -exec bash -c 'mv "$0" "\${0%.test.ts}.ts"' {} \\;`,
@@ -780,7 +846,7 @@ var package_default = {
780
846
  "@resvg/resvg-wasm": "^2.6.2",
781
847
  "@tscircuit/circuit-json-util": "^0.0.75",
782
848
  "@types/bun": "latest",
783
- "circuit-json": "^0.0.406",
849
+ "circuit-json": "^0.0.425",
784
850
  "looks-same": "^10.0.1",
785
851
  "occt-import-js": "^0.0.23",
786
852
  poppygl: "^0.0.17",
@@ -792,8 +858,8 @@ var package_default = {
792
858
  },
793
859
  dependencies: {
794
860
  "circuit-json-to-connectivity-map": "^0.0.22",
795
- "circuit-json-to-gltf": "^0.0.93",
796
- "circuit-to-svg": "^0.0.327",
861
+ "circuit-json-to-gltf": "^0.0.100",
862
+ "circuit-to-svg": "^0.0.345",
797
863
  "schematic-symbols": "^0.0.202",
798
864
  stepts: "^0.0.4"
799
865
  }
@@ -805,7 +871,7 @@ var VERSION = package_default.version;
805
871
  // lib/pill-geometry.ts
806
872
  import {
807
873
  AdvancedFace as AdvancedFace2,
808
- Axis2Placement3D as Axis2Placement3D2,
874
+ Axis2Placement3D as Axis2Placement3D3,
809
875
  CartesianPoint as CartesianPoint3,
810
876
  Circle,
811
877
  CylindricalSurface,
@@ -903,7 +969,7 @@ function createArcEdge(repo, centerX, centerY, z, radius, startAngle, endAngle,
903
969
  new Direction3("", Math.cos(refAngle), Math.sin(refAngle), 0)
904
970
  );
905
971
  const placement = repo.add(
906
- new Axis2Placement3D2("", centerPoint, normalDir, refDir)
972
+ new Axis2Placement3D3("", centerPoint, normalDir, refDir)
907
973
  );
908
974
  const circle = repo.add(new Circle("", placement, radius));
909
975
  return repo.add(new EdgeCurve2("", startVertex, endVertex, circle, false));
@@ -1289,7 +1355,7 @@ function createCylindricalWall(repo, centerX, centerY, radius, startAngle, endAn
1289
1355
  new CartesianPoint3("", centerRotated.x, centerRotated.y, zMin)
1290
1356
  );
1291
1357
  const bottomPlacement = repo.add(
1292
- new Axis2Placement3D2(
1358
+ new Axis2Placement3D3(
1293
1359
  "",
1294
1360
  bottomCenter,
1295
1361
  repo.add(new Direction3("", 0, 0, -1)),
@@ -1303,7 +1369,7 @@ function createCylindricalWall(repo, centerX, centerY, radius, startAngle, endAn
1303
1369
  const topCenter = repo.add(
1304
1370
  new CartesianPoint3("", centerRotated.x, centerRotated.y, zMax)
1305
1371
  );
1306
- const topPlacement = repo.add(new Axis2Placement3D2("", topCenter, zDir, xDir));
1372
+ const topPlacement = repo.add(new Axis2Placement3D3("", topCenter, zDir, xDir));
1307
1373
  const topCircle = repo.add(new Circle("", topPlacement, radius));
1308
1374
  const topArc = repo.add(new EdgeCurve2("", topEnd, topStart, topCircle, false));
1309
1375
  const v1 = repo.add(
@@ -1348,7 +1414,7 @@ function createCylindricalWall(repo, centerX, centerY, radius, startAngle, endAn
1348
1414
  ])
1349
1415
  );
1350
1416
  const cylinderPlacement = repo.add(
1351
- new Axis2Placement3D2("", bottomCenter, zDir, xDir)
1417
+ new Axis2Placement3D3("", bottomCenter, zDir, xDir)
1352
1418
  );
1353
1419
  const cylinderSurface = repo.add(
1354
1420
  new CylindricalSurface("", cylinderPlacement, radius)
@@ -1422,7 +1488,7 @@ function createPlanarWall(repo, startX, startY, endX, endY, rotation, centerX0,
1422
1488
  );
1423
1489
  const planeOrigin = repo.add(new CartesianPoint3("", start.x, start.y, zMin));
1424
1490
  const placement = repo.add(
1425
- new Axis2Placement3D2("", planeOrigin, normalDir, refDir)
1491
+ new Axis2Placement3D3("", planeOrigin, normalDir, refDir)
1426
1492
  );
1427
1493
  const plane = repo.add(new Plane2("", placement));
1428
1494
  return repo.add(
@@ -1631,7 +1697,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1631
1697
  const xDir = repo.add(new Direction4("", 1, 0, 0));
1632
1698
  const zDir = repo.add(new Direction4("", 0, 0, 1));
1633
1699
  const bottomFrame = repo.add(
1634
- new Axis2Placement3D3(
1700
+ new Axis2Placement3D4(
1635
1701
  "",
1636
1702
  origin,
1637
1703
  repo.add(new Direction4("", 0, 0, -1)),
@@ -1664,7 +1730,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1664
1730
  )
1665
1731
  );
1666
1732
  const holePlacement = repo.add(
1667
- new Axis2Placement3D3(
1733
+ new Axis2Placement3D4(
1668
1734
  "",
1669
1735
  holeCenter,
1670
1736
  repo.add(new Direction4("", 0, 0, -1)),
@@ -1693,7 +1759,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1693
1759
  )
1694
1760
  );
1695
1761
  const topOrigin = repo.add(new CartesianPoint4("", 0, 0, halfBoardThickness));
1696
- const topFrame = repo.add(new Axis2Placement3D3("", topOrigin, zDir, xDir));
1762
+ const topFrame = repo.add(new Axis2Placement3D4("", topOrigin, zDir, xDir));
1697
1763
  const topPlane = repo.add(new Plane3("", topFrame));
1698
1764
  const topLoop = repo.add(
1699
1765
  new EdgeLoop3(
@@ -1720,7 +1786,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1720
1786
  )
1721
1787
  );
1722
1788
  const holePlacement = repo.add(
1723
- new Axis2Placement3D3("", holeCenter, zDir, xDir)
1789
+ new Axis2Placement3D4("", holeCenter, zDir, xDir)
1724
1790
  );
1725
1791
  const holeCircle = repo.add(new Circle2("", holePlacement, radius));
1726
1792
  const holeEdge = repo.add(
@@ -1758,7 +1824,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1758
1824
  const normalDir = repo.add(new Direction4("", edgeDir.y, -edgeDir.x, 0));
1759
1825
  const refDir = repo.add(new Direction4("", edgeDir.x, edgeDir.y, 0));
1760
1826
  const sideFrame = repo.add(
1761
- new Axis2Placement3D3("", bottomV1Pnt, normalDir, refDir)
1827
+ new Axis2Placement3D4("", bottomV1Pnt, normalDir, refDir)
1762
1828
  );
1763
1829
  const sidePlane = repo.add(new Plane3("", sideFrame));
1764
1830
  const sideLoop = repo.add(
@@ -1798,7 +1864,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1798
1864
  )
1799
1865
  );
1800
1866
  const bottomHolePlacement = repo.add(
1801
- new Axis2Placement3D3(
1867
+ new Axis2Placement3D4(
1802
1868
  "",
1803
1869
  bottomHoleCenter,
1804
1870
  repo.add(new Direction4("", 0, 0, -1)),
@@ -1829,7 +1895,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1829
1895
  )
1830
1896
  );
1831
1897
  const topHolePlacement = repo.add(
1832
- new Axis2Placement3D3("", topHoleCenter, zDir, xDir)
1898
+ new Axis2Placement3D4("", topHoleCenter, zDir, xDir)
1833
1899
  );
1834
1900
  const topHoleCircle = repo.add(new Circle2("", topHolePlacement, radius));
1835
1901
  const topHoleEdge = repo.add(
@@ -1842,7 +1908,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1842
1908
  ])
1843
1909
  );
1844
1910
  const holeCylinderPlacement = repo.add(
1845
- new Axis2Placement3D3("", bottomHoleCenter, zDir, xDir)
1911
+ new Axis2Placement3D4("", bottomHoleCenter, zDir, xDir)
1846
1912
  );
1847
1913
  const holeCylinderSurface = repo.add(
1848
1914
  new CylindricalSurface2("", holeCylinderPlacement, radius)
@@ -1976,8 +2042,15 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1976
2042
  geomContext
1977
2043
  )
1978
2044
  );
2045
+ const hasMappedItems = allSolids.some(
2046
+ (itemRef) => itemRef.resolve(repo).type === "MAPPED_ITEM"
2047
+ );
1979
2048
  const shapeRep = repo.add(
1980
- new AdvancedBrepShapeRepresentation(productName, allSolids, geomContext)
2049
+ hasMappedItems ? new ShapeRepresentation(productName, allSolids, geomContext) : new AdvancedBrepShapeRepresentation2(
2050
+ productName,
2051
+ allSolids,
2052
+ geomContext
2053
+ )
1981
2054
  );
1982
2055
  repo.add(new ShapeDefinitionRepresentation(productDefShape, shapeRep));
1983
2056
  const stepText = repo.toPartFile({ name: productName });
package/lib/index.ts CHANGED
@@ -37,7 +37,9 @@ import {
37
37
  StyledItem,
38
38
  MechanicalDesignGeometricPresentationRepresentation,
39
39
  AdvancedBrepShapeRepresentation,
40
+ ShapeRepresentation,
40
41
  ShapeDefinitionRepresentation,
42
+ type Entity,
41
43
  type Ref,
42
44
  } from "stepts"
43
45
  import { generateComponentMeshes } from "./mesh-generation"
@@ -614,7 +616,7 @@ export async function circuitJsonToStep(
614
616
  const solid = repo.add(new ManifoldSolidBrep(productName, shell))
615
617
 
616
618
  // Array to hold all solids (board + optional components)
617
- const allSolids: Ref<ManifoldSolidBrep>[] = [solid]
619
+ const allSolids: Ref<Entity>[] = [solid]
618
620
  const componentStyledItems: Ref<StyledItem>[] = []
619
621
  const solidsWithIntrinsicFaceStyles = new Set<string>()
620
622
 
@@ -750,9 +752,17 @@ export async function circuitJsonToStep(
750
752
  ),
751
753
  )
752
754
 
753
- // Shape representation with all solids
755
+ const hasMappedItems = allSolids.some(
756
+ (itemRef) => itemRef.resolve(repo).type === "MAPPED_ITEM",
757
+ )
754
758
  const shapeRep = repo.add(
755
- new AdvancedBrepShapeRepresentation(productName, allSolids, geomContext),
759
+ hasMappedItems
760
+ ? new ShapeRepresentation(productName, allSolids, geomContext)
761
+ : new AdvancedBrepShapeRepresentation(
762
+ productName,
763
+ allSolids,
764
+ geomContext,
765
+ ),
756
766
  )
757
767
  repo.add(new ShapeDefinitionRepresentation(productDefShape, shapeRep))
758
768
 
@@ -1,5 +1,5 @@
1
1
  import type { CircuitJson } from "circuit-json"
2
- import type { ManifoldSolidBrep, Ref, Repository } from "stepts"
2
+ import type { Entity, Ref, Repository } from "stepts"
3
3
 
4
4
  export type CadComponent = {
5
5
  type: "cad_component"
@@ -28,7 +28,7 @@ export type MergeTransform = {
28
28
  }
29
29
 
30
30
  export type MergeStepModelResult = {
31
- solids: Ref<ManifoldSolidBrep>[]
31
+ solids: Ref<Entity>[]
32
32
  handledComponentIds: Set<string>
33
33
  handledPcbComponentIds: Set<string>
34
34
  }
@@ -1,12 +1,15 @@
1
1
  import {
2
+ AdvancedBrepShapeRepresentation,
3
+ Axis2Placement3D,
2
4
  CartesianPoint,
3
5
  Direction,
4
- type Entity,
6
+ Entity,
5
7
  ManifoldSolidBrep,
6
8
  Ref,
7
9
  Repository,
8
10
  Unknown,
9
11
  parseRepository,
12
+ stepStr,
10
13
  } from "stepts"
11
14
  import { eid } from "stepts"
12
15
  import { EXCLUDED_ENTITY_TYPES } from "./step-model-merger/excluded-entity-types"
@@ -47,18 +50,24 @@ export async function mergeExternalStepModels(
47
50
  }
48
51
  }
49
52
 
50
- const solids: Ref<ManifoldSolidBrep>[] = []
53
+ const solids: Ref<Entity>[] = []
51
54
  const handledComponentIds = new Set<string>()
52
55
  const handledPcbComponentIds = new Set<string>()
56
+ const importedModels = new Map<string, ImportedStepModel>()
53
57
 
54
58
  for (const component of cadComponents) {
55
59
  const componentId = component.cad_component_id ?? ""
56
60
  const stepUrl = component.model_step_url!
57
61
 
58
62
  try {
59
- const stepText = fsMap?.[stepUrl] ?? (await readStepFile(stepUrl))
60
- if (!stepText.trim()) {
61
- throw new Error("STEP file is empty")
63
+ let importedModel = importedModels.get(stepUrl)
64
+ if (!importedModel) {
65
+ const stepText = fsMap?.[stepUrl] ?? (await readStepFile(stepUrl))
66
+ if (!stepText.trim()) {
67
+ throw new Error("STEP file is empty")
68
+ }
69
+ importedModel = importStepModelOnce(repo, stepText, stepUrl)
70
+ importedModels.set(stepUrl, importedModel)
62
71
  }
63
72
 
64
73
  const pcbComponent = component.pcb_component_id
@@ -71,9 +80,14 @@ export async function mergeExternalStepModels(
71
80
  rotation: asVector3(component.rotation),
72
81
  }
73
82
 
74
- const componentSolids = mergeSingleStepModel(repo, stepText, transform, {
75
- layer,
76
- boardThickness,
83
+ const componentSolids = createMappedStepModelInstance({
84
+ repo,
85
+ importedModel,
86
+ transform,
87
+ placement: {
88
+ layer,
89
+ boardThickness,
90
+ },
77
91
  })
78
92
  if (componentSolids.length > 0) {
79
93
  if (componentId) {
@@ -98,12 +112,47 @@ type PlacementOptions = {
98
112
  boardThickness?: number
99
113
  }
100
114
 
101
- function mergeSingleStepModel(
115
+ type ImportedStepModel = {
116
+ entries: RepositoryEntry[]
117
+ representationMap: Ref<RepresentationMap>
118
+ }
119
+
120
+ class RepresentationMap extends Entity {
121
+ readonly type = "REPRESENTATION_MAP"
122
+
123
+ constructor(
124
+ public mappingOrigin: Ref<Entity>,
125
+ public mappedRepresentation: Ref<Entity>,
126
+ ) {
127
+ super()
128
+ }
129
+
130
+ toStep(): string {
131
+ return `REPRESENTATION_MAP(${this.mappingOrigin},${this.mappedRepresentation})`
132
+ }
133
+ }
134
+
135
+ class MappedItem extends Entity {
136
+ readonly type = "MAPPED_ITEM"
137
+
138
+ constructor(
139
+ public name: string,
140
+ public mappingSource: Ref<RepresentationMap>,
141
+ public mappingTarget: Ref<Entity>,
142
+ ) {
143
+ super()
144
+ }
145
+
146
+ toStep(): string {
147
+ return `MAPPED_ITEM(${stepStr(this.name)},${this.mappingSource},${this.mappingTarget})`
148
+ }
149
+ }
150
+
151
+ function importStepModelOnce(
102
152
  targetRepo: Repository,
103
153
  stepText: string,
104
- transform: MergeTransform,
105
- placement?: PlacementOptions,
106
- ): Ref<ManifoldSolidBrep>[] {
154
+ modelName: string,
155
+ ): ImportedStepModel {
107
156
  const sourceRepo = parseRepository(stepText)
108
157
  let entries: RepositoryEntry[] = sourceRepo
109
158
  .entries()
@@ -112,9 +161,6 @@ function mergeSingleStepModel(
112
161
 
113
162
  entries = pruneInvalidEntries(entries)
114
163
 
115
- adjustTransformForPlacement(entries, transform, placement)
116
- applyTransform(entries, transform)
117
-
118
164
  const idMapping = allocateIds(targetRepo, entries)
119
165
  remapReferences(entries, idMapping)
120
166
 
@@ -134,7 +180,84 @@ function mergeSingleStepModel(
134
180
  }
135
181
  }
136
182
 
137
- return solids
183
+ const originPoint = targetRepo.add(new CartesianPoint("", 0, 0, 0))
184
+ const zDirection = targetRepo.add(new Direction("", 0, 0, 1))
185
+ const xDirection = targetRepo.add(new Direction("", 1, 0, 0))
186
+ const mappingOrigin = targetRepo.add(
187
+ new Axis2Placement3D("", originPoint, zDirection, xDirection),
188
+ )
189
+ const representation = targetRepo.add(
190
+ new AdvancedBrepShapeRepresentation(
191
+ modelName,
192
+ solids,
193
+ getGeomContext(targetRepo),
194
+ ),
195
+ )
196
+ const representationMap = targetRepo.add(
197
+ new RepresentationMap(mappingOrigin, representation),
198
+ )
199
+
200
+ return { entries, representationMap }
201
+ }
202
+
203
+ function createMappedStepModelInstance({
204
+ repo,
205
+ importedModel,
206
+ transform,
207
+ placement,
208
+ }: {
209
+ repo: Repository
210
+ importedModel: ImportedStepModel
211
+ transform: MergeTransform
212
+ placement?: PlacementOptions
213
+ }): Ref<Entity>[] {
214
+ adjustTransformForPlacement(importedModel.entries, transform, placement)
215
+
216
+ const placementTarget = createPlacementTarget(repo, transform)
217
+ const mappedItem = repo.add(
218
+ new MappedItem("", importedModel.representationMap, placementTarget),
219
+ )
220
+
221
+ return [mappedItem]
222
+ }
223
+
224
+ function createPlacementTarget(
225
+ repo: Repository,
226
+ transform: MergeTransform,
227
+ ): Ref<Axis2Placement3D> {
228
+ const rotation = toRadians(transform.rotation)
229
+ const [refX, refY, refZ] = transformDirection([1, 0, 0], rotation)
230
+ const [axisX, axisY, axisZ] = transformDirection([0, 0, 1], rotation)
231
+
232
+ const axis = repo.add(new Direction("", axisX, axisY, axisZ))
233
+ const refDirection = repo.add(new Direction("", refX, refY, refZ))
234
+ const origin = repo.add(
235
+ new CartesianPoint(
236
+ "",
237
+ transform.translation.x,
238
+ transform.translation.y,
239
+ transform.translation.z,
240
+ ),
241
+ )
242
+
243
+ return repo.add(new Axis2Placement3D("", origin, axis, refDirection))
244
+ }
245
+
246
+ function getGeomContext(repo: Repository): Ref<Entity> {
247
+ for (const [id, entity] of repo.entries()) {
248
+ if (entity.type === "GEOMETRIC_REPRESENTATION_CONTEXT") {
249
+ return new Ref<Entity>(id)
250
+ }
251
+ if (
252
+ entity instanceof Unknown &&
253
+ entity.args.some((arg) =>
254
+ arg.includes("GEOMETRIC_REPRESENTATION_CONTEXT"),
255
+ )
256
+ ) {
257
+ return new Ref<Entity>(id)
258
+ }
259
+ }
260
+ throw new Error("GEOMETRIC_REPRESENTATION_CONTEXT is missing")
138
261
  }
139
262
 
140
263
  type RepositoryEntry = readonly [number, Entity]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-json-to-step",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.27",
4
+ "version": "0.0.29",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "pull-reference": "git clone https://github.com/tscircuit/circuit-json.git && find circuit-json/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\; && git clone https://github.com/tscircuit/stepts.git && find stepts/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\;",
@@ -19,7 +19,7 @@
19
19
  "@resvg/resvg-wasm": "^2.6.2",
20
20
  "@tscircuit/circuit-json-util": "^0.0.75",
21
21
  "@types/bun": "latest",
22
- "circuit-json": "^0.0.406",
22
+ "circuit-json": "^0.0.425",
23
23
  "looks-same": "^10.0.1",
24
24
  "occt-import-js": "^0.0.23",
25
25
  "poppygl": "^0.0.17",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "circuit-json-to-connectivity-map": "^0.0.22",
34
- "circuit-json-to-gltf": "^0.0.93",
35
- "circuit-to-svg": "^0.0.327",
34
+ "circuit-json-to-gltf": "^0.0.100",
35
+ "circuit-to-svg": "^0.0.345",
36
36
  "schematic-symbols": "^0.0.202",
37
37
  "stepts": "^0.0.4"
38
38
  }
@@ -32,6 +32,17 @@
32
32
  "layer": "top",
33
33
  "rotation": 0
34
34
  },
35
+ {
36
+ "type": "cad_component",
37
+ "cad_component_id": "cad_component_1",
38
+ "pcb_component_id": "pcb_component_1",
39
+ "source_component_id": "source_component_1",
40
+ "position": { "x": 15, "y": 10, "z": 0.8 },
41
+ "rotation": { "x": 0, "y": 0, "z": 0 },
42
+ "model_origin_alignment": "center_of_component_on_board_surface",
43
+ "anchor_alignment": "center_of_component_on_board_surface",
44
+ "show_as_bounding_box": true
45
+ },
35
46
  {
36
47
  "type": "source_component",
37
48
  "source_component_id": "source_component_2",
@@ -48,5 +59,16 @@
48
59
  "height": 1.25,
49
60
  "layer": "top",
50
61
  "rotation": 90
62
+ },
63
+ {
64
+ "type": "cad_component",
65
+ "cad_component_id": "cad_component_2",
66
+ "pcb_component_id": "pcb_component_2",
67
+ "source_component_id": "source_component_2",
68
+ "position": { "x": 20, "y": 10, "z": 0.8 },
69
+ "rotation": { "x": 0, "y": 0, "z": 90 },
70
+ "model_origin_alignment": "center_of_component_on_board_surface",
71
+ "anchor_alignment": "center_of_component_on_board_surface",
72
+ "show_as_bounding_box": true
51
73
  }
52
74
  ]
@@ -20,6 +20,11 @@ const EXPECTED_COMPONENT_CENTERS = (circuitJson as CadComponentJson[])
20
20
  x: item.position?.x ?? 0,
21
21
  y: item.position?.y ?? 0,
22
22
  }))
23
+ const EXPECTED_UNIQUE_STEP_MODEL_COUNT = new Set(
24
+ (circuitJson as CadComponentJson[])
25
+ .filter((item) => item.type === "cad_component" && item.model_step_url)
26
+ .map((item) => item.model_step_url),
27
+ ).size
23
28
 
24
29
  const fixturesDir = fileURLToPath(
25
30
  new URL("../../fixtures/kicad-models/", import.meta.url),
@@ -99,6 +104,9 @@ test("kicad-step: merges KiCad STEP models referenced via model_step_url", async
99
104
  fsMap,
100
105
  })
101
106
 
107
+ const outputPath = "debug-output/kicad-step.step"
108
+ await Bun.write(outputPath, stepText)
109
+
102
110
  expect(stepText).toContain("KiCadStepMerge")
103
111
  const solidCount = (stepText.match(/MANIFOLD_SOLID_BREP/g) || []).length
104
112
  expect(solidCount).toBeGreaterThanOrEqual(3)
@@ -114,10 +122,16 @@ test("kicad-step: merges KiCad STEP models referenced via model_step_url", async
114
122
  (entity) => entity.name === "KiCadStepMerge",
115
123
  )
116
124
  expect(boardSolids.length).toBe(1)
117
- const componentSolids = solids.length - boardSolids.length
118
- expect(componentSolids).toBeGreaterThanOrEqual(
125
+ const uniqueComponentSolids = solids.length - boardSolids.length
126
+ expect(uniqueComponentSolids).toBeGreaterThanOrEqual(
127
+ EXPECTED_UNIQUE_STEP_MODEL_COUNT,
128
+ )
129
+ expect(stepText.match(/MAPPED_ITEM/g) ?? []).toHaveLength(
119
130
  EXPECTED_COMPONENT_CENTERS.length,
120
131
  )
132
+ expect(stepText.match(/REPRESENTATION_MAP/g) ?? []).toHaveLength(
133
+ EXPECTED_UNIQUE_STEP_MODEL_COUNT,
134
+ )
121
135
 
122
136
  try {
123
137
  const occtBoard = await importStepWithOcct(stepText)
@@ -2479,6 +2479,7 @@
2479
2479
  },
2480
2480
  "pcb_component_id": "pcb_component_0",
2481
2481
  "source_component_id": "source_component_0",
2482
+ "show_as_bounding_box": true,
2482
2483
  "model_obj_url": "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=2a4bc2358b36497d9ab2a66ab6419ba3&pn=C165948&cachebust_origin="
2483
2484
  },
2484
2485
  {
@@ -4,6 +4,9 @@ import { importStepWithOcct } from "../../utils/occt/importer"
4
4
  import circuitJson from "./repro01.json"
5
5
 
6
6
  test("basics04: convert circuit json with components to STEP", async () => {
7
+ // This fixture intentionally marks the OBJ cad_component as a fallback box in its circuit-json so
8
+ // the test stays local and deterministic. Core should not emit
9
+ // show_as_bounding_box together with a real model URL.
7
10
  const stepText = await circuitJsonToStep(circuitJson as any, {
8
11
  includeComponents: true,
9
12
  productName: "TestPCB_with_components",
@@ -16,8 +16,8 @@ test("repro03: reproduces fallback boxes for hole wrapper components", async ()
16
16
 
17
17
  // Current repro behavior: these are fallback boxes for wrapper components,
18
18
  // not intended physical component models.
19
- expect(stepText).toContain("Xpattern1")
20
- expect(stepText).toContain("Xpattern4")
19
+ expect(stepText).not.toContain("Xpattern1")
20
+ expect(stepText).not.toContain("Xpattern4")
21
21
 
22
22
  const outputPath = "debug-output/repro03.step"
23
23
  await Bun.write(outputPath, stepText)