| 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 []; | |
| } | |
| } | |