const BLOOD={count:6e4,streakWidthCm:.07,streakSec:.06,streakMinCm:.18,streakMaxCm:1.3,coronarySizeCm:.07,coronaryMinRCm:.1,clear:.35,absorbCm:1.2,alphaCm:.8,colors:{art:13116204,ven:6950944},dyeColors:{art:[16734784,7737881],ven:[14172266,4591648]},arterial:["LA_cavity","LV_cavity","AORTA","PULM_VEINS"],flows:{LA_cavity:["PULM_VEINS","MV"],LV_cavity:["MV","AV"],RA_cavity:["CAVAL","TV"],RV_cavity:["TV","PV"],AORTA:["AO_ROOT"],PA:["PA_ROOT"],SVC:["SVC"],IVC:["IVC"],PULM_VEINS:["PULM_VEINS"]},chambers:{LA_cavity:"LA",LV_cavity:"LV",RA_cavity:"RA",RV_cavity:"RV"},wallCm:.08,stuckSec:2.5,jetLenCm:4,inertiaSec:.02,swirlGain:{ventricle:4,atrium:2.2},swirlMaxCm:50,swirlUp:.05,swirlDown:.5,swirlBrake:.05,curlAmp:.3,curlFloor:.8,curlScale:.35},COMPUTE_COMMON=`
precision highp float;
precision highp sampler3D;
uniform sampler3D uFlowA, uFlowB, uDist, uInside;
uniform vec3 uOrigin, uSize;
uniform float uUmaxA, uUmaxB, uQa, uQb, uHasB, uDt, uTime, uH;
uniform float uVolScale;   // \u5FC3\u8154 V_basis / V(t)\uFF1ABasis \u7A7A\u95F4\u91CC\u6309\u5F53\u524D\u5B9E\u9645\u5BB9\u79EF\u6362\u7B97\u901F\u5EA6\uFF0C\u9897\u7C92\u5BC6\u5EA6\u968F\u5BB9\u79EF\u7EBF\u6027\u53D8\uFF08\u800C\u4E0D\u662F\u6307\u6570\u8870\u51CF\uFF09\uFF1B\u8840\u7BA1 1
// \u5C04\u6D41\uFF08\u72ED\u7A84\u74E3\u53E3\u987A\u6D41\u8FDB\u6765 / \u5173\u95ED\u4E0D\u5168\u53CD\u6D41\u8FDB\u6765\uFF09\uFF1A\u5589\u90E8\u4E2D\u5FC3 c\u3001\u6307\u5411\u533A\u5185\u7684\u8F74 n\u3001\u5589\u90E8\u534A\u5F84 rT\u3001\u5589\u901F sp\uFF080 = \u5173\uFF09\u3002
// \u52BF\u6D41\u6CA1\u6709\u60EF\u6027\uFF0C\u8FC7\u4E86\u53E3\u5C31\u6563\u5F00\uFF1B\u771F\u5B9E\u5C04\u6D41\u51B2\u8FDB\u8154\u91CC 3\u20134 cm\uFF1A\u5C04\u6D41\u67F1\u5185\u628A\u901F\u5EA6\u6DF7\u6210\u6CBF\u8F74\u7684\u5589\u901F\uFF0C\u6CBF\u7A0B\u8870\u51CF\u3001\u8FB9\u7F18\u6E10\u9690
uniform vec4 uJetA, uJetAN, uJetB, uJetBN;   // (c.xyz, rT) / (n.xyz, sp)
uniform float uJetLen, uWallCm;
uniform vec4 uPorts[8];                      // \u5404\u7AEF\u53E3\u76D8\uFF1A\u4E2D\u5FC3 xyz\uFF0Cw = \u534A\u5F84\uFF08\u51FA\u53E3\u4E3A\u6B63\u3001\u5165\u53E3\u4E3A\u8D1F\uFF09
uniform int uNPorts;
// \u771F\u6DB2\u4F53\uFF08fluid.jsx\uFF0Cplan/real-fluid D4 / E4\uFF09\uFF1A\u9897\u7C92\u4F4D\u7F6E\u5728 Basis \u5750\u6807\uFF0C\u901F\u5EA6 = \u6C42\u89E3\u5668\u5728\u5F62\u53D8\u540E\u4F4D\u7F6E p + d(p) \u7684\u901F\u5EA6 \u2212 \u58C1\u4F4D\u79FB\u901F\u7387 \u1E0B(p)
// \uFF08\u5FFD\u7565 J\u207B\xB9\uFF0C\u8FD1\u58C1\u8BEF\u5DEE \u2264 15\xB0 / 30%\uFF09\u3002\u4F4D\u79FB\u53EA\u91C7\u6C42\u89E3\u5668\u7ED9\u7684\u4E00\u5F20\uFF08\u5355\u952E = \u952E\u4F4D\u79FB\u573A \xD7 \u6743\u91CD\uFF0C\u591A\u952E = \u6C42\u89E3\u5668\u6BCF\u5E27\u5408\u6210\uFF09\uFF0C\u5F53\u524D / \u4E0A\u4E00\u5E27\u5404\u4E00\u5F20
uniform sampler2D uFluidVel;
uniform float uFluidOn, uFluidDt, uFH, uDispScaleNow, uDispScalePrev;
uniform ivec3 uFDims;
uniform int uFCols;
uniform vec3 uFOrigin, uDefOrigin, uDefSize;
uniform sampler3D uDispNow, uDispPrev;
vec3 dispNow(vec3 p){ return uDispScaleNow * texture(uDispNow, (p - uDefOrigin) / uDefSize).xyz; }
vec3 dispPrev(vec3 p){ return uDispScalePrev * texture(uDispPrev, (p - uDefOrigin) / uDefSize).xyz; }
ivec2 fTexel(ivec3 c){ int ty = c.z / uFCols; int tx = c.z - ty * uFCols; return ivec2(tx * uFDims.x + c.x, ty * uFDims.y + c.y); }
vec3 fFetch(ivec3 c){ return texelFetch(uFluidVel, fTexel(clamp(c, ivec3(0), uFDims - 1)), 0).xyz; }
float fComp(vec3 v, int k){ return k == 0 ? v.x : (k == 1 ? v.y : v.z); }
float fSample(int k, vec3 L){
  vec3 Lc = L - 0.5 + 0.5 * vec3(k == 0 ? 1.0 : 0.0, k == 1 ? 1.0 : 0.0, k == 2 ? 1.0 : 0.0);
  vec3 b = floor(Lc); vec3 f = Lc - b; ivec3 i0 = ivec3(b);
  float s = 0.0;
  for (int dz = 0; dz < 2; dz++) for (int dy = 0; dy < 2; dy++) for (int dx = 0; dx < 2; dx++){
    float w = (dx == 1 ? f.x : 1.0 - f.x) * (dy == 1 ? f.y : 1.0 - f.y) * (dz == 1 ? f.z : 1.0 - f.z);
    s += w * fComp(fFetch(i0 + ivec3(dx, dy, dz)), k);
  }
  return s;
}
vec3 fluidVelAt(vec3 p){
  vec3 d = dispNow(p);
  vec3 dd = uFluidDt > 0.0 ? (d - dispPrev(p)) / uFluidDt : vec3(0.0);
  vec3 L = (p + d - uFOrigin) / uFH;
  return vec3(fSample(0, L), fSample(1, L), fSample(2, L)) - dd;
}
float hash(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
vec3 uvwOf(vec3 p){ return (p - uOrigin) / uSize; }
float distAt(vec3 p){ return texture(uDist, uvwOf(p)).r * 2.5 - 0.5; }   // \u6709\u7B26\u53F7\u8DDD\u58C1\uFF08cm\uFF09\uFF1A\u8154\u5916\u4E3A\u8D1F\uFF08flowfield.py DIST_MIN/MAX\uFF09
float insideAt(vec3 p){ return texture(uInside, uvwOf(p)).r; }
vec3 potVelAt(vec3 p){
  vec3 uvw = uvwOf(p);
  vec4 a = texture(uFlowA, uvw);
  vec3 v = (a.xyz * 2.0 - 1.0) * a.a * uUmaxA * uQa;
  if (uHasB > 0.5){ vec4 b = texture(uFlowB, uvw); v += (b.xyz * 2.0 - 1.0) * b.a * uUmaxB * uQb; }
  return v * uVolScale;
}
// \u5355\u51FA\u53E3\uFF08HLSL \u5BF9"\u5206\u652F\u91CC\u63D0\u524D return\u3001\u540E\u9762\u8FD8\u6709\u4EE3\u7801"\u7684\u51FD\u6570\u62A5 potentially uninitialized variable \u8B66\u544A\uFF09
vec3 velAt(vec3 p){ vec3 v; if (uFluidOn > 0.5) v = fluidVelAt(p); else v = potVelAt(p); return v; }
vec3 jet(vec3 p, vec3 v, vec4 cr, vec4 ns){
  if (ns.w <= 0.0) return v;
  vec3 rel = p - cr.xyz;
  float along = dot(rel, ns.xyz);
  if (along < 0.0 || along > uJetLen) return v;
  float rho = length(rel - along * ns.xyz);
  float core = cr.w * (1.0 + 0.8 * along / uJetLen);
  if (rho > core) return v;
  float k = (1.0 - along / uJetLen) * (1.0 - smoothstep(0.6 * core, core, rho));
  return mix(v, ns.xyz * ns.w, k);
}
vec3 fieldAt(vec3 p){ vec3 v = velAt(p); if (uFluidOn < 0.5) v = jet(p, jet(p, v, uJetA, uJetAN), uJetB, uJetBN); return v; }   // \u771F\u6DB2\u4F53\uFF1A\u5C04\u6D41\u7531\u6C42\u89E3\u5668\u7ED9\uFF0C\u4E0D\u6574\u5F62
vec3 gradDist(vec3 p){
  float e = uH;
  return vec3(distAt(p + vec3(e, 0, 0)) - distAt(p - vec3(e, 0, 0)),
              distAt(p + vec3(0, e, 0)) - distAt(p - vec3(0, e, 0)),
              distAt(p + vec3(0, 0, e)) - distAt(p - vec3(0, 0, e))) / (2.0 * e);
}`,VEL_GLSL=COMPUTE_COMMON+`
uniform vec4 uSwirl;        // \u6DA1\u8F74 xyz\uFF08\u5355\u4F4D\uFF09\u3001\u89D2\u901F\u5EA6 w\uFF08rad/s\uFF09
uniform vec3 uSwirlC;       // \u6DA1\u8F74\u8FC7\u7684\u70B9\uFF08\u8154\u8D28\u5FC3\uFF09
uniform float uSwirlMax, uInertia, uCurlAmp, uCurlFloor, uCurlScale;
vec3 hash3(vec3 p){
  p = vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6)));
  return fract(sin(p) * 43758.5453);
}
float vnoise(vec3 x){
  vec3 i = floor(x), f = fract(x); f = f * f * (3.0 - 2.0 * f);
  return mix(mix(mix(hash3(i).x, hash3(i + vec3(1,0,0)).x, f.x), mix(hash3(i + vec3(0,1,0)).x, hash3(i + vec3(1,1,0)).x, f.x), f.y),
             mix(mix(hash3(i + vec3(0,0,1)).x, hash3(i + vec3(1,0,1)).x, f.x), mix(hash3(i + vec3(0,1,1)).x, hash3(i + vec3(1,1,1)).x, f.x), f.y), f.z);
}
vec3 noise3(vec3 p){ return vec3(vnoise(p), vnoise(p + vec3(31.4, 17.2, 9.1)), vnoise(p + vec3(-12.3, 44.1, 23.7))) * 2.0 - 1.0; }
// \u65E0\u6563\u5EA6\u7684\u6E4D\u52A8\u573A\uFF08\u65CB\u5EA6\uFF09
vec3 curlNoise(vec3 p){
  float e = 0.12;
  vec3 dx = noise3(p + vec3(e, 0, 0)) - noise3(p - vec3(e, 0, 0));
  vec3 dy = noise3(p + vec3(0, e, 0)) - noise3(p - vec3(0, e, 0));
  vec3 dz = noise3(p + vec3(0, 0, e)) - noise3(p - vec3(0, 0, e));
  return vec3(dy.z - dz.y, dz.x - dx.z, dx.y - dy.x) / (2.0 * e);
}
void main(){
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 p = texture2D(tPos, uv);
  vec3 offOld = texture2D(tVel, uv).xyz;    // \u4E0A\u4E00\u5E27\u7684\u9644\u52A0\u5206\u91CF
  if (p.w < -0.5){ gl_FragColor = vec4(0.0); return; }     // \u7B49\u5F85\u6C60\uFF1A\u9644\u52A0\u5206\u91CF\u6E05\u96F6\uFF0C\u653E\u51FA\u65F6\u53EA\u8DDF\u573A\u8D70
  vec3 pos = p.xyz;
  vec3 vf = fieldAt(pos);
  vec3 vs = vec3(0.0);
  if (uSwirl.w > 0.0){
    vec3 t = cross(uSwirl.xyz, pos - uSwirlC);
    float tl = length(t);
    float sp = min(uSwirl.w * tl, uSwirlMax);
    vs = tl > 1e-5 ? t * (sp / tl) : vec3(0.0);
  }
  float dw = distAt(pos);
  float wallFade = smoothstep(0.0, 0.25, dw);     // \u8D34\u58C1\u5904\u6DA1\u65CB / \u6E4D\u52A8\u6536\u5C0F\uFF0C\u9897\u7C92\u4E0D\u5F80\u58C1\u91CC\u94BB
  vs *= wallFade;
  // \u7AEF\u53E3\u9644\u8FD1\u6E4D\u52A8\u6536\u5C0F\uFF1A\u51FA\u53E3\u524D\u6D41\u7EBF\u6C47\u805A\uFF0C\u968F\u673A\u6270\u52A8\u628A\u9897\u7C92\u6253\u79BB\u51FA\u53E3\u5C42\uFF0C\u5C04\u8840\u671F\u51FA\u533A\u5C11 7 \u4E2A\u767E\u5206\u70B9
  float portFade = 1.0;
  for (int i = 0; i < 8; i++){
    if (i >= uNPorts) break;
    float r = abs(uPorts[i].w);
    portFade = min(portFade, smoothstep(r * 1.2, r * 1.2 + 1.2, distance(pos, uPorts[i].xyz)));
  }
  // \u771F\u6DB2\u4F53\uFF08C\uFF09\u5F00\u7740\u65F6\u4E0D\u52A0\u4EBA\u9020\u6E4D\u52A8\uFF1A\u6D41\u7EBF\u53EA\u8DDF\u6C42\u89E3\u5668\u8D70\uFF08\u7B2C\u56DB\u8F6E\uFF1B\u4E4B\u524D\u7559\u4E86 0.3 \u500D\uFF09
  float amp = (uCurlAmp * (length(vf) + length(vs)) + uCurlFloor) * wallFade * portFade * (uFluidOn > 0.5 ? 0.0 : 1.0);
  vec3 vc = curlNoise(pos * uCurlScale + vec3(0.0, uTime * 0.12, 0.0)) * 0.25 * amp;
  float k = 1.0 - exp(-uDt / uInertia);
  vec3 v = vf + mix(offOld, vs + vc, k);
  // \u8D34\u58C1\uFF1A\u79BB\u58C1 < 3 \u4E2A wallCm \u65F6\u53BB\u6389\u6307\u5411\u58C1\u7684\u5206\u91CF\uFF08\u4E0D\u7136\u5E26\u7740\u60EF\u6027\u9876\u7740\u58C1\uFF09
  float d = dw;
  if (d < uWallCm * 3.0){
    vec3 g = gradDist(pos);
    float gl = length(g);
    if (gl > 1e-4){
      g /= gl;
      float vn = dot(v, g);
      if (vn < 0.0) v -= vn * g * (1.0 - smoothstep(uWallCm, uWallCm * 3.0, d));
    }
  }
  gl_FragColor = vec4(v - vf, 0.0);
}`,DRAW_GLSL=COMPUTE_COMMON+`
void main(){
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 p = texture2D(tPos, uv);
  if (p.w < -0.5){ gl_FragColor = vec4(0.0); return; }
  gl_FragColor = vec4(fieldAt(p.xyz) + texture2D(tVel, uv).xyz, 0.0);
}`,POS_GLSL=COMPUTE_COMMON+`
uniform sampler2D uInlet, uOutlet;
uniform float uNInlet, uNOutlet, uVolRemain, uStuckSec;   // uVolRemain\uFF1A\u5FC3\u8154 V_max \u2212 V(t)\uFF08mL\uFF09\uFF0C\u8840\u7BA1 0
// \u771F\u6DB2\u4F53\uFF08C\uFF0C\u7B2C\u56DB\u8F6E\uFF09\uFF1A\u5404\u7AEF\u5E3D\u8FD9\u4E00\u5E27\u7684\u6D41\u91CF \u03A6\uFF08\u6C42\u89E3\u5668 tCapVel \u7684 w\uFF0C\u8FDB\u672C\u533A\u4E3A\u6B63\uFF0C16 = FLUID_MAX_CAPS\uFF09\uFF0CuCapSide +1 \u5165\u53E3\u4FA7 / \u22121 \u51FA\u53E3\u4FA7\u3002
// \u5F00\u7740\u65F6\u653E\u51FA\u3001\u51FA\u533A\u3001\u505C\u6EDE\u90FD\u6309\u5B83\uFF0C\u4E0D\u6309\u6D41\u91CF\u8868\uFF08uQa / uQb \u53EA\u7ED9 A \u7528\uFF09
uniform sampler2D uCapFlux;
uniform float uCapSide[16];
uniform int uNCaps;
// \u51FA\u58C1\u65F6\u79BB\u6B63\u5728\u51FA\u6D41\u7684\u7AEF\u53E3\u8FD1\u7684\u7B97\u51FA\u533A\uFF0C\u522B\u5904\u7B97\u6F0F\u58C1\u9000\u56DE
bool nearOpenPort(vec3 pos, float qOut, float qIn){
  for (int i = 0; i < 8; i++){
    if (i >= uNPorts) break;
    vec4 pt = uPorts[i];
    bool outPort = pt.w > 0.0;
    if ((outPort && qOut > 0.0) || (!outPort && qIn < 0.0)){
      if (distance(pos, pt.xyz) < abs(pt.w) * 1.8 + 0.6) return true;
    }
  }
  return false;
}
vec3 spawnAt(sampler2D tex, float n, float r){ float i = floor(r * n); return texture2D(tex, vec2((i + 0.5) / n, 0.5)).xyz; }
void main(){
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 p = texture2D(tPos, uv);
  float r1 = hash(uv + uTime), r2 = hash(uv * 1.73 + uTime * 0.37), r3 = hash(uv * 2.31 + uTime * 0.53);
  // \u4E24\u4E2A\u7AEF\u53E3\u7684\u5165\u6D41\uFF08\u22650\uFF09\uFF1A\u5165\u53E3 Q_a\u207A\uFF1B\u51FA\u53E3\u9006\u6D41\u8FDB\u6765 \u2014\u2014 \u5FC3\u8154 (\u2212Q_b)\u207A\uFF0C\u8840\u7BA1 (\u2212Q_a)\u207A\u3002qIn = \u5165\u53E3\u4FA7\u51C0\u6D41\u5165\u3001qOut = \u51FA\u53E3\u4FA7\u51C0\u6D41\u51FA
  float qIn = uQa, qOut = (uHasB > 0.5) ? uQb : uQa;
  float inA = max(uQa, 0.0);
  float inB = (uHasB > 0.5) ? max(-uQb, 0.0) : max(-uQa, 0.0);
  float qStuck = abs(uQa) + abs(uQb);
  if (uFluidOn > 0.5){
    float fa = 0.0, fb = 0.0;
    for (int i = 0; i < 16; i++){
      if (i >= uNCaps) break;
      float phi = texelFetch(uCapFlux, ivec2(i, 0), 0).w;
      if (uCapSide[i] > 0.5) fa += phi; else if (uCapSide[i] < -0.5) fb += phi;
    }
    qIn = fa; qOut = -fb; inA = max(fa, 0.0); inB = max(fb, 0.0); qStuck = abs(fa) + abs(fb);
  }
  if (p.w < -0.5){
    // \u7B49\u5F85\u6C60\uFF1Aw = \u22121 \u2212 R\uFF0CR \u6309\u5165\u6D41\u91CF\u9012\u51CF\uFF0C\u5230 0 \u5728\u6709\u5165\u6D41\u7684\u7AEF\u53E3\u653E\u51FA
    float R = -p.w - 1.0 - (inA + inB) * uDt;
    if (R <= 0.0 && inA + inB > 0.0){
      bool atInlet = r1 * (inA + inB) < inA;
      vec3 s = atInlet ? spawnAt(uInlet, uNInlet, r2) : spawnAt(uOutlet, uNOutlet, r2);
      s += (vec3(r1, r2, r3) - 0.5) * uH;
      vec3 s2 = s + fieldAt(s) * uDt * r3;   // \u8FD9\u4E00\u5E27\u91CC\u7684\u968F\u673A\u51FA\u751F\u65F6\u523B\uFF1A\u653E\u51FA\u7684\u9897\u7C92\u6CBF\u6D41\u5411\u644A\u5F00\uFF0C\u4E0D\u6392\u6210\u4E00\u6392\uFF08\u51B2\u51FA\u58C1\u7684\u5C31\u7559\u5728\u539F\u5730\uFF09
      gl_FragColor = vec4(insideAt(s2) > 0.1 ? s2 : s, 0.0);
    } else gl_FragColor = vec4(p.xyz, -1.0 - max(R, 0.0));
    return;
  }
  float stuck = p.w;
  vec3 pos = p.xyz;
  vec3 vOff = texture2D(tVel, uv).xyz;       // \u9897\u7C92\u76F8\u5BF9\u573A\u7684\u9644\u52A0\u5206\u91CF\uFF08\u6DA1\u65CB / \u6E4D\u52A8\uFF0C\u5E26\u677E\u5F1B\uFF09
  vec3 vf0 = fieldAt(pos);                   // \u5E27\u521D\u7684\u573A\uFF1A\u5B50\u6B65\u91CC\u6309\u573A\u6CBF\u8DEF\u7684\u53D8\u5316\u4FEE\u6B63\uFF08\u51FA\u53E3\u9644\u8FD1\u6D41\u7EBF\u6C47\u805A\uFF0C\u76F4\u7EBF\u8D70\u4F1A\u649E\u5230\u74E3\u65C1\u7684\u58C1\u3001\u8FDB\u4E0D\u4E86\u51FA\u53E3\u5C42\uFF09
  vec3 v = vf0 + vOff;
  float speed = length(v);
  float fieldSpeed = length(vf0);
  // \u5B50\u6B65 \u2264 \u4E00\u4E2A\u4F53\u7D20\uFF0C\u6BCF\u4E2A\u5B50\u6B65\u67E5\u7EC8\u70B9\u548C\u4E2D\u70B9\u662F\u5426\u8FDB\u4E86\u51FA\u53E3\u5C42\uFF08\u987A\u6D41\uFF09/ \u5165\u53E3\u5C42\uFF08\u9006\u6D41\uFF09\uFF1A\u51FA\u53E3\u662F\u5438\u6536\u9762\uFF0C\u9897\u7C92\u4E0D\u80FD\u8D8A\u8FC7\u4E00\u5C42\u539A\u7684\u51FA\u53E3\u5C42
  // \u843D\u5230\u8154\u5916\u518D\u88AB\u58C1\u63A8\u56DE\u6765 \u2014\u2014 \u90A3\u6837\u5C04\u8840\u671F\u53EA\u6709\u7EA6 1/3 \u7684\u9897\u7C92\u771F\u6B63\u51FA\u53BB\uFF0C\u5176\u4F59\u6324\u5728\u4E3B\u52A8\u8109\u74E3\u4E0B
  int n = int(clamp(ceil(1.5 * speed * uDt / uH), 1.0, 32.0));
  float dt = uDt / float(n);
  bool exited = false;
  for (int i = 0; i < 32; i++){
    if (i >= n) break;
    vec3 vs = fieldAt(pos) + vOff;
    vec3 mid = pos + vs * (0.5 * dt);
    pos += vs * dt;
    float cm = insideAt(mid), c = insideAt(pos);
    if ((qOut > 0.0 && ((cm < 0.4 && cm > 0.1) || (c < 0.4 && c > 0.1))) ||
        (qIn < 0.0 && ((cm < 0.7 && cm > 0.4) || (c < 0.7 && c > 0.4)))){ exited = true; break; }
    float d = distAt(pos);
    if (c < 0.1 || d < uWallCm){
      vec3 g = gradDist(pos);
      float gl = length(g);
      if (gl > 1e-4) pos += g / gl * (uWallCm - d + 0.02);
    }
  }
  // \u51FA\u533A \u2192 \u8FDB\u7B49\u5F85\u6C60\uFF0C\u7968 R = V_max \u2212 V(t)\u3002\u5E26\u60EF\u6027 / \u6E4D\u52A8\u540E\u9897\u7C92\u6CBF\u58C1\u6ED1\uFF0C\u58C1\u7684\u51F9\u89D2\u5904\u63A8\u56DE\u4F1A\u5931\u624B\u6F0F\u5230\u8154\u5916\uFF1A\u6F0F\u51FA\u53BB\u7684\u9000\u56DE\u4E0A\u4E00\u5E27\u7684\u4F4D\u7F6E
  // \uFF08\u901F\u5EA6\u901A\u9053\u4E0B\u4E00\u5E27\u4F1A\u53BB\u6389\u6307\u5411\u58C1\u7684\u5206\u91CF\uFF09\uFF0C\u4E0D\u5F53\u51FA\u533A \u2014\u2014 \u5F53\u51FA\u533A\u7684\u8BDD\u6F0F\u6389\u7684\u9897\u7C92\u6512\u7740 R \u7B49\u4E0B\u4E00\u6B21\u5145\u76C8\uFF0C\u8212\u5F20\u672B\u8FD8\u6709\u4E00\u534A\u5728\u7B49
  if (exited){ gl_FragColor = vec4(pos, -1.0 - uVolRemain); return; }
  if (insideAt(pos) < 0.1){
    // \u74E3\u5E73\u9762 / \u65AD\u53E3\u9644\u8FD1\u7A7F\u51FA\u53BB\u7684\u662F\u771F\u51FA\u533A\uFF08\u51FA\u53E3\u76D8\u5916\u5708\u3001\u6D41\u51FA\u9053\u58C1\u65C1\u7684\u9897\u7C92\u4ECE\u76D8\u5C42\u65C1\u8FB9\u8D8A\u8FC7\u5E73\u9762\uFF09\uFF1B\u522B\u5904\u662F\u6F0F\u58C1
    if (nearOpenPort(pos, qOut, qIn)){ gl_FragColor = vec4(pos, -1.0 - uVolRemain); return; }
    pos = p.xyz;
    if (insideAt(pos) < 0.1){ gl_FragColor = vec4(pos, -1.0 - uVolRemain); return; }
  }
  // \u505C\u6EDE\uFF1A\u573A\u6709\u6D41\u5374\u51E0\u4E4E\u4E0D\u52A8\u7684\u6B7B\u89D2\uFF0C\u8D85\u65F6 \u2192 \u7B49\u5F85\u6C60 R = 0\uFF08\u6709\u5165\u6D41\u5C31\u7ACB\u523B\u56DE\u6765\uFF09
  if (fieldSpeed < 0.03 && qStuck > 2.0) stuck += uDt; else stuck = 0.0;
  if (stuck > uStuckSec){ gl_FragColor = vec4(pos, -1.0); return; }
  gl_FragColor = vec4(pos, min(stuck, 9.9));
}`,STREAK_VERT=`
precision highp sampler3D;
attribute vec2 corner;        // x\uFF1A0 \u5C3E / 1 \u5934\uFF1By\uFF1A\xB11 \u4E24\u4FA7
attribute vec2 ref;
uniform sampler2D tPos, tVel;
uniform sampler3D uDef0, uDef1, uDef2, uDef3, uDef4, uDef5, uDef6;
uniform float uW[7];
uniform int uNDef;
uniform vec3 uDefOrigin, uDefSize;
uniform float uWidthCm, uStreakSec, uStreakMin, uStreakMax;
uniform sampler3D uDist;      // \u8DDD\u58C1\u573A\uFF08\u4E09\u7EBF\u6027\uFF1B\u8154\u5916\u4F53\u7D20\u4E3A 0\uFF0C\u6240\u4EE5\u8D34\u58C1\u5904\u7684\u63D2\u503C\u504F\u5C0F \u2014\u2014 \u4F5C\u4E3A"\u79BB\u58C1\u591F\u4E0D\u591F\u8FDC"\u7684\u5224\u636E\u6B63\u597D\u4FDD\u5B88\uFF09
uniform vec3 uOrigin, uSize;
${FLUID_VOL_GLSL}            // \u771F\u6DB2\u4F53\u7684\u5BFC\u51FA\u56FE\u96C6\uFF08r = \u65B0\u8840\u67D3\u6599\uFF09\uFF0C\u5750\u6807 = \u5F62\u53D8\u540E\u5C40\u90E8\uFF08\u9897\u7C92\u7684 head\uFF09
uniform float uDyeOn;
uniform vec3 uDyeOrigin, uDyeSize;
varying vec2 vUv;
varying float vLen, vDye;
#include <clipping_planes_pars_vertex>
float distAt(vec3 p){ return texture(uDist, (p - uOrigin) / uSize).r * 2.5 - 0.5; }   // \u6709\u7B26\u53F7\u8DDD\u58C1\uFF08cm\uFF09
// Basis \u70B9 q \u5728\u5F53\u524D\u5F62\u6001\u952E\u6743\u91CD\u4E0B\u7684\u4F4D\u79FB\uFF08\u4E0E\u7F51\u683C\u540C\u4E00\u5957\u5F62\u53D8\u573A\uFF09
vec3 dispAt(vec3 q){
  vec3 uvw = (q - uDefOrigin) / uDefSize;
  vec3 d = vec3(0.0);
  if (uNDef > 0) d += uW[0] * texture(uDef0, uvw).xyz;
  if (uNDef > 1) d += uW[1] * texture(uDef1, uvw).xyz;
  if (uNDef > 2) d += uW[2] * texture(uDef2, uvw).xyz;
  if (uNDef > 3) d += uW[3] * texture(uDef3, uvw).xyz;
  if (uNDef > 4) d += uW[4] * texture(uDef4, uvw).xyz;
  if (uNDef > 5) d += uW[5] * texture(uDef5, uvw).xyz;
  if (uNDef > 6) d += uW[6] * texture(uDef6, uvw).xyz;
  return d;
}
void main(){
  vDye = -1.0;
  vec4 p = texture2D(tPos, ref);
  if (p.w < -0.5){ gl_Position = vec4(2.0, 2.0, 2.0, 1.0); vUv = vec2(0.0); vLen = 0.0; return; }
  vec3 vel = texture2D(tVel, ref).xyz;
  float sp = length(vel);
  float len = clamp(sp * uStreakSec, uStreakMin, uStreakMax);
  vec3 dir = sp > 1e-4 ? vel / sp : vec3(0.0, 1.0, 0.0);
  // \u80F6\u56CA\u6574\u6761\u90FD\u5F97\u79BB\u58C1\u81F3\u5C11\u534A\u5BBD\uFF1A\u5934\u5728\u9897\u7C92\u4F4D\u7F6E\uFF1B\u5C3E\u7AEF\u548C\u4E2D\u70B9\u6309\u8DDD\u58C1\u573A\u68C0\u67E5\uFF0C\u4E0D\u591F\u5C31\u5BF9\u534A\u7F29\uFF0C\u7F29\u5230\u5E95\u8FD8\u4E0D\u884C\u5C31\u7F29\u6210\u5706\u70B9\u3002
  // \u66FE\u53EA\u6309\u91CC\u5916\u7EB9\u7406\uFF08\u6700\u8FD1\u4F53\u7D20\uFF09\u67E5\u5C3E\u7AEF\uFF1A\u79BB\u58C1 0.07 cm \u4EE5\u5185\u7684\u8154\u5916\u70B9\u4E5F\u7B97"\u91CC\u9762"\uFF0C\u8D34\u7740\u5BF9\u4FA7\u58C1\u7684\u6D41\u7EBF\u4ECE\u58C1\u91CC\u900F\u51FA\u4E00\u622A
  //\uFF082026-10-02 \u5B8C\u6574\u5916\u89C2\u540E\u9762\u89C2\u4E3B\u52A8\u8109\u5F13\u4E00\u7247"\u6D6E"\u5728\u58C1\u4E0A\u7684\u77ED\u5212\uFF1B\u7EFF\u7403\u653E\u5230\u540C\u4E00\u4F4D\u7F6E\u88AB\u58C1\u6B63\u5E38\u6321\u4F4F\uFF09
  float wB = uWidthCm * 0.5;
  float need = wB + 0.04;        // \u534A\u5BBD\u4E4B\u5916\u518D\u7559 0.4 mm\uFF1A\u5F62\u6001\u952E\u4F4D\u79FB\u573A\u4E0E\u58C1\u9876\u70B9\u7684\u8BEF\u5DEE\uFF08P90 \u2264 0.03 cm\uFF09\u5728\u63A0\u5C04\u89D2\u4E0B\u4E5F\u4F1A\u653E\u5927\u6210\u4E00\u6761\u77ED\u5212
  if (distAt(p.xyz) < need){ gl_Position = vec4(2.0, 2.0, 2.0, 1.0); vUv = vec2(0.0); vLen = 0.0; return; }   // \u8D34\u58C1\u5230\u653E\u4E0D\u4E0B\uFF1A\u4E0D\u753B\uFF08\u6A21\u62DF\u628A\u9897\u7C92\u63A8\u5230 \u2265 wallCm 0.08\uFF0C\u53EA\u6709\u8FC7\u6E21\u5E27\u4F1A\u78B0\u5230\uFF09
  for (int i = 0; i < 6; i++){
    float L = len + 2.0 * wB;
    if (distAt(p.xyz - dir * L) < need || distAt(p.xyz - dir * (0.5 * L)) < need) len *= 0.5; else break;
  }
  if (distAt(p.xyz - dir * (len + 2.0 * wB)) < need) len = 0.0;
  // \u5C3E\u5DF4\u6309\u5B83\u81EA\u5DF1\u90A3\u4E00\u70B9\u7684\u4F4D\u79FB\u8D70\uFF0C\u4E0D\u662F\u628A\u5934\u7684\u4F4D\u79FB\u5E73\u79FB\u8FC7\u53BB\uFF1A\u79BB\u58C1\u68C0\u67E5\u662F\u5728 Basis \u91CC\u6CBF Basis \u65B9\u5411\u505A\u7684\uFF0C\u4E34\u5E8A\u72B6\u6001\u7F29\u653E / \u5FC3\u5BA4\u6536\u7F29\u8BA9\u5404\u5904\u4F4D\u79FB\u4E0D\u4E00\u6837\uFF0C
  // \u5E73\u79FB\u8FC7\u53BB\u7684\u5C3E\u5DF4\u4F1A\u4F38\u51FA\u58C1 \u2014\u2014 \u5FC3\u8870\u3001\u4E8C\u72ED\u3001\u4E8C\u95ED\u65F6\u5927\u8840\u7BA1 5\u201314% \u7684\u6D41\u7EBF\u5C3E\u5DF4\u6216\u8FB9\u51FA\u58C1\u3001\u6700\u6DF1 0.5 cm\uFF082026-10-06\uFF0Ctemp/polish-1006 __leakTails\uFF09
  vec3 tailB = p.xyz - dir * len;
  vec3 head = p.xyz + dispAt(p.xyz), tail = tailB + dispAt(tailB);
  if (uDyeOn > 0.5) vDye = volAt(clamp((head - uDyeOrigin) / uDyeSize, 0.0, 1.0)).r;
  vec3 hv = (modelViewMatrix * vec4(head, 1.0)).xyz, tv = (modelViewMatrix * vec4(tail, 1.0)).xyz;
  vec3 along = hv - tv;
  float alen = length(along);
  vec3 ad = alen > 1e-5 ? along / alen : vec3(1.0, 0.0, 0.0);
  vec3 side = cross(ad, normalize(hv));
  float sl = length(side);
  side = sl > 1e-4 ? side / sl : vec3(0.0, 1.0, 0.0);
  float w = uWidthCm * length(vec3(modelViewMatrix[0])) * 0.5;
  // \u6574\u6761\u80F6\u56CA\u5F80\u540E\u9000\u534A\u5BBD\uFF1A\u5706\u5934\u7684\u9876\u70B9\u6B63\u597D\u843D\u5728\u9897\u7C92\u4F4D\u7F6E\uFF0C\u4E0D\u4F38\u51FA\u58C1\u5916\uFF08\u9897\u7C92\u79BB\u58C1 \u2265 wallCm\uFF0C\u4F46\u5706\u5934\u591A\u51FA\u7684\u534A\u5BBD\u4F1A\u4ECE\u65AD\u53E3 / \u8F6E\u5ED3\u8FB9\u900F\u51FA\u6765\uFF09
  hv -= ad * w; tv -= ad * w;
  vec3 pv = mix(tv, hv, corner.x) + side * (w * corner.y) + ad * (w * (corner.x * 2.0 - 1.0));
  vec4 mvPosition = vec4(pv, 1.0);
  gl_Position = projectionMatrix * mvPosition;
  vUv = corner;
  vLen = alen / w;
  #include <clipping_planes_vertex>
}`,STREAK_FRAG=`
uniform vec3 uColor, uColorFresh, uColorOld;
uniform vec3 uLight;
varying vec2 vUv;
varying float vLen, vDye;
#include <clipping_planes_pars_fragment>
void main(){
  #include <clipping_planes_fragment>
  // \u80F6\u56CA SDF\uFF1A\u6CBF\u7EBF\u5750\u6807 a\uFF08\u5355\u4F4D = \u534A\u5BBD\uFF09\uFF0C\u7EBF\u6BB5 [0, vLen]\uFF0C\u4E24\u7AEF\u5706\u5934
  float a = vUv.x * (vLen + 2.0) - 1.0;
  float da = a - clamp(a, 0.0, vLen);
  float r2 = da * da + vUv.y * vUv.y;
  if (r2 > 1.0) discard;
  // \u5706\u622A\u9762\u7684\u6CD5\u7EBF\uFF1A\u4E24\u4FA7\u5F80\u91CC\u8F6C\u3001\u5706\u5934\u5F80\u7AEF\u70B9\u8F6C
  float z = sqrt(1.0 - r2);
  vec3 n = normalize(vec3(da * 0.6, vUv.y, z));
  float diff = max(0.0, dot(n, uLight));
  float spec = pow(max(0.0, dot(normalize(uLight + vec3(0.0, 0.0, 1.0)), n)), 24.0);
  float headness = clamp(a / max(vLen, 1e-3), 0.0, 1.0);     // 0 \u5C3E \u2192 1 \u5934
  vec3 base = vDye < 0.0 ? uColor : mix(uColorOld, uColorFresh, clamp(vDye, 0.0, 1.0));
  vec3 col = base * (0.28 + 0.75 * diff) * (0.55 + 0.45 * headness) + vec3(spec * 0.25 * headness);
  gl_FragColor = vec4(col, 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}`;function _insideCentroid(F){const[e,o,n]=F.insideSection.shape,f=F.insideSection.view,d=F.origin,r=F.h;let x=0,w=0,S=0,p=0;for(let i=0;i<f.length;i++){if(!f[i])continue;const v=i%e,m=Math.floor(i/e)%o,s=Math.floor(i/(e*o));x+=v,w+=m,S+=s,p++}return new THREE.Vector3(d[0]+(x/p+.5)*r,d[1]+(w/p+.5)*r,d[2]+(S/p+.5)*r)}class RegionParticles{constructor(e,o,n,f,d){this.region=e,this.W=Math.max(8,Math.ceil(Math.sqrt(o))),this.count=this.W*this.W;const r=new f(this.W,this.W,n),x=r.createTexture(),w=r.createTexture(),{positions:S}=_sampleInside(e.insideSection,e.origin,e.h,this.count);x.image.data.set(S);const p=!!e.flowTex.fill,i=p?e.flowTex.fill:e.flowTex.flow,v=p?e.flowTex.eject:e.flowTex.flow,m=e.meta.fields,s=p?m.fill.umax:m.flow.umax,D=p?m.eject.umax:0;this.velVar=r.addVariable("tVel",VEL_GLSL,w),this.posVar=r.addVariable("tPos",POS_GLSL,x),this.drawVar=r.addVariable("tDraw",DRAW_GLSL,r.createTexture()),r.setVariableDependencies(this.velVar,[this.posVar,this.velVar]),r.setVariableDependencies(this.posVar,[this.posVar,this.velVar]),r.setVariableDependencies(this.drawVar,[this.posVar,this.velVar]),this.swirl={axis:new THREE.Vector3(0,1,0),center:_insideCentroid(e),omega:0,gain:0},this.swirlU=new THREE.Vector4(0,1,0,0),this.swirlC=this.swirl.center;const c=[];for(const[u,E]of[["outlets",1],["inlets",-1]])for(const L of e.meta.bounds[u]||[])c.length<8&&c.push(new THREE.Vector4(L.center[0],L.center[1],L.center[2],E*Math.max(L.radius||.5,.3)));const l=c.length;for(;c.length<8;)c.push(new THREE.Vector4(0,0,0,0));const C=()=>({uFlowA:{value:i},uFlowB:{value:v},uDist:{value:e.dist},uInside:{value:e.inside},uOrigin:{value:new THREE.Vector3(...e.origin)},uSize:{value:new THREE.Vector3(...e.size)},uUmaxA:{value:s},uUmaxB:{value:D},uHasB:{value:p?1:0},uQa:{value:0},uQb:{value:0},uDt:{value:0},uTime:{value:0},uH:{value:e.h},uVolScale:{value:1},uJetA:{value:new THREE.Vector4},uJetAN:{value:new THREE.Vector4},uJetB:{value:new THREE.Vector4},uJetBN:{value:new THREE.Vector4},uJetLen:{value:BLOOD.jetLenCm},uWallCm:{value:BLOOD.wallCm},uPorts:{value:c},uNPorts:{value:l},uFluidVel:{value:null},uFluidOn:{value:0},uFluidDt:{value:0},uFH:{value:1},uFDims:{value:new THREE.Vector3(1,1,1)},uFCols:{value:1},uFOrigin:{value:new THREE.Vector3},uDefOrigin:{value:new THREE.Vector3(...e.meta.deform.origin)},uDefSize:{value:new THREE.Vector3(...e.meta.deform.dims.map(u=>u*e.meta.deform.h))},uDispNow:{value:e.dist},uDispPrev:{value:e.dist},uDispScaleNow:{value:0},uDispScalePrev:{value:0}});Object.assign(this.velVar.material.uniforms,C(),{uSwirl:{value:this.swirlU},uSwirlC:{value:this.swirlC},uSwirlMax:{value:BLOOD.swirlMaxCm},uInertia:{value:BLOOD.inertiaSec},uCurlAmp:{value:BLOOD.curlAmp},uCurlFloor:{value:p?BLOOD.curlFloor:BLOOD.curlFloor*.5},uCurlScale:{value:BLOOD.curlScale}}),Object.assign(this.drawVar.material.uniforms,C()),Object.assign(this.posVar.material.uniforms,C(),{uInlet:{value:e.inlet.tex},uOutlet:{value:e.outlet.tex},uNInlet:{value:e.inlet.n},uNOutlet:{value:e.outlet.n},uVolRemain:{value:0},uStuckSec:{value:BLOOD.stuckSec},uCapFlux:{value:null},uCapSide:{value:new Float32Array(16)},uNCaps:{value:0}});const _=r.init();if(_)throw new Error(`GPGPU ${e.name}: ${_}`);this.gpu=r,this.chamber=p;const O=u=>{const E=(e.meta.bounds[u]||[]).find(L=>L.kind==="valve");return E?{valve:E.valve,c:new THREE.Vector3(...E.center),n:new THREE.Vector3(...E.axis).normalize(),r:E.radius}:null};this.portA=O("inlets"),this.portB=O("outlets"),this.jetA=null,this.jetB=null,p&&this._setupSwirl(e);const z=new THREE.InstancedBufferGeometry;z.setAttribute("position",new THREE.Float32BufferAttribute([0,0,0,0,0,0,0,0,0,0,0,0],3)),z.setAttribute("corner",new THREE.Float32BufferAttribute([0,-1,1,-1,1,1,0,1],2)),z.setIndex([0,1,2,0,2,3]);const y=new Float32Array(this.count*2);for(let u=0;u<this.count;u++)y[2*u]=(u%this.W+.5)/this.W,y[2*u+1]=(Math.floor(u/this.W)+.5)/this.W;z.setAttribute("ref",new THREE.InstancedBufferAttribute(y,2)),z.instanceCount=this.count;const R=e.meta.deform.keys.filter(u=>e.deform[u]);this.keys=R;const P={};for(let u=0;u<7;u++)P["uDef"+u]={value:e.deform[R[u]]||e.deform[R[0]]||e.dist};this.mat=new THREE.ShaderMaterial({uniforms:{tPos:{value:null},tVel:{value:null},...P,uW:{value:new Float32Array(7)},uNDef:{value:R.length},uDefOrigin:{value:new THREE.Vector3(...e.meta.deform.origin)},uDefSize:{value:new THREE.Vector3(...e.meta.deform.dims.map(u=>u*e.meta.deform.h))},uWidthCm:{value:BLOOD.streakWidthCm},uStreakSec:{value:BLOOD.streakSec},uStreakMin:{value:BLOOD.streakMinCm},uStreakMax:{value:BLOOD.streakMaxCm},uDist:{value:e.dist},uOrigin:{value:new THREE.Vector3(...e.origin)},uSize:{value:new THREE.Vector3(...e.size)},uColor:{value:new THREE.Color(d)},uLight:{value:new THREE.Vector3(.35,.6,.72).normalize()},uVol:{value:null},uVolDims:{value:new THREE.Vector3(1,1,1)},uVolAtlas:{value:new THREE.Vector2(1,1)},uVolCols:{value:1},uDyeOn:{value:0},uDyeOrigin:{value:new THREE.Vector3},uDyeSize:{value:new THREE.Vector3(1,1,1)},uColorFresh:{value:new THREE.Color(BLOOD.dyeColors[BLOOD.arterial.includes(e.name)?"art":"ven"][0])},uColorOld:{value:new THREE.Color(BLOOD.dyeColors[BLOOD.arterial.includes(e.name)?"art":"ven"][1])}},vertexShader:STREAK_VERT,fragmentShader:STREAK_FRAG,transparent:!1,depthWrite:!0,clipping:!0,side:THREE.DoubleSide}),this.points=new THREE.Mesh(z,this.mat),this.points.name=e.name+"_blood",this.points.frustumCulled=!1,this.points.raycast=()=>{},this.points.castShadow=!1,this.points.renderOrder=2}_setupSwirl(e){const o=p=>{const i=new THREE.Vector3;for(const v of p)i.add(new THREE.Vector3(...v.center));return p.length?i.divideScalar(p.length):null},n=o(e.meta.bounds.inlets||[]),f=o(e.meta.bounds.outlets||[]);if(!n||!f)return;const d=this.swirl.center,r=n.clone().sub(d),x=f.clone().sub(d),w=new THREE.Vector3().crossVectors(r,x);if(w.lengthSq()<1e-6)return;w.normalize(),new THREE.Vector3().crossVectors(w,r).dot(x)>0&&w.negate(),this.swirl.axis.copy(w);const S=/LV|RV/.test(e.name);this.swirl.gain=S?BLOOD.swirlGain.ventricle:BLOOD.swirlGain.atrium,this.swirlU.set(w.x,w.y,w.z,0)}setState(e){const o=e&&e.valveMod||{},n=this.region,f=s=>{const D=s&&o[s];return D&&D.open!=null?`${s}_open`:null},d=(s,D)=>{const c=f(D),l=c&&n.flowTex[`${s}@${c}`]?`${s}@${c}`:s;return[n.flowTex[l],n.meta.fields[l].umax,l]},r=f(this.portA&&this.portA.valve),x=f(this.portB&&this.portB.valve),w=r&&n.inlets[r]||n.inlet,S=x&&n.outlets[x]||n.outlet,p=this.chamber?d("fill",this.portA&&this.portA.valve):d("flow",this.portA&&this.portA.valve),i=this.chamber?d("eject",this.portB&&this.portB.valve):p;for(const s of[this.velVar.material.uniforms,this.posVar.material.uniforms])s.uFlowA.value=p[0],s.uUmaxA.value=p[1],s.uFlowB.value=i[0],s.uUmaxB.value=i[1];const v=this.posVar.material.uniforms;v.uInlet.value=w.tex,v.uNInlet.value=w.n,v.uOutlet.value=S.tex,v.uNOutlet.value=S.n;const m=(s,D,c)=>{const l=s&&o[s.valve],C=l&&l[c];if(!s||C==null||C>=.98)return null;const _=Math.min(s.r,1.4),O=_*Math.sqrt(C);return{c:s.c,n:s.n.clone().multiplyScalar(D),rT:O,area:Math.PI*O*O}};this.jetA=m(this.portA,1,"open"),this.jetB=m(this.portB,-1,"leak");for(const s of[this.velVar.material.uniforms,this.posVar.material.uniforms,this.drawVar.material.uniforms]){const D=(c,l,C)=>{c?(l.value.set(c.c.x,c.c.y,c.c.z,c.rT),C.value.set(c.n.x,c.n.y,c.n.z,0)):(l.value.set(0,0,0,0),C.value.set(0,0,0,0))};D(this.jetA,s.uJetA,s.uJetAN),D(this.jetB,s.uJetB,s.uJetBN)}}setFluid(e){this.fluid=e;for(const f of[this.velVar.material.uniforms,this.posVar.material.uniforms,this.drawVar.material.uniforms])f.uFluidOn.value=e?1:0,e&&(f.uFDims.value.set(...e.dims),f.uFCols.value=e.layout.cols,f.uFOrigin.value.set(...e.origin),f.uFH.value=e.h);const o=this.posVar.material.uniforms;o.uNCaps.value=e?e.caps.length:0,e&&(o.uCapFlux.value=e.tCapVel.texture,o.uCapSide.value.fill(0),e.caps.forEach((f,d)=>{o.uCapSide.value[d]=f.side==="inlets"?1:-1}));const n=this.mat.uniforms;n.uDyeOn.value=e?1:0,e&&(e.bindVol(n),n.uDyeOrigin.value.set(...e.origin),n.uDyeSize.value.set(...e.size))}update(e,o,n,f,d,r,x){if(this.fluid){const i=this.fluid;for(const v of[this.velVar.material.uniforms,this.posVar.material.uniforms,this.drawVar.material.uniforms])v.uFluidVel.value=i.tVel.texture,v.uFluidDt.value=i.dtFrame,v.uDispNow.value=i.dispNowTex,v.uDispPrev.value=i.dispPrevTex,v.uDispScaleNow.value=i.dispScaleNow,v.uDispScalePrev.value=i.dispScalePrev;this.swirl.omega=0,this.swirlU.w=0}if(this.chamber&&this.swirl.gain>0&&!this.fluid){const i=Math.max(o,0)+Math.max(-n,0),v=this.swirl.gain*i/Math.max(this.region.meta.volume_ml,1),m=this.swirl,s=v>m.omega?BLOOD.swirlUp:n>5?BLOOD.swirlBrake:BLOOD.swirlDown;e>0&&(m.omega+=(v-m.omega)*(1-Math.exp(-e/s))),this.swirlU.w=m.omega}const w=this.jetA&&o>0?o*d/this.jetA.area:0,S=this.jetB&&this.chamber&&n<0?-n*d/this.jetB.area:0;for(const i of[this.velVar.material.uniforms,this.posVar.material.uniforms,this.drawVar.material.uniforms])i.uQa.value=o,i.uQb.value=n,i.uVolScale.value=d,i.uDt.value=e,i.uTime.value=r%1e3,i.uJetAN.value.w=w,i.uJetBN.value.w=S;this.posVar.material.uniforms.uVolRemain.value=f,this.gpu.compute(),this.mat.uniforms.tPos.value=this.gpu.getCurrentRenderTarget(this.posVar).texture,this.mat.uniforms.tVel.value=this.gpu.getCurrentRenderTarget(this.drawVar).texture;const p=this.mat.uniforms.uW.value;for(let i=0;i<7;i++)p[i]=i<this.keys.length&&x[this.keys[i]]||0}dispose(){this.gpu.dispose(),this.points.geometry.dispose(),this.mat.dispose()}}async function buildBlood(F,e,o={}){const n=!!(window.GFX&&window.GFX.tsl),f=n?typeof RegionParticlesTSL=="function"?RegionParticlesTSL:null:RegionParticles,d=n?null:window.GPUComputationRenderer;if(!n&&!d)throw new Error("window.GPUComputationRenderer \u672A\u6302\u8F7D\uFF08src/boot.js\uFF09");f||console.warn("[blood] TSL \u8DEF\u5F84\u7684\u9897\u7C92\u5C1A\u672A\u5B9E\u73B0\uFF08plan/webgpu B3\uFF09\uFF1A\u53EA\u753B\u8840\u58F3\u4E0E\u51A0\u8109\u9897\u7C92");const r=await loadFlowData(o.base||"assets/flow",o.onProgress),x=F.userData.parts,w=x.chambers.userData.cavities,S=o.count||BLOOD.count,p=Object.values(r.regions).reduce((t,a)=>t+a.meta.volume_ml,0),i={};for(const[t,a]of Object.entries(r.regions)){const h=BLOOD.chambers[t],g=h?w[h]:x.vessels.children.find(k=>k.name===t);if(!g){console.warn("\u8840\u6DB2\uFF1A\u627E\u4E0D\u5230\u7F51\u683C",t);continue}const A=Math.max(2e3,Math.round(S*a.meta.volume_ml/p)),V=BLOOD.arterial.includes(t)?BLOOD.colors.art:BLOOD.colors.ven;if(!f)continue;const b=new f(a,A,e,d,V);b.mesh=g,b.points.position.copy(g.position),b.points.quaternion.copy(g.quaternion),b.points.scale.copy(g.scale),(h?g.parent:x.vessels).add(b.points),i[t]=b}const v=buildCoronaryBlood(r.manifest,x.coronaries,{sizeCm:BLOOD.coronarySizeCm,minR:BLOOD.coronaryMinRCm}),m=buildBloodShells(F,e,r,i);let s=e.domElement.height;const D=t=>{const a={},h=t.morphTargetDictionary,g=t.morphTargetInfluences;if(h&&g)for(const[A,V]of Object.entries(h))a[A]=g[V];return a};let c=0,l=null,C=null,_=!1;const O={},z=()=>!!(l&&l.enabled&&(u==="transp"||u==="cor"||u==="full"&&E));if(o.fluid&&!n&&typeof FluidSystem=="function")try{l=new FluidSystem(r,e,{substeps:o.fluidSub,cycles:o.fluidCycles,vortEps:o.fluidVort,maccormack:o.fluidMac,chambers:o.fluidChambers});for(const[t,a]of Object.entries(l.solvers))i[t]&&i[t].setFluid(a);m.setFluid(l),console.info("[fluid] \u6C42\u89E3\u5668",Object.fromEntries(Object.entries(l.solvers).map(([t,a])=>[t,a.dims.join("\xD7")+"\uFF0C\u7AEF\u5E3D "+a.caps.length])))}catch(t){console.error("[fluid] \u5EFA\u6C42\u89E3\u5668\u5931\u8D25",t),l&&l.dispose(),l=null}let y=null,R=!1,P=null,u="full",E=!0;const L=F.userData.clipGroup||F,N=()=>{for(const[t,a]of Object.entries(i)){const h=!BLOOD.chambers[t],g=u==="transp"||u==="cor"||u==="full"&&E&&h;a.points.visible=g&&!(R&&FLIP_REGIONS.includes(t))}y&&(y.points.visible=R&&(u==="transp"||u==="cor"))};return{data:r,systems:i,coronary:v,shells:m,BLOOD,get flip(){return y},get fluid(){return l},setFluidOn(t){if(!l)return!1;l.enabled=!!t;for(const[a,h]of Object.entries(l.solvers))i[a]&&i[a].setFluid(l.enabled?h:null);return m.setFluid(l.enabled?l:null),l.setExport(z()),v.setVisible((u==="transp"||u==="cor")&&!l.enabled),_=!1,!0},async setFlip(t){R=!!t,R&&!y&&(P||(P=loadFlipReplay(o.flip||"assets/flow/flip_normal.bin").then(a=>(y=new FlipReplay(a,L,BLOOD.colors.art,BLOOD.coronarySizeCm*1.3),y)).catch(a=>{console.error("[blood] \u771F\u6D41\u4F53\u56DE\u653E\u8F7D\u5165\u5931\u8D25",a),R=!1,P=null})),await P),N()},update(t,a,h){if(!h)return;c+=a,s=e.domElement.height,v.update(t,a,h,s),y&&y.points.visible&&y.show(t,s);const g=z();if(g&&!_){l.reset();for(const A of Object.keys(O))delete O[A]}if(_=g,g){const A=C||STATES.normal,V={};for(const T of Object.keys(l.solvers))i[T]&&(V[T]=D(i[T].mesh));const b=F.userData.parts.valves&&F.userData.parts.valves.userData.valves,k=T=>{const M=b&&b[T]&&b[T].userData.leaflets&&b[T].userData.leaflets[0];return M&&M.userData.open!=null?M.userData.open:valveOpenAt(t,A,T)};l.step(t,a,A,h,V,k)}for(const[A,V]of Object.entries(i)){if(!V.points.visible)continue;const[b,k]=BLOOD.flows[A],T=flowAt(h,b,t),M=k?flowAt(h,k,t):0,B=BLOOD.chambers[A],I=B?volumeAt(h,B,t):0,W=B?Math.max(0,h.Vmax[B]-I):0,J=B?V.region.meta.volume_ml/Math.max(I,1):1;if(V.fluid&&_){let U=0;if(B){const j=fluidMeshVolume(V.mesh,D(V.mesh)),H=O[A]=O[A]||[];for(H.push([c,j]);H.length&&H[0][0]<c-1.6;)H.shift();U=Math.max(0,Math.max(...H.map(q=>q[1]))-j)}V.update(a,0,0,U,1,c,D(V.mesh))}else V.update(a,T,M,W,J,c,D(V.mesh))}m.update(h,t,c)},setState(t){C=t;for(const a of Object.values(i))a.setState(t)},setMode(t,a){u=t,E=a,N(),v.setVisible((t==="transp"||t==="cor")&&!(l&&l.enabled)),m.setMode(t,a),l&&(l.setExport(z()),l.setFluxOnly(h=>t==="full"&&!!BLOOD.chambers[h]))},setClear(t){m.setClear(t)},preRender(t,a){m.preRender(t,a)},attachSection(t){m.attachSection(t)},setCapDepth(t){m.setCapDepth(t)},setClipping(t){for(const a of Object.values(i))a.mat.clippingPlanes=t||null,a.mat.needsUpdate=!0;y&&(y.mat.clippingPlanes=t||null,y.mat.needsUpdate=!0),v.setClipping(t),m.setClipping(t)},dispose(){for(const t of Object.values(i))t.points.parent&&t.points.parent.remove(t.points),t.dispose();y&&y.dispose(),l&&l.dispose(),v.dispose(),m.dispose()}}}Object.assign(window,{buildBlood,BLOOD});
