Compare commits
84 Commits
11d1dc4339
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f323dfd559 | |||
| 0c4da6e022 | |||
| d0f3431ef4 | |||
| b961a806d9 | |||
| 1ddc9a810a | |||
| b2bd2791a5 | |||
| b527eb1139 | |||
| 6a0552c843 | |||
| a5c530a907 | |||
| 41f13ade5e | |||
| b0d8c72636 | |||
| 4dd63668b0 | |||
| 77bbbd548e | |||
| 7c17a27ec3 | |||
| 68248737ec | |||
| 13361a30a5 | |||
| 627cd740f1 | |||
| af6e22fd91 | |||
| 7b786fcf9f | |||
| c9902a2e15 | |||
| 99df1cd1c1 | |||
| f169238057 | |||
| f0e0fe0ef6 | |||
| 19702b6052 | |||
| 1674254a3c | |||
| aeb04bcf2f | |||
| e29bce42e2 | |||
| d34772a3fa | |||
| add45567d8 | |||
| 3294151a92 | |||
| 776b5faffe | |||
| c5a58da6a3 | |||
| be16e3554f | |||
| 6cd9e33e65 | |||
| e161377245 | |||
| 1efd81bb0b | |||
| c0c5cd6195 | |||
| 0c4db1904c | |||
| 52f4be1a96 | |||
| 8faa7e9be4 | |||
| adff7caffd | |||
| 8dc4bb0aaa | |||
| ef62ffac9f | |||
| 72462b49ef | |||
| 181f471806 | |||
| 8a0a08000e | |||
| 6ee9489a5f | |||
| 6e70a32ffe | |||
| 5b3e47edea | |||
| 8d67078a90 | |||
| 9c3c3573bc | |||
| f695fcccd0 | |||
| 2f14631ad0 | |||
| 0ba752c28e | |||
| 82d4c94255 | |||
| f4497ef59d | |||
| e685b8b11d | |||
| 67329d0468 | |||
| 6ab976a82b | |||
| 4bff9b39f3 | |||
| e6a6128aca | |||
| 654abaaaf4 | |||
| 1a8fb26a29 | |||
| b66352f9b9 | |||
| 2c9b0345c4 | |||
| 9080370bd1 | |||
| 044057675e | |||
| 8de4efc3e2 | |||
| c3ea2b4478 | |||
| e2f1afe225 | |||
| efc91f8b19 | |||
| de9cf60adc | |||
| c6f8b271a9 | |||
| b917ca72c9 | |||
| e6f64a6a97 | |||
| 3a92f5afba | |||
| 86a7bd143f | |||
| 865bc956c7 | |||
| c063a3a6f3 | |||
| d5046659fd | |||
| 28884670e4 | |||
| 98c71e73e2 | |||
| c27a52243f | |||
| ce13f9cad2 |
+80
-2
@@ -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="["http://localhost:8000/media/atlas/dicom/0fb1283bab62d691bdaf7c7e47b8c674b600cddb.dcm", "http://localhost:8000/media/atlas/dicom/c98950f460ed153c14bd1d0d32e712397ace4564.dcm", "http://localhost:8000/media/atlas/dicom/512bfc7f9d07d54ae8f92af1ab98be390174d618.dcm", "http://localhost:8000/media/atlas/dicom/5f0a6b97da797b7639361732599bfcf607c17dfc.dcm", "http://localhost:8000/media/atlas/dicom/c5f854a8c2ac36da60c8a5a2bc215639d5302d20.dcm", "http://localhost:8000/media/atlas/dicom/0cec8d98bd80818c38c657b9c5934b30316920a2.dcm", "http://localhost:8000/media/atlas/dicom/613166d33e9e35898238f0bd334969c500822bbf.dcm", "http://localhost:8000/media/atlas/dicom/48801a3aa1fea5d943cc934b74e9bc7273c1e944.dcm", "http://localhost:8000/media/atlas/dicom/db8ddb477387a41b023efae98e7e40be890886ef.dcm", "http://localhost:8000/media/atlas/dicom/8720dc9b0b3b2c596f1d87acc951818307b7ad6a.dcm", "http://localhost:8000/media/atlas/dicom/24d1e5397fa97dd0c904568a20e4649fea13d1cf.dcm", "http://localhost:8000/media/atlas/dicom/bb586d17474988a07b1c90afce456abd7f69a753.dcm", "http://localhost:8000/media/atlas/dicom/24fa49c0e482bb885c92b71f74451f558940cb4b.dcm", "http://localhost:8000/media/atlas/dicom/6d9b09614f74b21a0b2778a5129d22b3cbafd3da.dcm", "http://localhost:8000/media/atlas/dicom/39963ce191beafe943a9cf980f7320adad74c1d5.dcm", "http://localhost:8000/media/atlas/dicom/09adc694456f1e51a5ab9c1cf7eaf9d748c938db.dcm", "http://localhost:8000/media/atlas/dicom/d4f6521a9e9a26975e1218ba3b7f48a78ac46d8f.dcm", "http://localhost:8000/media/atlas/dicom/0e1490ed17cdf2ceb1bcf069400f8f47a4437561.dcm", "http://localhost:8000/media/atlas/dicom/2cb43b50760d9335df9a5f2d6bd645daf2a0c917.dcm", "http://localhost:8000/media/atlas/dicom/9b02fae29bf6e775c5ba94b0b7158e10d4697ff6.dcm", "http://localhost:8000/media/atlas/dicom/9628d1d2fb457bc6f03efa8c980ef3165092230f.dcm", "http://localhost:8000/media/atlas/dicom/b879d6488a79d38ba374366f5543aab49fa8cb86.dcm"]"
|
||||
|
||||
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="["http://localhost:8000/media/atlas/dicom/4596ea7654b7a5074d2c3f40039c556b11fb01ae.dcm", "http://localhost:8000/media/atlas/dicom/b2fa301bc731d15be3e000402c9a79c114d17aa5.dcm", "http://localhost:8000/media/atlas/dicom/66310ef801fbecb37c5ced160c450dfd5ecc6d2d.dcm", "http://localhost:8000/media/atlas/dicom/63d2d117bf256a76a61190066f91eda6c4d67c8f.dcm", "http://localhost:8000/media/atlas/dicom/181a3575ea7fb8bf8f6449ff61e6e369a3247454.dcm", "http://localhost:8000/media/atlas/dicom/7c5c0538339266ac8897fa21c481b2f61172068e.dcm", "http://localhost:8000/media/atlas/dicom/1681410681308af7111af6644fed598fde304b96.dcm", "http://localhost:8000/media/atlas/dicom/4adcd3d7a64a9c59b530f730ce0d991bf7f27813.dcm", "http://localhost:8000/media/atlas/dicom/671e6004c7fb402a627a2282bbf304d7b4dcc12b.dcm", "http://localhost:8000/media/atlas/dicom/8d18ac56914b9de72b8aecced09654ec4b2c9bb7.dcm", "http://localhost:8000/media/atlas/dicom/74ce708a90dff0636fc4bc914fe47e8b6d9320bd.dcm", "http://localhost:8000/media/atlas/dicom/a96b6c9900570e076edf849a0ae3b52734ed2650.dcm", "http://localhost:8000/media/atlas/dicom/0183718d9336458478ec42130395d78e01492bd6.dcm", "http://localhost:8000/media/atlas/dicom/52eee968bbce5590274346eed43f1837f9daba11.dcm", "http://localhost:8000/media/atlas/dicom/961449f88197566c4643fcfd20f48ea4e810490b.dcm", "http://localhost:8000/media/atlas/dicom/1a16ad5b25e11739eadbcb8ea461072b100113a0.dcm", "http://localhost:8000/media/atlas/dicom/b89ddcc3f499059d2dedb0028a2bb1ce4abf5574.dcm", "http://localhost:8000/media/atlas/dicom/c1379c03a4e449c2fa88784d601cb766af907739.dcm", "http://localhost:8000/media/atlas/dicom/4fb532d5003c59a73f7e4248f365b1a8660ea710.dcm", "http://localhost:8000/media/atlas/dicom/e861b0de042a1f213cbccc995679aacd29c7ef53.dcm", "http://localhost:8000/media/atlas/dicom/1a854282afe264c3afff6a6e287cd646b2193a0c.dcm", "http://localhost:8000/media/atlas/dicom/f59d6d9bc33160abad3af8e2b246179b3574d884.dcm", "http://localhost:8000/media/atlas/dicom/04fadab0ed8b3f1925e601c4f9cfb792c95626c2.dcm", "http://localhost:8000/media/atlas/dicom/d45d0b12fb650e65daeeee4e5fa3b21c988597f6.dcm", "http://localhost:8000/media/atlas/dicom/e19c47b7787ac45dbc225319dfa143dc2c1fdf1f.dcm", "http://localhost:8000/media/atlas/dicom/dccabf3103a5be254a45457ce2d8d9ce9cb19a6a.dcm"]"
|
||||
data-auto-cache-stack="true"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6872
-729
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user