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:
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="viewer-wrap">
|
||||
<!-- 工具栏 -->
|
||||
<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="MPR">MPR多平面</el-radio-button>
|
||||
<el-radio-button value="MIP">MIP最大密度</el-radio-button>
|
||||
@@ -13,119 +12,72 @@
|
||||
</el-button-group>
|
||||
<el-divider direction="vertical"/>
|
||||
<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 class="vmain" ref="mainRef">
|
||||
<!-- 单屏模式(VR/MIP) -->
|
||||
<canvas v-show="mode!=='MPR'" ref="c3d" class="c3d"
|
||||
@mousedown="md" @mousemove="mm" @mouseup="mu" @mouseleave="mu"
|
||||
@wheel.prevent="onWheel" @contextmenu.prevent/>
|
||||
|
||||
<!-- MPR四格 -->
|
||||
<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">矢状 Sagittal</div><canvas ref="cSag" 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">轴位 Axial</div><canvas ref="cA" 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="cC" class="mpr-c"/></div>
|
||||
<div class="mpr-cell"><div class="mpr-h">3D预览</div><canvas ref="c3d2" class="mpr-c"/></div>
|
||||
</div>
|
||||
|
||||
<!-- 信息叠加层 -->
|
||||
<div class="ov ov-tl">
|
||||
<div>患者: {{ taskData.patientName || '—' }}</div>
|
||||
<div>{{ taskData.modality||'CT' }} | {{ taskData.bodyPart||'胸部' }} | {{ taskData.reconstructionType||'VR' }}</div>
|
||||
</div>
|
||||
<div class="ov ov-tr">
|
||||
<div>模式: {{ mode }} | 预设: {{ presetLabels[preset] }}</div>
|
||||
<div>窗位: {{ wl }} | 窗宽: {{ ww }}</div>
|
||||
<div>{{ taskData.patientName || '患者' }} | {{ taskData.modality||'CT' }} {{ taskData.bodyPart||'胸部' }}</div>
|
||||
<div>{{ mode }} {{ presetLabels[preset] }} | WL:{{ wl }} WW:{{ ww }}</div>
|
||||
</div>
|
||||
<div class="ov ov-bl">
|
||||
<div v-if="mode==='MPR'">Z:{{ zSlice }} Y:{{ ySlice }} X:{{ xSlice }}</div>
|
||||
<div v-else>旋转: {{ (rotX*57.3).toFixed(0) }}°,{{ (rotY*57.3).toFixed(0) }}° | 缩放: {{ zoom.toFixed(1) }}x</div>
|
||||
<div v-if="mode==='MPR'">Z:{{ zs }} Y:{{ ys }} X:{{ xs }}</div>
|
||||
<div v-else>旋转:{{ (rx*57.3).toFixed(0) }}° 缩放:{{ zm.toFixed(1) }}x</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 mode=ref('VR')
|
||||
const preset=ref('bone')
|
||||
const mainRef=ref(null)
|
||||
const c3d=ref(null)
|
||||
const c3d2=ref(null)
|
||||
const cAxial=ref(null)
|
||||
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 mode=ref('VR'),preset=ref('bone')
|
||||
const mainRef=ref(null),c3d=ref(null),c3d2=ref(null)
|
||||
const cA=ref(null),cS=ref(null),cC=ref(null)
|
||||
const wl=ref(40),ww=ref(400)
|
||||
const rx=ref(0.4),ry=ref(-0.6),rz=ref(0),zm=ref(1.2)
|
||||
const zs=ref(32),ys=ref(32),xs=ref(32)
|
||||
|
||||
const presetNames=['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
|
||||
let vol=null
|
||||
let dragging=false,lastM={x:0,y:0}
|
||||
let raf=null
|
||||
let vol=null,dragging=false,lm={x:0,y:0},raf=null,ready=false
|
||||
|
||||
// Transfer function
|
||||
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]],
|
||||
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]],
|
||||
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]],
|
||||
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]],
|
||||
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]]
|
||||
// Transfer functions: [minHU, maxHU, R, G, B, alpha]
|
||||
const TF={
|
||||
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,.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,.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,.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,.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(){
|
||||
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++){
|
||||
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)
|
||||
let hu=-1000
|
||||
if(bd<1){
|
||||
if(bd>0.88)hu=-100+(Math.random()-.5)*30
|
||||
else if(bd>0.78)hu=45+(Math.random()-.5)*16
|
||||
if(bd>.88)hu=-100+(Math.random()-.5)*30
|
||||
else if(bd>.78)hu=45+(Math.random()-.5)*16
|
||||
else{
|
||||
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}
|
||||
}
|
||||
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}}
|
||||
if(hu===-1000){
|
||||
const hd=Math.sqrt((dx/6)**2+((dy+3)/5)**2+((dz+3)/7)**2)
|
||||
if(hd<1)hu=45+(Math.random()-.5)*10
|
||||
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
|
||||
}
|
||||
}
|
||||
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}}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,175 +85,154 @@ function genVol(){
|
||||
}
|
||||
}
|
||||
|
||||
// Volume render (VR or MIP)
|
||||
function renderVR(canvas,isMIP){
|
||||
if(!canvas||!vol)return
|
||||
const ctx=canvas.getContext('2d')
|
||||
const W=canvas.width,H=canvas.height
|
||||
function szCanvas(c){
|
||||
if(!c||!c.parentElement)return
|
||||
const r=c.parentElement.getBoundingClientRect()
|
||||
if(r.width<1||r.height<1)return
|
||||
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 d=id.data
|
||||
const half=SZ/2
|
||||
const cosX=Math.cos(rotX.value),sinX=Math.sin(rotX.value)
|
||||
const cosY=Math.cos(rotY.value),sinY=Math.sin(rotY.value)
|
||||
const cosZ=Math.cos(rotZ.value),sinZ=Math.sin(rotZ.value)
|
||||
const tfName=preset.value
|
||||
const half=32
|
||||
const cX=Math.cos(rx.value),sX=Math.sin(rx.value)
|
||||
const cY=Math.cos(ry.value),sY=Math.sin(ry.value)
|
||||
const cZ=Math.cos(rz.value),sZ=Math.sin(rz.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 px=0;px<W;px+=2){
|
||||
const ndcX=((px/W)-.5)*2/zoom.value+panX.value
|
||||
const ndcY=((py/H)-.5)*2/zoom.value+panY.value
|
||||
let rx=ndcX*half,ry=ndcY*half,rz=-half*1.5
|
||||
// Rotate Y
|
||||
let tx=rx*cosY+rz*sinY;tz=-rx*sinY+rz*cosY;rx=tx;rz=tz
|
||||
// Rotate X
|
||||
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
|
||||
for(let py=0;py<H;py+=step){
|
||||
for(let px=0;px<W;px+=step){
|
||||
const nx=((px/W)-.5)*2/zm.value
|
||||
const ny=((py/H)-.5)*2/zm.value
|
||||
let r0=nx*half,c0=ny*half,t0=-half*1.5
|
||||
let t1=r0*cY+t0*sY;t0=-r0*sY+t0*cY;r0=t1
|
||||
t1=c0*cX-t0*sX;t0=c0*sX+t0*cX;c0=t1
|
||||
t1=r0*cZ-c0*sZ;c0=r0*sZ+c0*cZ;r0=t1
|
||||
|
||||
let rA=0,gA=0,bA=0,aA=0
|
||||
for(let s=0;s<128&&aA<0.95;s++){
|
||||
let ra=0,ga=0,ba=0,aa=0
|
||||
for(let s=0;s<100&&aa<.95;s++){
|
||||
const t=s*1.5-half*1.5
|
||||
const vx=Math.round(rx+t+half)
|
||||
const vy=Math.round(ry+half)
|
||||
const vz=Math.round(rz+half)
|
||||
const vx=Math.round(r0+t+half),vy=Math.round(c0+half),vz=Math.round(t0+half)
|
||||
if(vx<0||vx>=SZ||vy<0||vy>=SZ||vz<0||vz>=SZ)continue
|
||||
const hu=vol[vz*SZ*SZ+vy*SZ+vx]
|
||||
if(isMIP){
|
||||
if(mip){
|
||||
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{
|
||||
const stop=tfLookup(hu,tfName)
|
||||
if(stop&&stop[5]>.01){
|
||||
const da=stop[5]*1.5*.015
|
||||
const st=tfl(hu,tfn)
|
||||
if(st&&st[5]>.01){
|
||||
const da=st[5]*1.5*.015
|
||||
const sh=.7+.3*Math.abs(Math.sin(hu*.01))
|
||||
rA+=da*stop[1]*sh/255
|
||||
gA+=da*stop[2]*sh/255
|
||||
bA+=da*stop[3]*sh/255
|
||||
aA+=da
|
||||
ra+=da*st[1]*sh/255;ga+=da*st[2]*sh/255;ba+=da*st[3]*sh/255;aa+=da
|
||||
}
|
||||
}
|
||||
}
|
||||
const idx=(py*W+px)*4
|
||||
d[idx]=Math.min(255,Math.max(0,rA*255))
|
||||
d[idx+1]=Math.min(255,Math.max(0,gA*255))
|
||||
d[idx+2]=Math.min(255,Math.max(0,bA*255))
|
||||
d[idx+3]=255
|
||||
// Fill 2x2 block
|
||||
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}
|
||||
if(py+1<H){
|
||||
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}
|
||||
const v=Math.min(255,Math.max(0,ra*255))
|
||||
const g=Math.min(255,Math.max(0,ga*255))
|
||||
const b=Math.min(255,Math.max(0,ba*255))
|
||||
// Fill step x step block
|
||||
for(let dy2=0;dy2<step&&py+dy2<H;dy2++){
|
||||
for(let dx2=0;dx2<step&&px+dx2<W;dx2++){
|
||||
const i=((py+dy2)*W+(px+dx2))*4
|
||||
d[i]=v;d[i+1]=g;d[i+2]=b;d[i+3]=255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.putImageData(id,0,0)
|
||||
}
|
||||
|
||||
// MPR slice render
|
||||
function renderSlice(canvas,axis,sIdx){
|
||||
if(!canvas||!vol)return
|
||||
const ctx=canvas.getContext('2d')
|
||||
const W=canvas.width,H=canvas.height
|
||||
function renderSlice(cv,axis,idx){
|
||||
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 d=id.data
|
||||
const low=wl.value-ww.value/2,high=wl.value+ww.value/2
|
||||
for(let py=0;py<H;py+=2){
|
||||
for(let px=0;px<W;px+=2){
|
||||
const lo=wl.value-ww.value/2,hi=wl.value+ww.value/2
|
||||
const step=Math.max(1,Math.floor(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
|
||||
let hu
|
||||
if(axis==='z'){
|
||||
const z=Math.min(SZ-1,Math.max(0,Math.round(sIdx)))
|
||||
hu=vol[z*SZ*SZ+Math.round(ny)*SZ+Math.round(nx)]
|
||||
}else if(axis==='x'){
|
||||
const x=Math.min(SZ-1,Math.max(0,Math.round(sIdx)))
|
||||
hu=vol[Math.round(ny)*SZ*SZ+Math.round(nx)*SZ+x]
|
||||
}else{
|
||||
const y=Math.min(SZ-1,Math.max(0,Math.round(sIdx)))
|
||||
hu=vol[Math.round(nx)*SZ*SZ+y*SZ+Math.round(ny)]
|
||||
const zi=Math.min(SZ-1,Math.max(0,Math.round(idx)))
|
||||
const xi=Math.min(SZ-1,Math.max(0,Math.round(nx)))
|
||||
const yi=Math.min(SZ-1,Math.max(0,Math.round(ny)))
|
||||
if(axis==='z')hu=vol[zi*SZ*SZ+yi*SZ+xi]
|
||||
else if(axis==='x')hu=vol[yi*SZ*SZ+xi*SZ+zi]
|
||||
else hu=vol[xi*SZ*SZ+yi*SZ+zi]
|
||||
const v=hu<=lo?0:hu>=hi?255:255*(hu-lo)/(hi-lo)
|
||||
for(let dy2=0;dy2<step&&py+dy2<H;dy2++){
|
||||
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)
|
||||
}
|
||||
|
||||
function frame(){
|
||||
if(!ready){raf=requestAnimationFrame(frame);return}
|
||||
if(mode.value==='MPR'){
|
||||
nextTick(()=>{
|
||||
if(cAxial.value)renderSlice(cAxial.value,'z',zSlice.value)
|
||||
if(cSag.value)renderSlice(cSag.value,'x',xSlice.value)
|
||||
if(cCor.value)renderSlice(cCor.value,'y',ySlice.value)
|
||||
if(cA.value)renderSlice(cA.value,'z',zs.value)
|
||||
if(cS.value)renderSlice(cS.value,'x',xs.value)
|
||||
if(cC.value)renderSlice(cC.value,'y',ys.value)
|
||||
if(c3d2.value)renderVR(c3d2.value,false)
|
||||
})
|
||||
}else{
|
||||
const c=c3d.value
|
||||
if(c)renderVR(c,mode.value==='MIP')
|
||||
if(c3d.value)renderVR(c3d.value,mode.value==='MIP')
|
||||
}
|
||||
raf=requestAnimationFrame(frame)
|
||||
}
|
||||
|
||||
function szCanvas(c){
|
||||
if(!c)return
|
||||
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 md(e){dragging=true;lm={x:e.clientX,y:e.clientY}}
|
||||
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}}
|
||||
function mu(){dragging=false}
|
||||
function onWheel(e){
|
||||
zoom.value*=e.deltaY>0?.9:1.1
|
||||
zoom.value=Math.max(.3,Math.min(5,zoom.value))
|
||||
}
|
||||
function onWheel(e){zm.value*=e.deltaY>0?.9:1.1;zm.value=Math.max(.3,Math.min(5,zm.value))}
|
||||
|
||||
function setPreset(p){
|
||||
preset.value=p
|
||||
const wc={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]
|
||||
const m={bone:[400,2500],soft:[40,400],lung:[-600,1500],angio:[300,600],skin:[50,250]}
|
||||
wl.value=m[p][0];ww.value=m[p][1]
|
||||
}
|
||||
function onModeChange(){
|
||||
nextTick(()=>{
|
||||
if(mode.value==='MPR'){
|
||||
[cAxial,cSag,cCor,c3d2].forEach(c=>{if(c.value)szCanvas(c.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 resetView(){rx.value=.4;ry.value=-.6;rz.value=0;zm.value=1.2}
|
||||
|
||||
function resizeAll(){
|
||||
if(mode.value==='MPR'){
|
||||
[cA,cS,cC,c3d2].forEach(c=>{if(c.value)szCanvas(c.value)})
|
||||
}else{
|
||||
szCanvas(c3d.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize(){
|
||||
if(mode.value==='MPR'){[cAxial,cSag,cCor,c3d2].forEach(c=>{if(c.value)szCanvas(c.value)})}
|
||||
else szCanvas(c3d.value)
|
||||
}
|
||||
// Watch mode changes to resize canvases
|
||||
watch(mode,()=>{nextTick(()=>{resizeAll()})})
|
||||
|
||||
onMounted(()=>{
|
||||
genVol()
|
||||
// Wait for DOM to be fully rendered, then start
|
||||
nextTick(()=>{
|
||||
szCanvas(c3d.value)
|
||||
window.addEventListener('resize',handleResize)
|
||||
frame()
|
||||
setTimeout(()=>{
|
||||
resizeAll()
|
||||
ready=true
|
||||
frame()
|
||||
},100)
|
||||
})
|
||||
window.addEventListener('resize',resizeAll)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
if(raf)cancelAnimationFrame(raf)
|
||||
window.removeEventListener('resize',handleResize)
|
||||
window.removeEventListener('resize',resizeAll)
|
||||
ready=false
|
||||
})
|
||||
</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-cell{position:relative;background:#000;overflow:hidden}
|
||||
.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-tl{top:8px;left:8px}.ov-tr{top:8px;right:8px;text-align:right}
|
||||
.ov-bl{bottom:8px;left:8px}
|
||||
.ov-tl{top:8px;left:8px}.ov-bl{bottom:8px;left:8px}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user