basic
This commit is contained in:
@@ -264,6 +264,157 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
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 [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
|
||||
// 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];
|
||||
if (isExternalImageId(firstId)) {
|
||||
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
|
||||
element.innerHTML = '';
|
||||
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.
|
||||
img.setAttribute('draggable', 'false');
|
||||
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
|
||||
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user