Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.
51 changes: 51 additions & 0 deletions test/basics/separate-solids/separate-solids.test.ts
Original file line number Diff line number Diff line change
@@ -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)