circuit-json-to-step 0.0.23 → 0.0.24

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
@@ -27,14 +27,6 @@ import {
27
27
  Circle as Circle2,
28
28
  ClosedShell as ClosedShell2,
29
29
  ManifoldSolidBrep as ManifoldSolidBrep4,
30
- ColourRgb,
31
- FillAreaStyleColour,
32
- FillAreaStyle,
33
- SurfaceStyleFillArea,
34
- SurfaceSideStyle,
35
- SurfaceStyleUsage,
36
- PresentationStyleAssignment,
37
- StyledItem,
38
30
  MechanicalDesignGeometricPresentationRepresentation,
39
31
  AdvancedBrepShapeRepresentation,
40
32
  ShapeDefinitionRepresentation
@@ -46,6 +38,43 @@ import "stepts";
46
38
  // lib/scene-box-to-step.ts
47
39
  import { ClosedShell, ManifoldSolidBrep } from "stepts";
48
40
 
41
+ // lib/step-style-utils.ts
42
+ import {
43
+ ColourRgb,
44
+ FillAreaStyle,
45
+ FillAreaStyleColour,
46
+ PresentationStyleAssignment,
47
+ StyledItem,
48
+ SurfaceSideStyle,
49
+ SurfaceStyleFillArea,
50
+ SurfaceStyleUsage
51
+ } from "stepts";
52
+ function createStyleCache() {
53
+ return /* @__PURE__ */ new Map();
54
+ }
55
+ function createStyledItem(repo, options) {
56
+ const { itemRef, rgb, styleCache, name = "color" } = options;
57
+ const key = rgb.map((value) => value.toFixed(6)).join(",");
58
+ let presStyle = styleCache.get(key);
59
+ if (!presStyle) {
60
+ const color = repo.add(new ColourRgb("", rgb[0], rgb[1], rgb[2]));
61
+ const fillColor = repo.add(new FillAreaStyleColour("", color));
62
+ const fillStyle = repo.add(new FillAreaStyle("", [fillColor]));
63
+ const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle));
64
+ const surfaceSide = repo.add(new SurfaceSideStyle("", [surfaceFill]));
65
+ const surfaceUsage = repo.add(new SurfaceStyleUsage(".BOTH.", surfaceSide));
66
+ presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]));
67
+ styleCache.set(key, presStyle);
68
+ }
69
+ return repo.add(new StyledItem(name, [presStyle], itemRef));
70
+ }
71
+ function createStyledItems(repo, options) {
72
+ const { itemRefs, rgb, styleCache, name } = options;
73
+ return itemRefs.map(
74
+ (itemRef) => createStyledItem(repo, { itemRef, rgb, styleCache, name })
75
+ );
76
+ }
77
+
49
78
  // lib/scene-geometry.ts
50
79
  function rotatePoint3(point, rotation) {
51
80
  if (!rotation) return point;
@@ -219,9 +248,16 @@ function createSceneBoxSolid(repo, box) {
219
248
  [vertices[3], vertices[7], vertices[4], vertices[0]]
220
249
  ].map((faceVertices) => createFaceFromVertices(repo, faceVertices));
221
250
  const shell = repo.add(new ClosedShell("", faces));
222
- return repo.add(new ManifoldSolidBrep("Component", shell));
251
+ return {
252
+ solid: repo.add(new ManifoldSolidBrep(box.label ?? "Component", shell)),
253
+ styledItems: [],
254
+ usesIntrinsicFaceStyles: false,
255
+ styleTargets: faces
256
+ };
223
257
  }
224
258
  function createSceneMeshSolid(repo, box) {
259
+ const styleCache = createStyleCache();
260
+ const styledItems = [];
225
261
  const faces = box.mesh.triangles.map((triangle) => {
226
262
  const vertices = triangle.vertices.map((vertex) => {
227
263
  const rotated = rotatePoint3(vertex, box.rotation);
@@ -232,10 +268,38 @@ function createSceneMeshSolid(repo, box) {
232
268
  };
233
269
  return createVertex(repo, translated);
234
270
  });
235
- return createFaceFromVertices(repo, vertices);
271
+ const face = createFaceFromVertices(repo, vertices);
272
+ const faceColor = normalizeTriangleColor(triangle.color);
273
+ if (faceColor) {
274
+ styledItems.push(
275
+ createStyledItem(repo, {
276
+ itemRef: face,
277
+ rgb: faceColor,
278
+ styleCache
279
+ })
280
+ );
281
+ }
282
+ return face;
236
283
  });
237
284
  const shell = repo.add(new ClosedShell("", faces));
238
- return repo.add(new ManifoldSolidBrep("Component", shell));
285
+ return {
286
+ solid: repo.add(new ManifoldSolidBrep(box.label ?? "Component", shell)),
287
+ styledItems,
288
+ usesIntrinsicFaceStyles: faces.length > 0 && styledItems.length === faces.length,
289
+ styleTargets: []
290
+ };
291
+ }
292
+ function normalizeTriangleColor(color) {
293
+ if (!Array.isArray(color) || color.length < 3) return null;
294
+ const scale = color.some((value) => value > 1) ? 255 : 1;
295
+ return [
296
+ clampColorChannel(color[0] / scale),
297
+ clampColorChannel(color[1] / scale),
298
+ clampColorChannel(color[2] / scale)
299
+ ];
300
+ }
301
+ function clampColorChannel(value) {
302
+ return Math.max(0, Math.min(1, value));
239
303
  }
240
304
 
241
305
  // lib/mesh-generation.ts
@@ -320,17 +384,6 @@ var EXCLUDED_ENTITY_TYPES = /* @__PURE__ */ new Set([
320
384
  "PRODUCT_DEFINITION_SHAPE",
321
385
  "SHAPE_DEFINITION_REPRESENTATION",
322
386
  "ADVANCED_BREP_SHAPE_REPRESENTATION",
323
- "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
324
- "PRESENTATION_STYLE_ASSIGNMENT",
325
- "SURFACE_STYLE_USAGE",
326
- "SURFACE_SIDE_STYLE",
327
- "SURFACE_STYLE_FILL_AREA",
328
- "FILL_AREA_STYLE",
329
- "FILL_AREA_STYLE_COLOUR",
330
- "COLOUR_RGB",
331
- "STYLED_ITEM",
332
- "CURVE_STYLE",
333
- "DRAUGHTING_PRE_DEFINED_CURVE_FONT",
334
387
  "PRODUCT_RELATED_PRODUCT_CATEGORY",
335
388
  "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
336
389
  "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
@@ -699,7 +752,7 @@ function normalizeStepNumericExponents(stepText) {
699
752
  var package_default = {
700
753
  name: "circuit-json-to-step",
701
754
  main: "dist/index.js",
702
- version: "0.0.22",
755
+ version: "0.0.23",
703
756
  type: "module",
704
757
  scripts: {
705
758
  "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"' {} \\;`,
@@ -717,7 +770,7 @@ var package_default = {
717
770
  "@resvg/resvg-wasm": "^2.6.2",
718
771
  "@tscircuit/circuit-json-util": "^0.0.75",
719
772
  "@types/bun": "latest",
720
- "circuit-json": "^0.0.286",
773
+ "circuit-json": "^0.0.406",
721
774
  "looks-same": "^10.0.1",
722
775
  "occt-import-js": "^0.0.23",
723
776
  poppygl: "^0.0.17",
@@ -1584,11 +1637,11 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1584
1637
  );
1585
1638
  const bottomHoleLoops = [];
1586
1639
  for (const hole of holes) {
1587
- const holeShape = hole.hole_shape || hole.shape;
1640
+ const holeShape = hole.hole_shape ?? hole.shape;
1588
1641
  if (holeShape === "circle") {
1589
- const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
1590
- const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
1591
- const radius = hole.hole_diameter / 2;
1642
+ const holeX = typeof hole.x === "number" ? hole.x : hole.x ?? 0;
1643
+ const holeY = typeof hole.y === "number" ? hole.y : hole.y ?? 0;
1644
+ const radius = (hole.hole_diameter ?? 0) / 2;
1592
1645
  const holeCenter = repo.add(
1593
1646
  new CartesianPoint4("", holeX, holeY, -halfBoardThickness)
1594
1647
  );
@@ -1624,10 +1677,7 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1624
1677
  const bottomFace = repo.add(
1625
1678
  new AdvancedFace3(
1626
1679
  "",
1627
- [
1628
- repo.add(new FaceOuterBound3("", bottomLoop, true)),
1629
- ...bottomHoleLoops
1630
- ],
1680
+ [repo.add(new FaceOuterBound3("", bottomLoop, true)), ...bottomHoleLoops],
1631
1681
  bottomPlane,
1632
1682
  true
1633
1683
  )
@@ -1643,11 +1693,11 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1643
1693
  );
1644
1694
  const topHoleLoops = [];
1645
1695
  for (const hole of holes) {
1646
- const holeShape = hole.hole_shape || hole.shape;
1696
+ const holeShape = hole.hole_shape ?? hole.shape;
1647
1697
  if (holeShape === "circle") {
1648
- const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
1649
- const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
1650
- const radius = hole.hole_diameter / 2;
1698
+ const holeX = typeof hole.x === "number" ? hole.x : hole.x ?? 0;
1699
+ const holeY = typeof hole.y === "number" ? hole.y : hole.y ?? 0;
1700
+ const radius = (hole.hole_diameter ?? 0) / 2;
1651
1701
  const holeCenter = repo.add(
1652
1702
  new CartesianPoint4("", holeX, holeY, halfBoardThickness)
1653
1703
  );
@@ -1721,11 +1771,11 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1721
1771
  }
1722
1772
  const holeCylindricalFaces = [];
1723
1773
  for (const hole of holes) {
1724
- const holeShape = hole.hole_shape || hole.shape;
1774
+ const holeShape = hole.hole_shape ?? hole.shape;
1725
1775
  if (holeShape === "circle") {
1726
- const holeX = typeof hole.x === "number" ? hole.x : hole.x.value;
1727
- const holeY = typeof hole.y === "number" ? hole.y : hole.y.value;
1728
- const radius = hole.hole_diameter / 2;
1776
+ const holeX = typeof hole.x === "number" ? hole.x : hole.x ?? 0;
1777
+ const holeY = typeof hole.y === "number" ? hole.y : hole.y ?? 0;
1778
+ const radius = (hole.hole_diameter ?? 0) / 2;
1729
1779
  const bottomHoleCenter = repo.add(
1730
1780
  new CartesianPoint4("", holeX, holeY, -halfBoardThickness)
1731
1781
  );
@@ -1809,9 +1859,17 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1809
1859
  }
1810
1860
  }
1811
1861
  const allFaces = [bottomFace, topFace, ...sideFaces, ...holeCylindricalFaces];
1862
+ const styleCache = createStyleCache();
1863
+ const boardStyledItems = createStyledItems(repo, {
1864
+ itemRefs: allFaces,
1865
+ rgb: [0.2, 0.6, 0.2],
1866
+ styleCache
1867
+ });
1812
1868
  const shell = repo.add(new ClosedShell2("", allFaces));
1813
1869
  const solid = repo.add(new ManifoldSolidBrep4(productName, shell));
1814
1870
  const allSolids = [solid];
1871
+ const componentStyledItems = [];
1872
+ const solidsWithIntrinsicFaceStyles = /* @__PURE__ */ new Set();
1815
1873
  let handledComponentIds = /* @__PURE__ */ new Set();
1816
1874
  let handledPcbComponentIds = /* @__PURE__ */ new Set();
1817
1875
  if (options.includeComponents && options.includeExternalMeshes) {
@@ -1824,6 +1882,9 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1824
1882
  handledComponentIds = mergeResult.handledComponentIds;
1825
1883
  handledPcbComponentIds = mergeResult.handledPcbComponentIds;
1826
1884
  allSolids.push(...mergeResult.solids);
1885
+ mergeResult.solids.forEach((solidRef) => {
1886
+ solidsWithIntrinsicFaceStyles.add(String(solidRef.id));
1887
+ });
1827
1888
  }
1828
1889
  if (options.includeComponents) {
1829
1890
  const pcbComponentIdsWithStepUrl = /* @__PURE__ */ new Set();
@@ -1863,21 +1924,39 @@ async function circuitJsonToStep(circuitJson, options = {}) {
1863
1924
  excludePcbComponentIds: handledPcbComponentIds,
1864
1925
  pcbComponentIdsWithStepUrl
1865
1926
  });
1866
- allSolids.push(...componentSolids);
1927
+ for (const componentSolid of componentSolids) {
1928
+ allSolids.push(componentSolid.solid);
1929
+ componentStyledItems.push(...componentSolid.styledItems);
1930
+ if (componentSolid.usesIntrinsicFaceStyles) {
1931
+ solidsWithIntrinsicFaceStyles.add(String(componentSolid.solid.id));
1932
+ } else if (componentSolid.styleTargets.length > 0) {
1933
+ componentStyledItems.push(
1934
+ ...createStyledItems(repo, {
1935
+ itemRefs: componentSolid.styleTargets,
1936
+ rgb: [0.75, 0.75, 0.75],
1937
+ styleCache
1938
+ })
1939
+ );
1940
+ solidsWithIntrinsicFaceStyles.add(String(componentSolid.solid.id));
1941
+ }
1942
+ }
1867
1943
  }
1868
1944
  }
1869
- const styledItems = [];
1870
- allSolids.forEach((solidRef, index) => {
1945
+ const styledItems = [
1946
+ ...boardStyledItems,
1947
+ ...componentStyledItems
1948
+ ];
1949
+ allSolids.forEach((itemRef, index) => {
1871
1950
  const isBoard = index === 0;
1872
- const [r, g, b] = isBoard ? [0.2, 0.6, 0.2] : [0.75, 0.75, 0.75];
1873
- const color = repo.add(new ColourRgb("", r, g, b));
1874
- const fillColor = repo.add(new FillAreaStyleColour("", color));
1875
- const fillStyle = repo.add(new FillAreaStyle("", [fillColor]));
1876
- const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle));
1877
- const surfaceSide = repo.add(new SurfaceSideStyle("", [surfaceFill]));
1878
- const surfaceUsage = repo.add(new SurfaceStyleUsage(".BOTH.", surfaceSide));
1879
- const presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]));
1880
- const styledItem = repo.add(new StyledItem("", [presStyle], solidRef));
1951
+ if (isBoard || solidsWithIntrinsicFaceStyles.has(String(itemRef.id))) {
1952
+ return;
1953
+ }
1954
+ const styledItem = createStyledItem(repo, {
1955
+ itemRef,
1956
+ rgb: [0.75, 0.75, 0.75],
1957
+ styleCache,
1958
+ name: ""
1959
+ });
1881
1960
  styledItems.push(styledItem);
1882
1961
  });
1883
1962
  repo.add(
package/lib/index.ts CHANGED
@@ -42,10 +42,27 @@ import {
42
42
  } from "stepts"
43
43
  import { generateComponentMeshes } from "./mesh-generation"
44
44
  import { mergeExternalStepModels } from "./step-model-merger"
45
+ import {
46
+ createStyleCache,
47
+ createStyledItem,
48
+ createStyledItems,
49
+ } from "./step-style-utils"
45
50
  import { normalizeStepNumericExponents } from "./step-text-utils"
46
51
  import { VERSION } from "./version"
47
52
  import { createPillCylindricalFaces, createPillHoleLoop } from "./pill-geometry"
48
53
 
54
+ type Hole = Extract<
55
+ CircuitJson[number],
56
+ { type: "pcb_hole" | "pcb_plated_hole" }
57
+ >
58
+ type RuntimeHole = Hole & {
59
+ x?: number | { value: number }
60
+ y?: number | { value: number }
61
+ hole_shape?: string
62
+ shape?: string
63
+ hole_diameter?: number
64
+ }
65
+
49
66
  export interface CircuitJsonToStepOptions {
50
67
  /** Board width in mm (optional if pcb_board is present) */
51
68
  boardWidth?: number
@@ -78,8 +95,9 @@ export async function circuitJsonToStep(
78
95
 
79
96
  // Extract pcb_board and holes from circuit JSON
80
97
  const pcbBoard = circuitJson.find((item) => item.type === "pcb_board")
81
- const holes: any[] = circuitJson.filter(
82
- (item) => item.type === "pcb_hole" || item.type === "pcb_plated_hole",
98
+ const holes = circuitJson.filter(
99
+ (item): item is RuntimeHole =>
100
+ item.type === "pcb_hole" || item.type === "pcb_plated_hole",
83
101
  )
84
102
 
85
103
  // Get dimensions from pcb_board or options
@@ -329,11 +347,11 @@ export async function circuitJsonToStep(
329
347
  const bottomHoleLoops: Ref<FaceBound>[] = []
330
348
  for (const hole of holes) {
331
349
  // Check shape (pcb_hole uses hole_shape, pcb_plated_hole uses shape)
332
- const holeShape = hole.hole_shape || hole.shape
350
+ const holeShape = hole.hole_shape ?? hole.shape
333
351
  if (holeShape === "circle") {
334
- const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
335
- const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
336
- const radius = hole.hole_diameter / 2
352
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x ?? 0)
353
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y ?? 0)
354
+ const radius = (hole.hole_diameter ?? 0) / 2
337
355
 
338
356
  const holeCenter = repo.add(
339
357
  new CartesianPoint("", holeX, holeY, -halfBoardThickness),
@@ -372,10 +390,7 @@ export async function circuitJsonToStep(
372
390
  const bottomFace = repo.add(
373
391
  new AdvancedFace(
374
392
  "",
375
- [
376
- repo.add(new FaceOuterBound("", bottomLoop, true)),
377
- ...bottomHoleLoops,
378
- ] as any,
393
+ [repo.add(new FaceOuterBound("", bottomLoop, true)), ...bottomHoleLoops],
379
394
  bottomPlane,
380
395
  true,
381
396
  ),
@@ -396,11 +411,11 @@ export async function circuitJsonToStep(
396
411
  const topHoleLoops: Ref<FaceBound>[] = []
397
412
  for (const hole of holes) {
398
413
  // Check shape (pcb_hole uses hole_shape, pcb_plated_hole uses shape)
399
- const holeShape = hole.hole_shape || hole.shape
414
+ const holeShape = hole.hole_shape ?? hole.shape
400
415
  if (holeShape === "circle") {
401
- const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
402
- const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
403
- const radius = hole.hole_diameter / 2
416
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x ?? 0)
417
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y ?? 0)
418
+ const radius = (hole.hole_diameter ?? 0) / 2
404
419
 
405
420
  const holeCenter = repo.add(
406
421
  new CartesianPoint("", holeX, holeY, halfBoardThickness),
@@ -434,7 +449,7 @@ export async function circuitJsonToStep(
434
449
  const topFace = repo.add(
435
450
  new AdvancedFace(
436
451
  "",
437
- [repo.add(new FaceOuterBound("", topLoop, true)), ...topHoleLoops] as any,
452
+ [repo.add(new FaceOuterBound("", topLoop, true)), ...topHoleLoops],
438
453
  topPlane,
439
454
  true,
440
455
  ),
@@ -489,11 +504,11 @@ export async function circuitJsonToStep(
489
504
  // Create cylindrical faces for holes
490
505
  const holeCylindricalFaces: Ref<AdvancedFace>[] = []
491
506
  for (const hole of holes) {
492
- const holeShape = hole.hole_shape || hole.shape
507
+ const holeShape = hole.hole_shape ?? hole.shape
493
508
  if (holeShape === "circle") {
494
- const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
495
- const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
496
- const radius = hole.hole_diameter / 2
509
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x ?? 0)
510
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y ?? 0)
511
+ const radius = (hole.hole_diameter ?? 0) / 2
497
512
 
498
513
  // Create circular edges at bottom and top
499
514
  const bottomHoleCenter = repo.add(
@@ -587,6 +602,12 @@ export async function circuitJsonToStep(
587
602
 
588
603
  // Collect all faces
589
604
  const allFaces = [bottomFace, topFace, ...sideFaces, ...holeCylindricalFaces]
605
+ const styleCache = createStyleCache()
606
+ const boardStyledItems = createStyledItems(repo, {
607
+ itemRefs: allFaces,
608
+ rgb: [0.2, 0.6, 0.2],
609
+ styleCache,
610
+ })
590
611
 
591
612
  // Create closed shell and solid
592
613
  const shell = repo.add(new ClosedShell("", allFaces))
@@ -594,6 +615,8 @@ export async function circuitJsonToStep(
594
615
 
595
616
  // Array to hold all solids (board + optional components)
596
617
  const allSolids: Ref<ManifoldSolidBrep>[] = [solid]
618
+ const componentStyledItems: Ref<StyledItem>[] = []
619
+ const solidsWithIntrinsicFaceStyles = new Set<string>()
597
620
 
598
621
  let handledComponentIds = new Set<string>()
599
622
  let handledPcbComponentIds = new Set<string>()
@@ -608,6 +631,9 @@ export async function circuitJsonToStep(
608
631
  handledComponentIds = mergeResult.handledComponentIds
609
632
  handledPcbComponentIds = mergeResult.handledPcbComponentIds
610
633
  allSolids.push(...mergeResult.solids)
634
+ mergeResult.solids.forEach((solidRef) => {
635
+ solidsWithIntrinsicFaceStyles.add(String(solidRef.id))
636
+ })
611
637
  }
612
638
 
613
639
  // Generate component mesh fallback if requested
@@ -677,24 +703,42 @@ export async function circuitJsonToStep(
677
703
  excludePcbComponentIds: handledPcbComponentIds,
678
704
  pcbComponentIdsWithStepUrl,
679
705
  })
680
- allSolids.push(...componentSolids)
706
+ for (const componentSolid of componentSolids) {
707
+ allSolids.push(componentSolid.solid)
708
+ componentStyledItems.push(...componentSolid.styledItems)
709
+ if (componentSolid.usesIntrinsicFaceStyles) {
710
+ solidsWithIntrinsicFaceStyles.add(String(componentSolid.solid.id))
711
+ } else if (componentSolid.styleTargets.length > 0) {
712
+ componentStyledItems.push(
713
+ ...createStyledItems(repo, {
714
+ itemRefs: componentSolid.styleTargets,
715
+ rgb: [0.75, 0.75, 0.75],
716
+ styleCache,
717
+ }),
718
+ )
719
+ solidsWithIntrinsicFaceStyles.add(String(componentSolid.solid.id))
720
+ }
721
+ }
681
722
  }
682
723
  }
683
724
 
684
725
  // Add presentation/styling for all solids
685
- const styledItems: Ref<StyledItem>[] = []
726
+ const styledItems: Ref<StyledItem>[] = [
727
+ ...boardStyledItems,
728
+ ...componentStyledItems,
729
+ ]
686
730
 
687
- allSolids.forEach((solidRef, index) => {
731
+ allSolids.forEach((itemRef, index) => {
688
732
  const isBoard = index === 0
689
- const [r, g, b] = isBoard ? [0.2, 0.6, 0.2] : [0.75, 0.75, 0.75]
690
- const color = repo.add(new ColourRgb("", r, g, b))
691
- const fillColor = repo.add(new FillAreaStyleColour("", color))
692
- const fillStyle = repo.add(new FillAreaStyle("", [fillColor]))
693
- const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle))
694
- const surfaceSide = repo.add(new SurfaceSideStyle("", [surfaceFill]))
695
- const surfaceUsage = repo.add(new SurfaceStyleUsage(".BOTH.", surfaceSide))
696
- const presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]))
697
- const styledItem = repo.add(new StyledItem("", [presStyle], solidRef))
733
+ if (isBoard || solidsWithIntrinsicFaceStyles.has(String(itemRef.id))) {
734
+ return
735
+ }
736
+ const styledItem = createStyledItem(repo, {
737
+ itemRef,
738
+ rgb: [0.75, 0.75, 0.75],
739
+ styleCache,
740
+ name: "",
741
+ })
698
742
  styledItems.push(styledItem)
699
743
  })
700
744
 
@@ -2,7 +2,7 @@ import type { CircuitJson } from "circuit-json"
2
2
  import type { Ref, Repository } from "stepts"
3
3
  import { ManifoldSolidBrep } from "stepts"
4
4
  import { createSceneBoxSolid } from "./scene-box-to-step"
5
- import type { SceneBox } from "./scene-geometry"
5
+ import type { GeneratedSceneSolid, SceneBox } from "./scene-geometry"
6
6
 
7
7
  export interface MeshGenerationOptions {
8
8
  /** Repository to add STEP entities to */
@@ -30,7 +30,7 @@ export interface MeshGenerationOptions {
30
30
  */
31
31
  export async function generateComponentMeshes(
32
32
  options: MeshGenerationOptions,
33
- ): Promise<Ref<ManifoldSolidBrep>[]> {
33
+ ): Promise<GeneratedSceneSolid[]> {
34
34
  const {
35
35
  repo,
36
36
  circuitJson,
@@ -41,7 +41,7 @@ export async function generateComponentMeshes(
41
41
  pcbComponentIdsWithStepUrl,
42
42
  } = options
43
43
 
44
- const solids: Ref<ManifoldSolidBrep>[] = []
44
+ const solids: GeneratedSceneSolid[] = []
45
45
 
46
46
  try {
47
47
  const filteredCircuitJson = circuitJson
@@ -1,13 +1,18 @@
1
- import type { Ref, Repository } from "stepts"
1
+ import type { Ref, Repository, StyledItem } from "stepts"
2
2
  import { ClosedShell, ManifoldSolidBrep } from "stepts"
3
- import type { SceneBox } from "./scene-geometry"
3
+ import type {
4
+ GeneratedSceneSolid,
5
+ SceneBox,
6
+ TriangleColor,
7
+ } from "./scene-geometry"
8
+ import { createStyleCache, createStyledItem } from "./step-style-utils"
4
9
  import { rotatePoint3 } from "./scene-geometry"
5
10
  import { createFaceFromVertices, createVertex } from "./step-brep-utils"
6
11
 
7
12
  export function createSceneBoxSolid(
8
13
  repo: Repository,
9
14
  box: SceneBox,
10
- ): Ref<ManifoldSolidBrep> {
15
+ ): GeneratedSceneSolid {
11
16
  if (box.mesh?.triangles?.length) {
12
17
  return createSceneMeshSolid(repo, box)
13
18
  }
@@ -65,13 +70,21 @@ export function createSceneBoxSolid(
65
70
  ].map((faceVertices) => createFaceFromVertices(repo, faceVertices))
66
71
 
67
72
  const shell = repo.add(new ClosedShell("", faces))
68
- return repo.add(new ManifoldSolidBrep("Component", shell))
73
+ return {
74
+ solid: repo.add(new ManifoldSolidBrep(box.label ?? "Component", shell)),
75
+ styledItems: [],
76
+ usesIntrinsicFaceStyles: false,
77
+ styleTargets: faces,
78
+ }
69
79
  }
70
80
 
71
81
  function createSceneMeshSolid(
72
82
  repo: Repository,
73
83
  box: SceneBox,
74
- ): Ref<ManifoldSolidBrep> {
84
+ ): GeneratedSceneSolid {
85
+ const styleCache = createStyleCache()
86
+ const styledItems: Ref<StyledItem>[] = []
87
+
75
88
  const faces = box.mesh!.triangles!.map((triangle) => {
76
89
  const vertices = triangle.vertices.map((vertex) => {
77
90
  const rotated = rotatePoint3(vertex, box.rotation)
@@ -83,9 +96,43 @@ function createSceneMeshSolid(
83
96
  return createVertex(repo, translated)
84
97
  })
85
98
 
86
- return createFaceFromVertices(repo, vertices)
99
+ const face = createFaceFromVertices(repo, vertices)
100
+ const faceColor = normalizeTriangleColor(triangle.color)
101
+ if (faceColor) {
102
+ styledItems.push(
103
+ createStyledItem(repo, {
104
+ itemRef: face,
105
+ rgb: faceColor,
106
+ styleCache,
107
+ }),
108
+ )
109
+ }
110
+ return face
87
111
  })
88
112
 
89
113
  const shell = repo.add(new ClosedShell("", faces))
90
- return repo.add(new ManifoldSolidBrep("Component", shell))
114
+ return {
115
+ solid: repo.add(new ManifoldSolidBrep(box.label ?? "Component", shell)),
116
+ styledItems,
117
+ usesIntrinsicFaceStyles:
118
+ faces.length > 0 && styledItems.length === faces.length,
119
+ styleTargets: [],
120
+ }
121
+ }
122
+
123
+ function normalizeTriangleColor(
124
+ color: TriangleColor,
125
+ ): [number, number, number] | null {
126
+ if (!Array.isArray(color) || color.length < 3) return null
127
+
128
+ const scale = color.some((value: number) => value > 1) ? 255 : 1
129
+ return [
130
+ clampColorChannel(color[0]! / scale),
131
+ clampColorChannel(color[1]! / scale),
132
+ clampColorChannel(color[2]! / scale),
133
+ ]
134
+ }
135
+
136
+ function clampColorChannel(value: number): number {
137
+ return Math.max(0, Math.min(1, value))
91
138
  }
@@ -1,3 +1,5 @@
1
+ import type { Entity, Ref, StyledItem, ManifoldSolidBrep } from "stepts"
2
+
1
3
  export type Point3 = { x: number; y: number; z: number }
2
4
 
3
5
  export type Rotation3 = { x: number; y: number; z: number }
@@ -5,8 +7,11 @@ export type Rotation3 = { x: number; y: number; z: number }
5
7
  export type Triangle = {
6
8
  vertices: [Point3, Point3, Point3]
7
9
  normal: Point3
10
+ color?: [number, number, number] | [number, number, number, number]
8
11
  }
9
12
 
13
+ export type TriangleColor = Triangle["color"]
14
+
10
15
  export type BoundingBox = {
11
16
  min: Point3
12
17
  max: Point3
@@ -16,12 +21,20 @@ export type SceneBox = {
16
21
  center: Point3
17
22
  size: Point3
18
23
  rotation?: Rotation3
24
+ label?: string
19
25
  mesh?: {
20
26
  boundingBox: BoundingBox
21
27
  triangles?: Triangle[]
22
28
  }
23
29
  }
24
30
 
31
+ export type GeneratedSceneSolid = {
32
+ solid: Ref<ManifoldSolidBrep>
33
+ styledItems: Ref<StyledItem>[]
34
+ usesIntrinsicFaceStyles: boolean
35
+ styleTargets: Ref<Entity>[]
36
+ }
37
+
25
38
  export function rotatePoint3(point: Point3, rotation?: Rotation3): Point3 {
26
39
  if (!rotation) return point
27
40
 
@@ -9,17 +9,6 @@ export const EXCLUDED_ENTITY_TYPES = new Set<string>([
9
9
  "PRODUCT_DEFINITION_SHAPE",
10
10
  "SHAPE_DEFINITION_REPRESENTATION",
11
11
  "ADVANCED_BREP_SHAPE_REPRESENTATION",
12
- "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION",
13
- "PRESENTATION_STYLE_ASSIGNMENT",
14
- "SURFACE_STYLE_USAGE",
15
- "SURFACE_SIDE_STYLE",
16
- "SURFACE_STYLE_FILL_AREA",
17
- "FILL_AREA_STYLE",
18
- "FILL_AREA_STYLE_COLOUR",
19
- "COLOUR_RGB",
20
- "STYLED_ITEM",
21
- "CURVE_STYLE",
22
- "DRAUGHTING_PRE_DEFINED_CURVE_FONT",
23
12
  "PRODUCT_RELATED_PRODUCT_CATEGORY",
24
13
  "NEXT_ASSEMBLY_USAGE_OCCURRENCE",
25
14
  "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION",
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  CartesianPoint,
3
3
  Direction,
4
+ type Entity,
4
5
  ManifoldSolidBrep,
5
6
  Ref,
6
7
  Repository,
@@ -136,7 +137,7 @@ function mergeSingleStepModel(
136
137
  return solids
137
138
  }
138
139
 
139
- type RepositoryEntry = readonly [number, any]
140
+ type RepositoryEntry = readonly [number, Entity]
140
141
 
141
142
  function adjustTransformForPlacement(
142
143
  entries: ReadonlyArray<RepositoryEntry>,
@@ -0,0 +1,63 @@
1
+ import type { Entity, Ref, Repository } from "stepts"
2
+ import {
3
+ ColourRgb,
4
+ FillAreaStyle,
5
+ FillAreaStyleColour,
6
+ PresentationStyleAssignment,
7
+ StyledItem,
8
+ SurfaceSideStyle,
9
+ SurfaceStyleFillArea,
10
+ SurfaceStyleUsage,
11
+ } from "stepts"
12
+
13
+ export type StyleCache = Map<string, Ref<PresentationStyleAssignment>>
14
+
15
+ type StyledItemOptions = {
16
+ itemRef: Ref<Entity>
17
+ rgb: [number, number, number]
18
+ styleCache: StyleCache
19
+ name?: string
20
+ }
21
+
22
+ type StyledItemsOptions = {
23
+ itemRefs: ReadonlyArray<Ref<Entity>>
24
+ rgb: [number, number, number]
25
+ styleCache: StyleCache
26
+ name?: string
27
+ }
28
+
29
+ export function createStyleCache(): StyleCache {
30
+ return new Map<string, Ref<PresentationStyleAssignment>>()
31
+ }
32
+
33
+ export function createStyledItem(
34
+ repo: Repository,
35
+ options: StyledItemOptions,
36
+ ): Ref<StyledItem> {
37
+ const { itemRef, rgb, styleCache, name = "color" } = options
38
+ const key = rgb.map((value) => value.toFixed(6)).join(",")
39
+ let presStyle = styleCache.get(key)
40
+
41
+ if (!presStyle) {
42
+ const color = repo.add(new ColourRgb("", rgb[0], rgb[1], rgb[2]))
43
+ const fillColor = repo.add(new FillAreaStyleColour("", color))
44
+ const fillStyle = repo.add(new FillAreaStyle("", [fillColor]))
45
+ const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle))
46
+ const surfaceSide = repo.add(new SurfaceSideStyle("", [surfaceFill]))
47
+ const surfaceUsage = repo.add(new SurfaceStyleUsage(".BOTH.", surfaceSide))
48
+ presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]))
49
+ styleCache.set(key, presStyle)
50
+ }
51
+
52
+ return repo.add(new StyledItem(name, [presStyle], itemRef))
53
+ }
54
+
55
+ export function createStyledItems(
56
+ repo: Repository,
57
+ options: StyledItemsOptions,
58
+ ): Ref<StyledItem>[] {
59
+ const { itemRefs, rgb, styleCache, name } = options
60
+ return itemRefs.map((itemRef) =>
61
+ createStyledItem(repo, { itemRef, rgb, styleCache, name }),
62
+ )
63
+ }
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.23",
4
+ "version": "0.0.24",
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.286",
22
+ "circuit-json": "^0.0.406",
23
23
  "looks-same": "^10.0.1",
24
24
  "occt-import-js": "^0.0.23",
25
25
  "poppygl": "^0.0.17",
@@ -11,12 +11,27 @@ import type { OcctMesh } from "../utils/occt/importer"
11
11
  // Ensure PNG matcher is loaded so we can reuse it
12
12
  import "./png-matcher"
13
13
 
14
+ type GLTFPrimitive = {
15
+ attributes: Record<string, number>
16
+ indices: number
17
+ material: number
18
+ mode: 4
19
+ }
20
+
21
+ type GLTFMaterial = {
22
+ pbrMetallicRoughness: {
23
+ baseColorFactor: [number, number, number, number]
24
+ metallicFactor: number
25
+ roughnessFactor: number
26
+ }
27
+ }
28
+
14
29
  type GLTF = {
15
30
  asset: { version: "2.0"; generator?: string }
16
31
  scene: number
17
32
  scenes: { nodes: number[] }[]
18
33
  nodes: { mesh: number }[]
19
- meshes: { primitives: any[] }[]
34
+ meshes: { primitives: GLTFPrimitive[] }[]
20
35
  buffers: { byteLength: number }[]
21
36
  bufferViews: {
22
37
  buffer: number
@@ -33,7 +48,7 @@ type GLTF = {
33
48
  min?: number[]
34
49
  max?: number[]
35
50
  }[]
36
- materials: any[]
51
+ materials: GLTFMaterial[]
37
52
  }
38
53
 
39
54
  function createFloat32Buffer(data: number[]): Uint8Array {
@@ -89,6 +104,25 @@ function gltfFromOcctMeshes(meshes: OcctMesh[]) {
89
104
  }
90
105
 
91
106
  const defaultColor: [number, number, number] = [0.82, 0.82, 0.82]
107
+ const materialCache = new Map<string, number>()
108
+
109
+ const getMaterialIndex = (color: [number, number, number]) => {
110
+ const key = color.map((value) => value.toFixed(6)).join(",")
111
+ const existing = materialCache.get(key)
112
+ if (typeof existing === "number") return existing
113
+
114
+ const matIndex =
115
+ gltf.materials.push({
116
+ pbrMetallicRoughness: {
117
+ baseColorFactor: [color[0], color[1], color[2], 1],
118
+ metallicFactor: 0,
119
+ roughnessFactor: 0.9,
120
+ },
121
+ }) - 1
122
+
123
+ materialCache.set(key, matIndex)
124
+ return matIndex
125
+ }
92
126
 
93
127
  for (const m of meshes) {
94
128
  const positions = (m.attributes.position?.array ?? []) as number[]
@@ -100,17 +134,6 @@ function gltfFromOcctMeshes(meshes: OcctMesh[]) {
100
134
  continue
101
135
  }
102
136
 
103
- // Material
104
- const [r, g, b] = (m.color ?? defaultColor) as [number, number, number]
105
- const matIndex =
106
- gltf.materials.push({
107
- pbrMetallicRoughness: {
108
- baseColorFactor: [r, g, b, 1],
109
- metallicFactor: 0,
110
- roughnessFactor: 0.9,
111
- },
112
- }) - 1
113
-
114
137
  // POSITION
115
138
  const posBufIdx = addBuffer(createFloat32Buffer(positions))
116
139
  const posBVIdx =
@@ -168,16 +191,55 @@ function gltfFromOcctMeshes(meshes: OcctMesh[]) {
168
191
  const attributes: Record<string, number> = { POSITION: posAccIdx }
169
192
  if (typeof normAccIdx === "number") attributes.NORMAL = normAccIdx
170
193
 
171
- const meshIndex =
172
- gltf.meshes.push({
173
- primitives: [
194
+ const hasFaceColors = m.brep_faces.some((face) => face.color !== null)
195
+ const primitives: GLTFPrimitive[] = hasFaceColors
196
+ ? m.brep_faces.flatMap((face) => {
197
+ const color = (face.color ?? m.color ?? defaultColor) as [
198
+ number,
199
+ number,
200
+ number,
201
+ ]
202
+ const faceIndices = indices.slice(face.first * 3, (face.last + 1) * 3)
203
+ if (!faceIndices.length) return []
204
+
205
+ const faceIdxBufIdx = addBuffer(createUint32Buffer(faceIndices))
206
+ const faceIdxBVIdx =
207
+ gltf.bufferViews.push({
208
+ buffer: faceIdxBufIdx,
209
+ byteLength: faceIndices.length * 4,
210
+ target: 34963, // ELEMENT_ARRAY_BUFFER
211
+ }) - 1
212
+ const faceIdxAccIdx =
213
+ gltf.accessors.push({
214
+ bufferView: faceIdxBVIdx,
215
+ componentType: 5125, // UNSIGNED_INT
216
+ count: faceIndices.length,
217
+ type: "SCALAR",
218
+ }) - 1
219
+
220
+ return [
221
+ {
222
+ attributes,
223
+ indices: faceIdxAccIdx,
224
+ material: getMaterialIndex(color),
225
+ mode: 4, // TRIANGLES
226
+ },
227
+ ]
228
+ })
229
+ : [
174
230
  {
175
231
  attributes,
176
232
  indices: idxAccIdx,
177
- material: matIndex,
233
+ material: getMaterialIndex(
234
+ (m.color ?? defaultColor) as [number, number, number],
235
+ ),
178
236
  mode: 4, // TRIANGLES
179
237
  },
180
- ],
238
+ ]
239
+
240
+ const meshIndex =
241
+ gltf.meshes.push({
242
+ primitives,
181
243
  }) - 1
182
244
 
183
245
  const nodeIndex = gltf.nodes.push({ mesh: meshIndex }) - 1
@@ -199,7 +261,7 @@ async function renderStepToPNG(
199
261
  }
200
262
 
201
263
  const { gltf, buffers } = gltfFromOcctMeshes(result.meshes)
202
- const scene = createSceneFromGLTF(gltf as any, { buffers, images: [] })
264
+ const scene = createSceneFromGLTF(gltf, { buffers, images: [] })
203
265
 
204
266
  const { bitmap } = renderSceneFromGLTF(
205
267
  scene,
@@ -224,19 +286,28 @@ async function renderStepToPNG(
224
286
  * await expect(stepContent).toMatchStepSnapshot(import.meta.path, "optionalName")
225
287
  */
226
288
  async function toMatchStepSnapshot(
227
- // biome-ignore lint/suspicious/noExplicitAny: bun doesn't expose
228
- this: any,
229
- stepContent: string | Uint8Array | ArrayBuffer,
289
+ this: unknown,
290
+ received: unknown,
230
291
  testPathOriginal: string,
231
292
  pngName?: string,
232
293
  ) {
233
- const png = await renderStepToPNG(stepContent)
294
+ if (
295
+ typeof received !== "string" &&
296
+ !(received instanceof Uint8Array) &&
297
+ !(received instanceof ArrayBuffer)
298
+ ) {
299
+ throw new Error(
300
+ "Expected STEP content to be a string, Uint8Array, or ArrayBuffer",
301
+ )
302
+ }
303
+
304
+ const png = await renderStepToPNG(received)
234
305
  // Delegate to existing PNG matcher for snapshot compare/update UX
235
306
  return await expect(png).toMatchPngSnapshot(testPathOriginal, pngName)
236
307
  }
237
308
 
238
309
  expect.extend({
239
- toMatchStepSnapshot: toMatchStepSnapshot as any,
310
+ toMatchStepSnapshot,
240
311
  })
241
312
 
242
313
  declare module "bun:test" {
@@ -70,7 +70,7 @@ let occtInstancePromise: Promise<OcctImport> | undefined
70
70
 
71
71
  async function loadOcct(): Promise<OcctImport> {
72
72
  if (!occtInstancePromise) {
73
- const imported = (await import("occt-import-js")) as any
73
+ const imported = await import("occt-import-js")
74
74
  const factory = resolveFactory(imported)
75
75
  occtInstancePromise = factory()
76
76
  }