diff --git a/lib/index.ts b/lib/index.ts index b1e627f..eabfb2b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -55,7 +55,7 @@ export interface CircuitJsonToStepOptions { boardThickness?: number /** Product name (default: "PCB") */ productName?: string - /** Include component meshes (default: false) */ + /** Include component meshes (default: true) */ includeComponents?: boolean /** Include external model meshes from model_*_url fields (default: false). Only applicable when includeComponents is true. */ includeExternalMeshes?: boolean @@ -86,6 +86,7 @@ export async function circuitJsonToStep( const boardWidth = options.boardWidth ?? pcbBoard?.width const boardHeight = options.boardHeight ?? pcbBoard?.height const boardThickness = options.boardThickness ?? pcbBoard?.thickness ?? 1.6 + const includeComponents = options.includeComponents ?? true const productName = options.productName ?? "PCB" // Get board center position (defaults to 0, 0 if not specified) @@ -568,7 +569,7 @@ export async function circuitJsonToStep( let handledComponentIds = new Set() let handledPcbComponentIds = new Set() - if (options.includeComponents && options.includeExternalMeshes) { + if (includeComponents && options.includeExternalMeshes) { const mergeResult = await mergeExternalStepModels({ repo, circuitJson, @@ -582,7 +583,7 @@ export async function circuitJsonToStep( // Generate component mesh fallback if requested // Only call mesh generation if there are components that need it - if (options.includeComponents) { + if (includeComponents) { // Build set of pcb_component_ids covered by cad_components with model_step_url const pcbComponentIdsWithStepUrl = new Set() for (const item of circuitJson) { diff --git a/lib/mesh-generation.ts b/lib/mesh-generation.ts index 1df41fe..b44e338 100644 --- a/lib/mesh-generation.ts +++ b/lib/mesh-generation.ts @@ -327,22 +327,24 @@ export async function generateComponentMeshes( renderBoardTextures: false, }) - // Extract or generate triangles from component boxes - const allTriangles: GLTFTriangle[] = [] - for (const box of scene3d.boxes) { + // Process each component box as a separate STEP solid. + // STEP requires each ManifoldSolidBrep to have a closed, watertight + // boundary — merging triangles from multiple disconnected boxes into + // one ClosedShell produces an invalid solid that viewers silently drop. + for (let i = 0; i < scene3d.boxes.length; i++) { + const box = scene3d.boxes[i] + + let boxTriangles: GLTFTriangle[] if (box.mesh && "triangles" in box.mesh) { - allTriangles.push(...box.mesh.triangles) + boxTriangles = box.mesh.triangles } else { - // Generate simple box mesh for this component - const boxTriangles = createBoxTriangles(box) - allTriangles.push(...boxTriangles) + boxTriangles = createBoxTriangles(box) } - } - // Create STEP faces from triangles if we have any - if (allTriangles.length > 0) { - // Transform triangles from GLTF XZ plane (Y=up) to STEP XY plane (Z=up) - const transformedTriangles = allTriangles.map((tri) => ({ + if (boxTriangles.length === 0) continue + + // Transform from GLTF coordinate system (Y-up) to STEP (Z-up) + const transformedTriangles = boxTriangles.map((tri) => ({ vertices: tri.vertices.map((v) => ({ x: v.x, y: v.z, // GLTF Z becomes STEP Y @@ -350,21 +352,22 @@ export async function generateComponentMeshes( })), normal: { x: tri.normal.x, - y: tri.normal.z, // GLTF Z becomes STEP Y - z: tri.normal.y, // GLTF Y becomes STEP Z + y: tri.normal.z, + z: tri.normal.y, }, })) + const componentFaces = createStepFacesFromTriangles( repo, transformedTriangles as any, ) - // Create closed shell and solid for components const componentShell = repo.add( new ClosedShell("", componentFaces as any), ) + const componentName = (box as any).name || `Component_${i + 1}` const componentSolid = repo.add( - new ManifoldSolidBrep("Components", componentShell), + new ManifoldSolidBrep(componentName, componentShell), ) solids.push(componentSolid) } diff --git a/test/basics/separate-solids/__snapshots__/separate-solids.snap.png b/test/basics/separate-solids/__snapshots__/separate-solids.snap.png new file mode 100644 index 0000000..b703933 Binary files /dev/null and b/test/basics/separate-solids/__snapshots__/separate-solids.snap.png differ diff --git a/test/basics/separate-solids/separate-solids.test.ts b/test/basics/separate-solids/separate-solids.test.ts new file mode 100644 index 0000000..d966d4e --- /dev/null +++ b/test/basics/separate-solids/separate-solids.test.ts @@ -0,0 +1,51 @@ +import { test, expect } from "bun:test" +import { circuitJsonToStep } from "../../../lib/index" +import { importStepWithOcct } from "../../utils/occt/importer" +import circuitJson from "../basics04/basics04.json" + +test("separate-solids: each component gets its own ManifoldSolidBrep", async () => { + const stepText = await circuitJsonToStep(circuitJson as any, { + includeComponents: true, + productName: "SeparateSolidsTest", + }) + + // Verify STEP format + expect(stepText).toContain("ISO-10303-21") + expect(stepText).toContain("END-ISO-10303-21") + + // Count ManifoldSolidBrep entries — should be at least 3: + // 1 for the board + 1 per component (2 components in basics04) + const solidMatches = + stepText.match(/MANIFOLD_SOLID_BREP\s*\(\s*'([^']*)'/g) || [] + const solidNames = solidMatches.map((m) => { + const nameMatch = m.match(/MANIFOLD_SOLID_BREP\s*\(\s*'([^']*)'/) + return nameMatch?.[1] ?? "" + }) + + console.log(` Solids found (${solidNames.length}):`) + for (const name of solidNames) { + console.log(` - ${name}`) + } + + // Board solid + at least 2 separate component solids + expect(solidNames.length).toBeGreaterThanOrEqual(3) + + // Each component should have its own named solid (not a single merged "Components") + expect(solidNames.some((n) => n === "Components")).toBe(false) + + // Validate STEP file with occt-import-js + const occtResult = await importStepWithOcct(stepText) + expect(occtResult.success).toBe(true) + // Should have multiple meshes (one per solid) + expect(occtResult.meshes.length).toBeGreaterThanOrEqual(3) + + console.log(` occt meshes: ${occtResult.meshes.length}`) + + // Write debug output + await Bun.write("debug-output/separate-solids.step", stepText) + + await expect(stepText).toMatchStepSnapshot( + import.meta.path, + "separate-solids", + ) +}, 30000)