UNPKG

@itk-viewer/element

Version:
191 lines (160 loc) 4.76 kB
import { LitElement, PropertyValues, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { ReadonlyQuat, ReadonlyVec3, quat, vec3 } from 'gl-matrix'; import { createArcballCamera, ArcballCamera } from '@itk-viewer/arcball'; import { Camera, Pose } from '@itk-viewer/viewer/camera.js'; import { SelectorController } from 'xstate-lit'; const PAN_SPEED = 1; const ZOOM_SPEED = 0.001; const bindCamera = ( camera: ArcballCamera, viewport: HTMLElement, onUpdate: ( center: ReadonlyVec3, rotation: ReadonlyQuat, distance: number, ) => unknown, ) => { let width = viewport.clientWidth; let height = viewport.clientHeight; const updateView = () => { onUpdate(camera.center, camera.rotation, camera.distance); }; const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { width = entry.contentRect.width; height = entry.contentRect.height; } }); resizeObserver.observe(viewport); let rotate = false; let pan = false; let scale = false; const onPointerDown = (e: PointerEvent) => { if (e.button === 0) { rotate = true; } else if (e.button === 1) { scale = true; } else if (e.button === 2) { pan = true; } }; viewport.addEventListener('pointerdown', onPointerDown); const onPointerUp = (e: PointerEvent) => { if (e.button === 0) { rotate = false; } else if (e.button === 1) { scale = false; } else if (e.button === 2) { pan = false; } }; window.addEventListener('pointerup', onPointerUp); let prevPointerX = 0; let prevPointerY = 0; const onPointerMove = (e: PointerEvent) => { const pointerX = e.offsetX; const pointerY = e.offsetY; if (rotate) { camera.rotate( [pointerX / width - 0.5, pointerY / height - 0.5], [prevPointerX / width - 0.5, prevPointerY / height - 0.5], ); } if (pan) { camera.pan([ (PAN_SPEED * (pointerX - prevPointerX)) / width, (PAN_SPEED * (pointerY - prevPointerY)) / height, ]); } if (scale) { const d = pointerY - prevPointerY; if (d) camera.distance *= Math.exp(d / height); } prevPointerX = pointerX; prevPointerY = pointerY; if (!rotate && !pan && !scale) return; updateView(); }; viewport.addEventListener('pointermove', onPointerMove); const onWheel = (e: WheelEvent) => { camera.zoom(ZOOM_SPEED * camera.distance * e.deltaY); updateView(); }; viewport.addEventListener('wheel', onWheel, { passive: false }); const preventDefault = (e: Event) => e.preventDefault(); viewport.addEventListener('contextmenu', preventDefault); const unBind = () => { resizeObserver.disconnect(); viewport.removeEventListener('pointerdown', onPointerDown); window.removeEventListener('pointerup', onPointerUp); viewport.removeEventListener('pointermove', onPointerMove); viewport.removeEventListener('wheel', onWheel); viewport.removeEventListener('contextmenu', preventDefault); }; return unBind; }; @customElement('itk-camera') export class ItkCamera extends LitElement { @property({ attribute: false }) actor: Camera | undefined; oldPose: Pose | undefined; pose: SelectorController<Camera, Pose> | undefined; cameraController: ArcballCamera; unBind: (() => unknown) | undefined; constructor() { super(); this.cameraController = createArcballCamera( [0, 0, -1], [0, 0, 0], [0, 1, 0], ); } connectedCallback(): void { super.connectedCallback(); this.unBind = bindCamera( this.cameraController, this, (center, rotation, distance) => { if (!this.actor) return; this.actor.send({ type: 'setPose', pose: { center, rotation, distance, }, }); }, ); } disconnectedCallback(): void { super.disconnectedCallback(); this.unBind?.(); } willUpdate(changedProperties: PropertyValues<this>) { if (changedProperties.has('actor') && this.actor) { this.pose = new SelectorController( this, this.actor, (state) => state?.context.pose, ); } if (this.pose?.value !== this.oldPose) { this.oldPose = this.pose?.value; if (this.pose?.value) { vec3.copy(this.cameraController.center, this.pose.value.center); quat.copy(this.cameraController.rotation, this.pose.value.rotation); this.cameraController.distance = this.pose.value.distance; } } } render() { return html`<slot></slot>`; } } declare global { interface HTMLElementTagNameMap { 'itk-camera': ItkCamera; } }