mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-22 19:52:18 -05:00
226 lines
6.0 KiB
TypeScript
226 lines
6.0 KiB
TypeScript
import * as THREE from 'three'
|
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
|
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'
|
|
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js'
|
|
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
|
class ObjectPreview {
|
|
canvas: HTMLCanvasElement
|
|
url: string
|
|
format: string
|
|
yUp: boolean
|
|
gridSizeX: number
|
|
gridSizeZ: number
|
|
scene: THREE.Scene
|
|
renderer: THREE.WebGLRenderer
|
|
camera: THREE.PerspectiveCamera
|
|
controls: OrbitControls
|
|
gridHelper: THREE.GridHelper
|
|
frame: number
|
|
progressIndicator: HTMLDivElement
|
|
constructor (
|
|
canvas: HTMLCanvasElement,
|
|
progressIndicator: HTMLDivElement,
|
|
url: string,
|
|
format: string,
|
|
yUp: boolean,
|
|
gridSizeX: number,
|
|
gridSizeZ: number
|
|
) {
|
|
this.canvas = canvas
|
|
this.progressIndicator = progressIndicator
|
|
this.url = url
|
|
this.format = format
|
|
this.yUp = yUp
|
|
this.gridSizeX = gridSizeX
|
|
this.gridSizeZ = gridSizeZ
|
|
const observer = new window.IntersectionObserver(
|
|
this.onIntersectionChanged.bind(this),
|
|
{}
|
|
)
|
|
observer.observe(canvas)
|
|
}
|
|
|
|
setup (): void {
|
|
this.scene = new THREE.Scene()
|
|
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas })
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
45,
|
|
this.canvas.clientWidth / this.canvas.clientHeight,
|
|
0.1,
|
|
1000
|
|
)
|
|
this.camera.position.z = 50
|
|
this.renderer.setSize(
|
|
this.canvas.clientWidth,
|
|
this.canvas.clientHeight,
|
|
false
|
|
)
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
|
this.controls.enableDamping = true
|
|
this.controls.enablePan = false
|
|
this.controls.enableZoom = false
|
|
}
|
|
|
|
onIntersectionChanged (entries, observer): void {
|
|
this.cleanup()
|
|
if (entries[0].isIntersecting === true) {
|
|
this.load(this.url, this.format)
|
|
}
|
|
}
|
|
|
|
load (url: string, format: string): void {
|
|
let loader: OBJLoader | STLLoader | ThreeMFLoader | PLYLoader | null = null
|
|
switch (format) {
|
|
case 'obj':
|
|
loader = new OBJLoader()
|
|
break
|
|
case 'stl':
|
|
loader = new STLLoader()
|
|
break
|
|
case '3mf':
|
|
loader = new ThreeMFLoader()
|
|
break
|
|
case 'ply':
|
|
loader = new PLYLoader()
|
|
break
|
|
}
|
|
if (loader !== null) {
|
|
loader.load(
|
|
url,
|
|
this.onLoad.bind(this),
|
|
this.onProgress.bind(this),
|
|
this.onLoadError.bind(this)
|
|
)
|
|
}
|
|
}
|
|
|
|
onProgress (xhr): void {
|
|
const percentage =
|
|
Math.floor((xhr.loaded / xhr.total) * 100).toString() + '%'
|
|
this.progressIndicator.style.width = percentage
|
|
this.progressIndicator.ariaValueNow = percentage
|
|
this.progressIndicator.textContent = percentage
|
|
}
|
|
|
|
onLoad (model): void {
|
|
this.setup()
|
|
const material = new THREE.MeshNormalMaterial({
|
|
flatShading: true
|
|
})
|
|
let object: THREE.Mesh | null = null
|
|
if (model.type === 'BufferGeometry') {
|
|
object = new THREE.Mesh(model, material)
|
|
} else {
|
|
model.traverse(function (node) {
|
|
if (node instanceof THREE.Mesh) {
|
|
node.material = material
|
|
}
|
|
})
|
|
object = model
|
|
}
|
|
|
|
if (object == null) return
|
|
// Transform to screen coords from print
|
|
if (!this.yUp) {
|
|
const coordSystemTransform = new THREE.Matrix4()
|
|
coordSystemTransform.set(
|
|
1,
|
|
0,
|
|
0,
|
|
0, // x -> x
|
|
0,
|
|
0,
|
|
1,
|
|
0, // z -> y
|
|
0,
|
|
-1,
|
|
0,
|
|
0, // y -> -z
|
|
0,
|
|
0,
|
|
0,
|
|
1
|
|
)
|
|
object.applyMatrix4(coordSystemTransform)
|
|
}
|
|
// Calculate bounding volumes
|
|
const bbox = new THREE.Box3().setFromObject(object)
|
|
const centre = new THREE.Vector3()
|
|
bbox.getCenter(centre)
|
|
const bsphere = new THREE.Sphere()
|
|
bbox.getBoundingSphere(bsphere)
|
|
const modelHeight = bbox.max.y - bbox.min.y
|
|
// Configure camera
|
|
this.camera.position.z = this.camera.position.x = bsphere.radius * 1.63
|
|
this.camera.position.y = bsphere.radius * 0.75
|
|
|
|
this.controls.target = new THREE.Vector3(0, modelHeight / 2, 0)
|
|
// Centre the model
|
|
object.position.set(-centre.x, -bbox.min.y, -centre.z)
|
|
this.scene.add(object)
|
|
// Add the grid
|
|
// TODO: use grid size Z here, see #834
|
|
this.gridHelper = new THREE.GridHelper(
|
|
this.gridSizeX,
|
|
this.gridSizeX / 10,
|
|
'magenta',
|
|
'cyan'
|
|
)
|
|
this.scene.add(this.gridHelper)
|
|
// Render first frame
|
|
this.onAnimationFrame()
|
|
}
|
|
|
|
onLoadError (): void {
|
|
this.progressIndicator.classList.add('bg-danger')
|
|
this.progressIndicator.style.width = '100%'
|
|
this.progressIndicator.ariaValueNow = '100%'
|
|
this.progressIndicator.textContent = 'Load Error'
|
|
}
|
|
|
|
stopAnimation (): void {
|
|
window.cancelAnimationFrame(this.frame)
|
|
}
|
|
|
|
onAnimationFrame (): void {
|
|
this.controls.update()
|
|
this.renderer.render(this.scene, this.camera)
|
|
this.frame = window.requestAnimationFrame(this.onAnimationFrame.bind(this))
|
|
}
|
|
|
|
cleanup (): void {
|
|
this.stopAnimation()
|
|
if (typeof this.scene !== 'undefined' && this.scene !== null) {
|
|
this.scene.traverse(function (node) {
|
|
if (node instanceof THREE.Mesh) {
|
|
node.geometry.dispose()
|
|
node.material.dispose()
|
|
}
|
|
})
|
|
}
|
|
if (typeof this.renderer !== 'undefined' && this.renderer !== null) {
|
|
this.renderer.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.querySelectorAll('[data-preview]').forEach((div) => {
|
|
const canvas = div.getElementsByTagName('canvas')[0]
|
|
canvas.height = canvas.width
|
|
canvas.renderer = new ObjectPreview(
|
|
canvas,
|
|
div.getElementsByClassName('progress-bar')[0] as HTMLDivElement,
|
|
canvas.dataset.previewUrl ?? '/',
|
|
canvas.dataset.format ?? '',
|
|
canvas.dataset.yUp === 'true',
|
|
parseInt(canvas.dataset.gridSizeX ?? '10', 10),
|
|
parseInt(canvas.dataset.gridSizeZ ?? '10', 10)
|
|
)
|
|
})
|
|
})
|
|
|
|
export { ObjectPreview }
|