basic
This commit is contained in:
@@ -264,6 +264,157 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable simple pan & zoom interactions for an <img> placed inside a viewport element.
|
||||||
|
// Attaches event listeners and returns a cleanup function.
|
||||||
|
function enableImagePanZoom(container: HTMLElement, img: HTMLImageElement) {
|
||||||
|
let scale = 1;
|
||||||
|
let translateX = 0;
|
||||||
|
let translateY = 0;
|
||||||
|
let isPanning = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let lastClientX = 0;
|
||||||
|
let lastClientY = 0;
|
||||||
|
|
||||||
|
img.style.transformOrigin = '0 0';
|
||||||
|
img.style.willChange = 'transform';
|
||||||
|
img.style.cursor = 'grab';
|
||||||
|
|
||||||
|
const clamp = (v: number, a = 0.1, b = 10) => Math.min(b, Math.max(a, v));
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWheel = (e: WheelEvent) => {
|
||||||
|
// Zoom on wheel
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = img.getBoundingClientRect();
|
||||||
|
const offsetX = e.clientX - rect.left;
|
||||||
|
const offsetY = e.clientY - rect.top;
|
||||||
|
const delta = -e.deltaY;
|
||||||
|
const zoomFactor = 1 + (delta > 0 ? 0.1 : -0.1) * Math.min(4, Math.abs(delta) / 100);
|
||||||
|
const newScale = clamp(scale * zoomFactor, 0.1, 10);
|
||||||
|
|
||||||
|
// Keep point under cursor stationary
|
||||||
|
translateX = (translateX - offsetX) * (newScale / scale) + offsetX;
|
||||||
|
translateY = (translateY - offsetY) * (newScale / scale) + offsetY;
|
||||||
|
scale = newScale;
|
||||||
|
apply();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
isPanning = true;
|
||||||
|
startX = translateX;
|
||||||
|
startY = translateY;
|
||||||
|
lastClientX = e.clientX;
|
||||||
|
lastClientY = e.clientY;
|
||||||
|
img.style.cursor = 'grabbing';
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isPanning) return;
|
||||||
|
const dx = e.clientX - lastClientX;
|
||||||
|
const dy = e.clientY - lastClientY;
|
||||||
|
translateX += dx;
|
||||||
|
translateY += dy;
|
||||||
|
lastClientX = e.clientX;
|
||||||
|
lastClientY = e.clientY;
|
||||||
|
apply();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
|
if (!isPanning) return;
|
||||||
|
isPanning = false;
|
||||||
|
img.style.cursor = 'grab';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDblClick = (e: MouseEvent) => {
|
||||||
|
// reset
|
||||||
|
scale = 1;
|
||||||
|
translateX = 0;
|
||||||
|
translateY = 0;
|
||||||
|
apply();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touch support: simple panning (single touch) and pinch-to-zoom (two touches)
|
||||||
|
let lastTouchDist = 0;
|
||||||
|
const getTouchDist = (t1: Touch, t2: Touch) => Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
||||||
|
|
||||||
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
isPanning = true;
|
||||||
|
lastClientX = e.touches[0].clientX;
|
||||||
|
lastClientY = e.touches[0].clientY;
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
lastTouchDist = getTouchDist(e.touches[0], e.touches[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchMove = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 1 && isPanning) {
|
||||||
|
const dx = e.touches[0].clientX - lastClientX;
|
||||||
|
const dy = e.touches[0].clientY - lastClientY;
|
||||||
|
translateX += dx;
|
||||||
|
translateY += dy;
|
||||||
|
lastClientX = e.touches[0].clientX;
|
||||||
|
lastClientY = e.touches[0].clientY;
|
||||||
|
apply();
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
const curDist = getTouchDist(e.touches[0], e.touches[1]);
|
||||||
|
if (lastTouchDist > 0) {
|
||||||
|
const zoomFactor = curDist / lastTouchDist;
|
||||||
|
const rect = img.getBoundingClientRect();
|
||||||
|
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
|
||||||
|
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
|
||||||
|
const newScale = clamp(scale * zoomFactor, 0.1, 10);
|
||||||
|
translateX = (translateX - centerX) * (newScale / scale) + centerX;
|
||||||
|
translateY = (translateY - centerY) * (newScale / scale) + centerY;
|
||||||
|
scale = newScale;
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
lastTouchDist = curDist;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 0) {
|
||||||
|
isPanning = false;
|
||||||
|
lastTouchDist = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach listeners to the container so we get events even if the img is not full-size
|
||||||
|
container.addEventListener('wheel', onWheel, { passive: false });
|
||||||
|
container.addEventListener('mousedown', onMouseDown as any);
|
||||||
|
window.addEventListener('mousemove', onMouseMove as any);
|
||||||
|
window.addEventListener('mouseup', onMouseUp as any);
|
||||||
|
img.addEventListener('dblclick', onDblClick as any);
|
||||||
|
container.addEventListener('touchstart', onTouchStart as any, { passive: false });
|
||||||
|
container.addEventListener('touchmove', onTouchMove as any, { passive: false });
|
||||||
|
container.addEventListener('touchend', onTouchEnd as any);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
container.removeEventListener('wheel', onWheel as any);
|
||||||
|
container.removeEventListener('mousedown', onMouseDown as any);
|
||||||
|
window.removeEventListener('mousemove', onMouseMove as any);
|
||||||
|
window.removeEventListener('mouseup', onMouseUp as any);
|
||||||
|
img.removeEventListener('dblclick', onDblClick as any);
|
||||||
|
container.removeEventListener('touchstart', onTouchStart as any);
|
||||||
|
container.removeEventListener('touchmove', onTouchMove as any);
|
||||||
|
container.removeEventListener('touchend', onTouchEnd as any);
|
||||||
|
// reset transform
|
||||||
|
img.style.transform = '';
|
||||||
|
img.style.cursor = '';
|
||||||
|
img.style.willChange = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
const [stackOrderModified, setStackOrderModified] = useState(false);
|
const [stackOrderModified, setStackOrderModified] = useState(false);
|
||||||
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
|
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
|
||||||
// Load annotations and viewer state from props if provided
|
// Load annotations and viewer state from props if provided
|
||||||
@@ -1274,6 +1425,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const firstId = imageIds[0];
|
const firstId = imageIds[0];
|
||||||
if (isExternalImageId(firstId)) {
|
if (isExternalImageId(firstId)) {
|
||||||
try {
|
try {
|
||||||
|
// Cleanup previous external handlers if any
|
||||||
|
try {
|
||||||
|
const prev = (element as any)._externalPanZoom;
|
||||||
|
if (prev && typeof prev.cleanup === 'function') prev.cleanup();
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
|
||||||
// Clear existing content and insert an image element
|
// Clear existing content and insert an image element
|
||||||
element.innerHTML = '';
|
element.innerHTML = '';
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
@@ -1285,6 +1442,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
// allow drag to open in new tab etc.
|
// allow drag to open in new tab etc.
|
||||||
img.setAttribute('draggable', 'false');
|
img.setAttribute('draggable', 'false');
|
||||||
element.appendChild(img);
|
element.appendChild(img);
|
||||||
|
|
||||||
|
// Attach pan/zoom interactions to this external image
|
||||||
|
const cleanup = enableImagePanZoom(element, img);
|
||||||
|
(element as any)._externalPanZoom = { cleanup };
|
||||||
|
|
||||||
// Do not attach DICOM-specific tools to external images
|
// Do not attach DICOM-specific tools to external images
|
||||||
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user