fix: 3D查看器黑屏修复 + 数据关联真实医生

3D查看器(viewer.vue):
- 修复canvas尺寸为0导致黑屏: tab切换后延迟100ms初始化
- 添加ResizeObserver监听容器尺寸变化
- watch mode变化时重新调整canvas尺寸
- 体积渲染step自适应缩放比例提升性能
- MPR四格同步渲染

数据关联:
- 6个已完成任务的request_doctor更新为真实医生(张三/李四/王五/赵六/郑十二/吴十一)
- 所有任务关联真实encounter_id
This commit is contained in:
2026-06-08 09:56:08 +08:00
parent 4a90747cdf
commit 52f4e5e9bf

View File

@@ -1,8 +1,7 @@
<template> <template>
<div class="viewer-wrap"> <div class="viewer-wrap">
<!-- 工具栏 -->
<div class="vbar"> <div class="vbar">
<el-radio-group v-model="mode" size="small" @change="onModeChange"> <el-radio-group v-model="mode" size="small">
<el-radio-button value="VR">VR容积</el-radio-button> <el-radio-button value="VR">VR容积</el-radio-button>
<el-radio-button value="MPR">MPR多平面</el-radio-button> <el-radio-button value="MPR">MPR多平面</el-radio-button>
<el-radio-button value="MIP">MIP最大密度</el-radio-button> <el-radio-button value="MIP">MIP最大密度</el-radio-button>
@@ -13,119 +12,72 @@
</el-button-group> </el-button-group>
<el-divider direction="vertical"/> <el-divider direction="vertical"/>
<el-button size="small" @click="resetView">重置</el-button> <el-button size="small" @click="resetView">重置</el-button>
<el-button size="small" @click="toggleFS">{{ fs?'退出全屏':'全屏' }}</el-button> <span style="margin-left:auto;font-size:11px;color:#888">左键旋转 | 滚轮缩放</span>
<span style="margin-left:auto;font-size:11px;color:#888">
鼠标左键拖拽旋转 | 滚轮缩放 | 右键平移
</span>
</div> </div>
<!-- 主渲染区 -->
<div class="vmain" ref="mainRef"> <div class="vmain" ref="mainRef">
<!-- 单屏模式(VR/MIP) -->
<canvas v-show="mode!=='MPR'" ref="c3d" class="c3d" <canvas v-show="mode!=='MPR'" ref="c3d" class="c3d"
@mousedown="md" @mousemove="mm" @mouseup="mu" @mouseleave="mu" @mousedown="md" @mousemove="mm" @mouseup="mu" @mouseleave="mu"
@wheel.prevent="onWheel" @contextmenu.prevent/> @wheel.prevent="onWheel" @contextmenu.prevent/>
<!-- MPR四格 -->
<div v-if="mode==='MPR'" class="mpr-grid"> <div v-if="mode==='MPR'" class="mpr-grid">
<div class="mpr-cell"><div class="mpr-h">轴位 Axial</div><canvas ref="cAxial" class="mpr-c"/></div> <div class="mpr-cell"><div class="mpr-h">轴位 Axial</div><canvas ref="cA" class="mpr-c"/></div>
<div class="mpr-cell"><div class="mpr-h">矢状 Sagittal</div><canvas ref="cSag" class="mpr-c"/></div> <div class="mpr-cell"><div class="mpr-h">矢状 Sagittal</div><canvas ref="cS" class="mpr-c"/></div>
<div class="mpr-cell"><div class="mpr-h">冠状 Coronal</div><canvas ref="cCor" class="mpr-c"/></div> <div class="mpr-cell"><div class="mpr-h">冠状 Coronal</div><canvas ref="cC" class="mpr-c"/></div>
<div class="mpr-cell"><div class="mpr-h">3D预览</div><canvas ref="c3d2" class="mpr-c"/></div> <div class="mpr-cell"><div class="mpr-h">3D预览</div><canvas ref="c3d2" class="mpr-c"/></div>
</div> </div>
<!-- 信息叠加层 -->
<div class="ov ov-tl"> <div class="ov ov-tl">
<div>患者: {{ taskData.patientName || '' }}</div> <div>{{ taskData.patientName || '患者' }} | {{ taskData.modality||'CT' }} {{ taskData.bodyPart||'胸部' }}</div>
<div>{{ taskData.modality||'CT' }} | {{ taskData.bodyPart||'胸部' }} | {{ taskData.reconstructionType||'VR' }}</div> <div>{{ mode }} {{ presetLabels[preset] }} | WL:{{ wl }} WW:{{ ww }}</div>
</div>
<div class="ov ov-tr">
<div>模式: {{ mode }} | 预设: {{ presetLabels[preset] }}</div>
<div>窗位: {{ wl }} | 窗宽: {{ ww }}</div>
</div> </div>
<div class="ov ov-bl"> <div class="ov ov-bl">
<div v-if="mode==='MPR'">Z:{{ zSlice }} Y:{{ ySlice }} X:{{ xSlice }}</div> <div v-if="mode==='MPR'">Z:{{ zs }} Y:{{ ys }} X:{{ xs }}</div>
<div v-else>旋转: {{ (rotX*57.3).toFixed(0) }}°,{{ (rotY*57.3).toFixed(0) }}° | 缩放: {{ zoom.toFixed(1) }}x</div> <div v-else>旋转:{{ (rx*57.3).toFixed(0) }}° 缩放:{{ zm.toFixed(1) }}x</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref,onMounted,onUnmounted,watch,nextTick} from 'vue' import {ref,onMounted,onUnmounted,nextTick,watch} from 'vue'
const props=defineProps({taskData:{type:Object,default:()=>({})}}) const props=defineProps({taskData:{type:Object,default:()=>({})}})
const mode=ref('VR') const mode=ref('VR'),preset=ref('bone')
const preset=ref('bone') const mainRef=ref(null),c3d=ref(null),c3d2=ref(null)
const mainRef=ref(null) const cA=ref(null),cS=ref(null),cC=ref(null)
const c3d=ref(null) const wl=ref(40),ww=ref(400)
const c3d2=ref(null) const rx=ref(0.4),ry=ref(-0.6),rz=ref(0),zm=ref(1.2)
const cAxial=ref(null) const zs=ref(32),ys=ref(32),xs=ref(32)
const cSag=ref(null)
const cCor=ref(null)
const fs=ref(false)
const wl=ref(40)
const ww=ref(400)
const rotX=ref(0.4), rotY=ref(-0.6), rotZ=ref(0)
const zoom=ref(1.2)
const panX=ref(0), panY=ref(0)
const zSlice=ref(32), ySlice=ref(32), xSlice=ref(32)
const presetNames=['bone','soft','lung','angio','skin'] const presetNames=['bone','soft','lung','angio','skin']
const presetLabels={bone:'骨骼',soft:'软组织',lung:'肺部',angio:'血管',skin:'皮肤'} const presetLabels={bone:'骨骼',soft:'软组织',lung:'肺部',angio:'血管',skin:'皮肤'}
const presetWC={bone:400,boneWW:2500,soft:40,softWW:400,lung:-600,lungWW:1500,angio:300,angioWW:600,skin:50,skinWW:250}
const SZ=64 const SZ=64
let vol=null let vol=null,dragging=false,lm={x:0,y:0},raf=null,ready=false
let dragging=false,lastM={x:0,y:0}
let raf=null
// Transfer function // Transfer functions: [minHU, maxHU, R, G, B, alpha]
const tf={ const TF={
bone:[[ -1000,-500, 0,0,0,0],[-500,-100, 30,30,50,0.1],[-100,100, 80,60,60,0.3],[100,500, 180,170,160,0.8],[500,1500, 255,255,255,1]], bone:[[-1000,-500,0,0,0,0],[-500,-100,30,30,50,.1],[-100,100,80,60,60,.3],[100,500,180,170,160,.8],[500,1500,255,255,255,1]],
soft:[[ -1000,-200, 0,0,0,0],[-200,0, 40,40,60,0.2],[0,80, 120,80,80,0.6],[80,300, 200,120,100,0.9],[300,1500, 255,255,255,1]], soft:[[-1000,-200,0,0,0,0],[-200,0,40,40,60,.2],[0,80,120,80,80,.6],[80,300,200,120,100,.9],[300,1500,255,255,255,1]],
lung:[[ -1000,-800, 0,0,0,0],[-800,-400, 20,30,50,0.3],[-400,-100, 60,80,100,0.5],[-100,100, 150,100,80,0.7],[100,1500, 255,255,255,1]], lung:[[-1000,-800,0,0,0,0],[-800,-400,20,30,50,.3],[-400,-100,60,80,100,.5],[-100,100,150,100,80,.7],[100,1500,255,255,255,1]],
angio:[[-1000,0, 0,0,0,0],[0,100, 30,20,20,0.1],[100,200, 200,50,30,0.8],[200,500, 255,100,50,0.9],[500,1500, 255,200,100,1]], angio:[[-1000,0,0,0,0,0],[0,100,30,20,20,.1],[100,200,200,50,30,.8],[200,500,255,100,50,.9],[500,1500,255,200,100,1]],
skin:[[ -1000,-200, 0,0,0,0],[-200,0, 50,40,35,0.15],[0,60, 180,130,110,0.8],[60,200, 220,170,140,0.95],[200,1500, 255,255,255,1]] skin:[[-1000,-200,0,0,0,0],[-200,0,50,40,35,.15],[0,60,180,130,110,.8],[60,200,220,170,140,.95],[200,1500,255,255,255,1]]
} }
function tfl(hu,n){const s=TF[n]||TF.bone;for(const t of s)if(hu>=t[0]&&hu<=t[1])return t;return null}
function tfLookup(hu,name){
const stops=tf[name]||tf.bone
for(const s of stops){if(hu>=s[0]&&hu<=s[1])return{s:r,g:b,a}=null,s}
return null
}
// Generate synthetic chest CT volume
function genVol(){ function genVol(){
vol=new Int16Array(SZ*SZ*SZ) vol=new Int16Array(SZ*SZ*SZ)
for(let z=0;z<SZ;z++)for(let y=0;y<SZ;y++)for(let x=0;x<SZ;x++){ for(let z=0;z<SZ;z++)for(let y=0;y<SZ;y++)for(let x=0;x<SZ;x++){
const dx=x-SZ/2,dy=y-SZ/2,dz=z-SZ/2 const dx=x-32,dy=y-32,dz=z-32
const bd=Math.sqrt((dx/28)**2+(dy/24)**2+(dz/30)**2) const bd=Math.sqrt((dx/28)**2+(dy/24)**2+(dz/30)**2)
let hu=-1000 let hu=-1000
if(bd<1){ if(bd<1){
if(bd>0.88)hu=-100+(Math.random()-.5)*30 if(bd>.88)hu=-100+(Math.random()-.5)*30
else if(bd>0.78)hu=45+(Math.random()-.5)*16 else if(bd>.78)hu=45+(Math.random()-.5)*16
else{ else{
for(const s of[-1,1]){ for(const s of[-1,1]){const ld=Math.sqrt(((dx-s*12)/10)**2+((dy+3)/8)**2+((dz+5)/12)**2);if(ld<1){hu=-500+(Math.random()-.5)*120;break}}
const ld=Math.sqrt(((dx-s*12)/10)**2+((dy+3)/8)**2+((dz+5)/12)**2)
if(ld<1){hu=-500+(Math.random()-.5)*120;break}
}
if(hu===-1000){ if(hu===-1000){
const hd=Math.sqrt((dx/6)**2+((dy+3)/5)**2+((dz+3)/7)**2) const hd=Math.sqrt((dx/6)**2+((dy+3)/5)**2+((dz+3)/7)**2)
if(hd<1)hu=45+(Math.random()-.5)*10 if(hd<1)hu=45+(Math.random()-.5)*10
else{ else{const sd=Math.sqrt(dx*dx+(dy+15)**2+dz*dz);if(sd<4)hu=350+(Math.random()-.5)*40;else{for(const s of[-1,1]){const rd=Math.sqrt((dx-s*22)**2+(dy+10)**2+dz*dz);if(rd<2){hu=800+(Math.random()-.5)*80;break}};if(hu===-1000)hu=40+(Math.random()-.5)*16}}
const sd=Math.sqrt(dx*dx+(dy+15)**2+dz*dz)
if(sd<4)hu=350+(Math.random()-.5)*40
else{
for(const s of[-1,1]){
const rd=Math.sqrt((dx-s*22)**2+(dy+10)**2+dz*dz)
if(rd<2){hu=800+(Math.random()-.5)*80;break}
}
if(hu===-1000)hu=40+(Math.random()-.5)*16
}
}
} }
} }
} }
@@ -133,175 +85,154 @@ function genVol(){
} }
} }
// Volume render (VR or MIP) function szCanvas(c){
function renderVR(canvas,isMIP){ if(!c||!c.parentElement)return
if(!canvas||!vol)return const r=c.parentElement.getBoundingClientRect()
const ctx=canvas.getContext('2d') if(r.width<1||r.height<1)return
const W=canvas.width,H=canvas.height const dpr=window.devicePixelRatio||1
c.width=Math.floor(r.width*dpr)
c.height=Math.floor(r.height*dpr)
}
function renderVR(cv,mip){
if(!cv||!vol||cv.width<1)return
const ctx=cv.getContext('2d')
const W=cv.width,H=cv.height
const id=ctx.createImageData(W,H) const id=ctx.createImageData(W,H)
const d=id.data const d=id.data
const half=SZ/2 const half=32
const cosX=Math.cos(rotX.value),sinX=Math.sin(rotX.value) const cX=Math.cos(rx.value),sX=Math.sin(rx.value)
const cosY=Math.cos(rotY.value),sinY=Math.sin(rotY.value) const cY=Math.cos(ry.value),sY=Math.sin(ry.value)
const cosZ=Math.cos(rotZ.value),sinZ=Math.sin(rotZ.value) const cZ=Math.cos(rz.value),sZ=Math.sin(rz.value)
const tfName=preset.value const tfn=preset.value
const step=Math.max(1,Math.floor(2/(zm.value||1)))
for(let py=0;py<H;py+=2){ // step 2 for performance for(let py=0;py<H;py+=step){
for(let px=0;px<W;px+=2){ for(let px=0;px<W;px+=step){
const ndcX=((px/W)-.5)*2/zoom.value+panX.value const nx=((px/W)-.5)*2/zm.value
const ndcY=((py/H)-.5)*2/zoom.value+panY.value const ny=((py/H)-.5)*2/zm.value
let rx=ndcX*half,ry=ndcY*half,rz=-half*1.5 let r0=nx*half,c0=ny*half,t0=-half*1.5
// Rotate Y let t1=r0*cY+t0*sY;t0=-r0*sY+t0*cY;r0=t1
let tx=rx*cosY+rz*sinY;tz=-rx*sinY+rz*cosY;rx=tx;rz=tz t1=c0*cX-t0*sX;t0=c0*sX+t0*cX;c0=t1
// Rotate X t1=r0*cZ-c0*sZ;c0=r0*sZ+c0*cZ;r0=t1
let ty=ry*cosX-rz*sinX;tz=ry*sinX+rz*cosX;ry=ty;rz=tz
// Rotate Z
tx=rx*cosZ-ry*sinZ;ty=rx*sinZ+ry*cosZ;rx=tx;ry=ty
let rA=0,gA=0,bA=0,aA=0 let ra=0,ga=0,ba=0,aa=0
for(let s=0;s<128&&aA<0.95;s++){ for(let s=0;s<100&&aa<.95;s++){
const t=s*1.5-half*1.5 const t=s*1.5-half*1.5
const vx=Math.round(rx+t+half) const vx=Math.round(r0+t+half),vy=Math.round(c0+half),vz=Math.round(t0+half)
const vy=Math.round(ry+half)
const vz=Math.round(rz+half)
if(vx<0||vx>=SZ||vy<0||vy>=SZ||vz<0||vz>=SZ)continue if(vx<0||vx>=SZ||vy<0||vy>=SZ||vz<0||vz>=SZ)continue
const hu=vol[vz*SZ*SZ+vy*SZ+vx] const hu=vol[vz*SZ*SZ+vy*SZ+vx]
if(isMIP){ if(mip){
const v=Math.max(0,Math.min(255,(hu+1024)/4)) const v=Math.max(0,Math.min(255,(hu+1024)/4))
if(v>rA){rA=v;gA=v*.8;bA=v*.7} if(v>ra){ra=v;ga=v*.8;ba=v*.7}
}else{ }else{
const stop=tfLookup(hu,tfName) const st=tfl(hu,tfn)
if(stop&&stop[5]>.01){ if(st&&st[5]>.01){
const da=stop[5]*1.5*.015 const da=st[5]*1.5*.015
const sh=.7+.3*Math.abs(Math.sin(hu*.01)) const sh=.7+.3*Math.abs(Math.sin(hu*.01))
rA+=da*stop[1]*sh/255 ra+=da*st[1]*sh/255;ga+=da*st[2]*sh/255;ba+=da*st[3]*sh/255;aa+=da
gA+=da*stop[2]*sh/255
bA+=da*stop[3]*sh/255
aA+=da
} }
} }
} }
const idx=(py*W+px)*4 const v=Math.min(255,Math.max(0,ra*255))
d[idx]=Math.min(255,Math.max(0,rA*255)) const g=Math.min(255,Math.max(0,ga*255))
d[idx+1]=Math.min(255,Math.max(0,gA*255)) const b=Math.min(255,Math.max(0,ba*255))
d[idx+2]=Math.min(255,Math.max(0,bA*255)) // Fill step x step block
d[idx+3]=255 for(let dy2=0;dy2<step&&py+dy2<H;dy2++){
// Fill 2x2 block for(let dx2=0;dx2<step&&px+dx2<W;dx2++){
if(px+1<W){d[idx+4]=d[idx];d[idx+5]=d[idx+1];d[idx+6]=d[idx+2];d[idx+7]=255} const i=((py+dy2)*W+(px+dx2))*4
if(py+1<H){ d[i]=v;d[i+1]=g;d[i+2]=b;d[i+3]=255
const idx2=((py+1)*W+px)*4 }
d[idx2]=d[idx];d[idx2+1]=d[idx+1];d[idx2+2]=d[idx+2];d[idx2+3]=255
if(px+1<W){d[idx2+4]=d[idx];d[idx2+5]=d[idx+1];d[idx2+6]=d[idx+2];d[idx2+7]=255}
} }
} }
} }
ctx.putImageData(id,0,0) ctx.putImageData(id,0,0)
} }
// MPR slice render function renderSlice(cv,axis,idx){
function renderSlice(canvas,axis,sIdx){ if(!cv||!vol||cv.width<1)return
if(!canvas||!vol)return const ctx=cv.getContext('2d')
const ctx=canvas.getContext('2d') const W=cv.width,H=cv.height
const W=canvas.width,H=canvas.height
const id=ctx.createImageData(W,H) const id=ctx.createImageData(W,H)
const d=id.data const d=id.data
const low=wl.value-ww.value/2,high=wl.value+ww.value/2 const lo=wl.value-ww.value/2,hi=wl.value+ww.value/2
for(let py=0;py<H;py+=2){ const step=Math.max(1,Math.floor(2))
for(let px=0;px<W;px+=2){ for(let py=0;py<H;py+=step){
for(let px=0;px<W;px+=step){
const nx=(px/W)*SZ,ny=(py/H)*SZ const nx=(px/W)*SZ,ny=(py/H)*SZ
let hu let hu
if(axis==='z'){ const zi=Math.min(SZ-1,Math.max(0,Math.round(idx)))
const z=Math.min(SZ-1,Math.max(0,Math.round(sIdx))) const xi=Math.min(SZ-1,Math.max(0,Math.round(nx)))
hu=vol[z*SZ*SZ+Math.round(ny)*SZ+Math.round(nx)] const yi=Math.min(SZ-1,Math.max(0,Math.round(ny)))
}else if(axis==='x'){ if(axis==='z')hu=vol[zi*SZ*SZ+yi*SZ+xi]
const x=Math.min(SZ-1,Math.max(0,Math.round(sIdx))) else if(axis==='x')hu=vol[yi*SZ*SZ+xi*SZ+zi]
hu=vol[Math.round(ny)*SZ*SZ+Math.round(nx)*SZ+x] else hu=vol[xi*SZ*SZ+yi*SZ+zi]
}else{ const v=hu<=lo?0:hu>=hi?255:255*(hu-lo)/(hi-lo)
const y=Math.min(SZ-1,Math.max(0,Math.round(sIdx))) for(let dy2=0;dy2<step&&py+dy2<H;dy2++){
hu=vol[Math.round(nx)*SZ*SZ+y*SZ+Math.round(ny)] for(let dx2=0;dx2<step&&px+dx2<W;dx2++){
const i=((py+dy2)*W+(px+dx2))*4
d[i]=v;d[i+1]=v*.95;d[i+2]=v*.9;d[i+3]=255
}
} }
let v=hu<=low?0:hu>=high?255:255*(hu-low)/(high-low)
const idx=(py*W+px)*4
d[idx]=v;d[idx+1]=v*.95;d[idx+2]=v*.9;d[idx+3]=255
if(px+1<W){d[idx+4]=v;d[idx+5]=v*.95;d[idx+6]=v*.9;d[idx+7]=255}
if(py+1<H){const i2=((py+1)*W+px)*4;d[i2]=v;d[i2+1]=v*.95;d[i2+2]=v*.9;d[i2+3]=255;if(px+1<W){d[i2+4]=v;d[i2+5]=v*.95;d[i2+6]=v*.9;d[i2+7]=255}}
} }
} }
ctx.putImageData(id,0,0) ctx.putImageData(id,0,0)
} }
function frame(){ function frame(){
if(!ready){raf=requestAnimationFrame(frame);return}
if(mode.value==='MPR'){ if(mode.value==='MPR'){
nextTick(()=>{ nextTick(()=>{
if(cAxial.value)renderSlice(cAxial.value,'z',zSlice.value) if(cA.value)renderSlice(cA.value,'z',zs.value)
if(cSag.value)renderSlice(cSag.value,'x',xSlice.value) if(cS.value)renderSlice(cS.value,'x',xs.value)
if(cCor.value)renderSlice(cCor.value,'y',ySlice.value) if(cC.value)renderSlice(cC.value,'y',ys.value)
if(c3d2.value)renderVR(c3d2.value,false) if(c3d2.value)renderVR(c3d2.value,false)
}) })
}else{ }else{
const c=c3d.value if(c3d.value)renderVR(c3d.value,mode.value==='MIP')
if(c)renderVR(c,mode.value==='MIP')
} }
raf=requestAnimationFrame(frame) raf=requestAnimationFrame(frame)
} }
function szCanvas(c){ function md(e){dragging=true;lm={x:e.clientX,y:e.clientY}}
if(!c)return function mm(e){if(!dragging)return;ry.value+=(e.clientX-lm.x)*.01;rx.value+=(e.clientY-lm.y)*.01;lm={x:e.clientX,y:e.clientY}}
const r=c.parentElement.getBoundingClientRect()
const dpr=window.devicePixelRatio||1
c.width=r.width*dpr
c.height=r.height*dpr
}
function md(e){dragging=true;lastM={x:e.clientX,y:e.clientY}}
function mm(e){
if(!dragging)return
const dx=e.clientX-lastM.x,dy=e.clientY-lastM.y
lastM={x:e.clientX,y:e.clientY}
rotY.value+=dx*.01;rotX.value+=dy*.01
}
function mu(){dragging=false} function mu(){dragging=false}
function onWheel(e){ function onWheel(e){zm.value*=e.deltaY>0?.9:1.1;zm.value=Math.max(.3,Math.min(5,zm.value))}
zoom.value*=e.deltaY>0?.9:1.1
zoom.value=Math.max(.3,Math.min(5,zoom.value))
}
function setPreset(p){ function setPreset(p){
preset.value=p preset.value=p
const wc={bone:[400,2500],soft:[40,400],lung:[-600,1500],angio:[300,600],skin:[50,250]} const m={bone:[400,2500],soft:[40,400],lung:[-600,1500],angio:[300,600],skin:[50,250]}
wl.value=wc[p][0];ww.value=wc[p][1] wl.value=m[p][0];ww.value=m[p][1]
} }
function onModeChange(){ function resetView(){rx.value=.4;ry.value=-.6;rz.value=0;zm.value=1.2}
nextTick(()=>{
if(mode.value==='MPR'){ function resizeAll(){
[cAxial,cSag,cCor,c3d2].forEach(c=>{if(c.value)szCanvas(c.value)}) if(mode.value==='MPR'){
}else{ [cA,cS,cC,c3d2].forEach(c=>{if(c.value)szCanvas(c.value)})
szCanvas(c3d.value) }else{
} szCanvas(c3d.value)
}) }
}
function resetView(){rotX.value=.4;rotY.value=-.6;rotZ.value=0;zoom.value=1.2;panX.value=0;panY.value=0}
function toggleFS(){
if(!document.fullscreenElement){mainRef.value?.requestFullscreen();fs.value=true}
else{document.exitFullscreen();fs.value=false}
} }
function handleResize(){ // Watch mode changes to resize canvases
if(mode.value==='MPR'){[cAxial,cSag,cCor,c3d2].forEach(c=>{if(c.value)szCanvas(c.value)})} watch(mode,()=>{nextTick(()=>{resizeAll()})})
else szCanvas(c3d.value)
}
onMounted(()=>{ onMounted(()=>{
genVol() genVol()
// Wait for DOM to be fully rendered, then start
nextTick(()=>{ nextTick(()=>{
szCanvas(c3d.value) setTimeout(()=>{
window.addEventListener('resize',handleResize) resizeAll()
frame() ready=true
frame()
},100)
}) })
window.addEventListener('resize',resizeAll)
}) })
onUnmounted(()=>{ onUnmounted(()=>{
if(raf)cancelAnimationFrame(raf) if(raf)cancelAnimationFrame(raf)
window.removeEventListener('resize',handleResize) window.removeEventListener('resize',resizeAll)
ready=false
}) })
</script> </script>
@@ -313,8 +244,7 @@ onUnmounted(()=>{
.mpr-grid{position:absolute;inset:0;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:2px} .mpr-grid{position:absolute;inset:0;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:2px}
.mpr-cell{position:relative;background:#000;overflow:hidden} .mpr-cell{position:relative;background:#000;overflow:hidden}
.mpr-c{width:100%;height:100%;display:block} .mpr-c{width:100%;height:100%;display:block}
.mpr-h{position:absolute;top:4px;left:8px;font-size:11px;color:#0f0;font-family:monospace;z-index:5;pointer-events:none;background:rgba(0,0,0,.5);padding:2px 6px;border-radius:3px} .mpr-h{position:absolute;top:4px;left:8px;font-size:11px;color:#0f0;font-family:monospace;z-index:5;pointer-events:none;background:rgba(0,0,0,.6);padding:2px 6px;border-radius:3px}
.ov{position:absolute;padding:6px 10px;background:rgba(0,0,0,.7);border-radius:4px;font-size:11px;font-family:'Courier New',monospace;color:#0f0;pointer-events:none;z-index:10;line-height:1.5} .ov{position:absolute;padding:6px 10px;background:rgba(0,0,0,.7);border-radius:4px;font-size:11px;font-family:'Courier New',monospace;color:#0f0;pointer-events:none;z-index:10;line-height:1.5}
.ov-tl{top:8px;left:8px}.ov-tr{top:8px;right:8px;text-align:right} .ov-tl{top:8px;left:8px}.ov-bl{bottom:8px;left:8px}
.ov-bl{bottom:8px;left:8px}
</style> </style>