import type { Meta, StoryObj } from "@storybook/react-webpack5"; import { useEffect, useRef, useState, type ReactElement } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { WebGPURenderer } from "three/webgpu"; import type { Label, LabelPoolBase } from "./common.ts"; import { LabelPool as LabelPoolWebGL } from "./webgl/index.ts"; import { LabelPool as LabelPoolWebGPU } from "./webgpu/index.ts"; const meta: Meta = { title: "LabelPool", component: BasicTemplate, tags: ["autodocs"], argTypes: { renderer: { control: false }, cameraMode: { control: "inline-radio", options: ["perspective", "orthographic"] }, lineHeight: { control: { type: "range", min: 0.5, max: 20, step: 0.01 } }, scaleFactor: { control: { type: "range", min: 0, max: 2, step: 0.01 } }, bgOpacity: { control: { type: "range", min: 0, max: 1, step: 0.01 } }, fgOpacity: { control: { type: "range", min: 0, max: 1, step: 0.01 } }, anchorPointX: { control: { type: "range", min: 0, max: 1, step: 0.01 } }, anchorPointY: { control: { type: "range", min: 0, max: 1, step: 0.01 } }, positionX: { control: { type: "range", min: -5, max: 5, step: 0.01 } }, positionY: { control: { type: "range", min: -5, max: 5, step: 0.01 } }, positionZ: { control: { type: "range", min: -5, max: 5, step: 0.01 } }, }, args: { text: "Hello world!\nExample", lineHeight: 1, scaleFactor: 1, foregroundColor: "#000000", backgroundColor: "#ffffff", fgOpacity: 1, bgOpacity: 1, cameraMode: "perspective", billboard: false, sizeAttenuation: true, anchorPointX: 0.5, anchorPointY: 0.5, positionX: 0, positionY: 0, positionZ: 0, }, }; export default meta; type StorySceneOptions = { renderer: "webgpu" | "webgpu-force-webgl" | "webgl"; logDepthBuffer: boolean; }; class StoryScene { labelPool: LabelPoolBase; perspectiveCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 2000); orthographicCamera = new THREE.OrthographicCamera(-5, 5, 5, -5, 0.1, 1000); scene = new THREE.Scene(); renderer?: WebGPURenderer | THREE.WebGLRenderer; initialized = false; controls?: OrbitControls; perspective = true; bgCube?: THREE.Mesh; private options: StorySceneOptions; constructor(options: StorySceneOptions) { this.options = options; this.labelPool = options.renderer === "webgl" ? new LabelPoolWebGL() : new LabelPoolWebGPU(); this.perspectiveCamera.position.set(4, 4, 4); this.scene.background = new THREE.Color(0xf0f0f0); this.scene.add(new THREE.AxesHelper(5)); // show transparency in snapshot tests const cubeGeometry = new THREE.BoxGeometry(0.2, 0.2, 2); const cubeMaterial = new THREE.MeshNormalMaterial(); this.bgCube = new THREE.Mesh(cubeGeometry, cubeMaterial); this.bgCube.position.set(0, 0, -0.8); this.scene.add(this.bgCube); } dispose() { this.controls?.dispose(); this.renderer?.dispose(); } render = () => { if (!this.initialized) { return; } this.renderer?.render( this.scene, this.perspective ? this.perspectiveCamera : this.orthographicCamera, ); }; setCanvas(canvas: HTMLCanvasElement) { if (this.options.renderer === "webgpu" || this.options.renderer === "webgpu-force-webgl") { this.renderer = new WebGPURenderer({ canvas, antialias: true, logarithmicDepthBuffer: this.options.logDepthBuffer, forceWebGL: this.options.renderer === "webgpu-force-webgl", }); this.renderer.init().then( () => { this.initialized = true; this.render(); }, (err: unknown) => { console.error("Failed to initialize renderer", err); }, ); } else { this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, logarithmicDepthBuffer: this.options.logDepthBuffer, }); this.initialized = true; } this.renderer.setPixelRatio(window.devicePixelRatio); this.perspectiveCamera.aspect = canvas.clientWidth / canvas.clientHeight; this.renderer.setSize(canvas.clientWidth, canvas.clientHeight); this.controls = new OrbitControls(this.perspectiveCamera, canvas); this.controls.target.set(0, 0, 0); this.controls.update(); this.orthographicCamera.position.copy(this.perspectiveCamera.position); this.orthographicCamera.rotation.copy(this.perspectiveCamera.rotation); this.orthographicCamera.updateProjectionMatrix(); this.controls.addEventListener("change", () => { this.orthographicCamera.position.copy(this.perspectiveCamera.position); this.orthographicCamera.rotation.copy(this.perspectiveCamera.rotation); this.orthographicCamera.updateProjectionMatrix(); this.render(); }); } } function BasicTemplate({ renderer = "webgl", text, lineHeight, scaleFactor, foregroundColor, backgroundColor, fgOpacity, bgOpacity, billboard, sizeAttenuation, cameraMode = "perspective", anchorPointX, anchorPointY, positionX, positionY, positionZ, logDepthBuffer = false, }: { renderer?: "webgpu" | "webgpu-force-webgl" | "webgl"; text: string; lineHeight: number; scaleFactor: number; foregroundColor: string; backgroundColor: string; fgOpacity: number; bgOpacity: number; cameraMode: "perspective" | "orthographic"; billboard: boolean; sizeAttenuation: boolean; anchorPointX: number; anchorPointY: number; positionX: number; positionY: number; positionZ: number; logDepthBuffer: boolean; }): ReactElement { const canvasRef = useRef(null); const [storyScene] = useState(() => new StoryScene({ renderer, logDepthBuffer })); const [label, setLabel] = useState