circuit-json-to-step 0.0.23 → 0.0.25
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 +131 -52
- package/lib/index.ts +75 -31
- package/lib/mesh-generation.ts +3 -3
- package/lib/scene-box-to-step.ts +54 -7
- package/lib/scene-geometry.ts +13 -0
- package/lib/step-model-merger/excluded-entity-types.ts +0 -11
- package/lib/step-model-merger.ts +2 -1
- package/lib/step-style-utils.ts +63 -0
- package/package.json +2 -2
- package/test/basics/basics01/__snapshots__/basics01.snap.png +0 -0
- package/test/basics/basics03/__snapshots__/basics03.snap.png +0 -0
- package/test/basics/basics04/__snapshots__/basics04.snap.png +0 -0
- package/test/basics/basics05/__snapshots__/basics05.snap.png +0 -0
- package/test/basics/basics06/__snapshots__/basics06.snap.png +0 -0
- package/test/fixtures/step-snapshot.ts +95 -24
- package/test/repros/kicad-step/__snapshots__/kicad-step-board.snap.png +0 -0
- package/test/repros/kicad-step/__snapshots__/resistor-fixture.snap.png +0 -0
- package/test/repros/kicad-step/__snapshots__/switch-fixture.snap.png +0 -0
- package/test/repros/repro01/__snapshots__/repro01.snap.png +0 -0
- package/test/repros/repro02/__snapshots__/repro02.snap.png +0 -0
- package/test/utils/occt/importer.ts +1 -1
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
1640
|
+
const holeShape = hole.hole_shape ?? hole.shape;
|
|
1588
1641
|
if (holeShape === "circle") {
|
|
1589
|
-
const holeX = typeof hole.x === "number" ? hole.x : hole.x
|
|
1590
|
-
const holeY = typeof hole.y === "number" ? hole.y : hole.y
|
|
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
|
|
1696
|
+
const holeShape = hole.hole_shape ?? hole.shape;
|
|
1647
1697
|
if (holeShape === "circle") {
|
|
1648
|
-
const holeX = typeof hole.x === "number" ? hole.x : hole.x
|
|
1649
|
-
const holeY = typeof hole.y === "number" ? hole.y : hole.y
|
|
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
|
|
1774
|
+
const holeShape = hole.hole_shape ?? hole.shape;
|
|
1725
1775
|
if (holeShape === "circle") {
|
|
1726
|
-
const holeX = typeof hole.x === "number" ? hole.x : hole.x
|
|
1727
|
-
const holeY = typeof hole.y === "number" ? hole.y : hole.y
|
|
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
|
-
|
|
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
|
-
|
|
1945
|
+
const styledItems = [
|
|
1946
|
+
...boardStyledItems,
|
|
1947
|
+
...componentStyledItems
|
|
1948
|
+
];
|
|
1949
|
+
allSolids.forEach((itemRef, index) => {
|
|
1871
1950
|
const isBoard = index === 0;
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
|
82
|
-
(item)
|
|
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
|
|
350
|
+
const holeShape = hole.hole_shape ?? hole.shape
|
|
333
351
|
if (holeShape === "circle") {
|
|
334
|
-
const holeX = typeof hole.x === "number" ? hole.x : (hole.x
|
|
335
|
-
const holeY = typeof hole.y === "number" ? hole.y : (hole.y
|
|
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
|
|
414
|
+
const holeShape = hole.hole_shape ?? hole.shape
|
|
400
415
|
if (holeShape === "circle") {
|
|
401
|
-
const holeX = typeof hole.x === "number" ? hole.x : (hole.x
|
|
402
|
-
const holeY = typeof hole.y === "number" ? hole.y : (hole.y
|
|
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]
|
|
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
|
|
507
|
+
const holeShape = hole.hole_shape ?? hole.shape
|
|
493
508
|
if (holeShape === "circle") {
|
|
494
|
-
const holeX = typeof hole.x === "number" ? hole.x : (hole.x
|
|
495
|
-
const holeY = typeof hole.y === "number" ? hole.y : (hole.y
|
|
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
|
-
|
|
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((
|
|
731
|
+
allSolids.forEach((itemRef, index) => {
|
|
688
732
|
const isBoard = index === 0
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
|
package/lib/mesh-generation.ts
CHANGED
|
@@ -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<
|
|
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:
|
|
44
|
+
const solids: GeneratedSceneSolid[] = []
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
const filteredCircuitJson = circuitJson
|
package/lib/scene-box-to-step.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
):
|
|
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
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/lib/scene-geometry.ts
CHANGED
|
@@ -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",
|
package/lib/step-model-merger.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
4
|
+
"version": "0.0.25",
|
|
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.
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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:
|
|
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:
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
stepContent: string | Uint8Array | ArrayBuffer,
|
|
289
|
+
this: unknown,
|
|
290
|
+
received: unknown,
|
|
230
291
|
testPathOriginal: string,
|
|
231
292
|
pngName?: string,
|
|
232
293
|
) {
|
|
233
|
-
|
|
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
|
|
310
|
+
toMatchStepSnapshot,
|
|
240
311
|
})
|
|
241
312
|
|
|
242
313
|
declare module "bun:test" {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 =
|
|
73
|
+
const imported = await import("occt-import-js")
|
|
74
74
|
const factory = resolveFactory(imported)
|
|
75
75
|
occtInstancePromise = factory()
|
|
76
76
|
}
|