Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -568,7 +569,7 @@ export async function circuitJsonToStep(
let handledComponentIds = new Set<string>()
let handledPcbComponentIds = new Set<string>()

if (options.includeComponents && options.includeExternalMeshes) {
if (includeComponents && options.includeExternalMeshes) {
const mergeResult = await mergeExternalStepModels({
repo,
circuitJson,
Expand All @@ -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<string>()
for (const item of circuitJson) {
Expand Down
35 changes: 19 additions & 16 deletions lib/mesh-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,44 +327,47 @@ 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
z: v.y, // GLTF Y becomes STEP Z
})),
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)
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions test/basics/separate-solids/separate-solids.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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)
Loading