new model viewer
Build and Push Docker Image / build (push) Successful in 20s

This commit is contained in:
jason
2026-04-23 09:06:40 -05:00
parent 0a47b90e21
commit 18b3463487
3 changed files with 86 additions and 25 deletions
+63 -9
View File
@@ -179,6 +179,24 @@ function makeMaterial(color: [number, number, number] | null): THREE.MeshStandar
})
}
// ---- Binary decode helpers (geometry v2) ---------------------------------
function b64ToFloat32(b64: string): Float32Array {
const bin = atob(b64)
const buf = new ArrayBuffer(bin.length)
const u8 = new Uint8Array(buf)
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i)
return new Float32Array(buf)
}
function b64ToUint32(b64: string): Uint32Array {
const bin = atob(b64)
const buf = new ArrayBuffer(bin.length)
const u8 = new Uint8Array(buf)
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i)
return new Uint32Array(buf)
}
// ---- STEP/STP loader (geometry JSON, pre-processed server-side) ----------
async function loadStepGeometry(): Promise<void> {
@@ -193,28 +211,64 @@ async function loadStepGeometry(): Promise<void> {
throw new Error(`Could not load geometry (HTTP ${res.status})`)
}
setLoading('Building 3D scene…')
const data = await res.json() as GeometryFile
// Stream the response body so there is visible activity during large downloads
let text: string
if (res.body) {
const reader = res.body.getReader()
const chunks: Uint8Array[] = []
let loaded = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
loaded += value.byteLength
setLoading(`Downloading geometry… ${Math.round(loaded / 1024)} KB`)
}
setLoading('Parsing geometry…')
text = new TextDecoder().decode(
chunks.reduce((acc, c) => { const m = new Uint8Array(acc.length + c.length); m.set(acc); m.set(c, acc.length); return m }, new Uint8Array(0))
)
} else {
text = await res.text()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = JSON.parse(text) as GeometryFile & { version: number; meshes: any[] }
if (!data.meshes || data.meshes.length === 0) {
throw new Error('Geometry file contains no meshes.')
}
setLoading('Building 3D scene…')
const group = new THREE.Group()
const isV2 = data.version >= 2
for (const mesh of data.meshes) {
const geo = new THREE.BufferGeometry()
geo.setAttribute('position', new THREE.Float32BufferAttribute(mesh.positions, 3))
if (mesh.normals && mesh.normals.length > 0) {
geo.setAttribute('normal', new THREE.Float32BufferAttribute(mesh.normals, 3))
let positions: Float32Array
let normals: Float32Array | null
let indices: Uint32Array | null
if (isV2) {
positions = b64ToFloat32(mesh.positions)
normals = mesh.normals ? b64ToFloat32(mesh.normals) : null
indices = mesh.indices ? b64ToUint32(mesh.indices) : null
} else {
// Legacy v1: plain number arrays
positions = new Float32Array(mesh.positions as number[])
normals = mesh.normals ? new Float32Array(mesh.normals as number[]) : null
indices = mesh.indices ? new Uint32Array(mesh.indices as number[]) : null
}
if (mesh.indices && mesh.indices.length > 0) {
geo.setIndex(new THREE.Uint32BufferAttribute(mesh.indices, 1))
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
if (normals && normals.length > 0) {
geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
}
if (!mesh.normals || mesh.normals.length === 0) {
if (indices && indices.length > 0) {
geo.setIndex(new THREE.Uint32BufferAttribute(indices, 1))
}
if (!normals || normals.length === 0) {
geo.computeVertexNormals()
}
+15 -10
View File
@@ -3,14 +3,14 @@ import fs from 'fs'
import path from 'path'
export interface GeometryMesh {
positions: number[]
normals: number[] | null
indices: number[]
positions: string // base64-encoded Float32Array
normals: string | null // base64-encoded Float32Array
indices: string // base64-encoded Uint32Array
color: [number, number, number] | null
}
export interface GeometryFile {
version: 1
version: 2
sourceFile: string
meshCount: number
meshes: GeometryMesh[]
@@ -45,15 +45,20 @@ export async function convertStepFile(
}
const geo: GeometryFile = {
version: 1,
version: 2,
sourceFile: path.basename(inputPath),
meshCount: result.meshes.length,
meshes: result.meshes.map(mesh => ({
positions: Array.from(mesh.attributes.position.array),
normals: mesh.attributes.normal ? Array.from(mesh.attributes.normal.array) : null,
indices: Array.from(mesh.index.array),
meshes: result.meshes.map(mesh => {
const pos = mesh.attributes.position.array
const nor = mesh.attributes.normal?.array
const idx = mesh.index.array
return {
positions: Buffer.from(pos.buffer, pos.byteOffset, pos.byteLength).toString('base64'),
normals: nor ? Buffer.from(nor.buffer, nor.byteOffset, nor.byteLength).toString('base64') : null,
indices: Buffer.from(idx.buffer, idx.byteOffset, idx.byteLength).toString('base64'),
color: mesh.color ? [mesh.color[0], mesh.color[1], mesh.color[2]] : null,
})),
}
}),
}
fs.writeFileSync(outputPath, JSON.stringify(geo))
+4 -2
View File
@@ -60,8 +60,10 @@ export function thumbnailOutputPath(uploadsDir: string, modelId: number | bigint
export function geometryToTriangles(geo: GeometryFile): ThumbTri[] {
const tris: ThumbTri[] = []
for (const mesh of geo.meshes) {
const pos = mesh.positions
const idx = mesh.indices
const posBuf = Buffer.from(mesh.positions, 'base64')
const idxBuf = Buffer.from(mesh.indices, 'base64')
const pos = new Float32Array(posBuf.buffer, posBuf.byteOffset, posBuf.byteLength / 4)
const idx = new Uint32Array(idxBuf.buffer, idxBuf.byteOffset, idxBuf.byteLength / 4)
const r = mesh.color ? Math.round(mesh.color[0] * 255) : 136
const g = mesh.color ? Math.round(mesh.color[1] * 255) : 153
const b = mesh.color ? Math.round(mesh.color[2] * 255) : 170