UNPKG

@itk-viewer/element

Version:
216 lines (212 loc) 8.2 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; /// <reference types="vite/client" /> import { css, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { createRef, ref } from 'lit/directives/ref.js'; import { SelectorController } from 'xstate-lit'; import { chunk } from '@itk-viewer/io/dimensionUtils.js'; import { createHyphaMachineConfig, createRemoteViewport, } from '@itk-viewer/remote-viewport/remote-viewport.js'; import { ItkViewport } from './itk-viewport.js'; import './itk-camera.js'; let ItkRemoteViewport = class ItkRemoteViewport extends ItkViewport { constructor() { super(); this.density = 30; this.canvas = createRef(); this.canvasCtx = null; this.lastRemoteOnlineValue = false; this.renderLoopRunning = false; this.lastFrameValue = undefined; this.frameData = undefined; this.cleanDimension = (v) => Math.max(1, Math.floor(v)); this.resizer = new ResizeObserver((entries) => { if (!entries.length) return; const { width, height } = entries[0].contentRect; const resolution = [width, height].map(this.cleanDimension); this.viewport.send({ type: 'setResolution', resolution, }); }); const { remote, viewport } = createRemoteViewport(createHyphaMachineConfig()); this.viewport = viewport; this.remote = remote; this.cameraActor = new SelectorController(this, this.viewport, (state) => state.context.camera); this.remoteOnline = new SelectorController(this, this.remote, (state) => state.matches('root.online')); this.frame = new SelectorController(this, this.remote, (state) => state.context.frame); this.bounds = new SelectorController(this, this.remote, ({ context: { imageWorldBounds, clipBounds } }) => { // Compute normalized bounds const ranges = chunk(2, imageWorldBounds).map(([min, max]) => max - min); const normalizedBounds = clipBounds.map((worldBound, index) => (100 * worldBound) / ranges[Math.floor(index / 2)]); const clipBoundsWithNormalized = clipBounds.map((bound, i) => [bound, normalizedBounds[i]]); return { imageWorldBounds, clipBounds, clipBoundsWithNormalized }; }, (a, b) => { if (!a || !b) return false; return (a.imageWorldBounds === b.imageWorldBounds && a.clipBounds === b.clipBounds); }); } putFrame() { if (!this.canvasCtx || !this.frame.value) return; const { size, data } = this.frame.value; if (!data) throw new Error('No data in frame'); const [width, height] = size; // Cache ImageData to avoid allocation if (!this.frameData || this.frameData.width !== width || this.frameData.height !== height) { this.canvas.value.width = width; this.canvas.value.height = height; this.frameData = this.canvasCtx.createImageData(width, height); } this.frameData.data.set(data); this.canvasCtx.putImageData(this.frameData, 0, 0); } startRenderLoop() { if (this.renderLoopRunning || !this.remoteOnline.value) return; this.renderLoopRunning = true; const render = () => { const remoteSnap = this.remote.getSnapshot(); if (!this.isConnected || remoteSnap.status === 'stopped') { this.renderLoopRunning = false; return; } this.remote.send({ type: 'render' }); requestAnimationFrame(render); }; requestAnimationFrame(render); } connectedCallback() { super.connectedCallback(); this.startRenderLoop(); } firstUpdated() { const canvas = this.canvas.value; if (!canvas) throw new Error('canvas not found'); this.canvasCtx = canvas.getContext('2d'); this.resizer.observe(canvas); } startConnection() { if (!this.serverConfig) return; this.remote.send({ type: 'connect', config: this.serverConfig, }); } willUpdate(changedProperties) { if (changedProperties.has('serverConfig')) { this.startConnection(); } if (changedProperties.has('density')) { this.remote.send({ type: 'updateRenderer', state: { density: this.density }, }); } if (this.frame.value && this.frame.value !== this.lastFrameValue) { this.lastFrameValue = this.frame.value; this.putFrame(); } if (this.remoteOnline.value !== this.lastRemoteOnlineValue) { this.lastRemoteOnlineValue = this.remoteOnline.value; if (this.remoteOnline.value) { this.startRenderLoop(); } } } onDensity(event) { const target = event.target; this.density = target.valueAsNumber; } onBounds(event, index) { const target = event.target; const normalizedBound = target.valueAsNumber; // Find dimension range to go from normalized to world bounds const { imageWorldBounds } = this.remote.getSnapshot().context; if (!imageWorldBounds) { console.error('No image world bounds'); return; } const [lowerBound, upperBound] = index % 2 === 0 ? [index, index + 1] : [index - 1, index]; const range = imageWorldBounds[upperBound] - imageWorldBounds[lowerBound]; const floor = imageWorldBounds[lowerBound]; const currentBounds = [ ...this.remote.getSnapshot().context.clipBounds, ]; currentBounds[index] = range * (normalizedBound / 100) + floor; this.remote.send({ type: 'setClipBounds', clipBounds: currentBounds, }); } render() { return html ` <h1>Remote viewport</h1> <p>Server Config: ${JSON.stringify(this.serverConfig)}</p> <div> Density: ${this.density} <input .valueAsNumber=${this.density} @change="${this.onDensity}" type="range" min="1.0" max="70.0" step="1.0" /> </div> ${this.bounds.value.clipBoundsWithNormalized.map(([world, normalized], index) => html ` <label >Bound: ${world} <input .valueAsNumber=${normalized} @change="${(e) => this.onBounds(e, index)}" type="range" min="0" max="100.0" step="1" /> </label> `)} <itk-camera .actor=${this.cameraActor.value} class="camera"> <canvas ${ref(this.canvas)} class="canvas"></canvas> </itk-camera> `; } static { this.styles = css ` :host { display: flex; flex-direction: column; } .camera { flex: 1; min-height: 0; overflow: hidden; } .canvas { width: 100%; height: 100%; `; } }; __decorate([ property({ type: Object, attribute: 'server-config' }) ], ItkRemoteViewport.prototype, "serverConfig", void 0); __decorate([ property({ type: Number }) ], ItkRemoteViewport.prototype, "density", void 0); ItkRemoteViewport = __decorate([ customElement('itk-remote-viewport') ], ItkRemoteViewport); export { ItkRemoteViewport }; //# sourceMappingURL=itk-remote-viewport.js.map