From 18b346348727ad4d15b09547a53b901818bc911d Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 23 Apr 2026 09:06:40 -0500 Subject: [PATCH] new model viewer --- src/client/viewer/main.ts | 72 ++++++++++++++++++++--- src/server/services/stepConverter.ts | 27 +++++---- src/server/services/thumbnailGenerator.ts | 12 ++-- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/client/viewer/main.ts b/src/client/viewer/main.ts index f72b961..eb0ec78 100644 --- a/src/client/viewer/main.ts +++ b/src/client/viewer/main.ts @@ -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 { @@ -193,28 +211,64 @@ async function loadStepGeometry(): Promise { 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() } diff --git a/src/server/services/stepConverter.ts b/src/server/services/stepConverter.ts index 3b875a6..969cbfe 100644 --- a/src/server/services/stepConverter.ts +++ b/src/server/services/stepConverter.ts @@ -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), - color: mesh.color ? [mesh.color[0], mesh.color[1], mesh.color[2]] : null, - })), + 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)) diff --git a/src/server/services/thumbnailGenerator.ts b/src/server/services/thumbnailGenerator.ts index a1621a1..371d68d 100644 --- a/src/server/services/thumbnailGenerator.ts +++ b/src/server/services/thumbnailGenerator.ts @@ -60,11 +60,13 @@ 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 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 + 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 for (let i = 0; i < idx.length; i += 3) { const i0 = idx[i] * 3, i1 = idx[i + 1] * 3, i2 = idx[i + 2] * 3 tris.push(makeTri(