Skip to content

Commit

Permalink
Merge pull request #165 from PaulHax/fix-lifecycle
Browse files Browse the repository at this point in the history
Fix VTK.js renderers DOM mount lifecycle
  • Loading branch information
thewtex authored Jul 31, 2024
2 parents 4ff0280 + 8fb3817 commit a0d9ae4
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 96 deletions.
6 changes: 6 additions & 0 deletions .changeset/loud-ears-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@itk-viewer/element': patch
'@itk-viewer/vtkjs': patch
---

Fix VTK.js renderers DOM mount lifecycle.
3 changes: 2 additions & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mount } from 'cypress-lit';
import { mount } from 'cypress-ct-lit';

Cypress.Commands.add('mount', mount);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/element/examples/change-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function setImage(imagePath: string) {

// don't reset on second image load
const view3d = document.querySelector<ItkView3d>('itk-view-3d');
view3d?.getActor()?.send({ type: 'setAutoCameraReset', enableReset: false });
view3d!.getActor()!.send({ type: 'setAutoCameraReset', enableReset: false });
}

document.addEventListener('DOMContentLoaded', async function () {
Expand Down
3 changes: 2 additions & 1 deletion packages/element/src/itk-camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ export class ItkCamera extends LitElement {
);
}

firstUpdated(): void {
connectedCallback(): void {
super.connectedCallback();
this.unBind = bindCamera(
this.cameraController,
this,
Expand Down
7 changes: 7 additions & 0 deletions packages/element/src/view-controls-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ export class ViewControls implements ReactiveController {
});
},
);
} else {
this.transferFunctionEditor?.remove();
this.transferFunctionEditor = undefined;
}
}

Expand Down Expand Up @@ -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());
}
}
}
24 changes: 21 additions & 3 deletions packages/vtkjs/src/view-2d-vtkjs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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('<div id="view-2d-vtkjs-container"></div>');
cy.mount(html`<div id="view-2d-vtkjs-container"></div>`);
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(
Expand All @@ -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`<div id="container1"></div>
<div id="container2"></div>`,
);
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] });
});
});
});
75 changes: 62 additions & 13 deletions packages/vtkjs/src/view-2d-vtkjs.machine.ts
Original file line number Diff line number Diff line change
@@ -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;
};

Expand All @@ -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 }
Expand All @@ -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
Expand All @@ -63,9 +77,6 @@ export const view2dLogic = setup({
}).createMachine({
context: ({ input: { parent } }) => {
return {
rendererContainer: GenericRenderWindow.newInstance({
listenWindowResize: false,
}),
camera: undefined,
axis: Axis.K,
parent,
Expand All @@ -85,22 +96,60 @@ 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: [
({ context }) => {
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 }),
),
}),
Expand Down
70 changes: 37 additions & 33 deletions packages/vtkjs/src/view-2d-vtkjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -26,10 +29,12 @@ const axisToSliceMode = {
} as const;

const setupContainer = (
rendererContainer: vtkGenericRenderWindow,
container: HTMLElement,
self: Actor<typeof view2dLogic>,
) => {
const rendererContainer = GenericRenderWindow.newInstance({
listenWindowResize: false,
});
rendererContainer.setContainer(container as HTMLElement);
rendererContainer.resize();

Expand All @@ -54,19 +59,22 @@ const setupContainer = (
const camera = renderer!.getActiveCamera();
camera.setParallelProjection(true);

return { actor, mapper, renderer, renderWindow };
return { actor, mapper, renderer, renderWindow, rendererContainer, resizer };
};

const createImplementation = () => {
let actor: vtkImageSlice | undefined = undefined;
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();
Expand All @@ -75,7 +83,8 @@ const createImplementation = () => {
renderer = undefined;
renderWindow?.delete();
renderWindow = undefined;
rendererContainer.setContainer(undefined as unknown as HTMLElement);
rendererContainer?.delete();
rendererContainer = undefined;
};

const render = () => {
Expand All @@ -87,52 +96,47 @@ const createImplementation = () => {
actions: {
setContainer: ({
event,
context: { rendererContainer },
self,
}: {
event: AnyEventObject;
context: Context;
self: unknown; // Actor<typeof view2dLogic>
}) => {
cleanupContainer();

const { container } = event as SetContainerEvent;
if (!container) {
return cleanupContainer(rendererContainer);
}
if (!container) return;

const scene = setupContainer(
rendererContainer,
container,
self as Actor<typeof view2dLogic>,
);

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();
}
if (actor && !renderer.getActors().includes(actor)) {
renderer.addActor(actor!);
}
render();
},
Expand Down
Loading

0 comments on commit a0d9ae4

Please sign in to comment.