basic reference line functions
This commit is contained in:
+246
-77
@@ -60,6 +60,7 @@ const MOUSE_BUTTONS = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// App definintion
|
||||||
function App() {
|
function App() {
|
||||||
const elementRef = useRef<HTMLDivElement>(null)
|
const elementRef = useRef<HTMLDivElement>(null)
|
||||||
const viewportRef = useRef<any>(null) // Store the viewport instance
|
const viewportRef = useRef<any>(null) // Store the viewport instance
|
||||||
@@ -82,28 +83,106 @@ function App() {
|
|||||||
const renderingEngineId = "mainRenderingEngine";
|
const renderingEngineId = "mainRenderingEngine";
|
||||||
const renderingEngineRef = useRef<any>(null);
|
const renderingEngineRef = useRef<any>(null);
|
||||||
|
|
||||||
// Store imageIds globally after loading so they can be reused
|
const [availableStacks, setAvailableStacks] = useState<string[][]>([]);
|
||||||
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
|
const [activeViewport, setActiveViewport] = useState<number | null>(0);
|
||||||
|
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
|
||||||
|
() => Array(MAX_VIEWPORTS).fill([])
|
||||||
|
);
|
||||||
|
const [selectedStackIdx, setSelectedStackIdx] = useState<number[]>(
|
||||||
|
() => Array(MAX_VIEWPORTS).fill(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load available stacks on mount
|
||||||
|
useEffect(() => {
|
||||||
|
availableImageIds().then(stacks => {
|
||||||
|
setAvailableStacks(stacks);
|
||||||
|
// Optionally initialize each viewport with the first stack
|
||||||
|
setLoadedImageIdsAndCacheMeta(stacks[0]);
|
||||||
|
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||||
|
const stack = availableStacks[stackIdx];
|
||||||
|
|
||||||
|
if (!stack) return;
|
||||||
|
setViewportImageIds(prev => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[viewportIdx] = stack;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setSelectedStackIdx(prev => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[viewportIdx] = stackIdx;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache imageIds and load metadata for this stack
|
||||||
|
await setLoadedImageIdsAndCacheMeta(stack);
|
||||||
|
};
|
||||||
|
|
||||||
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
|
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
|
||||||
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(false);
|
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
|
||||||
|
|
||||||
|
const sortViewportImageIdsBySliceLocation = () => {
|
||||||
|
setViewportImageIds(prev => {
|
||||||
|
const next = prev.map((imageIds, idx) => {
|
||||||
|
if (!imageIds || imageIds.length === 0) return imageIds;
|
||||||
|
|
||||||
|
const withSortKey = imageIds.map(id => {
|
||||||
|
const plane = metaData.get('imagePlaneModule', id);
|
||||||
|
let sortKey = null;
|
||||||
|
if (plane) {
|
||||||
|
if (plane.sliceLocation !== undefined) {
|
||||||
|
sortKey = Number(plane.sliceLocation);
|
||||||
|
} else if (
|
||||||
|
plane.imagePositionPatient &&
|
||||||
|
Array.isArray(plane.imagePositionPatient) &&
|
||||||
|
plane.imagePositionPatient.length === 3
|
||||||
|
) {
|
||||||
|
sortKey = Number(plane.imagePositionPatient[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { imageId: id, sortKey };
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = [...withSortKey].sort((a, b) => {
|
||||||
|
if (a.sortKey !== null && b.sortKey !== null) {
|
||||||
|
return a.sortKey - b.sortKey;
|
||||||
|
}
|
||||||
|
return a.imageId.localeCompare(b.imageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedIds = sorted.map(obj => obj.imageId);
|
||||||
|
|
||||||
|
// Log the sorted result for this viewport
|
||||||
|
console.log(`Viewport ${idx} sorted imageIds:`, sortedIds);
|
||||||
|
|
||||||
|
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
|
||||||
|
return imageIds;
|
||||||
|
}
|
||||||
|
return sortedIds;
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Wrap setLoadedImageIds to also cache DICOM metadata
|
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||||||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
||||||
|
console.log("Caching loaded image IDs:", imageIds);
|
||||||
|
const loadPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const imageId of imageIds) {
|
for (const imageId of imageIds) {
|
||||||
// Only cache if it's a local file (wadouri:blob:) or a file URL
|
|
||||||
if (imageId.startsWith("wadouri:")) {
|
if (imageId.startsWith("wadouri:")) {
|
||||||
try {
|
try {
|
||||||
// Extract the URL after "wadouri:"
|
|
||||||
const url = imageId.slice(8);
|
const url = imageId.slice(8);
|
||||||
// Only cache if it's a blob or file URL
|
|
||||||
if (url.startsWith("blob:") || url.startsWith("/")) {
|
if (url.startsWith("blob:") || url.startsWith("/")) {
|
||||||
//const response = await fetch(url);
|
|
||||||
//const arrayBuffer = await response.arrayBuffer();
|
|
||||||
//const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|
||||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||||
cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
// If load returns a promise, collect it
|
||||||
|
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||||||
|
if (result && typeof result.then === "function") {
|
||||||
|
loadPromises.push(result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
||||||
}
|
}
|
||||||
@@ -113,7 +192,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLoadedImageIds(imageIds);
|
|
||||||
|
// Wait for all metadata loads to finish
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
console.log("Image IDs cached:", imageIds);
|
||||||
|
|
||||||
|
// Trigger sort if enabled
|
||||||
|
if (orderBySliceLocation) {
|
||||||
|
sortViewportImageIdsBySliceLocation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -196,12 +283,14 @@ function App() {
|
|||||||
|
|
||||||
// Add this function to gather and format metadata:
|
// Add this function to gather and format metadata:
|
||||||
const handleShowMetadata = () => {
|
const handleShowMetadata = () => {
|
||||||
if (!loadedImageIds || loadedImageIds.length === 0) {
|
// Find the first visible viewport with images
|
||||||
|
const firstIdx = viewportImageIds.findIndex(ids => ids && ids.length > 0);
|
||||||
|
if (firstIdx === -1) {
|
||||||
setMetaModalContent("No images loaded.");
|
setMetaModalContent("No images loaded.");
|
||||||
setMetaModalOpen(true);
|
setMetaModalOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const imageId = loadedImageIds[0];
|
const imageId = viewportImageIds[firstIdx][0];
|
||||||
|
|
||||||
// Gather all available metadata modules for this imageId
|
// Gather all available metadata modules for this imageId
|
||||||
const modules = [
|
const modules = [
|
||||||
@@ -378,6 +467,7 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
|
console.log("Setting up..");
|
||||||
// 1. Initialize Cornerstone3D and tools ONCE
|
// 1. Initialize Cornerstone3D and tools ONCE
|
||||||
if (!running.current) {
|
if (!running.current) {
|
||||||
running.current = true;
|
running.current = true;
|
||||||
@@ -388,12 +478,6 @@ function App() {
|
|||||||
dicomImageLoaderInit({ maxWebWorkers: 1 });
|
dicomImageLoaderInit({ maxWebWorkers: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Load image IDs if not already loaded
|
|
||||||
let imageIds = loadedImageIds;
|
|
||||||
if (!imageIds) {
|
|
||||||
imageIds = await createImageIds();
|
|
||||||
setLoadedImageIdsAndCacheMeta(imageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Get or create the rendering engine
|
// 3. Get or create the rendering engine
|
||||||
let renderingEngine = renderingEngineRef.current;
|
let renderingEngine = renderingEngineRef.current;
|
||||||
@@ -420,6 +504,14 @@ function App() {
|
|||||||
if (!volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
if (!volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
||||||
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
|
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
|
||||||
}
|
}
|
||||||
|
// Set the active viewport as the source for reference lines
|
||||||
|
const sourceViewportId = `CT_${activeViewport}`;
|
||||||
|
stackToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
|
||||||
|
sourceViewportId,
|
||||||
|
});
|
||||||
|
volumeToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
|
||||||
|
sourceViewportId,
|
||||||
|
});
|
||||||
stackToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
stackToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||||
volumeToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
volumeToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||||
} else {
|
} else {
|
||||||
@@ -433,6 +525,10 @@ function App() {
|
|||||||
|
|
||||||
// 6. Enable and configure each viewport
|
// 6. Enable and configure each viewport
|
||||||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||||||
|
|
||||||
|
let imageIds = viewportImageIds[i];
|
||||||
|
if (!imageIds || imageIds.length === 0) continue;
|
||||||
|
|
||||||
const viewportId = `CT_${i}`;
|
const viewportId = `CT_${i}`;
|
||||||
const element = viewportRefs.current[i];
|
const element = viewportRefs.current[i];
|
||||||
if (!element) continue;
|
if (!element) continue;
|
||||||
@@ -494,20 +590,20 @@ function App() {
|
|||||||
// Restore scroll, properties, etc.
|
// Restore scroll, properties, etc.
|
||||||
} else if (viewportModes[i] === 'volume') {
|
} else if (viewportModes[i] === 'volume') {
|
||||||
|
|
||||||
let sortedImageIds = imageIds;
|
//let sortedImageIds = imageIds;
|
||||||
if (imageIds && imageIds.length > 1) {
|
//if (imageIds && imageIds.length > 1) {
|
||||||
sortedImageIds = [...imageIds].sort((a, b) => {
|
// sortedImageIds = [...imageIds].sort((a, b) => {
|
||||||
const metaA = metaData.get('imagePlaneModule', a);
|
// const metaA = metaData.get('imagePlaneModule', a);
|
||||||
const metaB = metaData.get('imagePlaneModule', b);
|
// const metaB = metaData.get('imagePlaneModule', b);
|
||||||
const sliceA = metaA && metaA.sliceLocation !== undefined ? Number(metaA.sliceLocation) : 0;
|
// const sliceA = metaA && metaA.sliceLocation !== undefined ? Number(metaA.sliceLocation) : 0;
|
||||||
const sliceB = metaB && metaB.sliceLocation !== undefined ? Number(metaB.sliceLocation) : 0;
|
// const sliceB = metaB && metaB.sliceLocation !== undefined ? Number(metaB.sliceLocation) : 0;
|
||||||
return sliceA - sliceB;
|
// return sliceA - sliceB;
|
||||||
});
|
// });
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Use a unique volumeId per viewport
|
// Use a unique volumeId per viewport
|
||||||
const volumeId = `myVolumeId_${i}`;
|
const volumeId = `myVolumeId_${i}`;
|
||||||
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: sortedImageIds });
|
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds });
|
||||||
volume.load();
|
volume.load();
|
||||||
viewport.setVolumes([{ volumeId }]);
|
viewport.setVolumes([{ volumeId }]);
|
||||||
viewport.render();
|
viewport.render();
|
||||||
@@ -611,11 +707,11 @@ function App() {
|
|||||||
});
|
});
|
||||||
cancelAnimationFrame(raf);
|
cancelAnimationFrame(raf);
|
||||||
};
|
};
|
||||||
}, [elementRef, updateOverlay, viewportGrid, loadedImageIds, viewportModes]);
|
}, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds]);
|
||||||
|
|
||||||
|
|
||||||
// Handler to reset the view (camera and window/level)
|
// Handler to reset the view (camera and window/level)
|
||||||
const handleDoubleClick = useCallback((viewportIdx: numebr) => {
|
const handleDoubleClick = useCallback((viewportIdx: number) => {
|
||||||
const viewportId = `CT_${viewportIdx}`;
|
const viewportId = `CT_${viewportIdx}`;
|
||||||
const renderingEngine = renderingEngineRef.current;
|
const renderingEngine = renderingEngineRef.current;
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
@@ -690,33 +786,39 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add this effect to re-order images by Slice Location after images are loaded and when the option changes:
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedImageIds || loadedImageIds.length === 0) return;
|
if (!referenceLinesEnabled) return;
|
||||||
|
const stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
|
||||||
|
const volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
|
||||||
|
const sourceViewportId = `CT_${activeViewport}`;
|
||||||
|
if (stackToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
|
||||||
|
stackToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
|
||||||
|
}
|
||||||
|
if (volumeToolGroup?.hasTool(ReferenceLinesTool.toolName)) {
|
||||||
|
volumeToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, { sourceViewportId });
|
||||||
|
}
|
||||||
|
}, [activeViewport, referenceLinesEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!orderBySliceLocation) return;
|
if (!orderBySliceLocation) return;
|
||||||
|
|
||||||
let cancelled = false;
|
// Only sort if all metadata is loaded for all viewports
|
||||||
|
const allLoaded = viewportImageIds.every(imageIds =>
|
||||||
const checkAndSort = () => {
|
!imageIds || imageIds.length === 0 ||
|
||||||
// Check if all metadata is loaded
|
imageIds.every(id => !!metaData.get("imagePlaneModule", id))
|
||||||
const allLoaded = loadedImageIds.every(
|
|
||||||
id => !!metaData.get("imagePlaneModule", id)
|
|
||||||
);
|
);
|
||||||
if (!allLoaded) {
|
if (!allLoaded) return;
|
||||||
// Try again soon
|
|
||||||
setTimeout(checkAndSort, 200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All metadata loaded, sort by slice location
|
setViewportImageIds(prev => {
|
||||||
const getSliceLocation = (imageId: string) => {
|
const next = prev.map(imageIds => {
|
||||||
const meta = metaData.get('imagePlaneModule', imageId);
|
if (!imageIds || imageIds.length === 0) return imageIds;
|
||||||
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withSliceLoc = loadedImageIds.map(id => ({
|
const withSliceLoc = imageIds.map(id => ({
|
||||||
imageId: id,
|
imageId: id,
|
||||||
sliceLoc: getSliceLocation(id),
|
sliceLoc: (() => {
|
||||||
|
const meta = metaData.get('imagePlaneModule', id);
|
||||||
|
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
||||||
|
})(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const sorted = [...withSliceLoc].sort((a, b) => {
|
const sorted = [...withSliceLoc].sort((a, b) => {
|
||||||
@@ -727,19 +829,15 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sortedIds = sorted.map(obj => obj.imageId);
|
const sortedIds = sorted.map(obj => obj.imageId);
|
||||||
const isSame =
|
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
|
||||||
loadedImageIds.length === sortedIds.length &&
|
return imageIds;
|
||||||
loadedImageIds.every((id, idx) => id === sortedIds[idx]);
|
|
||||||
|
|
||||||
if (!isSame && !cancelled) {
|
|
||||||
setLoadedImageIds(sortedIds);
|
|
||||||
}
|
}
|
||||||
};
|
return sortedIds;
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [orderBySliceLocation, availableStacks]);
|
||||||
|
|
||||||
checkAndSort();
|
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
|
||||||
}, [loadedImageIds, orderBySliceLocation]);
|
|
||||||
|
|
||||||
|
|
||||||
const viewports = [];
|
const viewports = [];
|
||||||
@@ -762,11 +860,17 @@ function App() {
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gridRow: Math.floor(i / viewportGrid.cols) + 1,
|
gridRow: Math.floor(i / viewportGrid.cols) + 1,
|
||||||
gridColumn: (i % viewportGrid.cols) + 1,
|
gridColumn: (i % viewportGrid.cols) + 1,
|
||||||
height: "100%", // <-- Add this line
|
height: "100%",
|
||||||
// Use visibility instead of display to hide but keep size/layout
|
|
||||||
visibility: isVisible ? "visible" : "hidden",
|
visibility: isVisible ? "visible" : "hidden",
|
||||||
pointerEvents: isVisible ? "auto" : "none",
|
pointerEvents: isVisible ? "auto" : "none",
|
||||||
|
// Remove outline, use only boxShadow for indicator
|
||||||
|
boxShadow: activeViewport === i
|
||||||
|
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
|
||||||
|
: undefined,
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: activeViewport === i ? 10 : 1,
|
||||||
}}
|
}}
|
||||||
|
onClick={() => setActiveViewport(i)}
|
||||||
onDoubleClick={() => handleDoubleClick(i)}
|
onDoubleClick={() => handleDoubleClick(i)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -997,7 +1101,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Position bar inside each viewport */}
|
{/* Position bar inside each viewport */}
|
||||||
{viewportModes[i] === "stack" && loadedImageIds && (
|
{viewportModes[i] === "stack" && viewportImageIds[i] && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -1008,7 +1112,7 @@ function App() {
|
|||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
zIndex: 30,
|
zIndex: 30,
|
||||||
display: loadedImageIds.length > 0 ? "flex" : "none",
|
display: viewportImageIds[i].length > 0 ? "flex" : "none",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
@@ -1032,7 +1136,7 @@ function App() {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loadedImageIds.map((id, idx) => {
|
{viewportImageIds[i].map((id, idx) => {
|
||||||
const loaded = !!metaData.get("imagePlaneModule", id);
|
const loaded = !!metaData.get("imagePlaneModule", id);
|
||||||
const renderingEngine = renderingEngineRef.current;
|
const renderingEngine = renderingEngineRef.current;
|
||||||
let currentIdx = -1;
|
let currentIdx = -1;
|
||||||
@@ -1051,7 +1155,7 @@ function App() {
|
|||||||
key={id}
|
key={id}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: `${100 / loadedImageIds.length}%`,
|
height: `${100 / viewportImageIds[i].length}%`,
|
||||||
background: isCurrent
|
background: isCurrent
|
||||||
? "linear-gradient(to right, #ffeb3b 60%, #ff9800 100%)"
|
? "linear-gradient(to right, #ffeb3b 60%, #ff9800 100%)"
|
||||||
: loaded
|
: loaded
|
||||||
@@ -1060,7 +1164,7 @@ function App() {
|
|||||||
opacity: loaded ? 0.95 : 0.4,
|
opacity: loaded ? 0.95 : 0.4,
|
||||||
transition: "background 0.3s, opacity 0.3s",
|
transition: "background 0.3s, opacity 0.3s",
|
||||||
borderBottom:
|
borderBottom:
|
||||||
idx < loadedImageIds.length - 1
|
idx < viewportImageIds[i].length - 1
|
||||||
? "1px solid #222"
|
? "1px solid #222"
|
||||||
: "none",
|
: "none",
|
||||||
cursor: loaded ? "pointer" : "default",
|
cursor: loaded ? "pointer" : "default",
|
||||||
@@ -1092,6 +1196,57 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
zIndex: 20,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: "8px",
|
||||||
|
background: "rgba(34,34,34,0.85)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "2px 8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
value={selectedStackIdx[i] || 0}
|
||||||
|
onChange={e => handleLoadStack(i, Number(e.target.value))}
|
||||||
|
style={{
|
||||||
|
background: "#222",
|
||||||
|
color: "#fff",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "2px 8px",
|
||||||
|
fontSize: "13px",
|
||||||
|
marginRight: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{availableStacks.map((stack, idx) => (
|
||||||
|
<option key={idx} value={idx}>
|
||||||
|
Stack {idx + 1} ({stack.length} images)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
background: "#1976d2",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "3px",
|
||||||
|
padding: "2px 12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "13px",
|
||||||
|
}}
|
||||||
|
onClick={() => handleLoadStack(i, selectedStackIdx[i] || 0)}
|
||||||
|
disabled={!availableStacks.length}
|
||||||
|
>
|
||||||
|
Load Stack
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1100,11 +1255,16 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: "relative", width: "99vw", height: "99vh", background: "#111",
|
position: "relative",
|
||||||
margin: 0, padding: 0,
|
width: "99vw",
|
||||||
overflow: "hidden", // Add this line
|
height: "99vh",
|
||||||
}}>
|
background: "#111",
|
||||||
|
padding: "12px", // <-- Add this line
|
||||||
|
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
|
||||||
|
background: "#222",
|
||||||
|
// overflow: "hidden",
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
{/* Side menu toggle button */}
|
{/* Side menu toggle button */}
|
||||||
<button
|
<button
|
||||||
@@ -1264,6 +1424,12 @@ function App() {
|
|||||||
>
|
>
|
||||||
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
|
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={sortViewportImageIdsBySliceLocation}
|
||||||
|
style={{ margin: 8, padding: 8 }}
|
||||||
|
>
|
||||||
|
Sort All Viewports by Slice Location
|
||||||
|
</button>
|
||||||
{/* Add more menu items here if needed */}
|
{/* Add more menu items here if needed */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1387,9 +1553,10 @@ function App() {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
|
margin: 3,
|
||||||
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
|
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
|
||||||
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
|
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
|
||||||
gap: "2px",
|
gap: "5px", // Increase gap for better visibility
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1620,6 +1787,7 @@ function setupTools() {
|
|||||||
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
||||||
stackToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
stackToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
||||||
stackToolGroup.addTool(SculptorTool.toolName, {});
|
stackToolGroup.addTool(SculptorTool.toolName, {});
|
||||||
|
stackToolGroup.addTool(ReferenceLinesTool.toolName);
|
||||||
// StackScrollTool mouse wheel binding
|
// StackScrollTool mouse wheel binding
|
||||||
stackToolGroup.setToolActive(StackScrollTool.toolName, {
|
stackToolGroup.setToolActive(StackScrollTool.toolName, {
|
||||||
bindings: [{ mouseButton: MouseBindings.Wheel }],
|
bindings: [{ mouseButton: MouseBindings.Wheel }],
|
||||||
@@ -1643,6 +1811,7 @@ function setupTools() {
|
|||||||
volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
||||||
volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
||||||
volumeToolGroup.addTool(SculptorTool.toolName, {});
|
volumeToolGroup.addTool(SculptorTool.toolName, {});
|
||||||
|
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
|
||||||
//volumeToolGroup.addTool(CrosshairsTool.toolName, {});
|
//volumeToolGroup.addTool(CrosshairsTool.toolName, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user