diff --git a/src/components/VtkThreeView.vue b/src/components/VtkThreeView.vue index 1e7d17f01..7e7ab3298 100644 --- a/src/components/VtkThreeView.vue +++ b/src/components/VtkThreeView.vue @@ -6,7 +6,9 @@ class="vtk-view" ref="vtkContainerRef" data-testid="vtk-view vtk-three-view" - /> + > + +
@@ -77,6 +79,7 @@ import type { Vector3 } from '@kitware/vtk.js/types'; import { useProxyManager } from '@/src/composables/proxyManager'; import ViewOverlayGrid from '@/src/components/ViewOverlayGrid.vue'; +import { onVTKEvent } from '@/src/composables/onVTKEvent'; import { useResizeObserver } from '../composables/useResizeObserver'; import { useCurrentImage } from '../composables/useCurrentImage'; import { useCameraOrientation } from '../composables/useCameraOrientation'; @@ -427,22 +430,51 @@ export default defineComponent({ // --- view proxy setup --- // - const { viewProxy, setContainer: setViewProxyContainer } = - useViewProxy(viewID, ViewProxyType.Volume); + const { viewProxy } = useViewProxy( + viewID, + ViewProxyType.Volume + ); + + const canvasRef = ref(null); + + const interactor = computed(() => viewProxy.value.getInteractor()); + onVTKEvent(interactor, 'onRenderEvent' as 'onModified', () => { + if (!canvasRef.value) return; + const ctx = canvasRef.value.getContext('2d'); + const src = viewProxy.value.getOpenGLRenderWindow().getCanvas(); + if (ctx && src) { + ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height); + ctx.drawImage(src, 0, 0); + } + }); onMounted(() => { viewProxy.value.setOrientationAxesVisibility(true); viewProxy.value.setOrientationAxesType('cube'); viewProxy.value.setBackground([0, 0, 0, 0]); - setViewProxyContainer(vtkContainerRef.value); + viewProxy.value.setInteractionContainer(canvasRef.value); + // setViewProxyContainer(vtkContainerRef.value); }); onBeforeUnmount(() => { - setViewProxyContainer(null); - viewProxy.value.setContainer(null); + // setViewProxyContainer(null); }); - useResizeObserver(vtkContainerRef, () => viewProxy.value.resize()); + useResizeObserver(vtkContainerRef, (entry) => { + const bbox = entry.contentRect; + if (!bbox.width || !bbox.height) return; + + canvasRef.value?.setAttribute( + 'width', + String(bbox.width * window.devicePixelRatio) + ); + canvasRef.value?.setAttribute( + 'height', + String(bbox.height * window.devicePixelRatio) + ); + + viewProxy.value.setSize(bbox.width, bbox.height); + }); // --- scene setup --- // @@ -606,6 +638,7 @@ export default defineComponent({ return { vtkContainerRef, + canvasRef, viewID, active: false, topLeftLabel: computed( @@ -628,4 +661,9 @@ export default defineComponent({ background-color: black; grid-template-columns: auto; } + +.ccc { + width: 100%; + height: 100%; +} diff --git a/src/components/VtkTwoView.vue b/src/components/VtkTwoView.vue index 542f9dc05..8a5b6e701 100644 --- a/src/components/VtkTwoView.vue +++ b/src/components/VtkTwoView.vue @@ -38,7 +38,9 @@ class="vtk-view" ref="vtkContainerRef" data-testid="vtk-view vtk-two-view" - /> + > + +
@@ -208,6 +210,7 @@ import { useToolSelectionStore } from '@/src/store/tools/toolSelection'; import { useAnnotationToolStore } from '@/src/store/tools'; import { doesToolFrameMatchViewAxis } from '@/src/composables/annotationTool'; import type { TypedArray } from '@kitware/vtk.js/types'; +import { onVTKEvent } from '@/src/composables/onVTKEvent'; import { useResizeObserver } from '../composables/useResizeObserver'; import { useOrientationLabels } from '../composables/useOrientationLabels'; import { getLPSAxisFromDir } from '../utils/lps'; @@ -397,8 +400,23 @@ export default defineComponent({ // --- view proxy setup --- // - const { viewProxy, setContainer: setViewProxyContainer } = - useViewProxy(viewID, ViewProxyType.Slice); + const { viewProxy } = useViewProxy( + viewID, + ViewProxyType.Slice + ); + + const canvasRef = ref(null); + + const interactor = computed(() => viewProxy.value.getInteractor()); + onVTKEvent(interactor, 'onRenderEvent' as 'onModified', () => { + if (!canvasRef.value) return; + const ctx = canvasRef.value.getContext('2d'); + const src = viewProxy.value.getOpenGLRenderWindow().getCanvas(); + if (ctx && src) { + ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height); + ctx.drawImage(src, 0, 0); + } + }); onBeforeMount(() => { // do this before mount, as the ManipulatorTools run onMounted @@ -407,12 +425,13 @@ export default defineComponent({ }); onMounted(() => { - setViewProxyContainer(vtkContainerRef.value); + // setViewProxyContainer(vtkContainerRef.value); + viewProxy.value.setInteractionContainer(canvasRef.value); viewProxy.value.setOrientationAxesVisibility(false); }); onBeforeUnmount(() => { - setViewProxyContainer(null); + // setViewProxyContainer(null); }); // --- Slicing setup --- // @@ -457,7 +476,21 @@ export default defineComponent({ // --- resizing --- // - useResizeObserver(vtkContainerRef, () => viewProxy.value.resize()); + useResizeObserver(vtkContainerRef, (entry) => { + const bbox = entry.contentRect; + if (!bbox.width || !bbox.height) return; + + canvasRef.value?.setAttribute( + 'width', + String(bbox.width * window.devicePixelRatio) + ); + canvasRef.value?.setAttribute( + 'height', + String(bbox.height * window.devicePixelRatio) + ); + + viewProxy.value.setSize(bbox.width, bbox.height); + }); // Used by SVG tool widgets for resizeCallback const toolContainer = ref(); @@ -877,6 +910,7 @@ export default defineComponent({ return { vtkContainerRef, + canvasRef, toolContainer, viewID, viewProxy, @@ -911,4 +945,8 @@ export default defineComponent({ height: 32px; cursor: pointer; } +.ccc { + width: 100%; + height: 100%; +} diff --git a/src/composables/useViewProxy.ts b/src/composables/useViewProxy.ts index e2a85ce5f..dd9413cae 100644 --- a/src/composables/useViewProxy.ts +++ b/src/composables/useViewProxy.ts @@ -1,6 +1,6 @@ import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import { computed, onUnmounted, ref, unref, watch, watchEffect } from 'vue'; -import { MaybeRef, useElementSize } from '@vueuse/core'; +import { computed, onUnmounted, ref, unref, watch } from 'vue'; +import { MaybeRef } from '@vueuse/core'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; import { Maybe } from '@/src/types'; import { ViewProxyType } from '../core/proxies'; @@ -23,7 +23,9 @@ export function useViewProxy( ); watch(viewProxy, (curViewProxy, oldViewProxy) => { - oldViewProxy.setContainer(null); + if (oldViewProxy !== curViewProxy) { + oldViewProxy.setContainer(null); + } curViewProxy.setContainer(container.value); }); @@ -42,21 +44,14 @@ export function useViewProxy( function useMountedViewProxy( viewProxy: MaybeRef> ) { - const mounted = ref(false); - const container = ref>(unref(viewProxy)?.getContainer()); onVTKEvent(viewProxy, 'onModified', () => { container.value = unref(viewProxy)?.getContainer(); }); - const { width, height } = useElementSize(container); - - const updateMounted = () => { - // view is considered mounted when the container has a non-zero size - mounted.value = !!(width.value && height.value); - }; - - watchEffect(() => updateMounted()); + const mounted = computed(() => { + return !!container.value; + }); return mounted; } diff --git a/src/vtk/LPSView2DProxy/index.d.ts b/src/vtk/LPSView2DProxy/index.d.ts index a3d0d6e11..402acd2c3 100644 --- a/src/vtk/LPSView2DProxy/index.d.ts +++ b/src/vtk/LPSView2DProxy/index.d.ts @@ -1,8 +1,12 @@ import { vec3 } from 'gl-matrix'; import { vtkView2DProxy } from '@kitware/vtk.js/Proxy/Core/View2DProxy'; -import { ViewProxyCustomizations } from '@/src/vtk/LPSView3DProxy'; +import vtkLPSView3DProxy, { + ViewProxyCustomizations, +} from '@/src/vtk/LPSView3DProxy'; -export interface vtkLPSView2DProxy extends vtkView2DProxy { +export interface vtkLPSView2DProxy + extends vtkView2DProxy, + ViewProxyCustomizations { resizeToFit(lookAxis: Vector3, viewUpAxis: Vector3, worldDims: Vector3); resetCamera(boundsToUse?: number[]); /** diff --git a/src/vtk/LPSView3DProxy/index.d.ts b/src/vtk/LPSView3DProxy/index.d.ts index dee06dd6b..705daf603 100644 --- a/src/vtk/LPSView3DProxy/index.d.ts +++ b/src/vtk/LPSView3DProxy/index.d.ts @@ -1,14 +1,22 @@ import { vec3 } from 'gl-matrix'; import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator'; +import { Maybe } from '@/src/types'; -export interface vtkLPSView3DProxy extends vtkViewProxy { +export interface ViewProxyCustomizations { removeAllRepresentations(): void; updateCamera(directionOfProjection: vec3, viewUp: vec3, focalPoint: vec3); getInteractorStyle2D(): vtkInteractorStyleManipulator; getInteractorStyle3D(): vtkInteractorStyleManipulator; + setInteractionContainer(el: Maybe): boolean; + getInteractionContainer(): Maybe; + setSize(w: number, h: number): boolean; } +export interface vtkLPSView3DProxy + extends vtkViewProxy, + ViewProxyCustomizations {} + export function commonViewCustomizations(publicAPI: any, model: any): void; // TODO extend, newInstance... diff --git a/src/vtk/LPSView3DProxy/index.js b/src/vtk/LPSView3DProxy/index.js index 879185fb6..5e53da7ae 100644 --- a/src/vtk/LPSView3DProxy/index.js +++ b/src/vtk/LPSView3DProxy/index.js @@ -1,10 +1,29 @@ import { vec3 } from 'gl-matrix'; import macro from '@kitware/vtk.js/macro'; import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; +import vtkOffscreenRenderWindowInteractor from '@/src/vtk/vtkOffscreenRenderWindowInteractor'; + +export function replaceInteractor(publicAPI, model) { + // prep to remove the old interactor + const style = model.interactor.getInteractorStyle(); + const orientationWidgetEnabled = model.orientationWidget.getEnabled(); + model.orientationWidget.setEnabled(false); + + // delete the old interactor in favor of the new one + model.interactor.delete(); + + model.interactor = vtkOffscreenRenderWindowInteractor.newInstance(); + model.interactor.setView(model._openGLRenderWindow); + model.interactor.setInteractorStyle(style); + model.orientationWidget.setInteractor(model.interactor); + model.orientationWidget.setEnabled(orientationWidgetEnabled); +} export function commonViewCustomizations(publicAPI, model) { const delayedRender = macro.debounce(model.renderWindow.render, 5); + replaceInteractor(publicAPI, model); + // override resize to avoid flickering from rendering later publicAPI.resize = () => { if (model.container) { @@ -68,6 +87,33 @@ export function commonViewCustomizations(publicAPI, model) { resetCamera(args); model.renderer.updateLightsGeometryToFollowCamera(); }; + + publicAPI.setSize = (width, height) => { + const container = publicAPI.getContainer(); + if (!container) throw new Error('No container'); + setTimeout(() => { + container.style.width = `${width}px`; + container.style.height = `${height}px`; + publicAPI.resize(); + }, 0); + }; + + publicAPI.setInteractionContainer = (el) => { + return model.interactor.setInteractionContainer(el); + }; + + publicAPI.getInteractionContainer = () => { + return model.interactor.getInteractionContainer(); + }; + + // initialize + + const container = document.createElement('div'); + container.style.display = 'block'; + container.style.visibility = 'hidden'; + container.style.position = 'absolute'; + document.body.appendChild(container); + publicAPI.setContainer(container); } function vtkLPSView3DProxy(publicAPI, model) { diff --git a/src/vtk/vtkOffscreenRenderWindowInteractor/index.ts b/src/vtk/vtkOffscreenRenderWindowInteractor/index.ts new file mode 100644 index 000000000..be38fe8ed --- /dev/null +++ b/src/vtk/vtkOffscreenRenderWindowInteractor/index.ts @@ -0,0 +1,97 @@ +import { Maybe } from '@/src/types'; +import * as macros from '@kitware/vtk.js/macros'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; + +export interface vtkOffscreenRenderWindowInteractor + extends vtkRenderWindowInteractor { + bindInteractionContainer(): void; + unbindInteractionContainer(): void; + setInteractionContainer(el: Maybe): boolean; + getInteractionContainer(): Maybe; +} + +interface Model { + interactor: vtkRenderWindowInteractor; + interactionContainer: Maybe; + [prop: string]: any; +} + +function replaceGetScreenEventPositionFor( + publicAPI: vtkOffscreenRenderWindowInteractor, + model: Model +) { + function updateCurrentRenderer(x: number, y: number) { + if (!model._forcedRenderer) { + model.currentRenderer = publicAPI.findPokedRenderer(x, y); + } + } + + function getScreenEventPositionFor(source: PointerEvent) { + if (!model.interactionContainer) + throw new Error('Interaction container is not set!'); + + const canvas = model._view.getCanvas(); + const bounds = model.interactionContainer.getBoundingClientRect(); + const scaleX = canvas.width / bounds.width; + const scaleY = canvas.height / bounds.height; + const position = { + x: scaleX * (source.clientX - bounds.left), + y: scaleY * (bounds.height - source.clientY + bounds.top), + z: 0, + }; + + updateCurrentRenderer(position.x, position.y); + return position; + } + + model._getScreenEventPositionFor = getScreenEventPositionFor; +} + +function vtkOffscreenRenderWindowInteractor( + publicAPI: vtkOffscreenRenderWindowInteractor, + model: Model +) { + model.classHierarchy.push('vtkOffscreenRenderWindowInteractor'); + + const { setInteractionContainer } = publicAPI; + + publicAPI.setInteractionContainer = (el: Maybe) => { + if (model.container) { + publicAPI.unbindEvents(); + } + + const changed = setInteractionContainer(el); + + if (el) { + publicAPI.bindEvents(el); + } + + return changed; + }; +} + +export function extend( + publicAPI: vtkOffscreenRenderWindowInteractor, + model: Model, + initialValues = {} +) { + // should happen before extending the RenderWindowInteractor + replaceGetScreenEventPositionFor(publicAPI, model); + + vtkRenderWindowInteractor.extend(publicAPI, model, initialValues); + + macros.setGet(publicAPI, model, ['interactionContainer']); + + vtkOffscreenRenderWindowInteractor( + publicAPI as vtkOffscreenRenderWindowInteractor, + model as Model + ); +} + +export const newInstance = macros.newInstance( + // @ts-ignore TODO fix this typing issue + extend, + 'vtkOffscreenRenderWindowInteractor' +); + +export default { newInstance, extend };