const COL={oxyDeep:8197654,oxyMid:11022890,deoxyDeep:1847147,deoxyMid:2374014,myocardium:9385257,myoDark:6168601,endocardium:8006948,valveHealthy:15984324,valveCalc:10124095,chordae:15785896,papillary:9061414,coronaryA:11872804,coronaryV:2572412,aorta:10694696,pulm:3097720},TARGET_HEIGHT=5.1,HEART_TILT=[0,0,0],VEIN_NODES=["CS","MCV","PVLV","SCV"],NODE_ANATOMY={LV_wall:"LV",RV_wall:"RV",LA_wall:"LA",RA_wall:"RA",LV_cavity:"LV",RV_cavity:"RV",LA_cavity:"LA",RA_cavity:"RA",AORTA:"AORTA",PA:"PA",SVC:"SVC",IVC:"IVC",PULM_VEINS:"PULM_VEINS",LAD:"LAD",LCx:"LCX",RCA:"RCA",PDA:"PDA",CS:"CS",MCV:"MCV",PVLV:"PVLV",SCV:"SCV",PAPILLARY:"PAPILLARY",PERICARDIUM:"PERICARDIUM"};function anatomyKeyOf(e){if(NODE_ANATOMY[e])return NODE_ANATOMY[e];const t=/^(MV|TV|AV|PV)_leaflet_\d+$/.exec(e);return t?t[1]:null}function myoMat(e){return new GFX.M.Physical({color:e,roughness:.58,metalness:0,clearcoat:.3,clearcoatRoughness:.5,sheen:.35,sheenRoughness:.7,sheenColor:new THREE.Color(16763302),transparent:!0,opacity:1,polygonOffset:!0,polygonOffsetFactor:1,polygonOffsetUnits:1})}function cavityMat(e){return new GFX.M.Physical({color:e,roughness:.32,metalness:0,transmission:.62,thickness:.9,ior:1.33,attenuationColor:new THREE.Color(e),attenuationDistance:.9,clearcoat:.45,clearcoatRoughness:.3})}function endocardiumMat(){return new GFX.M.Physical({color:COL.endocardium,roughness:.62,metalness:0,side:THREE.BackSide,clearcoat:.25,clearcoatRoughness:.5,sheen:.25,sheenRoughness:.7,sheenColor:new THREE.Color(16763302),polygonOffset:!0,polygonOffsetFactor:-1,polygonOffsetUnits:-1})}function vesselMat(e,t=!1){return new GFX.M.Physical({color:e,roughness:.46,metalness:.02,clearcoat:.35,sheen:.2,sheenColor:new THREE.Color(16765104),...t?{polygonOffset:!0,polygonOffsetFactor:1,polygonOffsetUnits:1}:{}})}function materialFor(e){return e.endsWith("_wall")?myoMat(COL.myocardium):e==="LV_cavity"?cavityMat(COL.oxyDeep):e==="RV_cavity"?cavityMat(COL.deoxyDeep):e==="LA_cavity"?cavityMat(COL.oxyMid):e==="RA_cavity"?cavityMat(COL.deoxyMid):e==="AORTA"?vesselMat(COL.aorta,!0):e==="PA"?vesselMat(COL.pulm,!0):e==="SVC"||e==="IVC"?vesselMat(COL.deoxyMid,!0):e==="PULM_VEINS"?vesselMat(COL.oxyMid,!0):["LAD","LCx","RCA","PDA"].includes(e)?vesselMat(COL.coronaryA):VEIN_NODES.includes(e)?vesselMat(COL.coronaryV):e==="CHORDAE"?new GFX.M.Standard({color:COL.chordae,roughness:.7}):e.startsWith("PAPILLARY")?new GFX.M.Standard({color:COL.papillary,roughness:.75}):e==="PERICARDIUM"?new GFX.M.Physical({color:13350043,roughness:.55,metalness:0,transparent:!0,opacity:.07,side:THREE.BackSide,depthWrite:!1,clearcoat:.2}):/^(MV|TV|AV|PV)_/.test(e)?new GFX.M.Physical({color:COL.valveHealthy,roughness:.38,metalness:0,clearcoat:.45,clearcoatRoughness:.4,sheen:.3,sheenRoughness:.6,sheenColor:new THREE.Color(16773336),polygonOffset:!0,polygonOffsetFactor:-1,polygonOffsetUnits:-1}):new GFX.M.Standard({color:10107434,roughness:.6})}function groupWithPivot(e,t){const o=e.map(n=>n.userData&&n.userData.pivot).find(n=>Array.isArray(n)&&n.length===3);let s;if(o)s=new THREE.Vector3(o[0],o[2],-o[1]);else{const n=new THREE.Box3,u=new THREE.Box3;for(const c of e)u.setFromBufferAttribute(c.geometry.attributes.position).applyMatrix4(c.matrixWorld),n.union(u);const i=n.getCenter(new THREE.Vector3);s=new THREE.Vector3(i.x,t==="top"?n.max.y:n.min.y,i.z)}const a=new THREE.Group;a.position.copy(s);for(const n of e)n.position.sub(s),a.add(n);return a}const FLOW={MV:["LA_cavity","LV_cavity"],TV:["RA_cavity","RV_cavity"],AV:["LV_cavity","AORTA"],PV:["RV_cavity","PA"]};function centroidOf(e){const t=e.geometry;return t.boundingSphere||t.computeBoundingSphere(),t.boundingSphere.center.clone().applyMatrix4(e.matrixWorld)}function valveFrame(e,t){const o=t.clone().normalize(),s=Math.abs(o.y)<.9?new THREE.Vector3(0,1,0):new THREE.Vector3(1,0,0),a=new THREE.Vector3().crossVectors(s,o).normalize(),n=new THREE.Vector3().crossVectors(o,a).normalize();return new THREE.Matrix4().makeBasis(a,n,o).setPosition(e)}function initLeafletOpen(e){for(const t of e){const o=t.morphTargetDictionary;(!o||o.OPEN_HALF===void 0||o.OPEN_FULL===void 0)&&console.warn(`${t.name}\uFF1AGLB \u91CC\u6CA1\u6709\u5F00\u5408\u952E OPEN_HALF / OPEN_FULL`),t.userData.open=0}}function setLeafletOpen(e,t){const o=e.morphTargetInfluences,s=e.morphTargetDictionary;if(!o||!s||s.OPEN_HALF===void 0)return;const a=s.OPEN_HALF,n=s.OPEN_FULL;if(t<=.5)o[a]=t*2,o[n]=0;else{const u=(t-.5)*2;o[a]=1-u,o[n]=u}}function buildValves(e){const t=new THREE.Group;t.name="valves";const o={},s=a=>{const n=e[a];return n&&n.isMesh?centroidOf(n):null};for(const a of["MV","TV","AV","PV"]){const n=[];for(let p=0;p<6;p++){const E=e[`${a}_leaflet_${p}`];E&&n.push(E)}const u=new THREE.Group;u.name=a;const i=[],c=new THREE.Vector3;let l=null;if(n.length){n.forEach(f=>c.add(centroidOf(f))),c.divideScalar(n.length);const p=[0,2,1],E=[1,1,-1],m=(f,d)=>{const V=[];for(let A=0;A<3;A++)for(let R=0;R<3;R++)V.push(E[A]*E[R]*f[d+p[A]*3+p[R]]);return{A:V,t:new THREE.Vector3(...[0,1,2].map(A=>E[A]*f[d+9+p[A]]))}},w=n.map(f=>f.userData.v_affine).find(f=>Array.isArray(f)&&f.length===12);if(w){l=m(w,0);const f=n.map(d=>d.userData.s_affine).find(d=>Array.isArray(d)&&d.length===48);f&&(l.S=["LV","RV","LA","RA"].map((d,V)=>({k:d,...m(f,V*12)})))}else console.warn(`${a}\uFF1AGLB \u91CC\u6CA1\u6709\u74E3\u819C\u8FD0\u52A8 v_affine\uFF0C\u74E3\u819C\u4E0D\u968F\u5FC3\u5BA4\u52A8`);const[T,v]=FLOW[a],L=s(T),r=s(v),y=L&&r?r.clone().sub(L):new THREE.Vector3(0,-1,0),h=valveFrame(c,y),M=h.clone().invert(),x=new THREE.Group;x.name=a+"_frame",x.applyMatrix4(h);for(const f of n){const d=new THREE.Matrix4().copy(M).multiply(f.matrixWorld);f.geometry.applyMatrix4(d);const V=new THREE.Matrix3().setFromMatrix4(d),A=new THREE.Matrix3().getNormalMatrix(d);for(const R of f.geometry.morphAttributes.position||[])R.applyMatrix3(V);for(const R of f.geometry.morphAttributes.normal||[])R.applyMatrix3(A);f.position.set(0,0,0),f.quaternion.identity(),f.scale.set(1,1,1),x.add(f),i.push(f)}initLeafletOpen(i),u.add(x)}u.matrixAutoUpdate=!1,u.userData={kind:"valve",anatomy:a,leaflets:i,center:c,affine:l},o[a]=u,t.add(u)}return t.userData={valves:o},t}function assembleHeart(e){e.updateMatrixWorld(!0);const t={};e.traverse(r=>{r.name&&(t[r.name]=r)}),e.traverse(r=>{if(!r.isMesh)return;r.material=materialFor(r.name),r.name.endsWith("_cavity")&&(r.userData.matTransp=r.material,r.userData.matEndo=endocardiumMat(),r.userData.matNone=new GFX.M.Basic({visible:!1})),r.castShadow=r.receiveShadow=!0,r.name==="PERICARDIUM"&&(r.castShadow=!1);const y=anatomyKeyOf(r.name);y&&typeof ANATOMY<"u"&&ANATOMY[y]&&(r.userData.anatomy=y)});const o=new THREE.Group;o.name="heart";const s=window.GFX&&window.GFX.tsl?new THREE.ClippingGroup:o;s!==o&&(s.name="clipped",o.add(s),o.userData.clipGroup=s);const a={},n={},u={},i=new THREE.Group;i.name="chambers";for(const r of["LV","RV","LA","RA"]){const y=t[`${r}_wall`],h=t[`${r}_cavity`];!y||!h||(a[r]=y,n[r]=h,u[r]=groupWithPivot([y,h],r==="LV"||r==="RV"?"top":"bottom"),i.add(u[r]))}i.userData={walls:a,cavities:n,pivots:u};const c=buildValves(t),l=new THREE.Group;l.name="vessels";for(const r of["AORTA","PA","SVC","IVC","PULM_VEINS"])t[r]&&l.add(t[r]);const p=new THREE.Group;p.name="coronaries";for(const r of["LAD","LCx","RCA","PDA",...VEIN_NODES])t[r]&&p.add(t[r]);const E=new THREE.Group;E.name="papillary";for(const r of["PAPILLARY","CHORDAE"])t[r]&&E.add(t[r]);const m=t.PERICARDIUM||new THREE.Object3D;m.name="pericardium",s.add(m,l,i,E,c,p),o.userData.parts={chambers:i,valves:c,vessels:l,coronaries:p,papillary:E,pericardium:m},o.updateMatrixWorld(!0);const w=new THREE.Box3().setFromObject(o),T=TARGET_HEIGHT/Math.max(w.getSize(new THREE.Vector3).y,1e-6);o.scale.setScalar(T),o.rotation.set(...HEART_TILT),o.updateMatrixWorld(!0);const v=new THREE.Box3;for(const r of["LV_wall","RV_wall","LA_wall","RA_wall"])t[r]&&v.expandByObject(t[r]);const L=v.isEmpty()?w.getCenter(new THREE.Vector3):v.getCenter(new THREE.Vector3);return o.position.sub(L),o.userData.autoScale=T,o}const HEART_BUILD="2127692560952dc18a1bee2c3595f73a";function loadHeart(e,t){return new Promise((o,s)=>{new GLTFLoader().load(e||"assets/heart.glb",a=>{try{const n=assembleHeart(a.scene);n.userData.build=HEART_BUILD,o(n)}catch(n){s(n)}},a=>{t&&a.lengthComputable&&a.total>0&&t(a.loaded/a.total)},a=>s(a))})}const SCALE_KEY_STEP=.25,TISSUE_OPACITY_DEFAULT=.16,TISSUE_GAIN={walls:1,vessels:.34/.16,papillary:.35/.16,coronaries:1/.16},TISSUE_HIDE=.02;function applyDisplayMode(e,t,o={}){const s=e.userData.parts,a=s.chambers.userData.cavities,n=t==="full",u=t==="transp",i=t==="cor";for(const c of["LV","RV","LA","RA"]){const l=a[c],p=c==="LA"||c==="RA";l.visible=u||i,l.material=i?p?l.userData.matEndo:l.userData.matNone:l.userData.matTransp}s.valves.visible=!n,s.papillary.visible=!n,s.pericardium.visible=n,setTissueOpacity(e,u?o.tissueOpacity??TISSUE_OPACITY_DEFAULT:1);for(const c of Object.values(s.chambers.userData.walls))c.material.polygonOffsetFactor=0;for(const c of s.vessels.children){const l=c.userData.tissueMat||c.material;l.polygonOffset&&(l.polygonOffsetFactor=0)}}function setTissueOpacity(e,t){const o=e.userData.parts,s=(a,n,u=!1)=>{const i=a.userData.tissueMat||a.material,c=Math.min(1,t*n),l=c<.999;i.userData&&i.userData.bloodShell||a.name.endsWith("_blood")||(i.transparent!==l&&(i.transparent=l,i.needsUpdate=!0),i.opacity=l?c:1,i.depthWrite=u||!l,i.visible=c>=TISSUE_HIDE)};for(const a of Object.values(o.chambers.userData.walls))s(a,TISSUE_GAIN.walls);for(const a of o.vessels.children)s(a,TISSUE_GAIN.vessels,!0);for(const a of o.papillary.children)s(a,TISSUE_GAIN.papillary);for(const a of o.coronaries.children)s(a,TISSUE_GAIN.coronaries)}Object.assign(window,{loadHeart,assembleHeart,setLeafletOpen,applyDisplayMode,setTissueOpacity,TISSUE_OPACITY_DEFAULT,VEIN_NODES,COL,TARGET_HEIGHT,HEART_TILT,SCALE_KEY_STEP});
