From 46a91cc806c18e25a0051d2ad500360b865a6ff8 Mon Sep 17 00:00:00 2001 From: Oleksandr Iefymchuk Date: Mon, 13 Nov 2023 18:50:12 +0200 Subject: [PATCH] EPMUII-8073-Zoom-in/out-improvements --- public/sprite.svg | 10 +- src/engine/Graphics2d.js | 152 ++++++++++++++++------------- src/engine/tools2d/ToolDistance.js | 12 ++- src/engine/tools2d/ToolPick.js | 6 +- src/engine/tools2d/ToolTypes.js | 9 +- src/ui/Main.module.css | 38 +++----- src/ui/UiZoomTools.jsx | 66 +++++++++++-- 7 files changed, 182 insertions(+), 111 deletions(-) diff --git a/public/sprite.svg b/public/sprite.svg index c01baef0..93f56aec 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -197,8 +197,14 @@ - - + + + + + + + + diff --git a/src/engine/Graphics2d.js b/src/engine/Graphics2d.js index a46d90cb..ae7d04b6 100644 --- a/src/engine/Graphics2d.js +++ b/src/engine/Graphics2d.js @@ -166,10 +166,6 @@ class Graphics2d extends React.Component { let wScreen = 0, hScreen = 0; - const xPos = store.render2dxPos; - const yPos = store.render2dyPos; - const zoom = store.render2dZoom; - if (mode2d === Modes2d.TRANSVERSE) { // calc screen rect based on physics volume slice size (z slice) const xyRratio = pbox.x / pbox.y; @@ -218,17 +214,15 @@ class Graphics2d extends React.Component { let zSlice = Math.floor(zDim * sliceRatio); zSlice = zSlice < zDim ? zSlice : zDim - 1; const zOff = zSlice * xyDim; - const xStep = (zoom * xDim) / wScreen; - const yStep = (zoom * yDim) / hScreen; + const xStep = xDim / wScreen; + const yStep = yDim / hScreen; let j = 0; - let ay = yPos * yDim; if (vol.m_bytesPerVoxel === ONE) { - for (let y = 0; y < hScreen; y++, ay += yStep) { - const ySrc = Math.floor(ay); + for (let y = 0; y < hScreen; y++) { + const ySrc = Math.floor(y * yStep); const yOff = ySrc * xDim; - let ax = xPos * xDim; - for (let x = 0; x < wScreen; x++, ax += xStep) { - const xSrc = Math.floor(ax); + for (let x = 0; x < wScreen; x++) { + const xSrc = Math.floor(x * xStep); const val = dataSrc[zOff + yOff + xSrc]; dataDst[j + 0] = val; dataDst[j + 1] = val; @@ -238,12 +232,11 @@ class Graphics2d extends React.Component { } // for (x) } // for (y) } else if (vol.m_bytesPerVoxel === FOUR) { - for (let y = 0; y < hScreen; y++, ay += yStep) { - const ySrc = Math.floor(ay); + for (let y = 0; y < hScreen; y++) { + const ySrc = Math.floor(y * yStep); const yOff = ySrc * xDim; - let ax = xPos * xDim; - for (let x = 0; x < wScreen; x++, ax += xStep) { - const xSrc = Math.floor(ax); + for (let x = 0; x < wScreen; x++) { + const xSrc = Math.floor(x * xStep); const val = dataSrc[(zOff + yOff + xSrc) * FOUR + OFF_3]; const val4 = val * FOUR; const rCol = roiPal256[val4 + 0]; @@ -305,17 +298,15 @@ class Graphics2d extends React.Component { let xSlice = Math.floor(xDim * sliceRatio); xSlice = xSlice < xDim ? xSlice : xDim - 1; - const yStep = (zoom * yDim) / wScreen; - const zStep = (zoom * zDim) / hScreen; + const yStep = yDim / wScreen; + const zStep = zDim / hScreen; let j = 0; - let az = yPos * zDim; if (vol.m_bytesPerVoxel === ONE) { - for (let y = 0; y < hScreen; y++, az += zStep) { - const zSrc = Math.floor(az); + for (let y = 0; y < hScreen; y++) { + const zSrc = Math.floor(y * zStep); const zOff = zSrc * xDim * yDim; - let ay = xPos * yDim; - for (let x = 0; x < wScreen; x++, ay += yStep) { - const ySrc = Math.floor(ay); + for (let x = 0; x < wScreen; x++) { + const ySrc = Math.floor(x * yStep); const yOff = ySrc * xDim; const val = dataSrc[zOff + yOff + xSlice]; @@ -328,12 +319,11 @@ class Graphics2d extends React.Component { } // for (x) } // for (y) } else if (vol.m_bytesPerVoxel === FOUR) { - for (let y = 0; y < hScreen; y++, az += zStep) { - const zSrc = Math.floor(az); + for (let y = 0; y < hScreen; y++) { + const zSrc = Math.floor(y * zStep); const zOff = zSrc * xDim * yDim; - let ay = xPos * yDim; - for (let x = 0; x < wScreen; x++, ay += yStep) { - const ySrc = Math.floor(ay); + for (let x = 0; x < wScreen; x++) { + const ySrc = Math.floor(x * yStep); const yOff = ySrc * xDim; const val = dataSrc[(zOff + yOff + xSlice) * FOUR + OFF_3]; const val4 = val * FOUR; @@ -398,17 +388,15 @@ class Graphics2d extends React.Component { ySlice = ySlice < yDim ? ySlice : yDim - 1; const yOff = ySlice * xDim; - const xStep = (zoom * xDim) / wScreen; - const zStep = (zoom * zDim) / hScreen; + const xStep = xDim / wScreen; + const zStep = zDim / hScreen; let j = 0; - let az = yPos * zDim; if (vol.m_bytesPerVoxel === ONE) { - for (let y = 0; y < hScreen; y++, az += zStep) { - const zSrc = Math.floor(az); + for (let y = 0; y < hScreen; y++) { + const zSrc = Math.floor(y * zStep); const zOff = zSrc * xDim * yDim; - let ax = xPos * xDim; - for (let x = 0; x < wScreen; x++, ax += xStep) { - const xSrc = Math.floor(ax); + for (let x = 0; x < wScreen; x++) { + const xSrc = Math.floor(x * xStep); const val = dataSrc[zOff + yOff + xSrc]; dataDst[j + 0] = val; @@ -420,12 +408,11 @@ class Graphics2d extends React.Component { } // for (x) } // for (y) } else if (vol.m_bytesPerVoxel === FOUR) { - for (let y = 0; y < hScreen; y++, az += zStep) { - const zSrc = Math.floor(az); + for (let y = 0; y < hScreen; y++) { + const zSrc = Math.floor(y * zStep); const zOff = zSrc * xDim * yDim; - let ax = xPos * xDim; - for (let x = 0; x < wScreen; x++, ax += xStep) { - const xSrc = Math.floor(ax); + for (let x = 0; x < wScreen; x++) { + const xSrc = Math.floor(x * xStep); const val = dataSrc[(zOff + yOff + xSrc) * FOUR + OFF_3]; const val4 = val * FOUR; const rCol = roiPal256[val4 + 0]; @@ -461,20 +448,26 @@ class Graphics2d extends React.Component { } renderReadyImage() { + const objCanvas = this.m_mount.current; + const ctx = objCanvas.getContext('2d'); + const store = this.props; + const zoom = store.render2dZoom; + const xPos = store.render2dxPos; + const yPos = store.render2dyPos; + const canvasWidth = objCanvas.width; + const canvasHeight = objCanvas.height; + const newImgWidth = canvasWidth / zoom; + const newImgHeight = canvasHeight / zoom; + if (!this.m_isMounted) { return; } - - const objCanvas = this.m_mount.current; if (objCanvas === null) { return; } - const ctx = objCanvas.getContext('2d'); // prepare canvas this.fillBackground(ctx); - const store = this.props; - const volSet = store.volumeSet; if (volSet.getNumVolumes() === 0) { return; @@ -491,35 +484,60 @@ class Graphics2d extends React.Component { const h = this.m_toolPick.m_hScreen; this.segm2d.render(ctx, w, h, this.imgData); } else { - ctx.putImageData(this.imgData, 0, 0); + createImageBitmap(this.imgData) + .then((imageBitmap) => { + ctx.drawImage(imageBitmap, xPos, yPos, canvasWidth, canvasHeight, 0, 0, newImgWidth, newImgHeight); + }) + .then(() => { + this.m_toolPick.render(ctx); + this.m_toolDistance.render(ctx, store); + this.m_toolAngle.render(ctx, store); + this.m_toolArea.render(ctx, store); + this.m_toolRect.render(ctx, store); + this.m_toolText.render(ctx, store); + this.m_toolEdit.render(ctx, store); + this.m_toolDelete.render(ctx, store); + }); } - - // render all tools - this.m_toolPick.render(ctx); - this.m_toolDistance.render(ctx, store); - this.m_toolAngle.render(ctx, store); - this.m_toolArea.render(ctx, store); - this.m_toolRect.render(ctx, store); - this.m_toolText.render(ctx, store); - this.m_toolEdit.render(ctx, store); - this.m_toolDelete.render(ctx, store); } onMouseWheel(evt) { + const objCanvas = this.m_mount.current; + const canvasRect = objCanvas.getBoundingClientRect(); + let xPosNew; + let yPosNew; const store = this.props; + const zoom = store.render2dZoom; const step = evt.deltaY * 2 ** -10; + let newZoom = zoom + step; - const zoom = store.render2dZoom; - let zoomNew = zoom + step; - let xPosNew = store.render2dxPos - ((step / 4) * evt.clientX) / evt.clientY; - let yPosNew = store.render2dyPos - ((step / 4) * evt.clientY) / evt.clientX; + if (step < 0) { + const mouseX = (evt.clientX - canvasRect.left) * zoom + store.render2dxPos; + const mouseY = (evt.clientY - canvasRect.top) * zoom + store.render2dyPos; + xPosNew = mouseX - (mouseX - store.render2dxPos) * (newZoom / zoom); + yPosNew = mouseY - (mouseY - store.render2dyPos) * (newZoom / zoom); + } else { + const initialX = canvasRect.width * zoom + store.render2dxPos; + const initialY = canvasRect.height * zoom + store.render2dyPos; + xPosNew = initialX - (initialX - store.render2dxPos) * (newZoom / zoom); + yPosNew = initialY - (initialY - store.render2dyPos) * (newZoom / zoom); + } - console.log(`onMouseWheel.evt = ${xPosNew}, ${yPosNew}`); - // console.log(`onMouseWheel. zoom.puml = ${zoom.puml} zoomNew = ${zoomNew}, xyPos = ${xPosNew},${yPosNew}`); - if (Math.abs(zoomNew) > 1 || Math.abs(zoomNew) < 0.02 || xPosNew < 0 || yPosNew < 0 || xPosNew > 1 || yPosNew > 1) { + if (xPosNew < 0) { + xPosNew = 0; + } + if (yPosNew < 0) { + yPosNew = 0; + } + if (newZoom > 1) { + newZoom = 1; + xPosNew = 0; + yPosNew = 0; + } + if (newZoom < 0.1) { return; } - store.dispatch({ type: StoreActionType.SET_2D_ZOOM, render2dZoom: zoomNew }); + store.dispatch({ type: StoreActionType.SET_2D_ZOOM, render2dZoom: newZoom }); store.dispatch({ type: StoreActionType.SET_2D_X_POS, render2dxPos: xPosNew }); store.dispatch({ type: StoreActionType.SET_2D_Y_POS, render2dyPos: yPosNew }); diff --git a/src/engine/tools2d/ToolDistance.js b/src/engine/tools2d/ToolDistance.js index 254832b5..a77bc873 100644 --- a/src/engine/tools2d/ToolDistance.js +++ b/src/engine/tools2d/ToolDistance.js @@ -111,9 +111,11 @@ class ToolDistance { const xDim = vol.m_xDim; const yDim = vol.m_yDim; const zDim = vol.m_zDim; + const objCanvas = store.graphics2d.m_mount.current; + const canvasRect = objCanvas.getBoundingClientRect(); const zoom = store.render2dZoom; - const xPos = store.render2dxPos; - const yPos = store.render2dyPos; + const xPos = store.render2dxPos / canvasRect.width; + const yPos = store.render2dyPos / canvasRect.height; const vTex = { x: 0.0, @@ -148,9 +150,11 @@ class ToolDistance { const xDim = vol.m_xDim; const yDim = vol.m_yDim; const zDim = vol.m_zDim; + const objCanvas = store.graphics2d.m_mount.current; + const canvasRect = objCanvas.getBoundingClientRect(); const zoom = store.render2dZoom; - const xPos = store.render2dxPos; - const yPos = store.render2dyPos; + const xPos = store.render2dxPos / canvasRect.width; + const yPos = store.render2dyPos / canvasRect.height; if (mode2d === Modes2d.TRANSVERSE) { // z const vScr.x = (xTex / xDim - xPos) / zoom; diff --git a/src/engine/tools2d/ToolPick.js b/src/engine/tools2d/ToolPick.js index 0bc5ffe5..1de1152d 100644 --- a/src/engine/tools2d/ToolPick.js +++ b/src/engine/tools2d/ToolPick.js @@ -58,9 +58,11 @@ class ToolPick { const xDim = vol.m_xDim; const yDim = vol.m_yDim; const zDim = vol.m_zDim; + const objCanvas = store.graphics2d.m_mount.current; + const canvasRect = objCanvas.getBoundingClientRect(); const zoom = store.render2dZoom; - const xPos = store.render2dxPos; - const yPos = store.render2dyPos; + const xPos = store.render2dxPos / canvasRect.width; + const yPos = store.render2dyPos / canvasRect.height; if (mode2d === Modes2d.TRANSVERSE) { // z: const vTex.x = Math.floor((xPos + xScr * zoom) * xDim); diff --git a/src/engine/tools2d/ToolTypes.js b/src/engine/tools2d/ToolTypes.js index f4450d20..590aad0f 100644 --- a/src/engine/tools2d/ToolTypes.js +++ b/src/engine/tools2d/ToolTypes.js @@ -19,9 +19,10 @@ const Tools2dType = { EDIT: 6, DELETE: 7, CLEAR: 8, - ZOOM: 9, - ZOOM_100: 10, - FILTER: 11, - NONE: 12, + ZOOM_IN: 9, + ZOOM_OUT: 10, + ZOOM_100: 11, + FILTER: 12, + NONE: 13, }; export default Tools2dType; diff --git a/src/ui/Main.module.css b/src/ui/Main.module.css index 5c2cbbd3..b5f8a3fe 100644 --- a/src/ui/Main.module.css +++ b/src/ui/Main.module.css @@ -67,7 +67,7 @@ height: 50px; position: absolute; right: 0; - top:50%; + top: 50%; z-index: 10; } .left { @@ -75,13 +75,13 @@ } .center div { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1; - user-select: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + user-select: none; } .settings { @@ -89,7 +89,6 @@ } @media screen and (min-width: 768px) { - .header { display: flex; position: relative; @@ -103,7 +102,7 @@ margin-top: 1rem; } - .header__panels { + .header__panels { display: flex; flex-direction: column; width: 25rem; @@ -156,20 +155,21 @@ } @media screen and (min-width: 1024px) { - .header { align-items: flex-start; } - .header div { - margin-bottom: 1rem; + .header div { + height: auto; + padding-bottom: 0; + margin-bottom: 0; } .header__right { align-items: stretch; } - .header__panels { + .header__panels { width: 100%; display: flex; flex-direction: row; @@ -203,11 +203,9 @@ .left { top: 8%; } - } @media screen and (min-width: 1024px) and (orientation: landscape) { - .header__panels { margin-right: 3rem; } @@ -218,13 +216,11 @@ } .left { - top:10%; + top: 10%; } - } @media screen and (min-width: 1024px) and (orientation: portrait) { - .header__panels { flex-wrap: wrap; } @@ -232,19 +228,15 @@ .settings { top: 4rem; } - } @media screen and (min-width: 1440px) { .header__panels { flex-wrap: nowrap; } - } - @media screen and (min-width: 2560px) and (orientation: landscape) { - .left { top: 5%; } diff --git a/src/ui/UiZoomTools.jsx b/src/ui/UiZoomTools.jsx index 17fcc57d..e9be4ffe 100644 --- a/src/ui/UiZoomTools.jsx +++ b/src/ui/UiZoomTools.jsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import Tools2dType from '../engine/tools2d/ToolTypes'; @@ -12,28 +12,76 @@ import { buttonsBuilder } from './Button/Button'; import { Container } from './Layout/Container'; const UiZoomTools = (props) => { + const MIN_ZOOM_THRESHOLD = 0.8; const [activeButton, setActiveButton] = useState(Tools2dType.NONE); + const zoomImage = (step, buttonId) => { + const currentZoom = props.render2dZoom; + let newZoom = Math.round((currentZoom + step) * 10) / 10; + const objCanvas = props.graphics2d.m_mount.current; + const canvasRect = objCanvas.getBoundingClientRect(); + let xPosNew; + let yPosNew; + + if (buttonId === Tools2dType.ZOOM_IN && newZoom > 0) { + xPosNew = props.render2dxPos + (canvasRect.width / 2) * Math.abs(step); + yPosNew = props.render2dyPos + (canvasRect.height / 2) * Math.abs(step); + } else if (buttonId === Tools2dType.ZOOM_OUT && newZoom < 1) { + const initialX = canvasRect.width * currentZoom + props.render2dxPos; + const initialY = canvasRect.height * currentZoom + props.render2dyPos; + xPosNew = initialX - (initialX - props.render2dxPos) * (newZoom / currentZoom); + yPosNew = initialY - (initialY - props.render2dyPos) * (newZoom / currentZoom); + } + + if (xPosNew < 0) { + xPosNew = 0; + } + if (yPosNew < 0) { + yPosNew = 0; + } + if (newZoom > 1) { + newZoom = 1; + xPosNew = 0; + yPosNew = 0; + } + if (newZoom < 0.1) { + return; + } + props.dispatch({ type: StoreActionType.SET_2D_ZOOM, render2dZoom: newZoom }); + props.dispatch({ type: StoreActionType.SET_2D_X_POS, render2dxPos: xPosNew }); + props.dispatch({ type: StoreActionType.SET_2D_Y_POS, render2dyPos: yPosNew }); + }; + const mediator = (buttonId) => { setActiveButton(buttonId); props.dispatch({ type: StoreActionType.SET_2D_TOOLS_INDEX, indexTools2d: buttonId }); - if (buttonId === Tools2dType.ZOOM_100) { + if (buttonId === Tools2dType.ZOOM_100 || (buttonId === Tools2dType.ZOOM_OUT && props.render2dZoom > MIN_ZOOM_THRESHOLD)) { props.dispatch({ type: StoreActionType.SET_2D_ZOOM, render2dZoom: 1.0 }); props.dispatch({ type: StoreActionType.SET_2D_X_POS, render2dxPos: 0.0 }); props.dispatch({ type: StoreActionType.SET_2D_Y_POS, render2dyPos: 0.0 }); - - props.graphics2d.forceUpdate(); - props.graphics2d.forceRender(); + } else { + zoomImage(buttonId === Tools2dType.ZOOM_IN ? -0.1 : 0.1, buttonId); } }; + useEffect(() => { + props.graphics2d.forceUpdate(); + props.graphics2d.forceRender(); + }, [props.render2dZoom]); + const buttons = [ { - icon: 'zoom', - caption: 'Zoom in/out', - handler: mediator.bind(null, Tools2dType.ZOOM), - id: Tools2dType.ZOOM, + icon: 'zoom_in', + caption: 'Zoom in', + handler: mediator.bind(null, Tools2dType.ZOOM_IN), + id: Tools2dType.ZOOM_IN, + }, + { + icon: 'zoom_out', + caption: 'Zoom out', + handler: mediator.bind(null, Tools2dType.ZOOM_OUT), + id: Tools2dType.ZOOM_OUT, }, { icon: 'zoom_100',