Compare commits
11
Commits
1674254a3c
...
7c17a27ec3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c17a27ec3 | ||
|
|
68248737ec | ||
|
|
13361a30a5 | ||
|
|
627cd740f1 | ||
|
|
af6e22fd91 | ||
|
|
7b786fcf9f | ||
|
|
c9902a2e15 | ||
|
|
99df1cd1c1 | ||
|
|
f169238057 | ||
|
|
f0e0fe0ef6 | ||
|
|
19702b6052 |
@@ -0,0 +1,72 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fluoro Multiframe Test</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
.dicom-viewer-root{width:100%;height:100vh;background:#222;color:#fff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main_viewer" class="dicom-viewer-root" data-auto-cache-stack="false"></div>
|
||||
|
||||
<!-- Provide noop React Refresh preamble to avoid plugin-react runtime errors -->
|
||||
<script>
|
||||
window.$RefreshReg$ = window.$RefreshReg$ || function() {};
|
||||
window.$RefreshSig$ = window.$RefreshSig$ || function() { return function(type) { return type; }; };
|
||||
</script>
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<script>
|
||||
// Viewer state for fluoro multiframe images (using relative paths from public/test_images/)
|
||||
const state = {
|
||||
"grid": {"rows": 1, "cols": 2},
|
||||
"modes": ["stack", "stack"],
|
||||
"stacks": [
|
||||
{"i": ["wadouri:test_images/fluro/series_2369/1.3.12.2.1107.5.4.5.180506.30000026031914043350300000126.4.dcm"]},
|
||||
{"i": ["wadouri:test_images/multibvalues/IMG_792_og2P5S6.dcm", "wadouri:test_images/multibvalues/IMG_793_3tKkg87.dcm"]}
|
||||
],
|
||||
"stackIdx": [0, 1],
|
||||
"stackPos": [0, 1],
|
||||
"props": [
|
||||
{"voiRange": {"lower": 0, "upper": 500}, "invert": false, "interpolationType": 1, "VOILUTFunction": "LINEAR", "colormap": {"name": "Grayscale", "opacity": []}},
|
||||
{"voiRange": {"lower": 0, "upper": 500}, "invert": false, "interpolationType": 1, "VOILUTFunction": "LINEAR", "colormap": {"name": "Grayscale", "opacity": []}}
|
||||
],
|
||||
"cam": [
|
||||
{"viewUp": [0, 0, 1], "viewPlaneNormal": [1, 0, 0], "position": [150, 0, 0], "focalPoint": [0, 0, 0], "parallelProjection": true, "parallelScale": 100, "viewAngle": 90, "flipHorizontal": false, "flipVertical": false, "rotation": 0},
|
||||
{"viewUp": [0, 0, 1], "viewPlaneNormal": [1, 0, 0], "position": [150, 0, 0], "focalPoint": [0, 0, 0], "parallelProjection": true, "parallelScale": 100, "viewAngle": 90, "flipHorizontal": false, "flipVertical": false, "rotation": 0}
|
||||
],
|
||||
"orientations": [null, null],
|
||||
"volumeSliceIndices": [null, null]
|
||||
};
|
||||
|
||||
function callWhenReady(fnName, payload) {
|
||||
const maxWait = 5000;
|
||||
const start = Date.now();
|
||||
(function check() {
|
||||
if (window[fnName] && typeof window[fnName] === 'function') {
|
||||
try {
|
||||
window[fnName](payload);
|
||||
console.log('imported viewer state via', fnName);
|
||||
} catch (e) {
|
||||
console.error('importViewerState call failed', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Date.now() - start > maxWait) {
|
||||
console.warn('Timed out waiting for', fnName);
|
||||
return;
|
||||
}
|
||||
setTimeout(check, 2000);
|
||||
})();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
callWhenReady('importViewerState', state);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,6 +23,7 @@
|
||||
<li><a href="/load-dir-test.html">load-dir-test.html</a> — directory loading test</li>
|
||||
<li><a href="/non-dicom-test.html">non-dicom-test.html</a> — non-DICOM handling test</li>
|
||||
<li><a href="/index.html">index.html</a> — project index</li>
|
||||
<li><a href="/fluoro-test.html">fluoro-test.html</a> — fluoro multiframe test</li>
|
||||
</ul>
|
||||
|
||||
<p>If you want these links added to the README, tell me and I will update it.</p>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2109
-306
File diff suppressed because it is too large
Load Diff
+105
-62
@@ -1,10 +1,34 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from "react-dom/client";
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import App from './App.tsx'
|
||||
import Nifti from './Nifti.tsx'
|
||||
import './index.css'
|
||||
import { createImageIds, availableImageIds } from "./lib/createImageIds.ts"
|
||||
import { availableImageIds } from "./lib/createImageIds.ts"
|
||||
|
||||
type MountOptions = {
|
||||
force?: boolean;
|
||||
containerIds?: string[];
|
||||
};
|
||||
|
||||
type ViewerDescriptor = {
|
||||
imageIds: string[];
|
||||
name?: string;
|
||||
caseId?: string;
|
||||
studyId?: string;
|
||||
};
|
||||
|
||||
type RootRecord = {
|
||||
root: Root;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
const rootRegistry = new WeakMap<HTMLElement, RootRecord>();
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mountDicomViewers?: (options?: MountOptions) => void;
|
||||
remountDicomViewer?: (containerId: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize incoming image ids/URLs and ensure DICOM entries use a valid wadouri: prefix.
|
||||
const normalizeImageId = (value: string) => {
|
||||
@@ -35,55 +59,30 @@ const toDescriptor = (arr: string[] | undefined) => {
|
||||
return { imageIds: toWadouri(arr) };
|
||||
};
|
||||
|
||||
export function mountDicomViewers() {
|
||||
// Test containers
|
||||
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
||||
test_containers.forEach((container) => {
|
||||
const id = container.id || '';
|
||||
const imageStacks = () => availableImageIds();
|
||||
createRoot(container).render(
|
||||
<App
|
||||
container_id={id}
|
||||
imageStacks={imageStacks}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Main containers
|
||||
const containers = document.querySelectorAll(".dicom-viewer-root");
|
||||
containers.forEach((container) => {
|
||||
const id = container.id || '';
|
||||
let imageStacks: () => Promise<any[]>;
|
||||
const isEmpty = container.getAttribute('data-empty') === 'true';
|
||||
|
||||
// New: support data-named-stacks (richer format) and fallback to data-images
|
||||
const dataNamed = container.getAttribute('data-named-stacks');
|
||||
const dataImages = container.getAttribute('data-images');
|
||||
|
||||
const parseToDescriptors = (parsed: any): Array<any> => {
|
||||
// Return array of descriptors: either simple string[] => { imageIds: string[] }
|
||||
// or objects: { imageIds: string[], name?, caseId?, studyId? }
|
||||
const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
|
||||
if (!parsed) return [];
|
||||
|
||||
// If top-level is an array of strings -> single unnamed stack
|
||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||
return [toDescriptor(parsed)];
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
const out: any[] = [];
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const out: Array<ViewerDescriptor> = [];
|
||||
|
||||
parsed.forEach((item, itemIdx) => {
|
||||
// Case object with nested stacks: { caseId, studyId, stacks: [ { name, imageIds } ] }
|
||||
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
||||
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
||||
const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
|
||||
|
||||
item.stacks.forEach((s: any, sIdx: number) => {
|
||||
let imageIds: string[] = [];
|
||||
if (Array.isArray(s)) imageIds = s;
|
||||
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
||||
else if (Array.isArray(s.i)) imageIds = s.i;
|
||||
|
||||
// Validation
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
||||
console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
|
||||
return;
|
||||
@@ -99,10 +98,8 @@ export function mountDicomViewers() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stack object: { imageIds, i, name, caseId, studyId }
|
||||
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
|
||||
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
|
||||
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
||||
console.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
|
||||
return;
|
||||
@@ -117,21 +114,82 @@ export function mountDicomViewers() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Plain array (stack)
|
||||
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
|
||||
out.push(toDescriptor(item));
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown / invalid entry
|
||||
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
|
||||
});
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
const getMountSignature = (container: HTMLElement): string => {
|
||||
return JSON.stringify({
|
||||
id: container.id || '',
|
||||
empty: container.getAttribute('data-empty') || '',
|
||||
namedStacks: container.getAttribute('data-named-stacks') || '',
|
||||
images: container.getAttribute('data-images') || '',
|
||||
autoCache: container.getAttribute('data-auto-cache-stack') || '',
|
||||
annotation: container.getAttribute('data-annotationjson') || '',
|
||||
viewerState: container.getAttribute('data-viewerstate') || '',
|
||||
});
|
||||
};
|
||||
|
||||
const shouldMountContainer = (container: HTMLElement, options?: MountOptions): boolean => {
|
||||
if (!options?.containerIds || options.containerIds.length === 0) return true;
|
||||
return options.containerIds.includes(container.id || '');
|
||||
};
|
||||
|
||||
const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]>, options?: MountOptions) => {
|
||||
const signature = getMountSignature(container);
|
||||
const existing = rootRegistry.get(container);
|
||||
|
||||
if (existing && !options?.force && existing.signature === signature) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [];
|
||||
if (existing) {
|
||||
existing.root.unmount();
|
||||
rootRegistry.delete(container);
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<App
|
||||
container_id={container.id || ''}
|
||||
imageStacks={imageStacks}
|
||||
autoCacheStack={(container.getAttribute('data-auto-cache-stack') === "true" || container.getAttribute('data-auto-cache-stack') === "1")}
|
||||
annotationJson={container.getAttribute('data-annotationjson') || undefined}
|
||||
viewerState={container.getAttribute('data-viewerstate') || undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
rootRegistry.set(container, { root, signature });
|
||||
};
|
||||
|
||||
export function mountDicomViewers(options?: MountOptions) {
|
||||
// Test containers
|
||||
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
||||
test_containers.forEach((container) => {
|
||||
if (!shouldMountContainer(container as HTMLElement, options)) return;
|
||||
const id = container.id || '';
|
||||
const imageStacks = () => availableImageIds();
|
||||
mountContainer(container as HTMLElement, imageStacks, options);
|
||||
});
|
||||
|
||||
// Main containers
|
||||
const containers = document.querySelectorAll(".dicom-viewer-root");
|
||||
containers.forEach((container) => {
|
||||
if (!shouldMountContainer(container as HTMLElement, options)) return;
|
||||
|
||||
let imageStacks: () => Promise<any[]>;
|
||||
const isEmpty = container.getAttribute('data-empty') === 'true';
|
||||
|
||||
const dataNamed = container.getAttribute('data-named-stacks');
|
||||
const dataImages = container.getAttribute('data-images');
|
||||
|
||||
if (isEmpty) {
|
||||
imageStacks = () => Promise.resolve([] as any[]);
|
||||
} else if (dataNamed) {
|
||||
@@ -152,7 +210,6 @@ export function mountDicomViewers() {
|
||||
console.error("Invalid data-images JSON:", e);
|
||||
parsed = [];
|
||||
}
|
||||
// Keep old behavior for data-images
|
||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
||||
} else if (Array.isArray(parsed)) {
|
||||
@@ -163,32 +220,18 @@ export function mountDicomViewers() {
|
||||
imageStacks = () => Promise.resolve([[]]);
|
||||
}
|
||||
} else {
|
||||
// No data provided: return no stacks (empty array) so viewer starts empty
|
||||
imageStacks = () => Promise.resolve([] as any[]);
|
||||
}
|
||||
|
||||
const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack');
|
||||
const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1";
|
||||
|
||||
const annotationJson = container.getAttribute('data-annotationjson');
|
||||
const viewerState = container.getAttribute('data-viewerstate');
|
||||
console.log("Mounting DICOM viewer:")
|
||||
console.log(viewerState, annotationJson, autoCacheStack);
|
||||
|
||||
createRoot(container).render(
|
||||
<App
|
||||
container_id={id}
|
||||
imageStacks={imageStacks}
|
||||
autoCacheStack={autoCacheStack}
|
||||
annotationJson={annotationJson ? annotationJson : undefined}
|
||||
viewerState={viewerState ? viewerState : undefined}
|
||||
/>
|
||||
);
|
||||
mountContainer(container as HTMLElement, imageStacks, options);
|
||||
});
|
||||
}
|
||||
|
||||
// Optionally, call it immediately for static containers:
|
||||
mountDicomViewers();
|
||||
|
||||
// Optionally, expose globally for dynamic use:
|
||||
// Expose globally for dynamic use.
|
||||
(window as any).mountDicomViewers = mountDicomViewers;
|
||||
(window as any).remountDicomViewer = (containerId: string) => {
|
||||
mountDicomViewers({ force: true, containerIds: [containerId] });
|
||||
};
|
||||
Reference in New Issue
Block a user