2026-04-22 15:47:27 -05:00
|
|
|
import '../style.css'
|
|
|
|
|
import * as THREE from 'three'
|
|
|
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
|
|
|
import { STLLoader } from 'three/addons/loaders/STLLoader.js'
|
|
|
|
|
import type { GeometryFile } from '../../server/services/stepConverter'
|
|
|
|
|
|
|
|
|
|
// ---- Data injected by viewer.ejs -----------------------------------------
|
|
|
|
|
|
|
|
|
|
declare const __STEPVIEW__: {
|
|
|
|
|
modelId: number
|
|
|
|
|
fileType: 'step' | 'stp' | 'stl'
|
|
|
|
|
shareUrl: string
|
|
|
|
|
hasPdfs: boolean
|
|
|
|
|
hasGeometry: boolean // pre-processed geometry JSON exists (STEP/STP only)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Scene state ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
let renderer: THREE.WebGLRenderer
|
|
|
|
|
let scene: THREE.Scene
|
|
|
|
|
let camera: THREE.PerspectiveCamera
|
|
|
|
|
let controls: OrbitControls
|
2026-04-23 09:29:18 -05:00
|
|
|
let keyLight: THREE.DirectionalLight
|
2026-04-22 15:47:27 -05:00
|
|
|
|
|
|
|
|
// ---- UI helpers ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function setLoading(msg: string) {
|
|
|
|
|
const el = document.getElementById('loading-msg')
|
|
|
|
|
if (el) el.textContent = msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideLoading() {
|
|
|
|
|
const overlay = document.getElementById('loading-overlay') as HTMLElement
|
|
|
|
|
overlay.style.opacity = '0'
|
|
|
|
|
setTimeout(() => { overlay.style.display = 'none' }, 300)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showError(msg: string) {
|
|
|
|
|
document.getElementById('loading-overlay')!.style.display = 'none'
|
|
|
|
|
const overlay = document.getElementById('error-overlay')!
|
|
|
|
|
overlay.classList.remove('hidden')
|
|
|
|
|
overlay.classList.add('flex')
|
|
|
|
|
const msgEl = document.getElementById('error-msg')
|
|
|
|
|
if (msgEl) msgEl.textContent = msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showToast(msg: string, duration = 2200) {
|
|
|
|
|
const toast = document.getElementById('toast')!
|
|
|
|
|
const toastMsg = document.getElementById('toast-msg')!
|
|
|
|
|
toastMsg.textContent = msg
|
|
|
|
|
toast.classList.remove('opacity-0', 'pointer-events-none')
|
|
|
|
|
toast.classList.add('opacity-100')
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
toast.classList.add('opacity-0', 'pointer-events-none')
|
|
|
|
|
toast.classList.remove('opacity-100')
|
|
|
|
|
}, duration)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Scene setup ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function buildScene(canvas: HTMLCanvasElement) {
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false })
|
|
|
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
|
|
|
renderer.setClearColor(0x0a0a0f, 1)
|
|
|
|
|
renderer.shadowMap.enabled = true
|
|
|
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
|
|
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
|
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
|
|
|
renderer.toneMappingExposure = 1.1
|
|
|
|
|
|
|
|
|
|
scene = new THREE.Scene()
|
|
|
|
|
|
|
|
|
|
// Subtle environment fog
|
|
|
|
|
scene.fog = new THREE.FogExp2(0x0a0a0f, 0.0008)
|
|
|
|
|
|
|
|
|
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.001, 10000)
|
|
|
|
|
camera.position.set(5, 3, 8)
|
|
|
|
|
|
|
|
|
|
// Lighting
|
|
|
|
|
const ambient = new THREE.AmbientLight(0xffffff, 0.5)
|
|
|
|
|
scene.add(ambient)
|
|
|
|
|
|
2026-04-23 09:29:18 -05:00
|
|
|
keyLight = new THREE.DirectionalLight(0xffffff, 2.0)
|
|
|
|
|
keyLight.position.set(10, 20, 10)
|
|
|
|
|
keyLight.castShadow = true
|
|
|
|
|
keyLight.shadow.mapSize.set(2048, 2048)
|
|
|
|
|
scene.add(keyLight)
|
|
|
|
|
scene.add(keyLight.target)
|
2026-04-22 15:47:27 -05:00
|
|
|
|
|
|
|
|
const fill = new THREE.DirectionalLight(0x8899cc, 0.4)
|
|
|
|
|
fill.position.set(-10, 5, -10)
|
|
|
|
|
scene.add(fill)
|
|
|
|
|
|
|
|
|
|
const rim = new THREE.DirectionalLight(0xffffff, 0.2)
|
|
|
|
|
rim.position.set(0, -5, -10)
|
|
|
|
|
scene.add(rim)
|
|
|
|
|
|
|
|
|
|
controls = new OrbitControls(camera, renderer.domElement)
|
|
|
|
|
controls.enableDamping = true
|
|
|
|
|
controls.dampingFactor = 0.06
|
|
|
|
|
controls.minDistance = 0.001
|
|
|
|
|
controls.maxDistance = 5000
|
|
|
|
|
controls.panSpeed = 0.8
|
|
|
|
|
controls.rotateSpeed = 0.6
|
|
|
|
|
controls.zoomSpeed = 1.2
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
camera.aspect = window.innerWidth / window.innerHeight
|
|
|
|
|
camera.updateProjectionMatrix()
|
|
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Render loop ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function startRenderLoop() {
|
|
|
|
|
const clock = new THREE.Clock()
|
|
|
|
|
function tick() {
|
|
|
|
|
requestAnimationFrame(tick)
|
|
|
|
|
controls.update()
|
|
|
|
|
renderer.render(scene, camera)
|
|
|
|
|
void clock
|
|
|
|
|
}
|
|
|
|
|
tick()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Camera fit ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function fitCamera(object: THREE.Object3D) {
|
|
|
|
|
const box = new THREE.Box3().setFromObject(object)
|
|
|
|
|
const size = box.getSize(new THREE.Vector3())
|
|
|
|
|
const center = box.getCenter(new THREE.Vector3())
|
|
|
|
|
const maxDim = Math.max(size.x, size.y, size.z)
|
|
|
|
|
|
|
|
|
|
if (maxDim === 0) return
|
|
|
|
|
|
|
|
|
|
const fovRad = camera.fov * (Math.PI / 180)
|
|
|
|
|
const dist = (maxDim / (2 * Math.tan(fovRad / 2))) * 1.6
|
|
|
|
|
|
|
|
|
|
camera.near = maxDim * 0.0005
|
|
|
|
|
camera.far = maxDim * 200
|
|
|
|
|
camera.updateProjectionMatrix()
|
|
|
|
|
|
|
|
|
|
camera.position.set(
|
|
|
|
|
center.x + dist * 0.55,
|
|
|
|
|
center.y + dist * 0.35,
|
|
|
|
|
center.z + dist,
|
|
|
|
|
)
|
|
|
|
|
camera.lookAt(center)
|
|
|
|
|
controls.target.copy(center)
|
|
|
|
|
controls.update()
|
2026-04-23 09:29:18 -05:00
|
|
|
|
|
|
|
|
// Scale fog so it stays subtle at any model size.
|
|
|
|
|
// density = 0.19/dist keeps fog-factor ≈ 0.95 at the viewing distance.
|
|
|
|
|
if (scene.fog instanceof THREE.FogExp2) {
|
|
|
|
|
scene.fog.density = 0.19 / dist
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Aim key light at the model center and fit its shadow frustum around
|
|
|
|
|
// the model so shadows are correct regardless of scale.
|
|
|
|
|
const lightDist = maxDim * 2
|
|
|
|
|
keyLight.position.set(
|
|
|
|
|
center.x + lightDist * 0.5,
|
|
|
|
|
center.y + lightDist,
|
|
|
|
|
center.z + lightDist * 0.5,
|
|
|
|
|
)
|
|
|
|
|
keyLight.target.position.copy(center)
|
|
|
|
|
keyLight.target.updateMatrixWorld()
|
|
|
|
|
|
|
|
|
|
const sc = keyLight.shadow.camera
|
|
|
|
|
const ext = maxDim * 1.5
|
|
|
|
|
sc.left = -ext; sc.right = ext
|
|
|
|
|
sc.top = ext; sc.bottom = -ext
|
|
|
|
|
sc.near = lightDist * 0.1
|
|
|
|
|
sc.far = lightDist * 4
|
|
|
|
|
sc.updateProjectionMatrix()
|
2026-04-22 15:47:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Ground grid ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function addGrid(object: THREE.Object3D) {
|
|
|
|
|
const box = new THREE.Box3().setFromObject(object)
|
|
|
|
|
const size = box.getSize(new THREE.Vector3())
|
|
|
|
|
const center = box.getCenter(new THREE.Vector3())
|
|
|
|
|
const maxDim = Math.max(size.x, size.z) * 3
|
|
|
|
|
|
|
|
|
|
const grid = new THREE.GridHelper(maxDim, 20, 0x1a1a2a, 0x1a1a2a)
|
|
|
|
|
grid.position.set(center.x, box.min.y - 0.001, center.z)
|
|
|
|
|
scene.add(grid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Material factory ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function makeMaterial(color: [number, number, number] | null): THREE.MeshStandardMaterial {
|
|
|
|
|
const c = color
|
|
|
|
|
? new THREE.Color(color[0], color[1], color[2])
|
|
|
|
|
: new THREE.Color(0x8fa3b8)
|
|
|
|
|
return new THREE.MeshStandardMaterial({
|
|
|
|
|
color: c,
|
|
|
|
|
roughness: 0.45,
|
|
|
|
|
metalness: 0.25,
|
|
|
|
|
side: THREE.DoubleSide,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 09:06:40 -05:00
|
|
|
// ---- 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:47:27 -05:00
|
|
|
// ---- STEP/STP loader (geometry JSON, pre-processed server-side) ----------
|
|
|
|
|
|
|
|
|
|
async function loadStepGeometry(): Promise<void> {
|
|
|
|
|
setLoading('Fetching geometry…')
|
|
|
|
|
const res = await fetch(`/files/geometry/${__STEPVIEW__.modelId}`)
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.json().catch(() => ({})) as { processing?: boolean }
|
|
|
|
|
if (body.processing) {
|
|
|
|
|
throw new Error('This model is still being processed. Please try again in a moment.')
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Could not load geometry (HTTP ${res.status})`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 09:06:40 -05:00
|
|
|
// 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[] }
|
2026-04-22 15:47:27 -05:00
|
|
|
|
|
|
|
|
if (!data.meshes || data.meshes.length === 0) {
|
|
|
|
|
throw new Error('Geometry file contains no meshes.')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 09:06:40 -05:00
|
|
|
setLoading('Building 3D scene…')
|
2026-04-22 15:47:27 -05:00
|
|
|
const group = new THREE.Group()
|
2026-04-23 09:06:40 -05:00
|
|
|
const isV2 = data.version >= 2
|
2026-04-22 15:47:27 -05:00
|
|
|
|
|
|
|
|
for (const mesh of data.meshes) {
|
|
|
|
|
const geo = new THREE.BufferGeometry()
|
|
|
|
|
|
2026-04-23 09:06:40 -05:00
|
|
|
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
|
2026-04-22 15:47:27 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 09:06:40 -05:00
|
|
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
|
|
|
if (normals && normals.length > 0) {
|
|
|
|
|
geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
2026-04-22 15:47:27 -05:00
|
|
|
}
|
2026-04-23 09:06:40 -05:00
|
|
|
if (indices && indices.length > 0) {
|
|
|
|
|
geo.setIndex(new THREE.Uint32BufferAttribute(indices, 1))
|
|
|
|
|
}
|
|
|
|
|
if (!normals || normals.length === 0) {
|
2026-04-22 15:47:27 -05:00
|
|
|
geo.computeVertexNormals()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group.add(new THREE.Mesh(geo, makeMaterial(mesh.color)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scene.add(group)
|
|
|
|
|
fitCamera(group)
|
|
|
|
|
addGrid(group)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- STL loader (client-side, Three.js built-in) -------------------------
|
|
|
|
|
|
|
|
|
|
async function loadStl(): Promise<void> {
|
|
|
|
|
setLoading('Fetching STL file…')
|
|
|
|
|
const loader = new STLLoader()
|
|
|
|
|
|
|
|
|
|
const geometry = await new Promise<THREE.BufferGeometry>((resolve, reject) => {
|
|
|
|
|
loader.load(
|
|
|
|
|
`/files/model/${__STEPVIEW__.modelId}`,
|
|
|
|
|
resolve,
|
|
|
|
|
(xhr) => {
|
|
|
|
|
if (xhr.total) {
|
|
|
|
|
const pct = Math.round((xhr.loaded / xhr.total) * 100)
|
|
|
|
|
setLoading(`Downloading STL… ${pct}%`)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
reject,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
setLoading('Building 3D scene…')
|
|
|
|
|
geometry.computeVertexNormals()
|
|
|
|
|
|
|
|
|
|
// Center geometry at origin
|
|
|
|
|
geometry.computeBoundingBox()
|
|
|
|
|
const center = new THREE.Vector3()
|
|
|
|
|
geometry.boundingBox!.getCenter(center)
|
|
|
|
|
geometry.translate(-center.x, -center.y, -center.z)
|
|
|
|
|
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, makeMaterial(null))
|
|
|
|
|
mesh.castShadow = true
|
|
|
|
|
mesh.receiveShadow = true
|
|
|
|
|
scene.add(mesh)
|
|
|
|
|
fitCamera(mesh)
|
|
|
|
|
addGrid(mesh)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Viewer toolbar ------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function wireToolbar() {
|
|
|
|
|
// Copy-link
|
|
|
|
|
const copyBtn = document.getElementById('copy-link-btn') as HTMLButtonElement | null
|
|
|
|
|
if (copyBtn) {
|
|
|
|
|
copyBtn.addEventListener('click', async () => {
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(__STEPVIEW__.shareUrl)
|
|
|
|
|
} catch {
|
|
|
|
|
const ta = Object.assign(document.createElement('textarea'), {
|
|
|
|
|
value: __STEPVIEW__.shareUrl,
|
|
|
|
|
style: 'position:fixed;opacity:0',
|
|
|
|
|
})
|
|
|
|
|
document.body.appendChild(ta)
|
|
|
|
|
ta.select()
|
|
|
|
|
document.execCommand('copy')
|
|
|
|
|
document.body.removeChild(ta)
|
|
|
|
|
}
|
|
|
|
|
const orig = copyBtn.innerHTML
|
|
|
|
|
copyBtn.textContent = '✓ Copied!'
|
|
|
|
|
setTimeout(() => { copyBtn.innerHTML = orig }, 2000)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset camera
|
|
|
|
|
const resetBtn = document.getElementById('reset-camera-btn')
|
|
|
|
|
if (resetBtn) {
|
|
|
|
|
resetBtn.addEventListener('click', () => {
|
|
|
|
|
// Re-fit camera to scene objects (exclude grid)
|
|
|
|
|
const objects = scene.children.filter(c => !(c instanceof THREE.GridHelper))
|
|
|
|
|
const group = new THREE.Group()
|
|
|
|
|
objects.forEach(o => group.add(o.clone()))
|
|
|
|
|
if (group.children.length) fitCamera(group)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wireframe toggle
|
|
|
|
|
let wireframe = false
|
|
|
|
|
const wireBtn = document.getElementById('wireframe-btn')
|
|
|
|
|
if (wireBtn) {
|
|
|
|
|
wireBtn.addEventListener('click', () => {
|
|
|
|
|
wireframe = !wireframe
|
|
|
|
|
scene.traverse(obj => {
|
|
|
|
|
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
|
|
|
|
obj.material.wireframe = wireframe
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
wireBtn.classList.toggle('text-accent', wireframe)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PDF panel
|
|
|
|
|
const pdfToggle = document.getElementById('pdf-toggle-btn')
|
|
|
|
|
const pdfPanel = document.getElementById('pdf-panel')
|
|
|
|
|
const pdfClose = document.getElementById('pdf-close-btn')
|
|
|
|
|
|
|
|
|
|
if (pdfToggle && pdfPanel) {
|
|
|
|
|
pdfToggle.addEventListener('click', () => pdfPanel.classList.toggle('closed'))
|
|
|
|
|
}
|
|
|
|
|
if (pdfClose && pdfPanel) {
|
|
|
|
|
pdfClose.addEventListener('click', () => pdfPanel.classList.add('closed'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PDF tabs
|
|
|
|
|
document.querySelectorAll<HTMLButtonElement>('.pdf-tab').forEach(tab => {
|
|
|
|
|
tab.addEventListener('click', () => {
|
|
|
|
|
const idx = tab.dataset.tab
|
|
|
|
|
document.querySelectorAll('.pdf-tab').forEach(t => {
|
|
|
|
|
t.classList.remove('border-accent', 'text-white')
|
|
|
|
|
t.classList.add('border-transparent', 'text-gray-500')
|
|
|
|
|
})
|
|
|
|
|
tab.classList.add('border-accent', 'text-white')
|
|
|
|
|
tab.classList.remove('border-transparent', 'text-gray-500')
|
|
|
|
|
document.querySelectorAll<HTMLElement>('.pdf-frame').forEach(f => {
|
|
|
|
|
f.classList.toggle('hidden', f.dataset.frame !== idx)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Boot ----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async function boot() {
|
|
|
|
|
const canvas = document.getElementById('viewer-canvas') as HTMLCanvasElement
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
buildScene(canvas)
|
|
|
|
|
startRenderLoop()
|
|
|
|
|
wireToolbar()
|
|
|
|
|
|
|
|
|
|
if (__STEPVIEW__.fileType === 'stl') {
|
|
|
|
|
await loadStl()
|
|
|
|
|
} else {
|
|
|
|
|
// STEP / STP
|
|
|
|
|
if (!__STEPVIEW__.hasGeometry) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'This model has not finished processing yet. ' +
|
|
|
|
|
'Please check back shortly or contact your administrator.'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
await loadStepGeometry()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hideLoading()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[StepView]', err)
|
|
|
|
|
showError(err instanceof Error ? err.message : String(err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boot()
|
|
|
|
|
|
|
|
|
|
export {}
|