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
316 changes: 278 additions & 38 deletions lib/mesh-generation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CircuitJson } from "circuit-json"
import type { Ref } from "stepts"
import type { Triangle as GLTFTriangle } from "circuit-json-to-gltf"
import type { Triangle as GLTFTriangle, Box3D } from "circuit-json-to-gltf"
import type { Repository } from "stepts"
import {
AdvancedFace,
Expand Down Expand Up @@ -37,7 +37,249 @@ export interface MeshGenerationOptions {
}

/**
* Generates triangles for a box mesh
* Creates a proper B-Rep box solid in STEP format.
* The box is defined by 8 vertices, 12 edges, and 6 rectangular faces,
* forming a valid manifold closed shell.
*
* Coordinates are in STEP convention (Z=up). The box parameter uses
* GLTF convention (Y=up), so we swap Y/Z during conversion.
*/
function createBRepBoxSolid(
repo: Repository,
box: {
center: { x: number; y: number; z: number }
size: { x: number; y: number; z: number }
},
label?: string,
): Ref<ManifoldSolidBrep> {
// Transform from GLTF (Y=up) to STEP (Z=up)
const center = { x: box.center.x, y: box.center.z, z: box.center.y }
const size = { x: box.size.x, y: box.size.z, z: box.size.y }

const halfX = size.x / 2
const halfY = size.y / 2
const halfZ = size.z / 2

// 8 corners of the box
// Bottom face (z = center.z - halfZ)
// 0: (-x, -y, -z) 1: (+x, -y, -z) 2: (+x, +y, -z) 3: (-x, +y, -z)
// Top face (z = center.z + halfZ)
// 4: (-x, -y, +z) 5: (+x, -y, +z) 6: (+x, +y, +z) 7: (-x, +y, +z)
const cornerCoords = [
[center.x - halfX, center.y - halfY, center.z - halfZ],
[center.x + halfX, center.y - halfY, center.z - halfZ],
[center.x + halfX, center.y + halfY, center.z - halfZ],
[center.x - halfX, center.y + halfY, center.z - halfZ],
[center.x - halfX, center.y - halfY, center.z + halfZ],
[center.x + halfX, center.y - halfY, center.z + halfZ],
[center.x + halfX, center.y + halfY, center.z + halfZ],
[center.x - halfX, center.y + halfY, center.z + halfZ],
]

const vertices = cornerCoords.map(([x, y, z]) =>
repo.add(new VertexPoint("", repo.add(new CartesianPoint("", x!, y!, z!)))),
)

// Helper to create an edge between two vertices
function createEdge(
v1: Ref<VertexPoint>,
v2: Ref<VertexPoint>,
): Ref<EdgeCurve> {
const p1 = v1.resolve(repo).pnt.resolve(repo)
const p2 = v2.resolve(repo).pnt.resolve(repo)
const dx = p2.x - p1.x
const dy = p2.y - p1.y
const dz = p2.z - p1.z
const length = Math.sqrt(dx * dx + dy * dy + dz * dz)
const dir = repo.add(
new Direction("", dx / length, dy / length, dz / length),
)
const vec = repo.add(new Vector("", dir, length))
const line = repo.add(new Line("", v1.resolve(repo).pnt, vec))
return repo.add(new EdgeCurve("", v1, v2, line, true))
}

// 12 edges of the box
// Bottom face edges (0-1, 1-2, 2-3, 3-0)
const bottomEdges = [
createEdge(vertices[0]!, vertices[1]!),
createEdge(vertices[1]!, vertices[2]!),
createEdge(vertices[2]!, vertices[3]!),
createEdge(vertices[3]!, vertices[0]!),
]
// Top face edges (4-5, 5-6, 6-7, 7-4)
const topEdges = [
createEdge(vertices[4]!, vertices[5]!),
createEdge(vertices[5]!, vertices[6]!),
createEdge(vertices[6]!, vertices[7]!),
createEdge(vertices[7]!, vertices[4]!),
]
// Vertical edges (0-4, 1-5, 2-6, 3-7)
const vertEdges = [
createEdge(vertices[0]!, vertices[4]!),
createEdge(vertices[1]!, vertices[5]!),
createEdge(vertices[2]!, vertices[6]!),
createEdge(vertices[3]!, vertices[7]!),
]

// Helper to create a planar face
function createFace(
edges: { edge: Ref<EdgeCurve>; forward: boolean }[],
normalX: number,
normalY: number,
normalZ: number,
originVertex: Ref<VertexPoint>,
refDirX: number,
refDirY: number,
refDirZ: number,
): Ref<AdvancedFace> {
const orientedEdges = edges.map((e) =>
repo.add(new OrientedEdge("", e.edge, e.forward)),
)
const loop = repo.add(new EdgeLoop("", orientedEdges))
const normalDir = repo.add(new Direction("", normalX, normalY, normalZ))
const refDir = repo.add(new Direction("", refDirX, refDirY, refDirZ))
const placement = repo.add(
new Axis2Placement3D(
"",
originVertex.resolve(repo).pnt,
normalDir,
refDir,
),
)
const plane = repo.add(new Plane("", placement))
return repo.add(
new AdvancedFace(
"",
[repo.add(new FaceOuterBound("", loop, true))],
plane,
true,
),
)
}

// Bottom face (z = -halfZ, normal pointing down)
// Loop: 0->1->2->3->0 (viewed from below, counterclockwise = clockwise from above)
const bottomFace = createFace(
[
{ edge: bottomEdges[0]!, forward: true },
{ edge: bottomEdges[1]!, forward: true },
{ edge: bottomEdges[2]!, forward: true },
{ edge: bottomEdges[3]!, forward: true },
],
0,
0,
-1,
vertices[0]!,
1,
0,
0,
)

// Top face (z = +halfZ, normal pointing up)
// Loop: 4->7->6->5->4 (viewed from above, counterclockwise)
const topFace = createFace(
[
{ edge: topEdges[3]!, forward: false },
{ edge: topEdges[2]!, forward: false },
{ edge: topEdges[1]!, forward: false },
{ edge: topEdges[0]!, forward: false },
],
0,
0,
1,
vertices[4]!,
1,
0,
0,
)

// Front face (y = -halfY, normal pointing -Y)
// Loop: 0->4->5->1->0
const frontFace = createFace(
[
{ edge: vertEdges[0]!, forward: true },
{ edge: topEdges[0]!, forward: true },
{ edge: vertEdges[1]!, forward: false },
{ edge: bottomEdges[0]!, forward: false },
],
0,
-1,
0,
vertices[0]!,
1,
0,
0,
)

// Back face (y = +halfY, normal pointing +Y)
// Loop: 2->6->7->3->2
const backFace = createFace(
[
{ edge: vertEdges[2]!, forward: true },
{ edge: topEdges[2]!, forward: true },
{ edge: vertEdges[3]!, forward: false },
{ edge: bottomEdges[2]!, forward: false },
],
0,
1,
0,
vertices[2]!,
-1,
0,
0,
)

// Left face (x = -halfX, normal pointing -X)
// Loop: 3->7->4->0->3
const leftFace = createFace(
[
{ edge: vertEdges[3]!, forward: true },
{ edge: topEdges[3]!, forward: true },
{ edge: vertEdges[0]!, forward: false },
{ edge: bottomEdges[3]!, forward: false },
],
-1,
0,
0,
vertices[3]!,
0,
-1,
0,
)

// Right face (x = +halfX, normal pointing +X)
// Loop: 1->5->6->2->1
const rightFace = createFace(
[
{ edge: vertEdges[1]!, forward: true },
{ edge: topEdges[1]!, forward: true },
{ edge: vertEdges[2]!, forward: false },
{ edge: bottomEdges[1]!, forward: false },
],
1,
0,
0,
vertices[1]!,
0,
1,
0,
)

const allFaces = [
bottomFace,
topFace,
frontFace,
backFace,
leftFace,
rightFace,
]
const shell = repo.add(new ClosedShell("", allFaces))
return repo.add(new ManifoldSolidBrep(label ?? "Component", shell))
}

/**
* Generates triangles for a box mesh (used for boxes with custom meshes)
*/
function createBoxTriangles(box: {
center: { x: number; y: number; z: number }
Expand Down Expand Up @@ -327,47 +569,45 @@ export async function generateComponentMeshes(
renderBoardTextures: false,
})

// Extract or generate triangles from component boxes
const allTriangles: GLTFTriangle[] = []
// Process each box individually
for (const box of scene3d.boxes) {
if (box.mesh && "triangles" in box.mesh) {
allTriangles.push(...box.mesh.triangles)
// Box has a custom mesh - use triangle-based approach
const meshTriangles = box.mesh.triangles
if (meshTriangles.length > 0) {
// Transform triangles from GLTF XZ plane (Y=up) to STEP XY plane (Z=up)
const transformedTriangles = meshTriangles.map(
(tri: GLTFTriangle) => ({
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
},
}),
)
const componentFaces = createStepFacesFromTriangles(
repo,
transformedTriangles as any,
)
const componentShell = repo.add(
new ClosedShell("", componentFaces as any),
)
const componentSolid = repo.add(
new ManifoldSolidBrep(box.label ?? "Component", componentShell),
)
solids.push(componentSolid)
}
} else {
// Generate simple box mesh for this component
const boxTriangles = createBoxTriangles(box)
allTriangles.push(...boxTriangles)
// Simple box - create proper B-Rep solid with 6 rectangular faces
const boxSolid = createBRepBoxSolid(repo, box, box.label)
solids.push(boxSolid)
}
}

// 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) => ({
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
},
}))
const componentFaces = createStepFacesFromTriangles(
repo,
transformedTriangles as any,
)

// Create closed shell and solid for components
const componentShell = repo.add(
new ClosedShell("", componentFaces as any),
)
const componentSolid = repo.add(
new ManifoldSolidBrep("Components", componentShell),
)
solids.push(componentSolid)
}
} catch (error) {
console.warn("Failed to generate component mesh:", error)
// Continue without components if generation fails
Expand Down
Binary file modified test/basics/basics04/__snapshots__/basics04.snap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading