|
import type { IViewer } from "./IViewer"; |
|
import * as SPLAT from "gsplat"; |
|
|
|
export class SplatViewer implements IViewer { |
|
canvas: HTMLCanvasElement; |
|
|
|
renderer: SPLAT.WebGLRenderer; |
|
scene: SPLAT.Scene; |
|
camera: SPLAT.Camera; |
|
controls: SPLAT.OrbitControls; |
|
splat: SPLAT.Splat | null; |
|
|
|
disposed: boolean = false; |
|
topoOnly: boolean = false; |
|
|
|
vertexCount: number = 0; |
|
|
|
constructor(canvas: HTMLCanvasElement) { |
|
this.canvas = canvas; |
|
|
|
this.renderer = new SPLAT.WebGLRenderer(canvas); |
|
this.renderer.renderProgram.outlineColor = new SPLAT.Color32(180, 180, 180); |
|
|
|
this.scene = new SPLAT.Scene(); |
|
this.camera = new SPLAT.Camera(); |
|
this.controls = new SPLAT.OrbitControls(this.camera, canvas); |
|
this.controls.orbitSpeed = 3.0; |
|
this.splat = null; |
|
|
|
this.handleResize = this.handleResize.bind(this); |
|
} |
|
|
|
async loadScene(url: string, loadingBarCallback?: (progress: number) => void, topoOnly?: boolean) { |
|
this.topoOnly = topoOnly ?? false; |
|
|
|
if (url.endsWith(".splat")) { |
|
this.splat = await SPLAT.Loader.LoadAsync(url, this.scene, (progress) => { |
|
loadingBarCallback?.(progress); |
|
}); |
|
} else if (url.endsWith(".ply")) { |
|
this.splat = await SPLAT.PLYLoader.LoadAsync(url, this.scene, (progress) => { |
|
loadingBarCallback?.(progress); |
|
}); |
|
} else { |
|
throw new Error("Unsupported file format"); |
|
} |
|
|
|
this.vertexCount = this.splat.data.vertexCount; |
|
|
|
const frame = () => { |
|
this.controls.update(); |
|
this.renderer.render(this.scene, this.camera); |
|
|
|
if (!this.disposed) { |
|
requestAnimationFrame(frame); |
|
} |
|
}; |
|
|
|
this.disposed = false; |
|
|
|
this.handleResize(); |
|
window.addEventListener("resize", this.handleResize); |
|
|
|
requestAnimationFrame(frame); |
|
} |
|
|
|
handleResize() { |
|
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight); |
|
} |
|
|
|
dispose() { |
|
window.removeEventListener("resize", this.handleResize); |
|
|
|
this.controls.dispose(); |
|
this.renderer.dispose(); |
|
|
|
this.disposed = true; |
|
} |
|
|
|
async capture(): Promise<string | null> { |
|
return new Promise((resolve) => { |
|
requestAnimationFrame(() => { |
|
const offscreenCanvas = document.createElement("canvas"); |
|
offscreenCanvas.width = 512; |
|
offscreenCanvas.height = 512; |
|
const offscreenContext = offscreenCanvas.getContext("2d") as CanvasRenderingContext2D; |
|
|
|
const x = (this.canvas.width - offscreenCanvas.width) / 2; |
|
const y = (this.canvas.height - offscreenCanvas.height) / 2; |
|
|
|
offscreenContext.drawImage( |
|
this.canvas, |
|
x, |
|
y, |
|
offscreenCanvas.width, |
|
offscreenCanvas.height, |
|
0, |
|
0, |
|
offscreenCanvas.width, |
|
offscreenCanvas.height |
|
); |
|
const dataUrl = offscreenCanvas.toDataURL("image/png"); |
|
offscreenCanvas.remove(); |
|
|
|
resolve(dataUrl); |
|
}); |
|
}); |
|
} |
|
|
|
setRenderMode(mode: string): void { |
|
if (!this.splat) return; |
|
|
|
if (mode === "wireframe") { |
|
this.splat.selected = true; |
|
} else { |
|
this.splat.selected = false; |
|
} |
|
} |
|
|
|
getStats(): { name: string; value: any }[] { |
|
return []; |
|
} |
|
} |
|
|