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