This commit is contained in:
@@ -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) ----------
|
// ---- STEP/STP loader (geometry JSON, pre-processed server-side) ----------
|
||||||
|
|
||||||
async function loadStepGeometry(): Promise<void> {
|
async function loadStepGeometry(): Promise<void> {
|
||||||
@@ -193,28 +211,64 @@ async function loadStepGeometry(): Promise<void> {
|
|||||||
throw new Error(`Could not load geometry (HTTP ${res.status})`)
|
throw new Error(`Could not load geometry (HTTP ${res.status})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading('Building 3D scene…')
|
// Stream the response body so there is visible activity during large downloads
|
||||||
const data = await res.json() as GeometryFile
|
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) {
|
if (!data.meshes || data.meshes.length === 0) {
|
||||||
throw new Error('Geometry file contains no meshes.')
|
throw new Error('Geometry file contains no meshes.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading('Building 3D scene…')
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
|
const isV2 = data.version >= 2
|
||||||
|
|
||||||
for (const mesh of data.meshes) {
|
for (const mesh of data.meshes) {
|
||||||
const geo = new THREE.BufferGeometry()
|
const geo = new THREE.BufferGeometry()
|
||||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(mesh.positions, 3))
|
|
||||||
|
|
||||||
if (mesh.normals && mesh.normals.length > 0) {
|
let positions: Float32Array
|
||||||
geo.setAttribute('normal', new THREE.Float32BufferAttribute(mesh.normals, 3))
|
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.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
||||||
geo.setIndex(new THREE.Uint32BufferAttribute(mesh.indices, 1))
|
if (normals && normals.length > 0) {
|
||||||
|
geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
||||||
}
|
}
|
||||||
|
if (indices && indices.length > 0) {
|
||||||
if (!mesh.normals || mesh.normals.length === 0) {
|
geo.setIndex(new THREE.Uint32BufferAttribute(indices, 1))
|
||||||
|
}
|
||||||
|
if (!normals || normals.length === 0) {
|
||||||
geo.computeVertexNormals()
|
geo.computeVertexNormals()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
export interface GeometryMesh {
|
export interface GeometryMesh {
|
||||||
positions: number[]
|
positions: string // base64-encoded Float32Array
|
||||||
normals: number[] | null
|
normals: string | null // base64-encoded Float32Array
|
||||||
indices: number[]
|
indices: string // base64-encoded Uint32Array
|
||||||
color: [number, number, number] | null
|
color: [number, number, number] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeometryFile {
|
export interface GeometryFile {
|
||||||
version: 1
|
version: 2
|
||||||
sourceFile: string
|
sourceFile: string
|
||||||
meshCount: number
|
meshCount: number
|
||||||
meshes: GeometryMesh[]
|
meshes: GeometryMesh[]
|
||||||
@@ -45,15 +45,20 @@ export async function convertStepFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const geo: GeometryFile = {
|
const geo: GeometryFile = {
|
||||||
version: 1,
|
version: 2,
|
||||||
sourceFile: path.basename(inputPath),
|
sourceFile: path.basename(inputPath),
|
||||||
meshCount: result.meshes.length,
|
meshCount: result.meshes.length,
|
||||||
meshes: result.meshes.map(mesh => ({
|
meshes: result.meshes.map(mesh => {
|
||||||
positions: Array.from(mesh.attributes.position.array),
|
const pos = mesh.attributes.position.array
|
||||||
normals: mesh.attributes.normal ? Array.from(mesh.attributes.normal.array) : null,
|
const nor = mesh.attributes.normal?.array
|
||||||
indices: Array.from(mesh.index.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,
|
color: mesh.color ? [mesh.color[0], mesh.color[1], mesh.color[2]] : null,
|
||||||
})),
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(geo))
|
fs.writeFileSync(outputPath, JSON.stringify(geo))
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ export function thumbnailOutputPath(uploadsDir: string, modelId: number | bigint
|
|||||||
export function geometryToTriangles(geo: GeometryFile): ThumbTri[] {
|
export function geometryToTriangles(geo: GeometryFile): ThumbTri[] {
|
||||||
const tris: ThumbTri[] = []
|
const tris: ThumbTri[] = []
|
||||||
for (const mesh of geo.meshes) {
|
for (const mesh of geo.meshes) {
|
||||||
const pos = mesh.positions
|
const posBuf = Buffer.from(mesh.positions, 'base64')
|
||||||
const idx = mesh.indices
|
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 r = mesh.color ? Math.round(mesh.color[0] * 255) : 136
|
||||||
const g = mesh.color ? Math.round(mesh.color[1] * 255) : 153
|
const g = mesh.color ? Math.round(mesh.color[1] * 255) : 153
|
||||||
const b = mesh.color ? Math.round(mesh.color[2] * 255) : 170
|
const b = mesh.color ? Math.round(mesh.color[2] * 255) : 170
|
||||||
|
|||||||
Reference in New Issue
Block a user