try to fix fluro
This commit is contained in:
@@ -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.
+301
-40
@@ -193,7 +193,23 @@ function withFrameQuery(imageId: string, frameNumber: number): string {
|
|||||||
if (hasFrameQuery(imageId)) {
|
if (hasFrameQuery(imageId)) {
|
||||||
return imageId.replace(/([?&])frame=\d+/i, `$1frame=${frame}`);
|
return imageId.replace(/([?&])frame=\d+/i, `$1frame=${frame}`);
|
||||||
}
|
}
|
||||||
return `${imageId}${imageId.includes("?") ? "&" : "?"}frame=${frame}`;
|
// Cornerstone's wadouri multiframe path expects '&frame=' specifically.
|
||||||
|
return `${imageId}&frame=${frame}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFrameNumberFromImageId(imageId: string): number | null {
|
||||||
|
const match = imageId.match(/[?&]frame=(\d+)/i);
|
||||||
|
if (!match) return null;
|
||||||
|
const parsed = Number(match[1]);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumericValueList(raw?: string | null): number[] {
|
||||||
|
if (!raw) return [];
|
||||||
|
return raw
|
||||||
|
.split(/\\|,/) // DICOM VM separator is '\\'; logs often show comma-separated values.
|
||||||
|
.map((part) => Number(part.trim()))
|
||||||
|
.filter((value) => Number.isFinite(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeToWadouriOrExternal(imageId: string): string {
|
function normalizeToWadouriOrExternal(imageId: string): string {
|
||||||
@@ -376,7 +392,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const [cineFpsByViewport, setCineFpsByViewport] = useState<number[]>(
|
const [cineFpsByViewport, setCineFpsByViewport] = useState<number[]>(
|
||||||
() => Array(MAX_VIEWPORTS).fill(12)
|
() => Array(MAX_VIEWPORTS).fill(12)
|
||||||
);
|
);
|
||||||
|
const [cineControlsOpenByViewport, setCineControlsOpenByViewport] = useState<boolean[]>(
|
||||||
|
() => Array(MAX_VIEWPORTS).fill(false)
|
||||||
|
);
|
||||||
const multiframeCountCacheRef = useRef<Record<string, number>>({});
|
const multiframeCountCacheRef = useRef<Record<string, number>>({});
|
||||||
|
const bValuesByBaseImageRef = useRef<Record<string, number[]>>({});
|
||||||
|
|
||||||
// Client-side generated thumbnails cache: stackIndex -> dataURL
|
// Client-side generated thumbnails cache: stackIndex -> dataURL
|
||||||
const [stackThumbnails, setStackThumbnails] = useState<Record<number, string>>({});
|
const [stackThumbnails, setStackThumbnails] = useState<Record<number, string>>({});
|
||||||
@@ -757,7 +777,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
stacksRaw.map(async (d) => {
|
stacksRaw.map(async (d) => {
|
||||||
if (Array.isArray(d)) {
|
if (Array.isArray(d)) {
|
||||||
const expanded = await expandMultiframeImageIds(d as string[]);
|
const expanded = await expandMultiframeImageIds(d as string[]);
|
||||||
return { imageIds: expanded };
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(expanded);
|
||||||
|
return { imageIds: expanded, seriesGroups };
|
||||||
}
|
}
|
||||||
const imageIdsRaw = Array.isArray((d as any).imageIds) ? (d as any).imageIds : (Array.isArray((d as any).i) ? (d as any).i : []);
|
const imageIdsRaw = Array.isArray((d as any).imageIds) ? (d as any).imageIds : (Array.isArray((d as any).i) ? (d as any).i : []);
|
||||||
const imageIds = await expandMultiframeImageIds(imageIdsRaw);
|
const imageIds = await expandMultiframeImageIds(imageIdsRaw);
|
||||||
@@ -788,6 +809,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!seriesGroups || seriesGroups.length === 0) {
|
||||||
|
seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||||
|
}
|
||||||
|
|
||||||
// try to preserve old studyInstanceUID extraction if needed (best-effort)
|
// try to preserve old studyInstanceUID extraction if needed (best-effort)
|
||||||
let studyInstanceUID: string | undefined = (d as any).studyInstanceUID;
|
let studyInstanceUID: string | undefined = (d as any).studyInstanceUID;
|
||||||
// optional: attempt to fetch first file and extract StudyInstanceUID if dicomParser available
|
// optional: attempt to fetch first file and extract StudyInstanceUID if dicomParser available
|
||||||
@@ -821,10 +846,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
// Seed viewports with first stack if present (preserve old behavior)
|
// Seed viewports with first stack if present (preserve old behavior)
|
||||||
if (normalized[0]) {
|
if (normalized[0]) {
|
||||||
const firstStack = normalized[0];
|
const firstStack = normalized[0];
|
||||||
const firstSeries = firstStack.seriesGroups?.[0]?.imageIds || firstStack.imageIds;
|
let firstGroups = firstStack.seriesGroups || [];
|
||||||
|
if (firstGroups.length === 0) {
|
||||||
|
await setLoadedImageIdsAndCacheMeta(firstStack.imageIds, { force: true });
|
||||||
|
firstGroups = await deriveSeriesGroupsFromImageIds(firstStack.imageIds);
|
||||||
|
normalized[0] = { ...firstStack, seriesGroups: firstGroups };
|
||||||
|
setAvailableStacks([...normalized]);
|
||||||
|
}
|
||||||
|
const firstSeries = firstGroups[0]?.imageIds || firstStack.imageIds;
|
||||||
await setLoadedImageIdsAndCacheMeta(firstSeries);
|
await setLoadedImageIdsAndCacheMeta(firstSeries);
|
||||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(firstSeries || []));
|
setViewportImageIds(Array(MAX_VIEWPORTS).fill(firstSeries || []));
|
||||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => firstStack.seriesGroups || []));
|
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => firstGroups));
|
||||||
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
||||||
} else {
|
} else {
|
||||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
|
setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
|
||||||
@@ -844,21 +876,26 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||||
const stackObj = availableStacks[stackIdx];
|
const stackObj = availableStacks[stackIdx];
|
||||||
if (!stackObj) return;
|
if (!stackObj) return;
|
||||||
let seriesGroups = stackObj.seriesGroups;
|
let stack = stackObj.imageIds;
|
||||||
const stack = stackObj.imageIds;
|
console.log("[handleLoadStack] Starting with imageIds:", stack.length, "ids. First:", stack[0]?.slice(0, 80));
|
||||||
|
|
||||||
|
// IMPORTANT: Expand multiframe images BEFORE caching metadata
|
||||||
|
const expandedStack = await expandMultiframeImageIds(stack);
|
||||||
|
console.log("[handleLoadStack] After expansion:", expandedStack.length, "ids. First:", expandedStack[0]?.slice(0, 80));
|
||||||
|
stack = expandedStack;
|
||||||
|
|
||||||
await setLoadedImageIdsAndCacheMeta(stack, { force: true });
|
await setLoadedImageIdsAndCacheMeta(stack, { force: true });
|
||||||
if (!seriesGroups || seriesGroups.length === 0) {
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(stack);
|
||||||
seriesGroups = deriveSeriesGroupsFromImageIds(stack);
|
console.log("[handleLoadStack] Derived series groups:", seriesGroups.length, "groups. First group has", seriesGroups[0]?.imageIds.length, "images");
|
||||||
setAvailableStacks(prev => {
|
setAvailableStacks(prev => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
if (!next[stackIdx]) return prev;
|
if (!next[stackIdx]) return prev;
|
||||||
next[stackIdx] = { ...next[stackIdx], seriesGroups };
|
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stack };
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const primarySeries = seriesGroups[0]?.imageIds || stack;
|
const primarySeries = seriesGroups[0]?.imageIds || stack;
|
||||||
|
console.log("[handleLoadStack] Setting viewport to primarySeries with", primarySeries.length, "ids. First:", primarySeries[0]?.slice(0, 80));
|
||||||
|
|
||||||
setLoadingState("displayset");
|
setLoadingState("displayset");
|
||||||
try {
|
try {
|
||||||
@@ -1129,10 +1166,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const shouldCache = Boolean(autoCacheStack || options?.force);
|
const shouldCache = Boolean(autoCacheStack || options?.force);
|
||||||
|
|
||||||
if (shouldCache) {
|
if (shouldCache) {
|
||||||
|
const urlsToLoad = new Set<string>();
|
||||||
for (const imageId of imageIds) {
|
for (const imageId of imageIds) {
|
||||||
if (imageId.startsWith("wadouri:")) {
|
if (imageId.startsWith("wadouri:")) {
|
||||||
try {
|
try {
|
||||||
const url = stripWadouriPrefix(imageId);
|
const baseUrl = stripWadouriPrefix(imageId).replace(/[?&]frame=\d+/gi, "");
|
||||||
|
if (baseUrl) {
|
||||||
|
urlsToLoad.add(baseUrl);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to cache metadata for ${imageId}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const url of urlsToLoad) {
|
||||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||||||
@@ -1142,10 +1190,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
} else {
|
} else {
|
||||||
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to cache metadata for ${imageId}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all metadata loads to finish
|
// Wait for all metadata loads to finish
|
||||||
@@ -1169,6 +1213,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const cacheKey = `wadouri:${url}`;
|
const cacheKey = `wadouri:${url}`;
|
||||||
const cached = multiframeCountCacheRef.current[cacheKey];
|
const cached = multiframeCountCacheRef.current[cacheKey];
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
|
console.log("[getNumberOfFramesForImageId] Cache hit:", cacheKey.slice(0, 60), "->", cached);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1182,6 +1227,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const multiframeMeta = metaData.get("multiframeModule", normalized) as any;
|
const multiframeMeta = metaData.get("multiframeModule", normalized) as any;
|
||||||
const fromMeta = Number(multiframeMeta?.numberOfFrames);
|
const fromMeta = Number(multiframeMeta?.numberOfFrames);
|
||||||
if (Number.isFinite(fromMeta) && fromMeta > 1) {
|
if (Number.isFinite(fromMeta) && fromMeta > 1) {
|
||||||
|
console.log("[getNumberOfFramesForImageId] Found in metadata:", fromMeta);
|
||||||
multiframeCountCacheRef.current[cacheKey] = Math.floor(fromMeta);
|
multiframeCountCacheRef.current[cacheKey] = Math.floor(fromMeta);
|
||||||
return Math.floor(fromMeta);
|
return Math.floor(fromMeta);
|
||||||
}
|
}
|
||||||
@@ -1194,6 +1240,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
if (cacheManager?.get) {
|
if (cacheManager?.get) {
|
||||||
const dataSet = cacheManager.get(url);
|
const dataSet = cacheManager.get(url);
|
||||||
const fromCache = readFramesFromDataSet(dataSet);
|
const fromCache = readFramesFromDataSet(dataSet);
|
||||||
|
console.log("[getNumberOfFramesForImageId] Found in cache manager:", fromCache);
|
||||||
multiframeCountCacheRef.current[cacheKey] = fromCache;
|
multiframeCountCacheRef.current[cacheKey] = fromCache;
|
||||||
return fromCache;
|
return fromCache;
|
||||||
}
|
}
|
||||||
@@ -1202,13 +1249,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80));
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
const fromFetch = readFramesFromDataSet(dataSet);
|
const fromFetch = readFramesFromDataSet(dataSet);
|
||||||
|
console.log("[getNumberOfFramesForImageId] Found via fetch:", fromFetch);
|
||||||
multiframeCountCacheRef.current[cacheKey] = fromFetch;
|
multiframeCountCacheRef.current[cacheKey] = fromFetch;
|
||||||
return fromFetch;
|
return fromFetch;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e);
|
||||||
multiframeCountCacheRef.current[cacheKey] = 1;
|
multiframeCountCacheRef.current[cacheKey] = 1;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -1216,6 +1266,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => {
|
const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => {
|
||||||
const expanded: string[] = [];
|
const expanded: string[] = [];
|
||||||
|
console.log("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds");
|
||||||
|
|
||||||
for (const raw of rawImageIds || []) {
|
for (const raw of rawImageIds || []) {
|
||||||
const imageId = normalizeToWadouriOrExternal(raw);
|
const imageId = normalizeToWadouriOrExternal(raw);
|
||||||
@@ -1225,6 +1276,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}
|
}
|
||||||
|
|
||||||
const frameCount = await getNumberOfFramesForImageId(imageId);
|
const frameCount = await getNumberOfFramesForImageId(imageId);
|
||||||
|
console.log("[expandMultiframeImageIds] frameCount for", imageId.slice(0, 80), ":", frameCount);
|
||||||
if (frameCount <= 1) {
|
if (frameCount <= 1) {
|
||||||
expanded.push(imageId);
|
expanded.push(imageId);
|
||||||
continue;
|
continue;
|
||||||
@@ -1233,16 +1285,87 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
for (let frame = 1; frame <= frameCount; frame++) {
|
for (let frame = 1; frame <= frameCount; frame++) {
|
||||||
expanded.push(withFrameQuery(imageId, frame));
|
expanded.push(withFrameQuery(imageId, frame));
|
||||||
}
|
}
|
||||||
|
console.log("[expandMultiframeImageIds] Expanded to", frameCount, "frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
return dedupeImageIds(expanded);
|
const result = dedupeImageIds(expanded);
|
||||||
|
console.log("[expandMultiframeImageIds] Output:", result.length, "total imageIds");
|
||||||
|
return result;
|
||||||
}, [getNumberOfFramesForImageId]);
|
}, [getNumberOfFramesForImageId]);
|
||||||
|
|
||||||
const deriveSeriesGroupsFromImageIds = useCallback((imageIds: string[]): StackSeriesGroup[] => {
|
const getFallbackBValuesForImageId = useCallback(async (imageId: string): Promise<number[]> => {
|
||||||
|
if (!imageId.startsWith("wadouri:")) return [];
|
||||||
|
|
||||||
|
const normalized = normalizeToWadouriOrExternal(imageId);
|
||||||
|
const baseUrl = stripWadouriPrefix(normalized).replace(/[?&]frame=\d+/i, "");
|
||||||
|
const cached = bValuesByBaseImageRef.current[baseUrl];
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const parseBValuesFromDataSet = (dataSet: any): number[] => {
|
||||||
|
if (!dataSet || typeof dataSet.string !== "function") return [];
|
||||||
|
const standard = parseNumericValueList(dataSet.string("x00189087")); // DiffusionBValue
|
||||||
|
if (standard.length > 0) return standard;
|
||||||
|
const privatePhilips = parseNumericValueList(dataSet.string("x20011003"));
|
||||||
|
if (privatePhilips.length > 0) return privatePhilips;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheManager = cornerstoneDICOMImageLoader?.wadouri?.dataSetCacheManager;
|
||||||
|
if (cacheManager?.get) {
|
||||||
|
const existing = cacheManager.get(baseUrl);
|
||||||
|
const fromExisting = parseBValuesFromDataSet(existing);
|
||||||
|
if (fromExisting.length > 0) {
|
||||||
|
bValuesByBaseImageRef.current[baseUrl] = fromExisting;
|
||||||
|
return fromExisting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue with fetch fallback.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(baseUrl);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
|
const fromFetch = parseBValuesFromDataSet(dataSet);
|
||||||
|
bValuesByBaseImageRef.current[baseUrl] = fromFetch;
|
||||||
|
return fromFetch;
|
||||||
|
} catch (e) {
|
||||||
|
bValuesByBaseImageRef.current[baseUrl] = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deriveSeriesGroupsFromImageIds = useCallback(async (imageIds: string[]): Promise<StackSeriesGroup[]> => {
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0) {
|
if (!Array.isArray(imageIds) || imageIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const frameOrderByBase = new Map<string, string[]>();
|
||||||
|
imageIds.forEach((id) => {
|
||||||
|
const base = stripWadouriPrefix(normalizeToWadouriOrExternal(id)).replace(/[?&]frame=\d+/i, "");
|
||||||
|
const arr = frameOrderByBase.get(base) || [];
|
||||||
|
arr.push(id);
|
||||||
|
frameOrderByBase.set(base, arr);
|
||||||
|
});
|
||||||
|
|
||||||
|
frameOrderByBase.forEach((arr, base) => {
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const fa = getFrameNumberFromImageId(a) || 1;
|
||||||
|
const fb = getFrameNumberFromImageId(b) || 1;
|
||||||
|
return fa - fb;
|
||||||
|
});
|
||||||
|
frameOrderByBase.set(base, arr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackByBase = new Map<string, number[]>();
|
||||||
|
await Promise.all(Array.from(frameOrderByBase.keys()).map(async (base) => {
|
||||||
|
const firstId = `wadouri:${base}`;
|
||||||
|
const values = await getFallbackBValuesForImageId(firstId);
|
||||||
|
fallbackByBase.set(base, values);
|
||||||
|
}));
|
||||||
|
|
||||||
const groups = new Map<string, { imageIds: string[]; seriesInstanceUID?: string; seriesDescription?: string; seriesNumber?: number; bValue?: number | null }>();
|
const groups = new Map<string, { imageIds: string[]; seriesInstanceUID?: string; seriesDescription?: string; seriesNumber?: number; bValue?: number | null }>();
|
||||||
|
|
||||||
imageIds.forEach((id) => {
|
imageIds.forEach((id) => {
|
||||||
@@ -1255,8 +1378,33 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const seriesNumber = Number(seriesMeta.seriesNumber);
|
const seriesNumber = Number(seriesMeta.seriesNumber);
|
||||||
const temporalPosition = Number(imageMeta.temporalPositionIdentifier);
|
const temporalPosition = Number(imageMeta.temporalPositionIdentifier);
|
||||||
const triggerTime = Number(imageMeta.triggerTime);
|
const triggerTime = Number(imageMeta.triggerTime);
|
||||||
const bValueRaw = Number(mrMeta.diffusionBValue ?? mrMeta.diffusionbValue ?? mrMeta.bValue);
|
|
||||||
const bValue = Number.isFinite(bValueRaw) ? bValueRaw : null;
|
let bValueRaw = Number(mrMeta.diffusionBValue ?? mrMeta.diffusionbValue ?? mrMeta.bValue);
|
||||||
|
let bValue = Number.isFinite(bValueRaw) ? bValueRaw : null;
|
||||||
|
|
||||||
|
if (bValue === null) {
|
||||||
|
const base = stripWadouriPrefix(normalizeToWadouriOrExternal(id)).replace(/[?&]frame=\d+/i, "");
|
||||||
|
const fallbackValues = fallbackByBase.get(base) || [];
|
||||||
|
if (fallbackValues.length > 0) {
|
||||||
|
const orderedIds = frameOrderByBase.get(base) || [id];
|
||||||
|
const framePos = Math.max(0, orderedIds.indexOf(id));
|
||||||
|
const totalForBase = Math.max(1, orderedIds.length);
|
||||||
|
|
||||||
|
if (fallbackValues.length === 1) {
|
||||||
|
bValue = fallbackValues[0];
|
||||||
|
} else if (fallbackValues.length === totalForBase) {
|
||||||
|
bValue = fallbackValues[Math.min(fallbackValues.length - 1, framePos)];
|
||||||
|
} else if (fallbackValues.length === 2 && totalForBase % 2 === 0) {
|
||||||
|
bValue = framePos < totalForBase / 2 ? fallbackValues[0] : fallbackValues[1];
|
||||||
|
} else {
|
||||||
|
const bucket = Math.min(
|
||||||
|
fallbackValues.length - 1,
|
||||||
|
Math.floor((framePos / totalForBase) * fallbackValues.length)
|
||||||
|
);
|
||||||
|
bValue = fallbackValues[bucket];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const temporalKey = Number.isFinite(temporalPosition) ? `t${temporalPosition}` : "";
|
const temporalKey = Number.isFinite(temporalPosition) ? `t${temporalPosition}` : "";
|
||||||
const triggerKey = Number.isFinite(triggerTime) ? `tr${Math.round(triggerTime)}` : "";
|
const triggerKey = Number.isFinite(triggerTime) ? `tr${Math.round(triggerTime)}` : "";
|
||||||
@@ -1298,7 +1446,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
});
|
});
|
||||||
|
|
||||||
return ordered;
|
return ordered;
|
||||||
}, []);
|
}, [getFallbackBValuesForImageId]);
|
||||||
|
|
||||||
|
|
||||||
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
||||||
@@ -1364,6 +1512,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
resolution: string,
|
resolution: string,
|
||||||
spacing: string,
|
spacing: string,
|
||||||
focalPoint: string,
|
focalPoint: string,
|
||||||
|
frameIndex?: number,
|
||||||
|
totalFrames?: number,
|
||||||
}>>(() =>
|
}>>(() =>
|
||||||
Array(viewportGrid.rows * viewportGrid.cols).fill({
|
Array(viewportGrid.rows * viewportGrid.cols).fill({
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -1961,17 +2111,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}
|
}
|
||||||
return frames;
|
return frames;
|
||||||
});
|
});
|
||||||
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||||
stacks.push({
|
stacks.push({
|
||||||
imageIds,
|
imageIds,
|
||||||
studyInstanceUID: g.studyInstanceUID,
|
studyInstanceUID: g.studyInstanceUID,
|
||||||
seriesInstanceUID: g.seriesInstanceUID,
|
seriesInstanceUID: g.seriesInstanceUID,
|
||||||
seriesGroups: [{
|
seriesGroups,
|
||||||
key: String(g.seriesInstanceUID || key),
|
|
||||||
label: `Series ${stacks.length + 1}`,
|
|
||||||
imageIds,
|
|
||||||
seriesInstanceUID: g.seriesInstanceUID,
|
|
||||||
bValue: null,
|
|
||||||
}],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2086,6 +2231,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
let resolution = "";
|
let resolution = "";
|
||||||
let spacing = "";
|
let spacing = "";
|
||||||
let focalPoint = "";
|
let focalPoint = "";
|
||||||
|
let frameIndex = undefined;
|
||||||
|
let totalFrames = undefined;
|
||||||
|
|
||||||
if (viewportModes[viewportIdx] === "stack") {
|
if (viewportModes[viewportIdx] === "stack") {
|
||||||
currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||||||
@@ -2098,6 +2245,35 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
const currentImageId = imageIds[currentIndex];
|
const currentImageId = imageIds[currentIndex];
|
||||||
if (currentImageId) {
|
if (currentImageId) {
|
||||||
|
console.log("[updateOverlay] Current image:", currentImageId.slice(0, 80), "frameIndex status:", frameIndex, "totalFrames:", totalFrames);
|
||||||
|
// Detect frame index from imageId query
|
||||||
|
if (currentImageId.includes('&frame=')) {
|
||||||
|
const match = currentImageId.match(/&frame=(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
frameIndex = parseInt(match[1], 10);
|
||||||
|
console.log("[updateOverlay] Frame detected:", frameIndex);
|
||||||
|
// Try to get total frames from metadata
|
||||||
|
const baseImageId = currentImageId.split('&frame=')[0];
|
||||||
|
const multiframeMeta = metaData.get("multiframeModule", baseImageId) as any;
|
||||||
|
if (multiframeMeta?.numberOfFrames) {
|
||||||
|
totalFrames = multiframeMeta.numberOfFrames;
|
||||||
|
console.log("[updateOverlay] Total frames from metadata:", totalFrames);
|
||||||
|
} else {
|
||||||
|
// Fallback: count unique frame indices in imageIds
|
||||||
|
const frameNumbers = imageIds.map(id => {
|
||||||
|
const m = id.match(/&frame=(\d+)/);
|
||||||
|
return m ? parseInt(m[1], 10) : null;
|
||||||
|
}).filter(n => n !== null && typeof n === 'number');
|
||||||
|
if (frameNumbers.length > 0) {
|
||||||
|
totalFrames = Math.max(...frameNumbers as number[]);
|
||||||
|
console.log("[updateOverlay] Total frames from imageIds:", totalFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[updateOverlay] No frame query in imageId. imageIds.length:", imageIds.length);
|
||||||
|
}
|
||||||
|
|
||||||
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
|
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
|
||||||
const planeMeta = metaData.get("imagePlaneModule", currentImageId) as any;
|
const planeMeta = metaData.get("imagePlaneModule", currentImageId) as any;
|
||||||
const pixelMeta = metaData.get("imagePixelModule", currentImageId) as any;
|
const pixelMeta = metaData.get("imagePixelModule", currentImageId) as any;
|
||||||
@@ -2222,6 +2398,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
resolution,
|
resolution,
|
||||||
spacing,
|
spacing,
|
||||||
focalPoint,
|
focalPoint,
|
||||||
|
frameIndex,
|
||||||
|
totalFrames,
|
||||||
};
|
};
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -2278,7 +2456,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const imageIds = await expandMultiframeImageIds(rawImageIds.map(normalizeToWadouriOrExternal));
|
const imageIds = await expandMultiframeImageIds(rawImageIds.map(normalizeToWadouriOrExternal));
|
||||||
if (imageIds.length === 0) return;
|
if (imageIds.length === 0) return;
|
||||||
await setLoadedImageIdsAndCacheMeta(imageIds, { force: true });
|
await setLoadedImageIdsAndCacheMeta(imageIds, { force: true });
|
||||||
const seriesGroups = deriveSeriesGroupsFromImageIds(imageIds);
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||||
const primarySeries = seriesGroups[0]?.imageIds || imageIds;
|
const primarySeries = seriesGroups[0]?.imageIds || imageIds;
|
||||||
|
|
||||||
// Add the incoming stack to the panel and capture its index.
|
// Add the incoming stack to the panel and capture its index.
|
||||||
@@ -2393,19 +2571,20 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
const applyStackDrop = async (viewportIdx: number, stackIdx: number, requestedZone: ViewportDropZone) => {
|
const applyStackDrop = async (viewportIdx: number, stackIdx: number, requestedZone: ViewportDropZone) => {
|
||||||
const stackObj = availableStacks[stackIdx];
|
const stackObj = availableStacks[stackIdx];
|
||||||
if (!stackObj) return;
|
if (!stackObj) return;
|
||||||
let seriesGroups = stackObj.seriesGroups;
|
let stackImageIds = stackObj.imageIds || [];
|
||||||
const stackImageIds = stackObj.imageIds || [];
|
|
||||||
|
// IMPORTANT: Expand multiframe images BEFORE caching metadata
|
||||||
|
const expandedImageIds = await expandMultiframeImageIds(stackImageIds);
|
||||||
|
stackImageIds = expandedImageIds;
|
||||||
|
|
||||||
await setLoadedImageIdsAndCacheMeta(stackImageIds, { force: true });
|
await setLoadedImageIdsAndCacheMeta(stackImageIds, { force: true });
|
||||||
if (!seriesGroups || seriesGroups.length === 0) {
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(stackImageIds);
|
||||||
seriesGroups = deriveSeriesGroupsFromImageIds(stackImageIds);
|
|
||||||
setAvailableStacks(prev => {
|
setAvailableStacks(prev => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
if (!next[stackIdx]) return prev;
|
if (!next[stackIdx]) return prev;
|
||||||
next[stackIdx] = { ...next[stackIdx], seriesGroups };
|
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stackImageIds };
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const primarySeries = seriesGroups[0]?.imageIds || stackImageIds;
|
const primarySeries = seriesGroups[0]?.imageIds || stackImageIds;
|
||||||
|
|
||||||
const resolvedZone = resolveDropZone(requestedZone);
|
const resolvedZone = resolveDropZone(requestedZone);
|
||||||
@@ -3238,6 +3417,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
focalPoint: "",
|
focalPoint: "",
|
||||||
}));
|
}));
|
||||||
setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false));
|
setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false));
|
||||||
|
setCineControlsOpenByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||||
}, [viewportGrid]);
|
}, [viewportGrid]);
|
||||||
|
|
||||||
// Re-apply bindings when tool sets or ctrl changes
|
// Re-apply bindings when tool sets or ctrl changes
|
||||||
@@ -3324,7 +3504,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
||||||
const expandedImageIds = await expandMultiframeImageIds(wadouriImageIds);
|
const expandedImageIds = await expandMultiframeImageIds(wadouriImageIds);
|
||||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||||
const seriesGroups = deriveSeriesGroupsFromImageIds(expandedImageIds);
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||||
|
|
||||||
// Try to extract StudyInstanceUID from the first image if possible
|
// Try to extract StudyInstanceUID from the first image if possible
|
||||||
@@ -3496,6 +3676,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => []));
|
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => []));
|
||||||
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
||||||
setCinePlayingByViewport(Array(MAX_VIEWPORTS).fill(false));
|
setCinePlayingByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||||
|
setCineControlsOpenByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||||
setViewportModes(Array(MAX_VIEWPORTS).fill('stack'));
|
setViewportModes(Array(MAX_VIEWPORTS).fill('stack'));
|
||||||
Object.values(stackThumbnailsRef.current).forEach((thumbnail) => {
|
Object.values(stackThumbnailsRef.current).forEach((thumbnail) => {
|
||||||
if (typeof thumbnail === 'string' && thumbnail.startsWith('blob:')) {
|
if (typeof thumbnail === 'string' && thumbnail.startsWith('blob:')) {
|
||||||
@@ -3612,7 +3793,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
if (!Array.isArray(imageIds) || imageIds.length === 0) return;
|
if (!Array.isArray(imageIds) || imageIds.length === 0) return;
|
||||||
const expandedImageIds = await expandMultiframeImageIds(imageIds);
|
const expandedImageIds = await expandMultiframeImageIds(imageIds);
|
||||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||||
const seriesGroups = deriveSeriesGroupsFromImageIds(expandedImageIds);
|
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||||
// Optionally extract StudyInstanceUID if not provided
|
// Optionally extract StudyInstanceUID if not provided
|
||||||
let uid = studyInstanceUID;
|
let uid = studyInstanceUID;
|
||||||
@@ -4755,6 +4936,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
||||||
|
{imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames ? (
|
||||||
|
<> [Frame: {imageInfos[i]?.frameIndex} / {imageInfos[i]?.totalFrames}]</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
||||||
@@ -4812,8 +4996,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
bottom: 8,
|
bottom: 8,
|
||||||
zIndex: 110, // above the bottom study selector (zIndex:20)
|
zIndex: 110, // above the bottom study selector (zIndex:20)
|
||||||
pointerEvents: "auto", // <-- ensure pointer events are enabled
|
pointerEvents: "auto", // <-- ensure pointer events are enabled
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
background: cineControlsOpenByViewport[i] ? "rgba(30,120,200,0.95)" : "rgba(0,0,0,0.7)",
|
||||||
|
color: "#fff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.2)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: "6px 9px",
|
||||||
|
cursor: "pointer",
|
||||||
|
minHeight: 30,
|
||||||
|
}}
|
||||||
|
title={cineControlsOpenByViewport[i] ? "Hide cine controls" : "Show cine controls"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCineControlsOpenByViewport((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
const nextOpen = !next[i];
|
||||||
|
next[i] = nextOpen;
|
||||||
|
if (!nextOpen) {
|
||||||
|
setCinePlayingByViewport((playing) => {
|
||||||
|
const p = [...playing];
|
||||||
|
p[i] = false;
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cine
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
className="preset-menu"
|
className="preset-menu"
|
||||||
style={{
|
style={{
|
||||||
@@ -4913,7 +5132,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{viewportModes[i] === "stack" && (
|
{viewportModes[i] === "stack" && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1 || (imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames)) && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -4934,7 +5153,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{imageInfos[i]?.total > 1 && (
|
{cineControlsOpenByViewport[i] && imageInfos[i]?.total > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
@@ -4978,6 +5197,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames && imageInfos[i]?.totalFrames > 1 && (
|
||||||
|
<>
|
||||||
|
<span style={{ color: "#fff", fontSize: 12, whiteSpace: "nowrap" }}>Frame</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={imageInfos[i]?.totalFrames || 1}
|
||||||
|
step={1}
|
||||||
|
value={Math.max(1, Math.min(imageInfos[i]?.totalFrames || 1, imageInfos[i]?.frameIndex || 1))}
|
||||||
|
onChange={(e) => {
|
||||||
|
const targetFrame = Number(e.target.value);
|
||||||
|
const imageIds = viewportImageIds[i] || [];
|
||||||
|
const targetIdx = imageIds.findIndex(id => {
|
||||||
|
const m = id.match(/&frame=(\d+)/);
|
||||||
|
return m && parseInt(m[1], 10) === targetFrame;
|
||||||
|
});
|
||||||
|
if (targetIdx >= 0) {
|
||||||
|
const viewport = renderingEngineRef.current?.getViewport(`CT_${i}`);
|
||||||
|
if (viewport && typeof csUtilities.jumpToSlice === "function") {
|
||||||
|
csUtilities.jumpToSlice(viewport.element, { imageIndex: targetIdx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: "#e0e0e0",
|
||||||
|
fontSize: 11,
|
||||||
|
minWidth: 50,
|
||||||
|
textAlign: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{imageInfos[i]?.frameIndex} / {imageInfos[i]?.totalFrames}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{(seriesGroupsByViewport[i] || []).length > 1 && (
|
{(seriesGroupsByViewport[i] || []).length > 1 && (
|
||||||
<>
|
<>
|
||||||
<span style={{ color: "#fff", fontSize: 12, whiteSpace: "nowrap" }}>Series</span>
|
<span style={{ color: "#fff", fontSize: 12, whiteSpace: "nowrap" }}>Series</span>
|
||||||
@@ -5239,7 +5495,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: viewportModes[i] === "stack" ? 56 : 8,
|
bottom: viewportModes[i] === "stack" && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1) ? 56 : 8,
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -5340,8 +5596,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
|
const blurHost = e.currentTarget as HTMLElement | null;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!(e.currentTarget as HTMLElement).contains(document.activeElement)) {
|
if (!blurHost) {
|
||||||
|
setOpenDropdownIdx(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!blurHost.contains(document.activeElement)) {
|
||||||
setOpenDropdownIdx(null);
|
setOpenDropdownIdx(null);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user