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="/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="/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="/index.html">index.html</a> — project index</li>
|
||||||
|
<li><a href="/fluoro-test.html">fluoro-test.html</a> — fluoro multiframe test</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>If you want these links added to the README, tell me and I will update it.</p>
|
<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.
+2190
-387
File diff suppressed because it is too large
Load Diff
+140
-97
@@ -1,10 +1,34 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import Nifti from './Nifti.tsx'
|
|
||||||
import './index.css'
|
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.
|
// Normalize incoming image ids/URLs and ensure DICOM entries use a valid wadouri: prefix.
|
||||||
const normalizeImageId = (value: string) => {
|
const normalizeImageId = (value: string) => {
|
||||||
@@ -35,103 +59,137 @@ const toDescriptor = (arr: string[] | undefined) => {
|
|||||||
return { imageIds: toWadouri(arr) };
|
return { imageIds: toWadouri(arr) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mountDicomViewers() {
|
const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
|
||||||
// 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? }
|
|
||||||
if (!parsed) return [];
|
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") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
return [toDescriptor(parsed)];
|
return [toDescriptor(parsed)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
const out: any[] = [];
|
return [];
|
||||||
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
|
const out: Array<ViewerDescriptor> = [];
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.push({
|
parsed.forEach((item, itemIdx) => {
|
||||||
imageIds: toWadouri(imageIds),
|
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
||||||
name: s.name || s.label || undefined,
|
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
||||||
caseId,
|
const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
|
||||||
studyId: s.studyId || studyId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack object: { imageIds, i, name, caseId, studyId }
|
item.stacks.forEach((s: any, sIdx: number) => {
|
||||||
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
|
let imageIds: string[] = [];
|
||||||
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
|
if (Array.isArray(s)) imageIds = s;
|
||||||
|
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
||||||
|
else if (Array.isArray(s.i)) imageIds = s.i;
|
||||||
|
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
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);
|
console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
imageIds: toWadouri(imageIds),
|
imageIds: toWadouri(imageIds),
|
||||||
name: item.name || item.label || undefined,
|
name: s.name || s.label || undefined,
|
||||||
caseId: item.caseId || item.caseUID || undefined,
|
caseId,
|
||||||
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
|
studyId: s.studyId || studyId,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain array (stack)
|
out.push({
|
||||||
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
|
imageIds: toWadouri(imageIds),
|
||||||
out.push(toDescriptor(item));
|
name: item.name || item.label || undefined,
|
||||||
return;
|
caseId: item.caseId || item.caseUID || undefined,
|
||||||
}
|
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Unknown / invalid entry
|
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
|
||||||
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
|
out.push(toDescriptor(item));
|
||||||
});
|
return;
|
||||||
return out;
|
}
|
||||||
|
|
||||||
|
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) {
|
if (isEmpty) {
|
||||||
imageStacks = () => Promise.resolve([] as any[]);
|
imageStacks = () => Promise.resolve([] as any[]);
|
||||||
} else if (dataNamed) {
|
} else if (dataNamed) {
|
||||||
@@ -152,7 +210,6 @@ export function mountDicomViewers() {
|
|||||||
console.error("Invalid data-images JSON:", e);
|
console.error("Invalid data-images JSON:", e);
|
||||||
parsed = [];
|
parsed = [];
|
||||||
}
|
}
|
||||||
// Keep old behavior for data-images
|
|
||||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
||||||
} else if (Array.isArray(parsed)) {
|
} else if (Array.isArray(parsed)) {
|
||||||
@@ -163,32 +220,18 @@ export function mountDicomViewers() {
|
|||||||
imageStacks = () => Promise.resolve([[]]);
|
imageStacks = () => Promise.resolve([[]]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No data provided: return no stacks (empty array) so viewer starts empty
|
|
||||||
imageStacks = () => Promise.resolve([] as any[]);
|
imageStacks = () => Promise.resolve([] as any[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack');
|
mountContainer(container as HTMLElement, imageStacks, options);
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, call it immediately for static containers:
|
// Optionally, call it immediately for static containers:
|
||||||
mountDicomViewers();
|
mountDicomViewers();
|
||||||
|
|
||||||
// Optionally, expose globally for dynamic use:
|
// Expose globally for dynamic use.
|
||||||
(window as any).mountDicomViewers = mountDicomViewers;
|
(window as any).mountDicomViewers = mountDicomViewers;
|
||||||
|
(window as any).remountDicomViewer = (containerId: string) => {
|
||||||
|
mountDicomViewers({ force: true, containerIds: [containerId] });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user