Compare commits

..

84 Commits

Author SHA1 Message Date
Ross f323dfd559 imrove multiuser sync support 2026-06-29 09:38:26 +01:00
Ross 0c4da6e022 multiuser sync updates 2026-06-22 09:37:08 +01:00
Ross d0f3431ef4 start ortho automode 2026-06-08 13:50:42 +01:00
Ross b961a806d9 start implmeentting proper crosshairs 2026-06-08 12:51:08 +01:00
Ross 1ddc9a810a update 2026-06-08 09:50:55 +01:00
Ross b2bd2791a5 fix some loading issues and rework cine button 2026-05-28 22:53:52 +01:00
Ross b527eb1139 many updates 2026-05-21 22:44:13 +01:00
Ross 6a0552c843 feat(mobile): implement mobile stack drag-and-drop functionality with improved viewport handling 2026-05-19 14:47:41 +01:00
Ross a5c530a907 feat(mobile): enhance mobile interaction with new touch controls and layout adjustments 2026-05-19 14:26:28 +01:00
Ross 41f13ade5e feat(b-values): implement parsing for multiple b-value formats and update default setting 2026-05-19 13:26:32 +01:00
Ross b0d8c72636 feat(logger): enhance logging configuration with minLevel and silent option
- Added a new LogLevel "silent" to suppress logging output.
- Replaced the debug boolean flag in LoggerConfig with a minLevel property to specify the minimum log level.
- Updated shouldLog function to respect the new minLevel configuration.
- Modified parseDebugFlag to return appropriate LogLevel based on truthy values.
- Updated main.tsx to accommodate changes in logging configuration, including parsing minLevel from attributes and options.
- Deprecated the debug flag in favor of minLevel for better clarity and control over logging behavior.
2026-05-19 12:55:44 +01:00
Ross 4dd63668b0 feat(logging): implement structured logging with configurable options
- Added a logger utility to manage logging levels and formats.
- Integrated logging into main application and non-DICOM test components.
- Enhanced error handling with logging for invalid JSON data.
- Introduced global functions to set and get logging configurations.
- Updated DICOM viewer mounting logic to support logging configurations.
2026-05-19 09:41:52 +01:00
Ross 77bbbd548e Enhance stack management by preventing duplicate stacks and optimizing stack index usage 2026-05-18 22:31:28 +01:00
Ross 7c17a27ec3 try to fix fluro 2026-05-18 17:26:27 +01:00
Ross 68248737ec Enhance stack management by adding series groups support and improving multiframe handling 2026-05-18 16:31:15 +01:00
Ross 13361a30a5 Refactor DICOM viewer mounting logic and enhance container management
- Introduced a new `mountContainer` function to handle individual container mounting and unmounting.
- Added `shouldMountContainer` to determine if a container should be mounted based on provided options.
- Enhanced `parseToDescriptors` to improve parsing of image stack data, supporting both named stacks and plain arrays.
- Updated `mountDicomViewers` to utilize the new mounting logic and streamline container processing.
- Exposed `remountDicomViewer` globally for dynamic remounting of specific containers.
- Removed redundant debug logging and improved error handling for invalid data.
2026-05-18 14:07:33 +01:00
Ross 627cd740f1 Enhance debugging capabilities by adding detailed logging for thumbnail generation and viewer mounting processes 2026-05-18 13:14:15 +01:00
Ross af6e22fd91 Enhance viewport preset management by adding active preset tracking and applying presets on image changes 2026-05-18 12:39:08 +01:00
Ross 7b786fcf9f Refactor stack thumbnail management with dedicated setter and localStorage integration for stack panel width 2026-05-18 12:33:28 +01:00
Ross c9902a2e15 Enhance loading state management by adding 'displayset' option and improve user feedback during data loading 2026-05-18 12:29:42 +01:00
Ross 99df1cd1c1 Add stripWadouriPrefix function and refactor image ID handling for improved URL normalization 2026-05-18 12:17:35 +01:00
Ross f169238057 Enhance viewer functionality with volume diagnostics and fullscreen toggle 2026-05-18 11:51:38 +01:00
Ross f0e0fe0ef6 Implement drag-and-drop functionality for viewport stack management with visual drop zone previews 2026-05-18 11:37:34 +01:00
Ross 19702b6052 Refactor CT presets and enhance modality-specific preset options for improved functionality 2026-05-18 10:48:28 +01:00
Ross 1674254a3c Add clearViewer function to reset viewer state and annotations 2026-05-11 11:34:36 +01:00
Ross aeb04bcf2f Refactor URL normalization to ensure valid wadouri: prefix for DICOM entries
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 15:01:47 +01:00
Ross e29bce42e2 Implement resizable stack panel with min/max width constraints and update side menu button positioning 2026-04-27 12:02:54 +01:00
Ross d34772a3fa Refactor fullscreen exit logic to use appRootRef for improved reliability 2026-04-27 09:43:41 +01:00
Ross add45567d8 Enhance metadata caching in setLoadedImageIdsAndCacheMeta and add forced warm-up for visible stack viewports 2026-04-27 09:34:07 +01:00
Ross 3294151a92 Add shift key support for reference cursors and update button text 2026-04-27 09:26:27 +01:00
Ross 776b5faffe Refactor grid button styles for improved visibility and interaction 2026-04-27 09:21:59 +01:00
Ross c5a58da6a3 Refactor annotation import handling to improve race condition management and type safety 2026-03-23 11:08:21 +00:00
Ross be16e3554f Add defensive guards and queueing for annotation imports to handle race conditions 2026-03-23 11:02:03 +00:00
Ross 6cd9e33e65 Refactor annotation delete button styles for improved UI consistency 2026-03-23 10:41:36 +00:00
Ross e161377245 Add test links page for easy access to viewer test cases 2026-03-23 10:10:39 +00:00
Ross 1efd81bb0b Add embedded viewer test page with initial layout and functionality 2026-03-23 10:03:42 +00:00
Ross c0c5cd6195 Enhance tool options dropdowns to include icons for improved UI clarity 2026-03-23 09:43:57 +00:00
Ross 0c4db1904c Enhance tool options with icons for a more compact UI and improve annotation display styles for better readability 2026-03-16 14:29:39 +00:00
Ross 52f4be1a96 Refactor viewport state restoration and enhance error handling for volume and stack loading 2026-03-16 14:08:40 +00:00
Ross 8faa7e9be4 Add directory loading support and enhance file handling in the viewer 2026-03-16 12:31:42 +00:00
Ross adff7caffd Enhance error handling for graphics rendering and update external image loader registration 2026-03-16 11:09:04 +00:00
Ross 8dc4bb0aaa Add graphics error handling and notification for WebGL issues 2026-03-16 10:53:26 +00:00
Ross ef62ffac9f add basic non dicom support 2025-12-15 11:39:44 +00:00
Ross 72462b49ef Add support for external image loading and enhance pan/zoom interactions 2025-12-15 09:15:27 +00:00
Ross 181f471806 basic 2025-12-15 09:07:36 +00:00
Ross 8a0a08000e add basic thumbnail support 2025-12-15 09:04:54 +00:00
Ross 6ee9489a5f Add debug logging for thumbnail generation and improve error handling 2025-10-13 09:56:26 +01:00
Ross 6e70a32ffe Enhance dropdown scrolling behavior to prevent wheel event propagation and allow smooth navigation 2025-10-06 13:14:54 +01:00
Ross 5b3e47edea Add thumbnail generation for image stacks and improve rendering logic 2025-09-22 15:13:01 +01:00
Ross 8d67078a90 Add reset button to viewport for restoring default properties 2025-09-22 11:58:35 +01:00
Ross 9c3c3573bc Increase z-index for bottom controls to ensure proper layering and interaction 2025-09-22 11:56:24 +01:00
Ross f695fcccd0 Add thumbnail image display for stacks and relocate settings button to viewport area 2025-09-22 11:34:16 +01:00
Ross 2f14631ad0 Enhance right-side stack panel interaction with hover button and settings button adjustments 2025-09-22 11:26:30 +01:00
Ross 0ba752c28e Update settings button position to avoid overlap with right-side controls 2025-09-22 11:04:19 +01:00
Ross 82d4c94255 Refactor right-side stack panel to simplify open state management and remove pinning functionality 2025-09-22 11:03:30 +01:00
Ross f4497ef59d Remove right-side stack panel implementation and update comments for clarity 2025-09-22 10:59:33 +01:00
Ross e685b8b11d Refactor stack panel functionality to remove top bar and implement toggle for right-side panel visibility 2025-09-22 10:58:05 +01:00
Ross 67329d0468 Refactor layout to integrate right-side stack panel with improved hover and pinning functionality 2025-09-22 10:26:49 +01:00
Ross 6ab976a82b Add pinnable right-side stack panel with hover functionality 2025-09-22 09:47:31 +01:00
Ross 4bff9b39f3 Add toggle functionality for top stack bar visibility and persistent button to reveal it 2025-09-22 09:41:03 +01:00
Ross e6a6128aca Replace stackId with studyId in named stacks and update App component to include studyId in stack entries 2025-09-15 14:28:08 +01:00
Ross 654abaaaf4 Implement drag-and-drop functionality for stack selection and enhance stack grouping by studyId in the DICOM viewer 2025-09-15 14:14:47 +01:00
Ross 1a8fb26a29 Add stackId support to named stacks for improved data handling 2025-09-15 14:03:57 +01:00
Ross b66352f9b9 Enhance stack handling by adding studyId support and improving descriptor parsing logic 2025-09-15 13:48:25 +01:00
Ross 2c9b0345c4 Implement code changes to enhance functionality and improve performance 2025-09-15 12:42:55 +01:00
Ross 9080370bd1 Add CSS styles for improved layout and add named stacks examples in DICOM viewer 2025-09-15 12:40:11 +01:00
Ross 044057675e Enhance validation for DICOM viewer input data
- Added validation checks for nested stacks and stack objects to ensure imageIds are valid arrays of strings.
- Implemented console warnings for invalid entries, including nested stacks and stack objects, to aid in debugging.
- Improved error handling for unknown or invalid entries in the parsed data.
2025-09-15 12:33:35 +01:00
Ross 8de4efc3e2 refactor image stack handling to support richer data formats and improve clarity 2025-09-15 12:25:57 +01:00
Ross c3ea2b4478 add cornerstoneDICOMImageLoader to global window interface for enhanced accessibility 2025-09-15 12:18:18 +01:00
Ross e2f1afe225 add cornerstone3d to global window interface for enhanced functionality 2025-09-08 11:45:40 +01:00
Ross efc91f8b19 add mouse tool bindings persistence with localStorage 2025-08-11 11:01:39 +01:00
Ross de9cf60adc refactor context menu and keyboard navigation handling for improved clarity and maintainability 2025-08-11 10:58:22 +01:00
Ross c6f8b271a9 fix some display set restoring issues 2025-06-30 11:27:22 +01:00
Ross b917ca72c9 scope oncontextmenu correctly 2025-06-30 10:05:05 +01:00
Ross e6f64a6a97 allow annotation navigation disabling 2025-06-30 09:40:50 +01:00
Ross 3a92f5afba try and make preset menu always clickable 2025-06-30 09:29:57 +01:00
Ross 86a7bd143f allow bar navigation for volume 2025-06-23 16:27:29 +01:00
Ross 865bc956c7 fix loading window / level when importing volume 2025-06-23 16:01:14 +01:00
Ross c063a3a6f3 . 2025-06-23 15:15:58 +01:00
Ross d5046659fd fix scroll and position reloaded for volume 2025-06-23 15:12:33 +01:00
Ross 28884670e4 fix viewport overlay 2025-06-23 11:48:46 +01:00
Ross 98c71e73e2 . 2025-06-23 11:31:14 +01:00
Ross c27a52243f correct export volume orientation 2025-06-23 10:46:03 +01:00
Ross ce13f9cad2 add a few new features 2025-06-23 09:53:09 +01:00
68 changed files with 8413 additions and 781 deletions
+80 -2
View File
@@ -5,6 +5,19 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<style>
html, body {
height: auto;
min-height: 100%;
overflow: unset!important;
margin: 0;
}
/* Make viewer roots flow normally on the page */
.dicom-viewer-root {
display: block;
}
</style>
</head>
<body>
<!-- <div id="root" class="dicom-viewer-test-root" style="max-height:500px; height:500px; overflow:auto;"></div> -->
@@ -12,9 +25,74 @@
data-images="[&quot;http://localhost:8000/media/atlas/dicom/0fb1283bab62d691bdaf7c7e47b8c674b600cddb.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/c98950f460ed153c14bd1d0d32e712397ace4564.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/512bfc7f9d07d54ae8f92af1ab98be390174d618.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/5f0a6b97da797b7639361732599bfcf607c17dfc.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/c5f854a8c2ac36da60c8a5a2bc215639d5302d20.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/0cec8d98bd80818c38c657b9c5934b30316920a2.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/613166d33e9e35898238f0bd334969c500822bbf.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/48801a3aa1fea5d943cc934b74e9bc7273c1e944.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/db8ddb477387a41b023efae98e7e40be890886ef.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/8720dc9b0b3b2c596f1d87acc951818307b7ad6a.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/24d1e5397fa97dd0c904568a20e4649fea13d1cf.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/bb586d17474988a07b1c90afce456abd7f69a753.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/24fa49c0e482bb885c92b71f74451f558940cb4b.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/6d9b09614f74b21a0b2778a5129d22b3cbafd3da.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/39963ce191beafe943a9cf980f7320adad74c1d5.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/09adc694456f1e51a5ab9c1cf7eaf9d748c938db.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/d4f6521a9e9a26975e1218ba3b7f48a78ac46d8f.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/0e1490ed17cdf2ceb1bcf069400f8f47a4437561.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/2cb43b50760d9335df9a5f2d6bd645daf2a0c917.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/9b02fae29bf6e775c5ba94b0b7158e10d4697ff6.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/9628d1d2fb457bc6f03efa8c980ef3165092230f.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/b879d6488a79d38ba374366f5543aab49fa8cb86.dcm&quot;]"
data-auto-cache-stack="true"
data-viewerstate='{"grid":{"rows":1,"cols":1},"modes":["stack"],"stacks":[{"i":["wadouri:http://localhost:8000/media/atlas/dicom/0fb1283bab62d691bdaf7c7e47b8c674b600cddb.dcm","wadouri:http://localhost:8000/media/atlas/dicom/c98950f460ed153c14bd1d0d32e712397ace4564.dcm","wadouri:http://localhost:8000/media/atlas/dicom/512bfc7f9d07d54ae8f92af1ab98be390174d618.dcm","wadouri:http://localhost:8000/media/atlas/dicom/5f0a6b97da797b7639361732599bfcf607c17dfc.dcm","wadouri:http://localhost:8000/media/atlas/dicom/c5f854a8c2ac36da60c8a5a2bc215639d5302d20.dcm","wadouri:http://localhost:8000/media/atlas/dicom/0cec8d98bd80818c38c657b9c5934b30316920a2.dcm","wadouri:http://localhost:8000/media/atlas/dicom/613166d33e9e35898238f0bd334969c500822bbf.dcm","wadouri:http://localhost:8000/media/atlas/dicom/48801a3aa1fea5d943cc934b74e9bc7273c1e944.dcm","wadouri:http://localhost:8000/media/atlas/dicom/db8ddb477387a41b023efae98e7e40be890886ef.dcm","wadouri:http://localhost:8000/media/atlas/dicom/8720dc9b0b3b2c596f1d87acc951818307b7ad6a.dcm","wadouri:http://localhost:8000/media/atlas/dicom/24d1e5397fa97dd0c904568a20e4649fea13d1cf.dcm","wadouri:http://localhost:8000/media/atlas/dicom/bb586d17474988a07b1c90afce456abd7f69a753.dcm","wadouri:http://localhost:8000/media/atlas/dicom/24fa49c0e482bb885c92b71f74451f558940cb4b.dcm","wadouri:http://localhost:8000/media/atlas/dicom/6d9b09614f74b21a0b2778a5129d22b3cbafd3da.dcm","wadouri:http://localhost:8000/media/atlas/dicom/39963ce191beafe943a9cf980f7320adad74c1d5.dcm","wadouri:http://localhost:8000/media/atlas/dicom/09adc694456f1e51a5ab9c1cf7eaf9d748c938db.dcm","wadouri:http://localhost:8000/media/atlas/dicom/d4f6521a9e9a26975e1218ba3b7f48a78ac46d8f.dcm","wadouri:http://localhost:8000/media/atlas/dicom/0e1490ed17cdf2ceb1bcf069400f8f47a4437561.dcm","wadouri:http://localhost:8000/media/atlas/dicom/2cb43b50760d9335df9a5f2d6bd645daf2a0c917.dcm","wadouri:http://localhost:8000/media/atlas/dicom/9b02fae29bf6e775c5ba94b0b7158e10d4697ff6.dcm","wadouri:http://localhost:8000/media/atlas/dicom/9628d1d2fb457bc6f03efa8c980ef3165092230f.dcm","wadouri:http://localhost:8000/media/atlas/dicom/b879d6488a79d38ba374366f5543aab49fa8cb86.dcm"],"u":"2.1163931708166249655895918785713305273005"}],"stackIdx":[0],"stackPos":[11],"props":[{"voiRange":{"lower":-707.7203723576732,"upper":1313.7029625973},"invert":false,"interpolationType":1,"VOILUTFunction":"LINEAR"}],"cam":[{"viewUp":[0.012033252051397651,-0.42083716478070027,-0.9070563687942972],"viewPlaneNormal":[0.9998766662057034,-0.004091402933210223,0.015162875697125407],"position":[215.72314005628635,-63.00598963917986,24.208010679564232],"focalPoint":[-0.06152398404300907,-62.12301873163119,20.935691053779152],"parallelProjection":true,"parallelScale":117.33333333333333,"viewAngle":90,"flipHorizontal":false,"flipVertical":false,"rotation":148.19404114160696}]}
'
data-viewerstate='{"grid":{"rows":1,"cols":3},"modes":["stack","volume","volume"],"stacks":[{"i":["wadouri:http://localhost:8000/media/atlas/dicom/b879d6488a79d38ba374366f5543aab49fa8cb86.dcm","wadouri:http://localhost:8000/media/atlas/dicom/9628d1d2fb457bc6f03efa8c980ef3165092230f.dcm","wadouri:http://localhost:8000/media/atlas/dicom/9b02fae29bf6e775c5ba94b0b7158e10d4697ff6.dcm","wadouri:http://localhost:8000/media/atlas/dicom/2cb43b50760d9335df9a5f2d6bd645daf2a0c917.dcm","wadouri:http://localhost:8000/media/atlas/dicom/0e1490ed17cdf2ceb1bcf069400f8f47a4437561.dcm","wadouri:http://localhost:8000/media/atlas/dicom/d4f6521a9e9a26975e1218ba3b7f48a78ac46d8f.dcm","wadouri:http://localhost:8000/media/atlas/dicom/09adc694456f1e51a5ab9c1cf7eaf9d748c938db.dcm","wadouri:http://localhost:8000/media/atlas/dicom/39963ce191beafe943a9cf980f7320adad74c1d5.dcm","wadouri:http://localhost:8000/media/atlas/dicom/6d9b09614f74b21a0b2778a5129d22b3cbafd3da.dcm","wadouri:http://localhost:8000/media/atlas/dicom/24fa49c0e482bb885c92b71f74451f558940cb4b.dcm","wadouri:http://localhost:8000/media/atlas/dicom/bb586d17474988a07b1c90afce456abd7f69a753.dcm","wadouri:http://localhost:8000/media/atlas/dicom/24d1e5397fa97dd0c904568a20e4649fea13d1cf.dcm","wadouri:http://localhost:8000/media/atlas/dicom/8720dc9b0b3b2c596f1d87acc951818307b7ad6a.dcm","wadouri:http://localhost:8000/media/atlas/dicom/db8ddb477387a41b023efae98e7e40be890886ef.dcm","wadouri:http://localhost:8000/media/atlas/dicom/48801a3aa1fea5d943cc934b74e9bc7273c1e944.dcm","wadouri:http://localhost:8000/media/atlas/dicom/613166d33e9e35898238f0bd334969c500822bbf.dcm","wadouri:http://localhost:8000/media/atlas/dicom/0cec8d98bd80818c38c657b9c5934b30316920a2.dcm","wadouri:http://localhost:8000/media/atlas/dicom/c5f854a8c2ac36da60c8a5a2bc215639d5302d20.dcm","wadouri:http://localhost:8000/media/atlas/dicom/5f0a6b97da797b7639361732599bfcf607c17dfc.dcm","wadouri:http://localhost:8000/media/atlas/dicom/512bfc7f9d07d54ae8f92af1ab98be390174d618.dcm","wadouri:http://localhost:8000/media/atlas/dicom/c98950f460ed153c14bd1d0d32e712397ace4564.dcm","wadouri:http://localhost:8000/media/atlas/dicom/0fb1283bab62d691bdaf7c7e47b8c674b600cddb.dcm"],"u":"2.1163931708166249655895918785713305273005"}],"stackIdx":[0,0,0],"stackPos":[0,173,10],"props":[{"voiRange":{"lower":-958.5,"upper":705.5},"invert":false,"interpolationType":1,"VOILUTFunction":"LINEAR"},{"voiRange":{"lower":-1798,"upper":1641},"invert":false,"colormap":{"name":"Grayscale","opacity":1,"threshold":null}},{"voiRange":{"lower":302,"upper":2069},"invert":false,"colormap":{"name":"Grayscale","opacity":1,"threshold":null}}],"cam":[{"viewUp":[-0.015545368194580078,-0.12045262008905411,0.9925973415374756],"viewPlaneNormal":[0.9998766662057034,-0.00409140293321022,0.015162875697125405],"position":[276.090926593084,-63.25300826357402,25.12347315106698],"focalPoint":[59.93107667300279,-62.36850213007119,21.845463926704127],"parallelProjection":true,"parallelScale":170.33890849893743,"viewAngle":90,"flipHorizontal":false,"flipVertical":false,"rotation":0},{"viewUp":[0,0,1],"viewPlaneNormal":[0,-1,0],"position":[-6.060784295600456,-273.9517816053456,20.844714115543866],"focalPoint":[-6.060784295600456,-46.51629576550185,20.844714115543866],"parallelProjection":true,"parallelScale":150.85189284207627,"viewAngle":90,"flipHorizontal":false,"flipVertical":false,"rotation":0},{"viewUp":[0,0,1],"viewPlaneNormal":[1,0,0],"position":[218.5103649207617,-62.09846953239139,20.844714115543866],"focalPoint":[-8.925120919082055,-62.09846953239139,20.844714115543866],"parallelProjection":true,"parallelScale":157.88941096127814,"viewAngle":90,"flipHorizontal":false,"flipVertical":false,"rotation":0}],"orientations":[null,"coronal","sagittal"],"volumeSliceIndices":[null,173,10]}'
></div>
<!--
<div
class="dicom-viewer-root"
id="viewer_named_simple"
data-named-stacks='[
{
"name": "CT Head (Study A)",
"imageIds": [
"http://localhost:8000/media/atlas/dicom/head_0001.dcm",
"http://localhost:8000/media/atlas/dicom/head_0002.dcm",
"http://localhost:8000/media/atlas/dicom/head_0003.dcm"
]
},
{
"name": "CT Chest (Study B)",
"imageIds": [
"wadouri:http://localhost:8000/media/atlas/dicom/chest_0001.dcm",
"wadouri:http://localhost:8000/media/atlas/dicom/chest_0002.dcm"
]
}
]'
style="width:600px;height:400px;"
></div>
<div
class="dicom-viewer-root"
id="viewer_named_cases"
data-named-stacks='[
{
"caseId": "CASE-001",
"studyId": "STUDY-001",
"stacks": [
{
"name": "Abdomen - Portal Venous",
"imageIds": [
"http://localhost:8000/media/atlas/dicom/abdomen_pv_0001.dcm",
"http://localhost:8000/media/atlas/dicom/abdomen_pv_0002.dcm"
]
},
{
"name": "Abdomen - Non-contrast",
"imageIds": [
"http://localhost:8000/media/atlas/dicom/abdomen_nc_0001.dcm"
]
}
]
},
{
"caseId": "CASE-002",
"studyId": "STUDY-002",
"stacks": [
{
"name": "Chest - CTA",
"imageIds": [
"http://localhost:8000/media/atlas/dicom/cta_0001.dcm",
"http://localhost:8000/media/atlas/dicom/cta_0002.dcm"
]
}
]
}
]'
style="width:600px;height:400px;margin-top:12px;"
></div> -->
<!-- <div id="root2" class="dicom-viewer-root" style="max-height:500px; height:500px; overflow:auto;"
data-images="[&quot;http://localhost:8000/media/atlas/dicom/4596ea7654b7a5074d2c3f40039c556b11fb01ae.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/b2fa301bc731d15be3e000402c9a79c114d17aa5.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/66310ef801fbecb37c5ced160c450dfd5ecc6d2d.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/63d2d117bf256a76a61190066f91eda6c4d67c8f.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/181a3575ea7fb8bf8f6449ff61e6e369a3247454.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/7c5c0538339266ac8897fa21c481b2f61172068e.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/1681410681308af7111af6644fed598fde304b96.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/4adcd3d7a64a9c59b530f730ce0d991bf7f27813.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/671e6004c7fb402a627a2282bbf304d7b4dcc12b.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/8d18ac56914b9de72b8aecced09654ec4b2c9bb7.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/74ce708a90dff0636fc4bc914fe47e8b6d9320bd.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/a96b6c9900570e076edf849a0ae3b52734ed2650.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/0183718d9336458478ec42130395d78e01492bd6.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/52eee968bbce5590274346eed43f1837f9daba11.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/961449f88197566c4643fcfd20f48ea4e810490b.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/1a16ad5b25e11739eadbcb8ea461072b100113a0.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/b89ddcc3f499059d2dedb0028a2bb1ce4abf5574.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/c1379c03a4e449c2fa88784d601cb766af907739.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/4fb532d5003c59a73f7e4248f365b1a8660ea710.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/e861b0de042a1f213cbccc995679aacd29c7ef53.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/1a854282afe264c3afff6a6e287cd646b2193a0c.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/f59d6d9bc33160abad3af8e2b246179b3574d884.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/04fadab0ed8b3f1925e601c4f9cfb792c95626c2.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/d45d0b12fb650e65daeeee4e5fa3b21c988597f6.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/e19c47b7787ac45dbc225319dfa143dc2c1fdf1f.dcm&quot;, &quot;http://localhost:8000/media/atlas/dicom/dccabf3103a5be254a45457ce2d8d9ce9cb19a6a.dcm&quot;]"
data-auto-cache-stack="true"
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && cp dist/index.js /home/ross/rad/rad/rad/static/dv3d/index.js",
"build": "tsc && vite build && cp dist/index.js /home/ross/penracourses/rad/rad/static/dv3d/index.js",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embedded Viewer — Test Page</title>
<style>
html,body{height:100%;margin:0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header{background:#111;color:#fff;padding:12px 20px}
header h1{margin:0;font-size:18px}
.page{display:flex;gap:20px;padding:20px}
.main{flex:1}
.sidebar{width:260px;background:#f6f6f6;padding:12px;border-radius:6px}
.dicom-viewer-root{width:100%;height:72vh;background:#222;color:#fff;border-radius:6px;overflow:hidden}
.controls{margin:12px 0}
.note{background:#eef;padding:10px;border-radius:4px}
</style>
</head>
<body>
<header>
<h1>Embedded Viewer Test</h1>
</header>
<div class="page">
<main class="main">
<p class="note">This page demonstrates the DICOM viewer embedded alongside other content.</p>
<div class="controls">
<button onclick="document.getElementById('main_viewer').scrollIntoView({behavior:'smooth'})">Scroll to viewer</button>
<button onclick="console.log('Example action')">Example action</button>
</div>
<div id="main_viewer" class="dicom-viewer-root" data-auto-cache-stack="false"></div>
<section style="margin-top:16px">
<h2>Related information</h2>
<p>Here you can place text, controls, or other UI components while the viewer stays mounted within the page layout.</p>
</section>
</main>
<aside class="sidebar">
<h3>Sidebar</h3>
<p>Extra controls, metadata, or notes can live here.</p>
<ul>
<li>Patient: Anonymous</li>
<li>Study: Demo stack</li>
<li>Notes: Test page</li>
</ul>
</aside>
</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>
// Reuse the same viewer state as the penra test for convenience
const state = {"grid":{"rows":1,"cols":3},"modes":["stack","stack","stack"],"stacks":[{"i":["wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_573_PN7j4gd.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_580_kBgyDxS.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_572_DYQisSg.dcm"]},{"i":["wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_588_tGfMjqb.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_592_Js4efiI.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_584_U8WCsNY.dcm"]},{"i":["wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_631_NxRBhMn.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_619_2cVhUyC.dcm","wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_607_VvL52Af.dcm"]}],"stackIdx":[0,2,1],"stackPos":[0,0,2],"props":[{"voiRange":{"lower":39.15,"upper":559.17},"invert":false,"interpolationType":1,"VOILUTFunction":"LINEAR","colormap":{"name":"Grayscale","opacity":[]}}],"cam":[null,null,null],"orientations":[null,null,null],"volumeSliceIndices":[null,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', () => {
(function loadStacksThenImport() {
if (!state.stacks || !Array.isArray(state.stacks)) {
callWhenReady('importViewerState_main_viewer', state);
return;
}
function pushStacks() {
const fnName = 'loadAdditionalStack_main_viewer';
if (window[fnName] && typeof window[fnName] === 'function') {
try {
state.stacks.forEach(s => {
if (s && Array.isArray(s.i) && s.i.length) {
window[fnName](s.i);
}
});
console.log('Requested viewer to load', state.stacks.length, 'stacks');
} catch (e) {
console.warn('loadAdditionalStack call failed', e);
}
setTimeout(() => { callWhenReady('importViewerState_main_viewer', state); }, 2000);
return true;
}
return false;
}
const started = pushStacks();
if (!started) {
const maxWait = 5000;
const start = Date.now();
(function waitForApi() {
if (pushStacks()) return;
if (Date.now() - start > maxWait) {
console.warn('Timed out waiting for loadAdditionalStack API; importing state directly');
callWhenReady('importViewerState_main_viewer', state);
return;
}
setTimeout(waitForApi, 2000);
})();
}
})();
});
</script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Empty Viewer 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="root" class="dicom-viewer-root" style="width:100%;height:100vh;"
data-auto-cache-stack="false"
data-empty="true"
></div>
<!-- Provide a small noop React Refresh preamble to avoid plugin-react preamble errors -->
<script>
// If plugin-react's preamble wasn't injected for some reason, provide no-op
// implementations so the runtime does not throw. This disables fast-refresh
// semantics for this page but keeps HMR from crashing.
window.$RefreshReg$ = window.$RefreshReg$ || function() {};
window.$RefreshSig$ = window.$RefreshSig$ || function() { return function(type) { return type; }; };
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+128
View File
@@ -0,0 +1,128 @@
<!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:https://www.penracourses.org.uk/media/atlas/dicom/1.3.12.2.1107.5.4.5.180506.30000026031914043350300000126.4.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>
+196
View File
@@ -0,0 +1,196 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Load Directory Test</title>
<style>
body { font-family: system-ui, Arial; padding: 18px; background: #f6f6f8; color: #111 }
.controls { margin: 12px 0 }
.file-row { display:flex; gap:8px; align-items:center; padding:6px 8px; border-bottom:1px solid #eee }
.path { font-family: monospace; color:#444; flex:1 }
button { padding:6px 10px }
#fileList { border:1px solid #ddd; background:#fff; border-radius:6px; max-height:60vh; overflow:auto }
</style>
</head>
<body>
<h2>Directory Loader (recursive)</h2>
<p>Select a folder (uses `webkitdirectory`) — files will be read recursively.
This page will call <code>window.loadDicomStackFromFiles(files)</code> if available in the viewer.</p>
<div class="controls">
<input id="dirInput" type="file" webkitdirectory directory multiple />
<label style="margin-left:12px"><input id="onlyDcm" type="checkbox" checked /> Show only <code>.dcm</code></label>
<button id="loadSelected">Load Selected</button>
<button id="loadAll">Load All</button>
<button id="openViewerAndSend">Open Viewer & Send</button>
<button id="clear">Clear</button>
</div>
<div id="fileList"></div>
<script>
(function(){
const input = document.getElementById('dirInput');
const fileListDiv = document.getElementById('fileList');
const onlyDcmChk = document.getElementById('onlyDcm');
function asArray(fileList){ return Array.prototype.slice.call(fileList || []); }
function renderFiles(files){
fileListDiv.innerHTML = '';
if (!files || files.length === 0) {
fileListDiv.innerHTML = '<div style="padding:12px;color:#666">No files selected</div>';
return;
}
const frag = document.createDocumentFragment();
files.forEach((f, idx) => {
const row = document.createElement('div');
row.className = 'file-row';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = true;
cb.dataset.idx = idx;
const pathEl = document.createElement('div');
pathEl.className = 'path';
// webkitRelativePath shows the folder structure when directory input used
pathEl.textContent = f.webkitRelativePath || f.name;
const sizeEl = document.createElement('div');
sizeEl.style.width = '90px';
sizeEl.style.textAlign = 'right';
sizeEl.style.color = '#666';
sizeEl.textContent = (f.size/1024).toFixed(1)+' kB';
row.appendChild(cb);
row.appendChild(pathEl);
row.appendChild(sizeEl);
frag.appendChild(row);
});
fileListDiv.appendChild(frag);
}
function filterFiles(files){
const onlyDcm = onlyDcmChk.checked;
if (!onlyDcm) return files;
return files.filter(f => f.name.toLowerCase().endsWith('.dcm'));
}
input.addEventListener('change', ()=>{
const all = asArray(input.files);
const filtered = filterFiles(all);
// Save filtered list on the element for retrieval
input._currentFiles = filtered;
renderFiles(filtered);
});
document.getElementById('loadSelected').addEventListener('click', ()=>{
const current = input._currentFiles || [];
if (!current.length) return alert('No files selected. Choose a folder first.');
// collect checked indices
const checked = Array.from(fileListDiv.querySelectorAll('input[type=checkbox]'))
.filter(cb=>cb.checked)
.map(cb=>parseInt(cb.dataset.idx,10))
.filter(n=>!Number.isNaN(n));
const chosen = checked.map(i=>current[i]).filter(Boolean);
if (!chosen.length) return alert('No files checked.');
sendToViewer(chosen);
});
document.getElementById('loadAll').addEventListener('click', ()=>{
const current = input._currentFiles || [];
if (!current.length) return alert('No files selected. Choose a folder first.');
sendToViewer(current);
});
document.getElementById('openViewerAndSend').addEventListener('click', async ()=>{
const current = input._currentFiles || [];
if (!current.length) return alert('No files selected. Choose a folder first.');
// Open the viewer root in a new tab/window
const viewerWin = window.open('/', '_blank');
if (!viewerWin) return alert('Failed to open viewer window (popup blocked).');
// Wait for the viewer window to expose loadDicomStackFromFiles
const timeoutMs = 15000;
const start = Date.now();
const poll = () => new Promise((resolve, reject) => {
const iv = setInterval(() => {
try {
if (viewerWin && (viewerWin.loadDicomStackFromFiles || viewerWin.loadDicomStackFromWadouriList)) {
clearInterval(iv);
resolve(true);
}
if (Date.now() - start > timeoutMs) {
clearInterval(iv);
reject(new Error('Timed out waiting for viewer API'));
}
} catch (e) {
// cross-origin or not ready
}
}, 250);
});
try {
await poll();
// call the function on the opened viewer window
if (viewerWin.loadDicomStackFromFiles) {
viewerWin.loadDicomStackFromFiles(current);
alert('Sent '+current.length+' file(s) to the newly opened viewer.');
} else if (viewerWin.loadDicomStackFromWadouriList) {
// fallback for remote URLs
const wadouriList = current.map(f => 'wadouri:' + URL.createObjectURL(f));
viewerWin.loadDicomStackFromWadouriList(wadouriList);
alert('Sent '+current.length+' wadouri image(s) to the newly opened viewer.');
} else {
alert('Viewer opened but API not available.');
}
} catch (e) {
console.error('openViewerAndSend failed', e);
alert('Timed out waiting for viewer to be ready. Check the viewer tab.');
}
});
document.getElementById('clear').addEventListener('click', ()=>{
input.value = null;
input._currentFiles = [];
renderFiles([]);
});
function sendToViewer(files){
// Prefer calling the viewer global if present
if (window.loadDicomStackFromFiles && typeof window.loadDicomStackFromFiles === 'function') {
try {
// The viewer accepts FileList or File[] per typing; pass array
window.loadDicomStackFromFiles(files);
alert('Sent '+files.length+' file(s) to viewer (window.loadDicomStackFromFiles).');
return;
} catch (e) {
console.error('loadDicomStackFromFiles failed', e);
alert('Viewer call failed (see console).');
return;
}
}
// Fallback: try posting to parent (if embedded)
if (window.parent && window.parent !== window) {
try {
window.parent.postMessage({ type: 'loadFiles', filesCount: files.length }, '*');
alert('Posted message to parent with '+files.length+' files.');
return;
} catch (e) {
// ignore
}
}
alert('Viewer integration function `window.loadDicomStackFromFiles` not found on this page.');
}
// Initial empty render
renderFiles([]);
})();
</script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DV3D — Non-DICOM Test</title>
<style>
html,body { height:100%; margin:0; background:#111 }
#non-dicom-root { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="non-dicom-root" class="dicom-viewer-root"></div>
<!-- Provide a small noop React Refresh preamble to avoid plugin-react preamble errors -->
<script>
// If plugin-react's preamble wasn't injected for some reason, provide no-op
// implementations so the runtime does not throw. This disables fast-refresh
// semantics for this page but keeps HMR from crashing.
window.$RefreshReg$ = window.$RefreshReg$ || function() {};
window.$RefreshSig$ = window.$RefreshSig$ || function() { return function(type) { return type; }; };
</script>
<!-- Vite can load TSX modules directly in dev mode -->
<script type="module" src="/src/nonDicomTest.tsx"></script>
</body>
</html>
+353
View File
@@ -0,0 +1,353 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Penra Courses — Viewer 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 to import
const state = {
grid: { rows: 1, cols: 3 },
modes: ["stack", "stack", "stack"],
stacks: [
{
i: [
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_573_PN7j4gd.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_580_kBgyDxS.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_572_DYQisSg.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_579_ya291yy.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_571_nThHs31.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_578_1eEKjB2.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_570_qT5wuZX.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_577_v2bo1kn.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_569_oV2dHz8.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_576_kgF1iXK.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_568_iY1z50L.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_575_BipYnZH.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_567_HawAeuC.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_574_FrcCKA7.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_566_AUDtXEf.dcm",
],
},
{
i: [
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_588_tGfMjqb.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_592_Js4efiI.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_584_U8WCsNY.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_595_vFPcByt.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_587_jnB3wGF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_591_1XZY7Le.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_583_C9KGrQ9.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_594_SqzeIRU.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_586_Hupum4d.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_590_pbyuWr4.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_582_6u9iQt1.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_593_rOYWenQ.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_585_4z2K1tj.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_589_F5fn0Wp.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_581_jmCHi0D.dcm",
],
},
{
i: [
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_631_NxRBhMn.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_619_2cVhUyC.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_607_VvL52Af.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_627_RVhaiKA.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_615_nQtPQHJ.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_603_bAAqdKn.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_623_wy2tc7i.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_611_zZY7rSC.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_599_PUL3TkF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_630_wIQBYJQ.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_618_oi80wJN.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_606_47SpfNJ.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_626_UNSOCYt.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_614_vcVgojF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_602_VoRYz3y.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_622_Rj39VCd.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_610_AhQNlyr.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_598_pnjpR3G.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_629_hKAK6Ph.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_617_smztH5n.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_605_EfNQSic.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_625_asIOR8i.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_613_Qaa63p7.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_601_E8lUIGL.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_621_X45Edxw.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_609_P3mWXkz.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_597_alsdinT.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_628_51IriRz.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_616_sc6CY4V.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_604_HG8um6d.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_624_qr7EmUd.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_612_NUr6yLq.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_600_ngZGDnx.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_620_FfONZ99.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_608_j131qxy.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_596_stFfHXn.dcm",
],
},
{
i: [
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_792_og2P5S6.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_793_3tKkg87.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_794_s8FB3dq.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_795_8HeRpXV.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_796_tllvzjH.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_797_AXieNgu.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_798_L8YapjK.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_799_kvuLUsW.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_800_i2eSNzF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_801_dR3KPL5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_802_cDgm138.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_803_Bj3PZyX.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_804_CEqcWo2.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_805_5R2LmiA.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_806_RgmKU6b.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_807_BKTkmJv.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_808_OQOf2MC.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_809_lxnU3mr.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_810_8vG5Ett.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_811_poX6D2s.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_812_k46Ws88.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_813_7Ft4JOE.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_814_V5lJKxR.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_815_1hFehRl.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_816_34UqY7Z.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_817_YG28gpL.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_818_wonhIz5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_819_bv3zaOx.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_820_xEjnfcI.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_821_cH9fPxl.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_822_lWmBHxs.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_823_ybix6Gz.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_824_q430lCK.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_825_28trFVc.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_826_7BF6HBF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_827_ipmO3HH.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_828_Zuppb9h.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_829_TMuror5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_830_Mnaj7oR.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_831_2U6zI7M.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_832_V9fvVdM.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_833_AAFaoOW.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_834_vxBJLIn.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_835_ZppFa6u.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_836_adRHoD3.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_837_jjfHCn3.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_838_N21h3ne.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_839_n6FWmeX.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_840_dErur0M.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_841_PcoTakk.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_842_UmTu7Fg.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_843_USByQXH.dcm",
],
}
],
stackIdx: [0, 2, 1],
stackPos: [0, 0, 2],
props: [
{
voiRange: { lower: 39.150000000000034, upper: 559.1700000000001 },
invert: false,
interpolationType: 1,
VOILUTFunction: "LINEAR",
colormap: { name: "Grayscale", opacity: [] },
},
{
voiRange: { lower: 23.330000000000013, upper: 333.23 },
invert: false,
interpolationType: 1,
VOILUTFunction: "LINEAR",
colormap: { name: "Grayscale", opacity: [] },
},
{
voiRange: { lower: 35.47000000000003, upper: 504.35 },
invert: false,
interpolationType: 1,
VOILUTFunction: "LINEAR",
colormap: { name: "Grayscale", opacity: [] },
},
],
cam: [
{
viewUp: [
-0.00001587091537658125, -0.015633516013622284,
0.9998778104782104,
],
viewPlaneNormal: [
0.9999420159200826, 0.010767119415860999, 0.00018422042758444397,
],
position: [
229.83617495377155, 41.64140067850747, 119.19293043555355,
],
focalPoint: [
-54.36633106082782, 38.581180913933935, 119.14057149238982,
],
parallelProjection: true,
parallelScale: 204.18618383081306,
viewAngle: 90,
flipHorizontal: false,
flipVertical: false,
rotation: 0,
},
{
viewUp: [
0.0010457660537213087, -0.9813110828399658, 0.1924252212047577,
],
viewPlaneNormal: [
0.04514460865590469, -0.19218279979693106, -0.9803202210357175,
],
position: [
-21.89042091369629, 36.68157196044922, 65.77458190917969,
],
focalPoint: [
-27.218482971191406, 59.3633918762207, 181.4740447998047,
],
parallelProjection: true,
parallelScale: 81.6656748487095,
viewAngle: 90,
flipHorizontal: false,
flipVertical: false,
rotation: 0,
},
{
viewUp: [
-0.00001587091537658125, -0.015633516013622284,
0.9998778104782104,
],
viewPlaneNormal: [
0.9999420159493561, 0.010767116601264741, 0.00018422603640028001,
],
position: [238.6157989501953, 43.7907600402832, 106.84664916992188],
focalPoint: [
-45.586692810058594, 40.73054122924805, 106.7942886352539,
],
parallelProjection: true,
parallelScale: 178.65974529053696,
viewAngle: 90,
flipHorizontal: false,
flipVertical: false,
rotation: 0,
},
],
orientations: [null, null, null],
volumeSliceIndices: [null, 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", () => {
// Ask the viewer to load the stacks first (via exposed API), then wait 2s and import the viewer state.
(function loadStacksThenImport() {
if (!state.stacks || !Array.isArray(state.stacks)) {
callWhenReady("importViewerState_main_viewer", state);
return;
}
function pushStacks() {
const fnName = "loadAdditionalStack_main_viewer";
if (window[fnName] && typeof window[fnName] === "function") {
try {
state.stacks.forEach((s) => {
if (s && Array.isArray(s.i) && s.i.length) {
// call the viewer API to load this stack into available stacks
window[fnName](s.i);
}
});
console.log(
"Requested viewer to load",
state.stacks.length,
"stacks",
);
} catch (e) {
console.warn("loadAdditionalStack call failed", e);
}
// Give the viewer 2 seconds to fetch/cache and mount imageIds, then import state
setTimeout(() => {
callWhenReady("importViewerState_main_viewer", state);
}, 2000);
return true;
}
return false;
}
const started = pushStacks();
if (!started) {
// wait for the viewer API then retry
const maxWait = 5000;
const start = Date.now();
(function waitForApi() {
if (pushStacks()) return;
if (Date.now() - start > maxWait) {
console.warn(
"Timed out waiting for loadAdditionalStack API; importing state directly",
);
callWhenReady("importViewerState_main_viewer", state);
return;
}
setTimeout(waitForApi, 2000);
})();
}
})();
});
</script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test Pages — Links</title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial;margin:24px;color:#111}
h1{margin-top:0}
ul{line-height:1.6}
a{color:#0366d6}
.note{background:#f6f8fa;padding:10px;border-radius:6px;margin-bottom:12px}
</style>
</head>
<body>
<h1>Available Test Pages</h1>
<p class="note">Open any of these pages directly in the dev server to view the corresponding test.</p>
<ul>
<li><a href="/penra-test.html">penra-test.html</a> — original Penra viewer test</li>
<li><a href="/embedded-viewer-test.html">embedded-viewer-test.html</a> — viewer embedded with surrounding content</li>
<li><a href="/empty-viewer.html">empty-viewer.html</a> — empty viewer placeholder</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="/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>
</body>
</html>
+6872 -729
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -49,4 +49,40 @@ html, body, #root {
/* In your global CSS */
.cornerstone-viewport, .cornerstone-canvas {
cursor: inherit !important;
}
/* Make annotation displays smaller and more compact inside viewports */
.cornerstone-viewport {
/* Target SVG text used by tools */
}
.cornerstone-viewport svg text,
.cornerstone-viewport .annotation-label,
.cornerstone-viewport .annotation-text,
.cornerstone-viewport .cornerstone-annotation,
.cornerstone-viewport .ct-annotation,
.cornerstone-viewport .annotation {
font-size: 11px !important;
line-height: 1 !important;
}
/* Smaller text boxes / labels */
.cornerstone-viewport .cornerstone-annotation-label,
.cornerstone-viewport .annotation-text {
padding: 2px 4px !important;
border-radius: 3px !important;
}
/* Reduce handle and marker sizes for compactness */
.cornerstone-viewport .handle,
.cornerstone-viewport .annotation-handle,
.cornerstone-viewport .ct-handle,
.cornerstone-viewport .cornerstone-handle {
width: 8px !important;
height: 8px !important;
}
/* If tools render HTML overlays, make them compact */
.cornerstone-viewport .ct-annotation-overlay {
font-size: 11px !important;
padding: 2px !important;
}
+181
View File
@@ -0,0 +1,181 @@
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
export type LogFormat = "compact" | "verbose" | "json";
export type LoggerConfig = {
minLevel: LogLevel;
format: LogFormat;
namespace: string;
dedupeWindowMs: number;
};
const LEVEL_ORDER: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4,
};
const DEFAULT_CONFIG: LoggerConfig = {
minLevel: "warn",
format: "compact",
namespace: "dv3d",
dedupeWindowMs: 1200,
};
let loggerConfig: LoggerConfig = { ...DEFAULT_CONFIG };
const lastMessageByFingerprint = new Map<string, number>();
type LogContext = {
namespace?: string;
dedupeKey?: string;
};
function nowIso() {
return new Date().toISOString();
}
function getConsoleMethod(level: LogLevel) {
if (level === "debug") return console.debug.bind(console);
if (level === "info") return console.info.bind(console);
if (level === "warn") return console.warn.bind(console);
return console.error.bind(console);
}
function serializeArg(arg: unknown): unknown {
if (arg instanceof Error) {
return {
name: arg.name,
message: arg.message,
stack: arg.stack,
};
}
return arg;
}
function shouldLog(level: LogLevel) {
return LEVEL_ORDER[level] >= LEVEL_ORDER[loggerConfig.minLevel];
}
function shouldDedupe(level: LogLevel) {
return level === "debug" || level === "info";
}
function buildFingerprint(level: LogLevel, args: unknown[], ctx?: LogContext): string {
const base = ctx?.dedupeKey || `${ctx?.namespace || loggerConfig.namespace}:${level}`;
const message = args
.map((arg) => {
if (typeof arg === "string") return arg;
try {
return JSON.stringify(serializeArg(arg));
} catch {
return String(arg);
}
})
.join("|");
return `${base}:${message}`;
}
function isDuplicate(level: LogLevel, args: unknown[], ctx?: LogContext): boolean {
if (!shouldDedupe(level) || loggerConfig.dedupeWindowMs <= 0) {
return false;
}
const fingerprint = buildFingerprint(level, args, ctx);
const t = Date.now();
const last = lastMessageByFingerprint.get(fingerprint);
lastMessageByFingerprint.set(fingerprint, t);
if (last === undefined) {
return false;
}
return t - last < loggerConfig.dedupeWindowMs;
}
function createPrefix(level: LogLevel, namespace?: string) {
const ns = namespace || loggerConfig.namespace;
if (loggerConfig.format === "compact") {
return `[${ns}] ${level.toUpperCase()}`;
}
if (loggerConfig.format === "verbose") {
return `[${nowIso()}] [${ns}] [${level.toUpperCase()}]`;
}
return "";
}
function log(level: LogLevel, args: unknown[], ctx?: LogContext) {
if (!shouldLog(level)) {
return;
}
if (isDuplicate(level, args, ctx)) {
return;
}
if (loggerConfig.format === "json") {
getConsoleMethod(level)({
ts: nowIso(),
namespace: ctx?.namespace || loggerConfig.namespace,
level,
message: args.map(serializeArg),
});
return;
}
const prefix = createPrefix(level, ctx?.namespace);
getConsoleMethod(level)(prefix, ...args);
}
export function setLoggerConfig(config: Partial<LoggerConfig>) {
loggerConfig = {
...loggerConfig,
...config,
};
return getLoggerConfig();
}
export function getLoggerConfig(): LoggerConfig {
return { ...loggerConfig };
}
export function parseLogFormat(value: unknown): LogFormat {
if (value === "compact" || value === "verbose" || value === "json") {
return value;
}
return DEFAULT_CONFIG.format;
}
/** Parse a log level string. Falls back to the default ("warn") for unknown values. */
export function parseLogLevel(value: unknown): LogLevel {
if (value === "debug" || value === "info" || value === "warn" || value === "error" || value === "silent") {
return value;
}
return DEFAULT_CONFIG.minLevel;
}
/** Backward-compat helper: maps a truthy debug flag to minLevel "debug" or "warn". */
export function parseDebugFlag(value: unknown): LogLevel {
if (typeof value === "boolean") {
return value ? "debug" : "warn";
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
const truthy = normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
return truthy ? "debug" : "warn";
}
if (typeof value === "number") {
return value !== 0 ? "debug" : "warn";
}
return "warn";
}
export const logger = {
debug: (...args: unknown[]) => log("debug", args),
info: (...args: unknown[]) => log("info", args),
warn: (...args: unknown[]) => log("warn", args),
error: (...args: unknown[]) => log("error", args),
child: (namespace: string) => ({
debug: (...args: unknown[]) => log("debug", args, { namespace }),
info: (...args: unknown[]) => log("info", args, { namespace }),
warn: (...args: unknown[]) => log("warn", args, { namespace }),
error: (...args: unknown[]) => log("error", args, { namespace }),
}),
};
+299 -49
View File
@@ -1,79 +1,329 @@
import React from 'react'
import { createRoot } from "react-dom/client";
import ReactDOM 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 React from "react";
import { createRoot, type Root } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { availableImageIds } from "./lib/createImageIds.ts";
import {
getLoggerConfig,
logger,
parseDebugFlag,
parseLogFormat,
parseLogLevel,
setLoggerConfig,
type LogFormat,
type LogLevel,
} from "./lib/logger";
// Helper to convert URLs to wadouri
const toWadouri = (arr: string[]) =>
arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`);
type MountOptions = {
force?: boolean;
containerIds?: string[];
logging?: {
/** Minimum log level to emit. Takes precedence over `debug`. */
minLevel?: LogLevel;
/** @deprecated Use minLevel instead. true maps to "debug", false maps to "warn". */
debug?: boolean;
format?: LogFormat;
};
};
type ViewerDescriptor = {
imageIds: string[];
name?: string;
caseId?: string;
studyId?: string;
series?: Array<{
key?: string;
label?: string;
imageIds?: string[];
i?: string[];
name?: string;
seriesId?: string | number;
seriesInstanceUID?: string;
bValue?: number | null;
groupValues?: Record<string, string>;
}>;
};
type RootRecord = {
root: Root;
signature: string;
};
const rootRegistry = new Map<HTMLElement, RootRecord>();
const mainLogger = logger.child("main");
declare global {
interface Window {
mountDicomViewers?: (options?: MountOptions) => void;
remountDicomViewer?: (containerId: string) => void;
setDicomViewerLogging?: (config: { minLevel?: LogLevel; debug?: boolean; format?: LogFormat }) => void;
getDicomViewerLogging?: () => { minLevel: LogLevel; format: LogFormat; namespace: string; dedupeWindowMs: number };
}
}
// Normalize incoming image ids/URLs and ensure DICOM entries use a valid wadouri: prefix.
const normalizeImageId = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return trimmed;
// Keep non-wadouri schemes untouched.
if (trimmed.startsWith("external:") || trimmed.startsWith("wadors:")) {
return trimmed;
}
// Fix malformed values like "wadouri::https://..." by removing extra leading colons.
if (trimmed.startsWith("wadouri:")) {
const rest = trimmed.slice("wadouri:".length).replace(/^:+/, "");
return `wadouri:${rest}`;
}
// Also handle malformed absolute URLs such as ":https://...".
const sanitized = trimmed.replace(/^:+/, "");
return `wadouri:${sanitized}`;
};
const toWadouri = (arr: string[]) => arr.map(normalizeImageId);
// Normalize a simple array -> descriptor object
const toDescriptor = (arr: string[] | undefined) => {
if (!arr) return { imageIds: [] as string[] };
return { imageIds: toWadouri(arr) };
};
const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
if (!parsed) return [];
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
return [toDescriptor(parsed)];
}
if (!Array.isArray(parsed)) {
return [];
}
const out: Array<ViewerDescriptor> = [];
parsed.forEach((item, itemIdx) => {
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;
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) {
mainLogger.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
return;
}
out.push({
imageIds: toWadouri(imageIds),
name: s.name || s.label || undefined,
caseId,
studyId: s.studyId || studyId,
series: Array.isArray(s.series) ? s.series : undefined,
});
});
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")) {
mainLogger.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
return;
}
out.push({
imageIds: toWadouri(imageIds),
name: item.name || item.label || undefined,
caseId: item.caseId || item.caseUID || undefined,
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
series: Array.isArray(item.series) ? item.series : undefined,
});
return;
}
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
out.push(toDescriptor(item));
return;
}
mainLogger.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 getContainerLogConfig = (container: HTMLElement, options?: MountOptions) => {
const urlParams = new URLSearchParams(window.location.search);
const urlDebug = urlParams.get("debug");
const localSyncDebug = typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem("multiuser_sync_debug_mode") : null;
const attrLevel = container.getAttribute("data-log-level");
const attrDebug = container.getAttribute("data-log-debug");
const attrFormat = container.getAttribute("data-log-format");
// Resolve minLevel: LocalStorage debug > URL debug parameter > data-log-level > options.minLevel > data-log-debug (legacy)
let minLevel: LogLevel;
if (localSyncDebug === "true") {
minLevel = "debug";
} else if (urlDebug !== null) {
minLevel = parseDebugFlag(urlDebug);
} else if (attrLevel !== null) {
minLevel = parseLogLevel(attrLevel);
} else if (options?.logging?.minLevel) {
minLevel = options.logging.minLevel;
} else if (attrDebug !== null) {
minLevel = parseDebugFlag(attrDebug);
} else if (options?.logging?.debug !== undefined) {
minLevel = parseDebugFlag(options.logging.debug);
} else {
minLevel = "warn";
}
const format = parseLogFormat(attrFormat ?? options?.logging?.format);
return { minLevel, format };
};
const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]>, options?: MountOptions) => {
const signature = getMountSignature(container);
const existing = rootRegistry.get(container);
const initialLoggerConfig = getContainerLogConfig(container, options);
if (existing && !options?.force && existing.signature === signature) {
return;
}
if (existing) {
existing.root.unmount();
rootRegistry.delete(container);
}
setLoggerConfig(initialLoggerConfig);
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}
initialLoggerConfig={initialLoggerConfig}
multiuserRoom={container.getAttribute("data-multiuser-room") || undefined}
username={container.getAttribute("data-username") || undefined}
isStaff={container.getAttribute("data-is-staff") === "true"}
/>
);
rootRegistry.set(container, { root, signature });
};
export function mountDicomViewers(options?: MountOptions) {
// Clean up any stale roots from elements that have been removed from the DOM
for (const [container, entry] of rootRegistry.entries()) {
if (!document.body.contains(container)) {
try {
mainLogger.info("Unmounting stale React root for container that was removed from DOM:", container.id);
entry.root.unmount();
} catch (e) {
mainLogger.warn("Failed to unmount stale React root", e);
}
rootRegistry.delete(container);
}
}
export function mountDicomViewers() {
// Test containers
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
test_containers.forEach((container) => {
const id = container.id || '';
const testContainers = document.querySelectorAll(".dicom-viewer-test-root");
testContainers.forEach((container) => {
if (!shouldMountContainer(container as HTMLElement, options)) return;
const imageStacks = () => availableImageIds();
createRoot(container).render(
<App
container_id={id}
imageStacks={imageStacks}
/>
);
mountContainer(container as HTMLElement, imageStacks, options);
});
// Main containers
const containers = document.querySelectorAll(".dicom-viewer-root");
containers.forEach((container) => {
const id = container.id || '';
let imageStacks;
if (!shouldMountContainer(container as HTMLElement, options)) return;
const dataImages = container.getAttribute('data-images');
if (dataImages) {
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) {
let parsed;
try {
parsed = JSON.parse(dataNamed);
} catch (e) {
mainLogger.error("Invalid data-named-stacks JSON:", e);
parsed = [];
}
const descriptors = parseToDescriptors(parsed);
imageStacks = () => Promise.resolve(descriptors);
} else if (dataImages) {
let parsed;
try {
parsed = JSON.parse(dataImages);
} catch (e) {
console.error("Invalid data-images JSON:", e);
mainLogger.error("Invalid data-images JSON:", e);
parsed = [];
}
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
} else if (Array.isArray(parsed)) {
imageStacks = () => Promise.resolve(parsed.map(
stack => Array.isArray(stack) ? toWadouri(stack) : []
));
imageStacks = () =>
Promise.resolve(parsed.map((stack) => (Array.isArray(stack) ? toWadouri(stack) : [])));
} else {
imageStacks = () => Promise.resolve([[]]);
}
} else {
imageStacks = () => Promise.resolve([[]]);
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:
(window as any).mountDicomViewers = mountDicomViewers;
window.mountDicomViewers = mountDicomViewers;
window.remountDicomViewer = (containerId: string) => {
mountDicomViewers({ force: true, containerIds: [containerId] });
};
window.setDicomViewerLogging = (config) => {
const minLevel = config.minLevel
? parseLogLevel(config.minLevel)
: config.debug !== undefined
? parseDebugFlag(config.debug)
: undefined;
setLoggerConfig({
minLevel,
format: config.format ? parseLogFormat(config.format) : undefined,
});
};
window.getDicomViewerLogging = () => getLoggerConfig();
+53
View File
@@ -0,0 +1,53 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import { logger } from './lib/logger'
const testLogger = logger.child('nonDicomTest')
// Sample external images (use CORS-friendly URLs). These are Wikimedia Commons images.
const sampleImages = [
'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png',
'https://upload.wikimedia.org/wikipedia/commons/6/6a/PNG_Test.png',
]
// Helper to prefix with `external:` scheme used by the app's loader
const toExternal = (urls: string[]) => urls.map(u => u.startsWith('external:') ? u : `external:${u}`)
function mount() {
const container = document.getElementById('non-dicom-root')
if (!container) return
const imageStacks = () => {
// Return a single stack with our sample images
return Promise.resolve([
{
imageIds: toExternal(sampleImages),
name: 'Non-DICOM Sample Stack',
},
])
}
// Ensure React Refresh globals exist before loading App so plugin-react
// preamble checks won't fail during HMR. We then dynamically import App.
// This guarantees the globals are set before App is evaluated.
// Provide no-op implementations when not present.
;(window as any).$RefreshReg$ = (window as any).$RefreshReg$ || function() {}
;(window as any).$RefreshSig$ = (window as any).$RefreshSig$ || function() { return function(type: any) { return type; }; }
import('./App').then(({ default: App }) => {
createRoot(container).render(
<React.StrictMode>
<App container_id={'nondicom'} imageStacks={imageStacks} />
</React.StrictMode>
)
}).catch(err => {
testLogger.error('Failed to load App for non-dicom test', err)
})
}
// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount)
} else {
mount()
}