From d3cd5ac6c6c8e352dc12cf4f1bf701d54f183301 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Jul 2024 12:38:17 -0400 Subject: [PATCH 1/4] fix(view-2d-vtkjs): revive vtkjs objects when container cycles --- cypress.config.ts | 3 +- cypress/support/component.ts | 2 +- package.json | 2 +- packages/element/examples/change-image.ts | 2 +- packages/vtkjs/src/view-2d-vtkjs.cy.ts | 24 ++++++- packages/vtkjs/src/view-2d-vtkjs.machine.ts | 75 +++++++++++++++++---- packages/vtkjs/src/view-2d-vtkjs.ts | 65 ++++++++++-------- pnpm-lock.yaml | 15 +++-- 8 files changed, 133 insertions(+), 55 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 5e83ae8d..c078cb1e 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -5,12 +5,13 @@ import viteConfig from './vite.config.js'; export default defineConfig({ component: { devServer: { - framework: 'svelte', + framework: 'cypress-ct-lit' as any, bundler: 'vite', viteConfig, }, watchForFileChanges: true, defaultCommandTimeout: 30000, + // If wanting a publicPath the same as the default in Vite 5 devServerPublicPathRoute: '', // needed for vite 5.0 https://github.com/cypress-io/cypress/issues/28347#issuecomment-2111054407 }, // videos are not reliable in github action: https://github.com/cypress-io/github-action/issues/337 diff --git a/cypress/support/component.ts b/cypress/support/component.ts index 9cd19799..c103f7ab 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -1,4 +1,4 @@ -import { mount } from 'cypress-lit'; +import { mount } from 'cypress-ct-lit'; Cypress.Commands.add('mount', mount); diff --git a/package.json b/package.json index 9f2a8b64..d5267213 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/eslint__js": "^8.42.3", "concurrently": "^8.2.2", "cypress": "^13.11.0", - "cypress-lit": "^0.0.8", + "cypress-ct-lit": "^0.4.1", "eslint": "^9.4.0", "follow-redirects": "^1.15.6", "globals": "^15.4.0", diff --git a/packages/element/examples/change-image.ts b/packages/element/examples/change-image.ts index 2e06f1ae..96cd5e50 100644 --- a/packages/element/examples/change-image.ts +++ b/packages/element/examples/change-image.ts @@ -19,7 +19,7 @@ async function setImage(imagePath: string) { // don't reset on second image load const view3d = document.querySelector('itk-view-3d'); - view3d?.getActor()?.send({ type: 'setAutoCameraReset', enableReset: false }); + view3d!.getActor()!.send({ type: 'setAutoCameraReset', enableReset: false }); } document.addEventListener('DOMContentLoaded', async function () { diff --git a/packages/vtkjs/src/view-2d-vtkjs.cy.ts b/packages/vtkjs/src/view-2d-vtkjs.cy.ts index 2c5c697f..b8b75f84 100644 --- a/packages/vtkjs/src/view-2d-vtkjs.cy.ts +++ b/packages/vtkjs/src/view-2d-vtkjs.cy.ts @@ -2,6 +2,7 @@ import { createActor, createMachine } from 'xstate'; import { setPipelineWorkerUrl, setPipelinesBaseUrl } from 'itk-wasm'; import { createLogic } from './view-2d-vtkjs.js'; import { ZarrMultiscaleSpatialImage } from '@itk-viewer/io/ZarrMultiscaleSpatialImage.js'; +import { html } from 'lit'; before(() => { const pipelineWorkerUrl = '/itk/web-workers/itk-wasm-pipeline.min.worker.js'; @@ -18,10 +19,10 @@ describe('View 2D vtk.js', () => { it('takes imageBuilt event', () => { const parent = createActor(createMachine({})).start(); const render = createActor(createLogic(), { input: { parent } }).start(); - cy.mount('
'); + cy.mount(html`
`); cy.get('#view-2d-vtkjs-container') - .then((button) => { - render.send({ type: 'setContainer', container: button[0] }); + .then((containers) => { + render.send({ type: 'setContainer', container: containers[0] }); }) .then(() => { return ZarrMultiscaleSpatialImage.fromUrl( @@ -35,4 +36,21 @@ describe('View 2D vtk.js', () => { render.send({ type: 'imageBuilt', image }); }); }); + + it('takes multiple set containers gracefully', () => { + const parent = createActor(createMachine({})).start(); + const render = createActor(createLogic(), { input: { parent } }).start(); + cy.mount( + html`
+
`, + ); + cy.get('#container1').then((containers) => { + render.send({ type: 'setContainer', container: containers[0] }); + render.send({ type: 'setContainer', container: undefined }); + }); + + cy.get('#container2').then((containers) => { + render.send({ type: 'setContainer', container: containers[0] }); + }); + }); }); diff --git a/packages/vtkjs/src/view-2d-vtkjs.machine.ts b/packages/vtkjs/src/view-2d-vtkjs.machine.ts index 20b43be7..571d356b 100644 --- a/packages/vtkjs/src/view-2d-vtkjs.machine.ts +++ b/packages/vtkjs/src/view-2d-vtkjs.machine.ts @@ -1,17 +1,23 @@ -import { AnyActorRef, Subscription, assign, sendTo, setup } from 'xstate'; -import GenericRenderWindow, { - vtkGenericRenderWindow, -} from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; +import { + AnyActorRef, + Subscription, + assign, + enqueueActions, + sendTo, + setup, +} from 'xstate'; import { BuiltImage } from '@itk-viewer/io/MultiscaleSpatialImage.js'; import { Camera, Pose } from '@itk-viewer/viewer/camera.js'; import { Axis, AxisType } from '@itk-viewer/viewer/slice-utils.js'; import { Image, ImageSnapshot } from '@itk-viewer/viewer/image.js'; export type Context = { - rendererContainer: vtkGenericRenderWindow; camera: Camera | undefined; parent: AnyActorRef; axis: AxisType; + builtImage?: BuiltImage; + sliceIndex?: number; + imageActor?: Image; imageSubscription?: Subscription; }; @@ -27,7 +33,7 @@ export const view2dLogic = setup({ events: | SetContainerEvent | { type: 'setResolution'; resolution: [number, number] } - | { type: 'imageBuilt'; image: BuiltImage } + | { type: 'imageBuilt'; image: BuiltImage; sliceIndex: number } | { type: 'setImageActor'; image: Image } | { type: 'imageSnapshot'; state: ImageSnapshot } | { type: 'setAxis'; axis: AxisType } @@ -38,7 +44,15 @@ export const view2dLogic = setup({ setContainer: () => { throw new Error('Function not implemented.'); }, - imageBuilt: () => { + applyBuiltImage: ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + __: { + image: BuiltImage; + sliceIndex: number; + }, + ) => { throw new Error('Function not implemented.'); }, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -63,9 +77,6 @@ export const view2dLogic = setup({ }).createMachine({ context: ({ input: { parent } }) => { return { - rendererContainer: GenericRenderWindow.newInstance({ - listenWindowResize: false, - }), camera: undefined, axis: Axis.K, parent, @@ -85,13 +96,48 @@ export const view2dLogic = setup({ axis, }), }, + enqueueActions(({ context, enqueue, self }) => { + const { builtImage: image, sliceIndex } = context; + if (image && sliceIndex !== undefined) { + enqueue({ + type: 'applyBuiltImage', + params: { image, sliceIndex }, + }); + } + if (context.imageActor) { + enqueue({ + type: 'imageSnapshot', + params: context.imageActor.getSnapshot(), + }); + } + if (context.camera) { + // get latest camera params + self.send({ + type: 'setCamera', + camera: context.camera, + }); + } + }), ], }, setResolution: { actions: ['forwardToParent'], }, imageBuilt: { - actions: [{ type: 'imageBuilt' }], + actions: [ + assign({ + builtImage: ({ event }) => event.image, + sliceIndex: ({ event }) => event.sliceIndex, + }), + { + type: 'applyBuiltImage', + params: ({ context: { builtImage, sliceIndex, camera } }) => ({ + image: builtImage!, + sliceIndex: sliceIndex!, + cameraPose: camera?.getSnapshot().context.pose, + }), + }, + ], }, setImageActor: { actions: [ @@ -99,8 +145,11 @@ export const view2dLogic = setup({ context.imageSubscription?.unsubscribe(); }, assign({ - imageSubscription: ({ event: { image }, self }) => - image.subscribe((state) => + imageActor: ({ event: { image } }) => image, + }), + assign({ + imageSubscription: ({ context: { imageActor }, self }) => + imageActor!.subscribe((state) => self.send({ type: 'imageSnapshot', state }), ), }), diff --git a/packages/vtkjs/src/view-2d-vtkjs.ts b/packages/vtkjs/src/view-2d-vtkjs.ts index a869a632..072040ef 100644 --- a/packages/vtkjs/src/view-2d-vtkjs.ts +++ b/packages/vtkjs/src/view-2d-vtkjs.ts @@ -2,7 +2,9 @@ import { Actor, AnyEventObject } from 'xstate'; import { mat4, vec3 } from 'gl-matrix'; import '@kitware/vtk.js/Rendering/Profiles/Volume.js'; -import { vtkGenericRenderWindow } from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; +import GenericRenderWindow, { + vtkGenericRenderWindow, +} from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper.js'; import { SlicingMode } from '@kitware/vtk.js/Rendering/Core/ImageMapper/Constants.js'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice.js'; @@ -18,6 +20,7 @@ import { } from './view-2d-vtkjs.machine.js'; import { AxisType } from '@itk-viewer/viewer/slice-utils.js'; import { ImageSnapshot } from '@itk-viewer/viewer/image.js'; +import { BuiltImage } from '@itk-viewer/io/MultiscaleSpatialImage.js'; const axisToSliceMode = { I: SlicingMode.I, @@ -26,10 +29,12 @@ const axisToSliceMode = { } as const; const setupContainer = ( - rendererContainer: vtkGenericRenderWindow, container: HTMLElement, self: Actor, ) => { + const rendererContainer = GenericRenderWindow.newInstance({ + listenWindowResize: false, + }); rendererContainer.setContainer(container as HTMLElement); rendererContainer.resize(); @@ -54,7 +59,7 @@ const setupContainer = ( const camera = renderer!.getActiveCamera(); camera.setParallelProjection(true); - return { actor, mapper, renderer, renderWindow }; + return { actor, mapper, renderer, renderWindow, rendererContainer, resizer }; }; const createImplementation = () => { @@ -62,11 +67,15 @@ const createImplementation = () => { let mapper: vtkImageMapper | undefined = undefined; let renderer: vtkRenderer | undefined = undefined; let renderWindow: vtkRenderWindow | undefined = undefined; + let rendererContainer: vtkGenericRenderWindow | undefined = undefined; + let resizer: ResizeObserver | undefined = undefined; const viewMat = mat4.create(); let addedActorToRenderer = false; - const cleanupContainer = (rendererContainer: vtkGenericRenderWindow) => { + const cleanupContainer = () => { + resizer?.disconnect(); + resizer = undefined; actor?.delete(); actor = undefined; mapper?.delete(); @@ -75,7 +84,9 @@ const createImplementation = () => { renderer = undefined; renderWindow?.delete(); renderWindow = undefined; - rendererContainer.setContainer(undefined as unknown as HTMLElement); + rendererContainer?.delete(); + rendererContainer = undefined; + addedActorToRenderer = false; }; const render = () => { @@ -87,52 +98,48 @@ const createImplementation = () => { actions: { setContainer: ({ event, - context: { rendererContainer }, self, }: { event: AnyEventObject; context: Context; self: unknown; // Actor }) => { + cleanupContainer(); + const { container } = event as SetContainerEvent; - if (!container) { - return cleanupContainer(rendererContainer); - } + if (!container) return; + const scene = setupContainer( - rendererContainer, container, self as Actor, ); + actor = scene.actor; mapper = scene.mapper; renderer = scene.renderer; renderWindow = scene.renderWindow; + rendererContainer = scene.rendererContainer; + resizer = scene.resizer; }, - imageBuilt: ({ - event, - context, - }: { - event: AnyEventObject; - context: Context; - }) => { - const { image, sliceIndex } = event; + applyBuiltImage: ( + _: unknown, + { + image, + sliceIndex, + }: { + image: BuiltImage; + sliceIndex: number; + }, + ) => { + if (!mapper || !renderer) return; const vtkImage = vtkITKHelper.convertItkToVtkImage(image); - mapper!.setInputData(vtkImage); - mapper!.setSlice(sliceIndex); + mapper.setInputData(vtkImage); + mapper.setSlice(sliceIndex); // add actor to renderer after mapper has data to avoid vtkjs message if (!addedActorToRenderer) { addedActorToRenderer = true; renderer!.addActor(actor!); - - const snap = context.camera?.getSnapshot(); - if (snap) { - toMat4(viewMat, snap.context.pose); - const cameraVtk = renderer!.getActiveCamera(); - cameraVtk.setViewMatrix(viewMat as mat4); - } else { - renderer!.resetCamera(); - } } render(); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d122942..fadac767 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,9 @@ importers: cypress: specifier: ^13.11.0 version: 13.11.0 - cypress-lit: - specifier: ^0.0.8 - version: 0.0.8(cypress@13.11.0) + cypress-ct-lit: + specifier: ^0.4.1 + version: 0.4.1(cypress@13.11.0)(lit@3.1.4) eslint: specifier: ^9.4.0 version: 9.4.0 @@ -2446,10 +2446,11 @@ packages: resolution: {integrity: sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==} engines: {node: '>= 0.1.90'} - cypress-lit@0.0.8: - resolution: {integrity: sha512-HOVhMYbgvN9mhlV1OH6XTwYGGN4hMeaIamkztRUtK7KvBItlnZBrjj6il7JToQ/CFcL4NoVr/d5B9rFS3V5jKA==} + cypress-ct-lit@0.4.1: + resolution: {integrity: sha512-iYVoBU605anRDstyxVWxQKFoiuzeA0gvj4I80aj0cym7VUCoarYFMjYA2CBEynLzJxS4yoTZC+jdff8dWYdgiA==} peerDependencies: cypress: '>=10.6.0' + lit: ^2.0.0 || ^3.0.0 cypress@13.11.0: resolution: {integrity: sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==} @@ -7940,9 +7941,11 @@ snapshots: csv-stringify: 5.6.5 stream-transform: 2.1.3 - cypress-lit@0.0.8(cypress@13.11.0): + cypress-ct-lit@0.4.1(cypress@13.11.0)(lit@3.1.4): dependencies: + '@cypress/mount-utils': 4.1.1 cypress: 13.11.0 + lit: 3.1.4 cypress@13.11.0: dependencies: From d6e828e152f733a65fae6e936edeab69f11922a0 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 25 Jul 2024 14:54:27 -0400 Subject: [PATCH 2/4] fix(itk-camera): rebind events in connectedCallback --- packages/element/src/itk-camera.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/element/src/itk-camera.ts b/packages/element/src/itk-camera.ts index ea9abfaa..0958f9cd 100644 --- a/packages/element/src/itk-camera.ts +++ b/packages/element/src/itk-camera.ts @@ -140,7 +140,8 @@ export class ItkCamera extends LitElement { ); } - firstUpdated(): void { + connectedCallback(): void { + super.connectedCallback(); this.unBind = bindCamera( this.cameraController, this, From b6a4e01de4db8777238528cfa3ff718641f720dc Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 26 Jul 2024 14:04:05 -0400 Subject: [PATCH 3/4] fix(view-controls-controller): remove and update on container cycle --- packages/element/src/view-controls-controller.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/element/src/view-controls-controller.ts b/packages/element/src/view-controls-controller.ts index 79447be3..70fdb770 100644 --- a/packages/element/src/view-controls-controller.ts +++ b/packages/element/src/view-controls-controller.ts @@ -132,6 +132,9 @@ export class ViewControls implements ReactiveController { }); }, ); + } else { + this.transferFunctionEditor?.remove(); + this.transferFunctionEditor = undefined; } } @@ -192,5 +195,9 @@ export class ViewControls implements ReactiveController { updateTransferFunctionEditor() { const rangeViewOnly = this.view === '2d'; this.transferFunctionEditor?.setRangeViewOnly(rangeViewOnly); + + if (this.imageActor) { + this.onImageActorSnapshot(this.imageActor.getSnapshot()); + } } } From 8fb3817cb3d7882939780402fb5d105009694f6a Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 29 Jul 2024 13:00:33 -0400 Subject: [PATCH 4/4] fix(view-3d-vtkjs): revive vtkjs objects when container cycles --- .changeset/loud-ears-sort.md | 6 ++ packages/vtkjs/src/view-2d-vtkjs.ts | 7 +-- packages/vtkjs/src/view-3d-vtkjs.machine.ts | 68 ++++++++++++++++----- packages/vtkjs/src/view-3d-vtkjs.ts | 54 +++++++++------- 4 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 .changeset/loud-ears-sort.md diff --git a/.changeset/loud-ears-sort.md b/.changeset/loud-ears-sort.md new file mode 100644 index 00000000..1c18d20b --- /dev/null +++ b/.changeset/loud-ears-sort.md @@ -0,0 +1,6 @@ +--- +'@itk-viewer/element': patch +'@itk-viewer/vtkjs': patch +--- + +Fix VTK.js renderers DOM mount lifecycle. diff --git a/packages/vtkjs/src/view-2d-vtkjs.ts b/packages/vtkjs/src/view-2d-vtkjs.ts index 072040ef..d24305fe 100644 --- a/packages/vtkjs/src/view-2d-vtkjs.ts +++ b/packages/vtkjs/src/view-2d-vtkjs.ts @@ -71,7 +71,6 @@ const createImplementation = () => { let resizer: ResizeObserver | undefined = undefined; const viewMat = mat4.create(); - let addedActorToRenderer = false; const cleanupContainer = () => { resizer?.disconnect(); @@ -86,7 +85,6 @@ const createImplementation = () => { renderWindow = undefined; rendererContainer?.delete(); rendererContainer = undefined; - addedActorToRenderer = false; }; const render = () => { @@ -137,9 +135,8 @@ const createImplementation = () => { mapper.setSlice(sliceIndex); // add actor to renderer after mapper has data to avoid vtkjs message - if (!addedActorToRenderer) { - addedActorToRenderer = true; - renderer!.addActor(actor!); + if (actor && !renderer.getActors().includes(actor)) { + renderer.addActor(actor!); } render(); }, diff --git a/packages/vtkjs/src/view-3d-vtkjs.machine.ts b/packages/vtkjs/src/view-3d-vtkjs.machine.ts index 12cf0bcb..dac033ef 100644 --- a/packages/vtkjs/src/view-3d-vtkjs.machine.ts +++ b/packages/vtkjs/src/view-3d-vtkjs.machine.ts @@ -1,16 +1,14 @@ -import { assign, setup, Subscription } from 'xstate'; -import GenericRenderWindow, { - vtkGenericRenderWindow, -} from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; +import { assign, setup, Subscription, enqueueActions } from 'xstate'; import { BuiltImage } from '@itk-viewer/io/MultiscaleSpatialImage.js'; import { Camera, ReadonlyPose } from '@itk-viewer/viewer/camera.js'; import { ViewportActor } from '@itk-viewer/viewer/viewport.js'; import { Image, ImageSnapshot } from '@itk-viewer/viewer/image.js'; export type Context = { - rendererContainer: vtkGenericRenderWindow; - camera: Camera | undefined; viewport: ViewportActor; + camera?: Camera; + builtImage?: BuiltImage; + imageActor?: Image; imageSubscription?: Subscription; }; @@ -37,7 +35,14 @@ export const view3dLogic = setup({ setContainer: () => { throw new Error('Function not implemented.'); }, - imageBuilt: () => { + applyBuiltImage: ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + __: { + image: BuiltImage; + }, + ) => { throw new Error('Function not implemented.'); }, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -61,9 +66,6 @@ export const view3dLogic = setup({ }).createMachine({ context: ({ input: { viewport } }) => { return { - rendererContainer: GenericRenderWindow.newInstance({ - listenWindowResize: false, - }), camera: undefined, viewport, }; @@ -74,7 +76,31 @@ export const view3dLogic = setup({ active: { on: { setContainer: { - actions: [{ type: 'setContainer' }], + actions: [ + { type: 'setContainer' }, + enqueueActions(({ context, enqueue, self }) => { + const { builtImage: image } = context; + if (image) { + enqueue({ + type: 'applyBuiltImage', + params: { image }, + }); + } + if (context.imageActor) { + enqueue({ + type: 'imageSnapshot', + params: context.imageActor.getSnapshot(), + }); + } + if (context.camera) { + // get latest camera params + self.send({ + type: 'setCamera', + camera: context.camera, + }); + } + }), + ], }, setResolution: { actions: [ @@ -88,7 +114,18 @@ export const view3dLogic = setup({ ], }, imageBuilt: { - actions: [{ type: 'imageBuilt' }], + actions: [ + assign({ + builtImage: ({ event }) => event.image, + }), + { + type: 'applyBuiltImage', + params: ({ context: { builtImage, camera } }) => ({ + image: builtImage!, + cameraPose: camera?.getSnapshot().context.pose, + }), + }, + ], }, setImageActor: { actions: [ @@ -96,8 +133,11 @@ export const view3dLogic = setup({ context.imageSubscription?.unsubscribe(); }, assign({ - imageSubscription: ({ event: { image }, self }) => - image.subscribe((state) => + imageActor: ({ event: { image } }) => image, + }), + assign({ + imageSubscription: ({ context: { imageActor }, self }) => + imageActor!.subscribe((state) => self.send({ type: 'imageSnapshot', state }), ), }), diff --git a/packages/vtkjs/src/view-3d-vtkjs.ts b/packages/vtkjs/src/view-3d-vtkjs.ts index 0a9898cb..82ada94b 100644 --- a/packages/vtkjs/src/view-3d-vtkjs.ts +++ b/packages/vtkjs/src/view-3d-vtkjs.ts @@ -10,22 +10,23 @@ import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer.js'; import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow.js'; import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper.js'; -import { vtkGenericRenderWindow } from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; +import GenericRenderWindow, { + vtkGenericRenderWindow, +} from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow.js'; import { getNodes } from '@itk-viewer/transfer-function-editor/PiecewiseUtils.js'; -import { - Context, - SetContainerEvent, - view3dLogic, -} from './view-3d-vtkjs.machine.js'; +import { SetContainerEvent, view3dLogic } from './view-3d-vtkjs.machine.js'; import { ReadonlyPose, toMat4 } from '@itk-viewer/viewer/camera.js'; import { ImageSnapshot } from '@itk-viewer/viewer/image.js'; +import { BuiltImage } from '@itk-viewer/io/MultiscaleSpatialImage.js'; const setupContainer = ( - rendererContainer: vtkGenericRenderWindow, container: HTMLElement, self: Actor, ) => { + const rendererContainer = GenericRenderWindow.newInstance({ + listenWindowResize: false, + }); rendererContainer.setContainer(container as HTMLElement); rendererContainer.resize(); @@ -47,20 +48,21 @@ const setupContainer = ( const actor = vtkVolume.newInstance(); actor.setMapper(mapper); - return { actor, mapper, renderer, renderWindow }; + return { actor, mapper, renderer, renderWindow, rendererContainer, resizer }; }; const createImplementation = () => { + let opacityFunction: vtkPiecewiseFunction | undefined = undefined; let actor: vtkVolume | undefined = undefined; let mapper: vtkVolumeMapper | undefined = undefined; let renderer: vtkRenderer | undefined = undefined; let renderWindow: vtkRenderWindow | undefined = undefined; - let opacityFunction: vtkPiecewiseFunction | undefined = undefined; + let rendererContainer: vtkGenericRenderWindow | undefined = undefined; + let resizer: ResizeObserver | undefined = undefined; const viewMat = mat4.create(); - let addedActorToRenderer = false; - const cleanupContainer = (rendererContainer: vtkGenericRenderWindow) => { + const cleanupContainer = () => { mapper?.delete(); mapper = undefined; actor?.delete(); @@ -69,7 +71,10 @@ const createImplementation = () => { renderer = undefined; renderWindow?.delete(); renderWindow = undefined; - rendererContainer.setContainer(undefined as unknown as HTMLElement); + resizer?.disconnect(); + resizer = undefined; + rendererContainer?.delete(); + rendererContainer = undefined; }; const render = () => { @@ -81,19 +86,16 @@ const createImplementation = () => { actions: { setContainer: ({ event, - context: { rendererContainer }, self, }: { event: AnyEventObject; - context: Context; self: unknown; // Actor }) => { const { container } = event as SetContainerEvent; if (!container) { - return cleanupContainer(rendererContainer); + return cleanupContainer(); } const scene = setupContainer( - rendererContainer, container, self as Actor, ); @@ -101,16 +103,24 @@ const createImplementation = () => { mapper = scene.mapper; renderer = scene.renderer; renderWindow = scene.renderWindow; + rendererContainer = scene.rendererContainer; + resizer = scene.resizer; }, - imageBuilt: ({ event }: { event: AnyEventObject }) => { - const { image } = event; + applyBuiltImage: ( + _: unknown, + { + image, + }: { + image: BuiltImage; + }, + ) => { + if (!mapper || !renderer) return; // have not set container yet const vtkImage = vtkITKHelper.convertItkToVtkImage(image); - mapper!.setInputData(vtkImage); + mapper.setInputData(vtkImage); // add actor to renderer after mapper has data to avoid vtk.js warning message - if (!addedActorToRenderer && mapper && actor) { - addedActorToRenderer = true; - renderer!.addVolume(actor!); + if (actor && !renderer.getActors().includes(actor)) { + renderer.addVolume(actor); const sampleDistance = 0.7 * Math.sqrt(