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
6 changes: 3 additions & 3 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function convertCircuitJsonTo3D(
const boxes: any[] = []

// Get PCB board
const pcbBoard = db.pcb_board.list()[0]
const pcbBoard = db.pcb_board?.list()[0]
if (!pcbBoard) {
throw new Error("No pcb_board found in circuit JSON")
}
Expand All @@ -66,8 +66,8 @@ export async function convertCircuitJsonTo3D(
})

// Add generic boxes for components
for (const component of db.pcb_component.list()) {
const sourceComponent = db.source_component.get(
for (const component of db.pcb_component?.list() ?? []) {
const sourceComponent = db.source_component?.get(
component.source_component_id,
)
const compHeight = Math.min(
Expand Down
11 changes: 7 additions & 4 deletions lib/converters/circuit-to-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from "../types"
import { loadSTL } from "../loaders/stl"
import { loadOBJ } from "../loaders/obj"
import { loadGLTF } from "../loaders/gltf"
import { renderBoardTextures } from "./board-renderer"
import { COORDINATE_TRANSFORMS } from "../utils/coordinate-transform"

Expand Down Expand Up @@ -94,8 +95,8 @@ export async function convertCircuitJsonTo3D(
const pcbComponentIdsWith3D = new Set<string>()

for (const cad of cadComponents) {
const { model_stl_url, model_obj_url } = cad
if (!model_stl_url && !model_obj_url) continue
const { model_stl_url, model_obj_url, model_gltf_url } = cad
if (!model_stl_url && !model_obj_url && !model_gltf_url) continue

pcbComponentIdsWith3D.add(cad.pcb_component_id)

Expand All @@ -122,8 +123,8 @@ export async function convertCircuitJsonTo3D(
center,
size,
color: componentColor,
meshUrl: model_stl_url || model_obj_url,
meshType: model_stl_url ? "stl" : "obj",
meshUrl: model_stl_url || model_obj_url || model_gltf_url,
meshType: model_stl_url ? "stl" : model_obj_url ? "obj" : "gltf",
}

// Add rotation if specified
Expand All @@ -139,6 +140,8 @@ export async function convertCircuitJsonTo3D(
box.mesh = await loadSTL(model_stl_url, defaultTransform)
} else if (model_obj_url) {
box.mesh = await loadOBJ(model_obj_url, defaultTransform)
} else if (model_gltf_url) {
box.mesh = await loadGLTF(model_gltf_url, defaultTransform)
}
} catch (error) {
console.warn(`Failed to load 3D model: ${error}`)
Expand Down
108 changes: 70 additions & 38 deletions lib/gltf/geometry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { Point3, Size3, STLMesh, OBJMesh, Triangle } from "../types"
import type {
Point3,
Size3,
STLMesh,
OBJMesh,
GLTFMesh,
Triangle,
} from "../types"

export interface MeshData {
positions: number[]
Expand Down Expand Up @@ -132,10 +139,10 @@ export function createBoxMesh(size: Size3): MeshData {
for (const face of faces) {
// Add vertices for this face
for (let i = 0; i < 4; i++) {
const vertex = face.vertices[i]
positions.push(vertex[0], vertex[1], vertex[2])
normals.push(face.normal[0], face.normal[1], face.normal[2])
texcoords.push(face.uvs[i][0], face.uvs[i][1])
const vertex = face.vertices[i]!
positions.push(vertex[0]!, vertex[1]!, vertex[2]!)
normals.push(face.normal[0]!, face.normal[1]!, face.normal[2]!)
texcoords.push(face.uvs[i]![0]!, face.uvs[i]![1]!)
}

// Add two triangles for the quad
Expand Down Expand Up @@ -268,10 +275,10 @@ export function createBoxMeshByFaces(size: Size3): FaceMeshData {

// Add vertices for this face
for (let i = 0; i < 4; i++) {
const vertex = face.vertices[i]
positions.push(vertex[0], vertex[1], vertex[2])
normals.push(face.normal[0], face.normal[1], face.normal[2])
texcoords.push(face.uvs[i][0], face.uvs[i][1])
const vertex = face.vertices[i]!
positions.push(vertex[0]!, vertex[1]!, vertex[2]!)
normals.push(face.normal[0]!, face.normal[1]!, face.normal[2]!)
texcoords.push(face.uvs[i]![0]!, face.uvs[i]![1]!)
}

result[faceName as keyof FaceMeshData] = {
Expand Down Expand Up @@ -360,6 +367,31 @@ export function createMeshFromOBJ(
: [{ meshData: createMeshFromSTL(objMesh), materialIndex: -1 }]
}

export function createMeshFromGLTF(gltfMesh: GLTFMesh): MeshData {
const positions: number[] = []
const normals: number[] = []
const texcoords: number[] = []
const indices: number[] = []

let vertexIndex = 0

for (const triangle of gltfMesh.triangles) {
// Add vertices
for (const vertex of triangle.vertices) {
positions.push(vertex.x, vertex.y, vertex.z)
normals.push(triangle.normal.x, triangle.normal.y, triangle.normal.z)
// Simple planar UV mapping
texcoords.push(vertex.x, vertex.z)
}

// Add indices (reverse winding for correct face orientation)
indices.push(vertexIndex, vertexIndex + 2, vertexIndex + 1)
vertexIndex += 3
}

return { positions, normals, texcoords, indices }
}

export function transformMesh(
mesh: MeshData,
translation: Point3,
Expand All @@ -385,42 +417,42 @@ export function transformMesh(

// Apply scale
if (scale) {
x *= scale.x
y *= scale.y
z *= scale.z
x = (x ?? 0) * scale.x!
y = (y ?? 0) * scale.y!
z = (z ?? 0) * scale.z!
}

// Apply rotation (simplified - proper rotation would use quaternions)
if (rotation) {
// Rotation around Y axis
const cosY = Math.cos(rotation.y)
const sinY = Math.sin(rotation.y)
const rx = x * cosY - z * sinY
const rz = x * sinY + z * cosY
const cosY = Math.cos(rotation.y!)
const sinY = Math.sin(rotation.y!)
const rx = x! * cosY - z! * sinY
const rz = x! * sinY + z! * cosY
x = rx
z = rz

// Rotation around X axis
const cosX = Math.cos(rotation.x)
const sinX = Math.sin(rotation.x)
const ry = y * cosX - z * sinX
const rz2 = y * sinX + z * cosX
const cosX = Math.cos(rotation.x!)
const sinX = Math.sin(rotation.x!)
const ry = y! * cosX - z * sinX
const rz2 = y! * sinX + z * cosX
y = ry
z = rz2

// Rotation around Z axis
const cosZ = Math.cos(rotation.z)
const sinZ = Math.sin(rotation.z)
const cosZ = Math.cos(rotation.z!)
const sinZ = Math.sin(rotation.z!)
const rx2 = x * cosZ - y * sinZ
const ry2 = x * sinZ + y * cosZ
x = rx2
y = ry2
}

// Apply translation
result.positions[i] = x + translation.x
result.positions[i + 1] = y + translation.y
result.positions[i + 2] = z + translation.z
result.positions[i] = (x ?? 0) + translation.x!
result.positions[i + 1] = (y ?? 0) + translation.y!
result.positions[i + 2] = (z ?? 0) + translation.z!
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire file was made messy. Remove all the weird optional type stuff. You can assert xyz is defined in a way that doesnt compromise readbility


// Also transform normals if there was rotation
Expand All @@ -432,24 +464,24 @@ export function transformMesh(

// Apply same rotations to normals
// Rotation around Y axis
const cosY = Math.cos(rotation.y)
const sinY = Math.sin(rotation.y)
const rnx = nx * cosY - nz * sinY
const rnz = nx * sinY + nz * cosY
const cosY = Math.cos(rotation.y!)
const sinY = Math.sin(rotation.y!)
const rnx = nx! * cosY - nz! * sinY
const rnz = nx! * sinY + nz! * cosY
nx = rnx
nz = rnz

// Rotation around X axis
const cosX = Math.cos(rotation.x)
const sinX = Math.sin(rotation.x)
const rny = ny * cosX - nz * sinX
const rnz2 = ny * sinX + nz * cosX
const cosX = Math.cos(rotation.x!)
const sinX = Math.sin(rotation.x!)
const rny = ny! * cosX - nz * sinX
const rnz2 = ny! * sinX + nz * cosX
ny = rny
nz = rnz2

// Rotation around Z axis
const cosZ = Math.cos(rotation.z)
const sinZ = Math.sin(rotation.z)
const cosZ = Math.cos(rotation.z!)
const sinZ = Math.sin(rotation.z!)
const rnx2 = nx * cosZ - ny * sinZ
const rny2 = nx * sinZ + ny * cosZ
nx = rnx2
Expand Down Expand Up @@ -480,9 +512,9 @@ export function getBounds(positions: number[]): { min: Point3; max: Point3 } {
maxZ = -Infinity

for (let i = 0; i < positions.length; i += 3) {
const x = positions[i]
const y = positions[i + 1]
const z = positions[i + 2]
const x = positions[i]!
const y = positions[i + 1]!
const z = positions[i + 2]!

minX = Math.min(minX, x)
minY = Math.min(minY, y)
Expand Down
44 changes: 26 additions & 18 deletions lib/gltf/gltf-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export class GLTFBuilder {
})

// Add node to scene
this.gltf.scenes![0].nodes!.push(nodeIndex)
if (this.gltf.scenes && this.gltf.scenes[0]) {
this.gltf.scenes[0].nodes!.push(nodeIndex)
}
}

private async addOBJMeshWithMaterials(
Expand Down Expand Up @@ -243,7 +245,9 @@ export class GLTFBuilder {
})

// Add node to scene
this.gltf.scenes![0].nodes!.push(nodeIndex)
if (this.gltf.scenes && this.gltf.scenes[0]) {
this.gltf.scenes[0].nodes!.push(nodeIndex)
}
}

private async addBoxWithFaceMaterials(
Expand Down Expand Up @@ -271,7 +275,7 @@ export class GLTFBuilder {
const textureIndex = await this.addTextureFromDataUrl(box.texture.top)
if (textureIndex !== -1) {
const material = this.materials[topMaterialIndex]
if (material.pbrMetallicRoughness) {
if (material && material.pbrMetallicRoughness) {
material.pbrMetallicRoughness.baseColorTexture = {
index: textureIndex,
}
Expand All @@ -297,7 +301,7 @@ export class GLTFBuilder {
const textureIndex = await this.addTextureFromDataUrl(box.texture.bottom)
if (textureIndex !== -1) {
const material = this.materials[bottomMaterialIndex]
if (material.pbrMetallicRoughness) {
if (material && material.pbrMetallicRoughness) {
material.pbrMetallicRoughness.baseColorTexture = {
index: textureIndex,
}
Expand Down Expand Up @@ -391,7 +395,9 @@ export class GLTFBuilder {
})

// Add node to scene
this.gltf.scenes![0].nodes!.push(nodeIndex)
if (this.gltf.scenes && this.gltf.scenes[0]) {
this.gltf.scenes[0].nodes!.push(nodeIndex)
}
}

private addMesh(
Expand Down Expand Up @@ -556,23 +562,23 @@ export class GLTFBuilder {
return [r, g, b, a]
} else if (color.startsWith("rgba(")) {
const match = color.match(/rgba\(([^)]+)\)/)
if (match) {
if (match && match[1]) {
const parts = match[1].split(",").map((s) => s.trim())
return [
parseFloat(parts[0]) / 255,
parseFloat(parts[1]) / 255,
parseFloat(parts[2]) / 255,
parseFloat(parts[3]),
parseFloat(parts[0]!) / 255,
parseFloat(parts[1]!) / 255,
parseFloat(parts[2]!) / 255,
parseFloat(parts[3]!),
]
}
} else if (color.startsWith("rgb(")) {
const match = color.match(/rgb\(([^)]+)\)/)
if (match) {
if (match && match[1]) {
const parts = match[1].split(",").map((s) => s.trim())
return [
parseFloat(parts[0]) / 255,
parseFloat(parts[1]) / 255,
parseFloat(parts[2]) / 255,
parseFloat(parts[0]!) / 255,
parseFloat(parts[1]!) / 255,
parseFloat(parts[2]!) / 255,
1,
]
}
Expand Down Expand Up @@ -601,8 +607,8 @@ export class GLTFBuilder {
return -1
}

const mimeType = `image/${base64Match[1]}`
const base64Data = base64Match[2]
const mimeType = `image/${base64Match[1]!}`
const base64Data = base64Match[2]!
const imageData = Uint8Array.from(atob(base64Data), (c) =>
c.charCodeAt(0),
)
Expand Down Expand Up @@ -652,7 +658,9 @@ export class GLTFBuilder {
byteOffset,
byteLength: imageData.length,
})
this.images[i].bufferView = bufferViewIndex
if (this.images[i]) {
this.images[i]!.bufferView = bufferViewIndex
}
}
}

Expand Down Expand Up @@ -761,7 +769,7 @@ export class GLTFBuilder {
const bytes = new Uint8Array(buffer)
let binary = ""
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
binary += String.fromCharCode(bytes[i]!)
}
return btoa(binary)
}
Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type {
BoundingBox,
STLMesh,
OBJMesh,
GLTFMesh,
OBJMaterial,
Color,
Box3D,
Expand All @@ -59,6 +60,7 @@ export type {
// Re-export loaders
export { loadSTL, clearSTLCache } from "./loaders/stl"
export { loadOBJ, clearOBJCache } from "./loaders/obj"
export { loadGLTF, clearGLTFCache } from "./loaders/gltf"

// Re-export converters
export { convertCircuitJsonTo3D } from "./converters/circuit-to-3d"
Expand Down
Loading