diff --git a/docs/plant-operation.html b/docs/plant-operation.html index 1485422..7276992 100644 --- a/docs/plant-operation.html +++ b/docs/plant-operation.html @@ -38,10 +38,15 @@ main{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content: svg{width:100%;height:100%;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent} svg.zoomed{cursor:grab} svg.dragging{cursor:grabbing} -.mapctl{position:absolute;right:12px;bottom:8px;display:flex;gap:2px;z-index:2} +/* motion layers: pipes and flow under the drawing, tokens over it (see "effects" in the script) */ +.fx{position:absolute;left:0;top:0;pointer-events:none} +#fxUnder{z-index:0} +svg{position:relative;z-index:1} +#fxOver{z-index:2} +.mapctl{position:absolute;right:12px;bottom:8px;display:flex;gap:2px;z-index:3} .mapctl button{background:rgba(24,34,56,.88);min-width:30px} #btnInfo,footer .tabs{display:none} -#hint{display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:6px 10px;color:var(--mute);font-size:12px;white-space:nowrap;z-index:2} +#hint{display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:6px 10px;color:var(--mute);font-size:12px;white-space:nowrap;z-index:3} footer .tabs button.close{margin-left:auto;min-width:32px} footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr 1fr 0.9fr;gap:10px;padding:8px 14px 10px;background:var(--panel);border-top:1px solid var(--stroke);min-height:0} .panel{background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:8px 12px;overflow:auto;min-height:0} @@ -239,12 +244,14 @@ html[data-theme=light] .screw{stroke:#7d8aa0}
+ +
@@ -298,6 +305,15 @@ const pick=a=>a[Math.floor(Math.random()*a.length)]; const f0=v=>Math.round(v).toString(),f1=v=>v.toFixed(1),f2=v=>v.toFixed(2); // first-order lag, stable for any step size const lag=(v,t,k,ds)=>v+(t-v)*(1-Math.exp(-k*ds)); +// Change-only writes: re-writing an unchanged text or attribute still makes the browser re-lay-out the SVG. +const setT=(e,v)=>{v=String(v);if(e.__t!==v){e.__t=v;e.textContent=v;}}; +const setA=(e,k,v)=>{v=String(v);const c=e.__a||(e.__a={});if(c[k]!==v){c[k]=v;e.setAttribute(k,v);}}; +const setS=(e,k,v)=>{const c=e.__s||(e.__s={});if(c[k]!==v){c[k]=v;e.style[k]=v;}}; +// Everything that moves every frame (pipe flow, tokens, records, screw, bubbles) is drawn on two canvases, +// one under and one over the SVG; the SVG keeps machines and labels. See "effects" further down. +const FX={flows:[],screws:[],bubbles:[]},SPR=[];let fxDirty=true; +function spr(kind,o){const t=Object.assign({kind,x:0,y:0,dead:false,cls:{}},o); + t.remove=()=>{t.dead=true;};t.classList={add:c=>{t.cls[c]=true;}};SPR.push(t);return t;} const MIN=60,HOUR=3600,DAY=86400; function dur(s){s=Math.max(0,s);if(s<100)return f0(s)+' s';if(s<6000)return f0(s/60)+' min';if(s<2*DAY)return f1(s/HOUR)+' h';return f1(s/DAY)+' d';} const PALETTE=['#e0552f','#3aa0ff','#ffbf1f','#3ddc84','#c86bff','#ff7ab6','#59d9c9']; @@ -484,7 +500,7 @@ function pressEnter(P,st){P.state=st;P.tIn=0; if(st==='LOAD'){const T=P.set.T;if(!materialOk(T)){P.state='NOPVC';log(P.name+': no released '+(T.kind==='black'?'black':'colour')+' compound, holding','alarm');return;} P.cakeKg=T.wt/1000+rnd(0.02,0.04);consume(T,P.cakeKg);P.cake=true;P.peakT=0;P.loadD=rnd(4,6.5);S.labels.pairs--; if(S.labels.pairs<=0){S.labels.pairs=1800;S.labels.lot='L-'+(parseInt(S.labels.lot.slice(2))+1);log('Label oven: new pre-dried stack, lot '+S.labels.lot,'info');} - if(vis()){const lb=el('circle',{r:3,fill:T.label,stroke:'#e6ecf7','stroke-width':1},L.tok);mover('l_'+P.id,lb,120,m=>m.node.remove());}} + if(vis()){const lb=spr('dot',{r:3,fill:T.label,stroke:'#e6ecf7',sw:1});mover('l_'+P.id,lb,120,m=>m.node.remove());}} if(st==='STEAM')P.cake=false; } function pressTick(P,ds){ @@ -650,11 +666,9 @@ function drawPress(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y, el('rect',{x:x+8,y:y+38,width:6,height:84,fill:'#2a3a56'},g); el('rect',{x:x+w-14,y:y+38,width:6,height:84,fill:'#2a3a56'},g); const px=x+20,pw=w-40; - m.botPl=el('rect',{x:px,y:y+100,width:pw,height:16,class:'platen'},g); el('rect',{x:px+8,y:y+97,width:pw-16,height:3,fill:'#b9c2cc'},g); - m.topPl=el('rect',{x:px,y:y+38,width:pw,height:16,class:'platen'},g); - m.topSt=el('rect',{x:px+8,y:y+54,width:pw-16,height:3,fill:'#b9c2cc'},g); - m.cake=el('g',{visibility:'hidden'},g);el('circle',{cx:x+w/2,cy:y+90,r:7,fill:'#3d3a33',stroke:'#8a7a55'},m.cake); + // platens, top stamper and cake move every cycle: drawn on the overlay canvas (drawFx), not in the SVG + m.plat={px,pw,yb:y+100,cx:x+w/2,cy:y+90}; m.pY=[y+38,y+80]; m.stateEl=el('text',{x:x+w/2,y:y+138,class:'mtag','text-anchor':'middle',style:'font-weight:700;fill:var(--ink)'},g,'OFF'); m.tagEls=[0,1,2].map(i=>el('text',{x:x+7,y:y+154+i*13,class:'mtag'},g,'')); @@ -671,7 +685,7 @@ function drawVex(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y,w, el('text',{x:x+w-8,y:y+82,class:'small','text-anchor':'end'},g,'cake B ▶'); m.zoneEls=[0,1,2].map(i=>el('rect',{x:x+10+i*40,y:y+88,width:38,height:26,class:'zone',style:'fill:var(--z0)'},g)); m.zoneT=[0,1,2].map(i=>el('text',{x:x+29+i*40,y:y+105,class:'ctxt','text-anchor':'middle'},g,'')); - el('line',{x1:x+10,y1:y+120,x2:x+w-10,y2:y+120,class:'screw'},g); + FX.screws.push({x1:x+10,x2:x+w-10,y:y+120}); m.g=g;g.addEventListener('click',()=>select(m.id));} function drawPppu(m){m.tagY=m.y+126;const g=drawBox(m); m.pos=[0,1,2,3].map(i=>{const px=m.x+7+(i&1)*68,py=m.y+36+(i>>1)*40;const r=el('rect',{x:px,y:py,width:64,height:36,rx:2,class:'pos'},g); @@ -680,7 +694,7 @@ function drawPdpu(m){m.tagY=m.y+126;const g=drawBox(m); m.cells=[0,1].map(i=>{const cx=m.x+7+i*74,cy=m.y+38,cw=70,ch=76;const cg=el('g',{class:'cell'},g); el('rect',{x:cx,y:cy,width:cw,height:ch,rx:2,class:'cellr'},cg); el('rect',{x:cx+3,y:cy+20,width:cw-6,height:ch-23,class:'liquid'},cg); - for(let b=0;b<3;b++)el('circle',{cx:cx+10+b*18,cy:cy+ch-5,r:1.6,class:'bub',style:'animation-delay:'+rnd(0,2.2).toFixed(2)+'s'},cg); + for(let b=0;b<3;b++)FX.bubbles.push({x:cx+10+b*18,y:cy+ch-5,d:rnd(0,2.2),cell:(m.id==='pdpu2'?2:0)+i}); el('rect',{x:cx+cw-11,y:cy+22,width:6,height:50,class:'thk'},cg); const tv=el('rect',{x:cx+cw-11,y:cy+72,width:6,height:0,class:'thkv'},cg); const t=[0,1,2,3].map(k=>el('text',{x:cx+4,y:cy+12+k*15,class:'ctxt'},cg,'')); @@ -697,9 +711,21 @@ const GATES={g1:gate('g1',160,116,'gate 1'),g2:gate('g2',330,116,'gate 2'),g3:ga function flash(id){if(S.quiet)return;GATES[id].flash=2;GATES[id].g.classList.add('ok');} /* pipes and routes */ +// Routes are polylines (M/H/V/L only) measured here, so a token never asks the DOM for a point on a +// path: getPointAtLength after a transform write forces a layout of the whole SVG on every call. const PATHS={}; -function route(id,d,cls){PATHS[id]=el('path',{d,class:'flow '+cls},L.pipes);PATHS[id].len=PATHS[id].getTotalLength();} -function pipe(d,cls,thin){el('path',{d,class:'pipe '+cls},L.pipes);el('path',{d,class:'flow '+cls+(thin?' thin':'')},L.pipes);} +function poly(d){if(/[^MHVL\d\s,.\-]/.test(d))throw new Error('route needs M/H/V/L only: '+d); + const segs=[],re=/([MHVL])\s*(-?[\d.]+)(?:\s*,\s*(-?[\d.]+))?/g;let x=0,y=0,len=0,sx=0,sy=0,m; + while((m=re.exec(d))){const c=m[1],a=+m[2],b=m[3]==null?0:+m[3]; + if(c==='M'){x=sx=a;y=sy=b;continue;}const nx=c==='V'?x:a,ny=c==='H'?y:c==='V'?a:b,l=Math.hypot(nx-x,ny-y); + segs.push({x0:x,y0:y,x1:nx,y1:ny,l0:len,l});len+=l;x=nx;y=ny;} + return {segs,len,sx,sy};} +function ptAt(p,pos){const g0=p.segs;if(!g0.length)return {x:p.sx,y:p.sy}; + for(const g of g0)if(pos<=g.l0+g.l){const k=g.l?Math.max(0,pos-g.l0)/g.l:0;return {x:g.x0+(g.x1-g.x0)*k,y:g.y0+(g.y1-g.y0)*k};} + const g=g0[g0.length-1];return {x:g.x1,y:g.y1};} +function flow(d,cls){const c=cls.split(' ');FX.flows.push({k:'flow',c:c[0],thin:c.includes('thin'),p2:new Path2D(d)});} +function route(id,d,cls){PATHS[id]=poly(d);flow(d,cls);} +function pipe(d,cls,thin){FX.flows.push({k:'pipe',c:cls,p2:new Path2D(d)});flow(d,cls+(thin?' thin':''));} // pipe rack y 616 to 650, drops into each press from below pipe('M100,676 V616 H1074','steam');['456','740','912','1074'].forEach(x=>pipe(`M${x},616 V512`,'steam')); pipe('M1080,621 H112 V676','cond',true); @@ -710,8 +736,8 @@ pipe('M574,676 V636 H444 V512','hyd');pipe('M574,636 H728 V512','hyd'); pipe('M300,676 V644 H2088 V34 H845 V46','chw',true);pipe('M1013,34 V46','chw',true); pipe('M716,676 V650 H2094 V40 H544 V46','di',true);pipe('M1162,40 V46','di',true); // record conveyor and edge-trim collection -el('path',{d:'M370,590 H1772',class:'conv'},L.pipes);el('path',{d:'M370,590 H1772',class:'convd'},L.pipes); -el('path',{d:'M392,574 H1104',class:'flow scrap thin'},L.pipes); +FX.flows.push({k:'conv',p2:new Path2D('M370,590 H1772')},{k:'convd',p2:new Path2D('M370,590 H1772')}); +flow('M392,574 H1104','scrap thin'); // material route('s_v','M47,436 V444 H272 V428','mat thin');route('s_r','M107,436 V444 H272 V428','reg thin');route('s_c','M167,436 V444 H272 V428','col thin'); route('f_vex','M272,316 V308 H554 V366','mat thin');route('f_c1','M272,316 V308 H859 V316','mat thin');route('f_c2','M272,316 V308 H1021 V316','mat thin'); @@ -728,15 +754,10 @@ route('carton','M1685,408 V430 H1483 V452','route thin'); /* ================================================================ tokens */ const movers=[]; -function mover(pid,node,v,onEnd){const p=PATHS[pid];const m={p,node,pos:0,v,onEnd};movers.push(m);place(node,p.getPointAtLength(0));return m;} -function place(node,pt){node.setAttribute('transform',`translate(${pt.x.toFixed(1)},${pt.y.toFixed(1)})`);} -function dot(fill,r){return el('circle',{r:r||2.4,fill},L.tok);} -function shape(kind){const g=el('g',{},L.tok); - if(kind==='L'){el('circle',{r:7,fill:'#1a1408',stroke:'#c9a24a'},g);el('circle',{r:2.4,fill:'#c9a24a'},g);} - else if(kind==='Ls'){el('circle',{r:7,fill:'#d9dee6',stroke:'#fff'},g);el('circle',{r:2.4,fill:'#8a97b0'},g);} - else if(kind==='S'){el('circle',{r:7,fill:'#b9c2cc',stroke:'#e6ecf7'},g);el('circle',{r:4.5,fill:'none',stroke:'#7d8794'},g);el('circle',{r:1.6,fill:'#3a4658'},g);} - else{el('circle',{r:7,fill:'#8e98a6',stroke:'#c8d2e4'},g);el('circle',{r:2.4,fill:'#c8d2e4'},g);} - return g;} +function mover(pid,node,v,onEnd){const p=PATHS[pid];const m={p,node,pos:0,v,onEnd};movers.push(m);place(node,ptAt(p,0));return m;} +function place(node,pt){node.x=pt.x;node.y=pt.y;} +function dot(fill,r){return spr('dot',{r:r||2.4,fill});} +function shape(kind){return spr('shape',{k:kind});} const hops=[]; function ctr(id){const m=M[id];return m?{x:m.x+m.w/2,y:m.y+m.h/2}:null;} function hop(from,to,it){if(!visHop()||!from)return;const a=ctr(from),b=ctr(to);if(!a||!b)return; @@ -744,7 +765,7 @@ function hop(from,to,it){if(!visHop()||!from)return;const a=ctr(from),b=ctr(to); function routeHop(pid,kind){mover(pid,shape(kind),320,m=>m.node.remove());} const RECS=[];const CONV0=370,CONVEND=1772,CONVY=590;const DROPX={pDA:392,pDB:676,pC1:839,pC2:1001}; const STATIONS=[{id:'optical',at:1185,dwell:2.2},{id:'finish',at:1329,dwell:2.5},{id:'pack',at:1483,dwell:3},{id:'ship',at:1672,dwell:0.5}]; -function mkRec(P,tp){const T=P.set.T;const g=el('g',{class:'rec'+(tp?' tp':'')},L.tok);el('circle',{r:9,class:'disc',style:'fill:'+T.vinyl},g);el('circle',{r:3.2,fill:T.label},g); +function mkRec(P,tp){const T=P.set.T;const g=spr('rec',{r:9,lr:3.2,vinyl:T.vinyl,label:T.label});if(tp)g.classList.add('tp'); const r={node:g,T,tp,ph:'trim',dwell:6,x:DROPX[P.id],y:544,pos:0,visited:{},n:P.cyc,pname:P.name};RECS.push(r);place(g,r);return r;} function recTick(ds){ const onC=RECS.filter(r=>r.ph==='conv'&&!r.gone).sort((a,b)=>b.pos-a.pos); @@ -764,7 +785,7 @@ function recStation(st,r){const T=r.T; log('Optical: '+r.pname+' record #'+r.n+' rejected ('+d+'), label punched, to the granulator','alarm'); mover('reject',r.node,90,m=>{S.punched++;S.punchUntil=S.t+8;m.node.remove();mover('p2g',dot('#c2b280',2.6),40,mm=>{mm.node.remove();S.gran.bin+=T.wt/1000;});});return;} S.opticalLast='pass #'+r.n; - if(r.tp||Math.random()<0.02){const g=el('g',{class:'rec'+(r.tp?' tp':'')},L.tok);el('circle',{r:7,class:'disc',style:'fill:'+T.vinyl},g);el('circle',{r:2.4,fill:T.label},g); + if(r.tp||Math.random()<0.02){const g=spr('rec',{r:7,lr:2.4,vinyl:T.vinyl,label:T.label});if(r.tp)g.classList.add('tp'); const tp=r.tp;mover('sample',g,60,m=>{m.node.remove();S.samples++;const sc=jit(98.2,1.2);S.audioLast=f1(sc);if(tp)S.tpToArtist++; log('Audio compare: '+T.sid+(tp?' test pressing':' record #'+r.n)+' score '+f1(sc)+', clicks '+f0(rnd(0,3))+'/side, noise floor OK'+(tp?', to the artist':''),tp?'warn':'ok');}); if(r.tp){r.gone=true;r.node.remove();}} @@ -788,7 +809,7 @@ function matTick(){ if(G.run&&S.t>=G.end){S.silo.r=Math.min(S.silo.rcap,S.silo.r+G.run*0.99);G.total+=G.run;log('Granulator 400: '+f1(G.run)+' kg ground to regrind, to the silo','info'); if(visHop())for(let i=0;i<3;i++)setTimeout(()=>mover('regrind',dot('#8ea36f',2.6),220,m=>m.node.remove()),i*250);G.run=0;} if(!G.run&&G.bin>=12&&shiftOn()){G.run=G.bin;G.bin=0;G.end=S.t+G.run/225*HOUR; - if(visHop())mover('trimcart',el('rect',{x:-7,y:-5,width:14,height:10,fill:'#5a4f33',stroke:'#c2b280'},L.tok),160,m=>m.node.remove());} + if(visHop())mover('trimcart',spr('rect',{w:14,h:10,fill:'#5a4f33',stroke:'#c2b280'}),160,m=>m.node.remove());} } /* ================================================================ utilities */ @@ -830,46 +851,44 @@ function tick(ds){S.t+=ds; /* ================================================================ render */ let feedAcc=0,cartonAcc=0; function animate(dt){ - for(let i=movers.length-1;i>=0;i--){const m=movers[i];m.pos+=m.v*dt;if(m.pos>=m.p.len){place(m.node,m.p.getPointAtLength(m.p.len));movers.splice(i,1);m.onEnd&&m.onEnd(m);}else place(m.node,m.p.getPointAtLength(m.pos));} + for(let i=movers.length-1;i>=0;i--){const m=movers[i];m.pos+=m.v*dt;if(m.pos>=m.p.len){place(m.node,ptAt(m.p,m.p.len));movers.splice(i,1);m.onEnd&&m.onEnd(m);}else place(m.node,ptAt(m.p,m.pos));} for(let i=hops.length-1;i>=0;i--){const h=hops[i];h.t+=dt;const k=Math.min(1,h.t/h.d);place(h.n,{x:h.a.x+(h.b.x-h.a.x)*k,y:h.a.y+(h.b.y-h.a.y)*k-Math.sin(k*Math.PI)*18});if(k>=1){h.n.remove();hops.splice(i,1);}} RECS.forEach(r=>{if(!r.gone)place(r.node,r.ph==='conv'?{x:CONV0+r.pos,y:CONVY}:{x:r.x,y:r.y});}); if(S.running&&!S.quiet&&S.speed<=600){feedAcc+=dt; if(feedAcc>0.7&&PRESS.some(running)){feedAcc=0;const k=pick(['s_v','s_v','s_r','s_c','f_vex','f_c1','f_c2']); const col=k==='s_r'?'#8ea36f':k==='s_c'?'#c86bff':'#b9c2d0';mover(k,dot(col),110,m=>m.node.remove()); if(S.ex35&&Math.random()<0.5)mover('f_ex35',dot('#c86bff'),110,m=>{m.node.remove();mover('ex2vex',dot('#c86bff'),20,mm=>mm.node.remove());});} - cartonAcc+=dt;if(cartonAcc>7){cartonAcc=0;mover('carton',el('rect',{x:-6,y:-4,width:12,height:8,fill:'#8a6d3b',stroke:'#c9a24a'},L.tok),50,m=>m.node.remove());}} + cartonAcc+=dt;if(cartonAcc>7){cartonAcc=0;mover('carton',spr('rect',{w:12,h:8,fill:'#8a6d3b',stroke:'#c9a24a'}),50,m=>m.node.remove());}} for(const k in GATES){const G=GATES[k];if(G.flash>0){G.flash-=dt;if(G.flash<=0)G.g.classList.remove('ok');}} - PRESS.forEach(P=>{const m=M[P.id];const ty=m.pY[0]+(m.pY[1]-m.pY[0])*(1-P.open);m.topPl.setAttribute('y',ty.toFixed(1));m.topSt.setAttribute('y',(ty+16).toFixed(1)); - const heat=P.state==='STEAM',cool=P.state==='COOL';m.topPl.classList.toggle('heat',heat);m.botPl.classList.toggle('heat',heat);m.topPl.classList.toggle('cool',cool);m.botPl.classList.toggle('cool',cool); - m.cake.setAttribute('visibility',P.cake?'visible':'hidden');}); + PRESS.forEach(P=>{const k=1-Math.exp(-6*dt);P.__h=(P.__h||0)+((P.state==='STEAM'?1:0)-(P.__h||0))*k;P.__c=(P.__c||0)+((P.state==='COOL'?1:0)-(P.__c||0))*k;}); } -const lampSet=(id,cls)=>{const m=M[id];if(m&&m.lamp)m.lamp.setAttribute('class','lamp '+cls);}; +const lampSet=(id,cls)=>{const m=M[id];if(m&&m.lamp)setA(m.lamp,'class','lamp '+cls);}; function pressLabel(P){if(P.state==='STOP')return 'STOP';if(P.state==='STEAM'&&P.waiting)return 'STEAM · WAIT';if(P.state==='CHANGE')return P.restart?'WARM-UP':'STAMPER CHANGE';if(P.state==='NOPVC')return 'NO PVC';return P.state;} function renderTags(){ - MACH.forEach(m=>{if(m.tags&&m.tagEls)m.tags.forEach((fn,i)=>{if(m.tagEls[i])m.tagEls[i].textContent=fn();});if(m.barEl)m.barEl.setAttribute('width',((m.w-14)*stFrac(m.st)).toFixed(1));}); + MACH.forEach(m=>{if(m.tags&&m.tagEls)m.tags.forEach((fn,i)=>{if(m.tagEls[i])setT(m.tagEls[i],fn());});if(m.barEl)setA(m.barEl,'width',((m.w-14)*stFrac(m.st)).toFixed(1));}); [['silo_v',S.silo.v/S.silo.vcap],['silo_r',S.silo.r/S.silo.rcap],['silo_c',S.silo.c/S.silo.ccap]].forEach(([id,l])=>{const m=M[id];l=Math.max(0,Math.min(1,l));const hh=(m.h-32)*l; - m.fillEl.setAttribute('y',(m.y+m.h-30-hh).toFixed(1));m.fillEl.setAttribute('height',hh.toFixed(1));m.lvlEl.textContent=f0(l*100)+' %';m.lamp.setAttribute('class','lamp '+(l<0.1?'alarm':l<0.25?'warn':'run'));}); + setA(m.fillEl,'y',(m.y+m.h-30-hh).toFixed(1));setA(m.fillEl,'height',hh.toFixed(1));setT(m.lvlEl,f0(l*100)+' %');setA(m.lamp,'class','lamp '+(l<0.1?'alarm':l<0.25?'warn':'run'));}); PRESS.forEach(P=>{const m=M[P.id];const T=P.set&&P.set.T; - m.stateEl.textContent=pressLabel(P);const amber=P.waiting||['CHANGE','UNMOUNT'].includes(P.state)||P.mode==='tp'; - m.stateEl.style.fill=P.state==='NOPVC'||P.state==='STOP'?'var(--alarm)':amber?'var(--warn)':(P.state==='IDLE'||P.state==='OFF')?'var(--mute)':'var(--ink)'; - m.tagEls[0].textContent=running(P)?'F '+f0(P.force)+' t · '+f0(P.moldT)+' °C':P.state==='STOP'?P.stopWhy:P.state==='OFF'?'night: shift 06:00':'mould '+f0(P.moldT)+' °C'; - m.tagEls[1].textContent=T?T.id+' · '+P.set.k:(P.state==='OFF'?'':'waiting for a set'); - m.tagEls[2].textContent=!T?'':P.mode==='tp'?'test press '+(TP_N-P.tpLeft)+'/'+TP_N:P.mode==='fa'?'first article':'run '+T.cycles+'/'+T.target; + setT(m.stateEl,pressLabel(P));const amber=P.waiting||['CHANGE','UNMOUNT'].includes(P.state)||P.mode==='tp'; + setS(m.stateEl,'fill',P.state==='NOPVC'||P.state==='STOP'?'var(--alarm)':amber?'var(--warn)':(P.state==='IDLE'||P.state==='OFF')?'var(--mute)':'var(--ink)'); + setT(m.tagEls[0],running(P)?'F '+f0(P.force)+' t · '+f0(P.moldT)+' °C':P.state==='STOP'?P.stopWhy:P.state==='OFF'?'night: shift 06:00':'mould '+f0(P.moldT)+' °C'); + setT(m.tagEls[1],T?T.id+' · '+P.set.k:(P.state==='OFF'?'':'waiting for a set')); + setT(m.tagEls[2],!T?'':P.mode==='tp'?'test press '+(TP_N-P.tpLeft)+'/'+TP_N:P.mode==='fa'?'first article':'run '+T.cycles+'/'+T.target); lampSet(P.id,P.state==='NOPVC'||P.state==='STOP'?'alarm':amber?'warn':running(P)?'run':'idle');}); ['tA','tB','tC1','tC2'].forEach((id,i)=>lampSet(id,running(PRESS[i])?'run':'idle')); const dRun=PRESS.slice(0,2).some(running); - M.vex.zoneT.forEach((t,i)=>t.textContent=f0(dRun?jit([132,146,156][i],1):[92,96,98][i])+'°'); - M.vex.zoneEls.forEach((z,i)=>{z.style.fill=dRun?'var(--z'+(i+1)+')':'var(--z0)';});lampSet('vex',dRun?'run':'idle');lampSet('ex35',S.ex35?'run':'idle'); - M.pppu.pos.forEach((p,i)=>{const w=ST.pppu.slots[i];p.r.setAttribute('class','pos'+(w?' on':''));p.a.textContent=w?w.it.T.sid+w.it.side+' 20 A':'free';p.b.textContent=w?dur(w.end-S.t):'';}); + M.vex.zoneT.forEach((t,i)=>setT(t,f0(dRun?jit([132,146,156][i],1):[92,96,98][i])+'°')); + M.vex.zoneEls.forEach((z,i)=>{setS(z,'fill',dRun?'var(--z'+(i+1)+')':'var(--z0)');});lampSet('vex',dRun?'run':'idle');lampSet('ex35',S.ex35?'run':'idle'); + M.pppu.pos.forEach((p,i)=>{const w=ST.pppu.slots[i];setA(p.r,'class','pos'+(w?' on':''));setT(p.a,w?w.it.T.sid+w.it.side+' 20 A':'free');setT(p.b,w?dur(w.end-S.t):'');}); lampSet('pppu',ST.pppu.slots.some(Boolean)?'run':ST.pppu.q.length?'warn':'idle'); [0,1].forEach(u=>{const m=M['pdpu'+(u+1)];let any=false,done=false; - m.cells.forEach((cv,i)=>{const c=CELLS[u*2+i];const j=c.job;cv.g.setAttribute('class','cell'+(c.ph==='plate'?' on':c.ph==='done'?' done':'')); + m.cells.forEach((cv,i)=>{const c=CELLS[u*2+i];const j=c.job;setA(cv.g,'class','cell'+(c.ph==='plate'?' on':c.ph==='done'?' done':'')); if(c.ph!=='idle')any=true;if(c.ph==='done')done=true; - const hh=50*(j?Math.min(1,c.mass/c.target):0);cv.tv.setAttribute('y',(cv.cy+72-hh).toFixed(1));cv.tv.setAttribute('height',hh.toFixed(1)); - cv.t[0].textContent=j?jl(j).replace(' ',''):'idle'; - cv.t[1].textContent=c.ph==='plate'?f0(c.I)+' A':c.ph==='load'?'load':c.ph==='unload'?'unload':c.ph==='done'?'DONE':''; - cv.t[2].textContent=j?f2(c.mass/massFor(1))+' mm':''; - cv.t[3].textContent=j&&c.ph!=='done'?dur(cellLeft(c)):c.ph==='done'?'waits':'';}); + const hh=50*(j?Math.min(1,c.mass/c.target):0);setA(cv.tv,'y',(cv.cy+72-hh).toFixed(1));setA(cv.tv,'height',hh.toFixed(1)); + setT(cv.t[0],j?jl(j).replace(' ',''):'idle'); + setT(cv.t[1],c.ph==='plate'?f0(c.I)+' A':c.ph==='load'?'load':c.ph==='unload'?'unload':c.ph==='done'?'DONE':''); + setT(cv.t[2],j?f2(c.mass/massFor(1))+' mm':''); + setT(cv.t[3],j&&c.ph!=='done'?dur(cellLeft(c)):c.ph==='done'?'waits':'');}); lampSet('pdpu'+(u+1),done?'warn':any?'run':'idle');}); ['pasu','pmpb','qc','vsf','vsp','vsc'].forEach(id=>lampSet(id,ST[id].slots[0]?'run':ST[id].q.length?'warn':'idle')); [['audio','intake'],['master','master'],['cut','cut']].forEach(([id,st])=>lampSet(id,cntStage(st)?'run':'idle')); @@ -879,13 +898,13 @@ function renderTags(){ lampSet('lab',S.lab.test||S.lab.queue.length?'warn':'run');lampSet('oven',anyRun?'run':'idle');lampSet('carton','run'); lampSet('boiler',S.boiler.p<9.5?'alarm':S.boiler.p<9.65?'warn':'run');lampSet('chiller',S.chiller.sup>24?'warn':'run'); ['comp','diro','zld','energy','trace','hvac','exh'].forEach(id=>lampSet(id,'run'));lampSet('hyd',dRun?'run':'idle'); - GATES.g1.c.textContent=cntStage('gate1')+' waiting';GATES.g2.c.textContent=cntStage('gate2')+' waiting';GATES.g3.c.textContent=TITLES.filter(T=>T.tp==='sent').length+' waiting'; - $('kClock').textContent=clockStr();$('kShift').textContent=shiftOn()?(hourOf()<14?'1 (06-14)':'2 (14-22)'):'night'; - $('kPress').textContent=PRESS.filter(running).length+'/4'; - $('kRate').textContent=S.cyc.length;$('kGood').textContent=S.day.good;$('kRej').textContent=S.day.rej;$('kYield').textContent=yieldStr(); - $('kGalv').textContent=galvCount();$('kRack').textContent=rackReady(); - $('kSteam').textContent=f2(S.boiler.p)+' bar';$('kSteam').style.color=S.boiler.p<9.5?'#ff3b3b':''; - $('kLoad').textContent=f0(S.energy.load)+' kW';$('kPV').textContent=f0(S.energy.pv)+' kW'; + setT(GATES.g1.c,cntStage('gate1')+' waiting');setT(GATES.g2.c,cntStage('gate2')+' waiting');setT(GATES.g3.c,TITLES.filter(T=>T.tp==='sent').length+' waiting'); + setT($('kClock'),clockStr());setT($('kShift'),shiftOn()?(hourOf()<14?'1 (06-14)':'2 (14-22)'):'night'); + setT($('kPress'),PRESS.filter(running).length+'/4'); + setT($('kRate'),S.cyc.length);setT($('kGood'),S.day.good);setT($('kRej'),S.day.rej);setT($('kYield'),yieldStr()); + setT($('kGalv'),galvCount());setT($('kRack'),rackReady()); + setT($('kSteam'),f2(S.boiler.p)+' bar');setS($('kSteam'),'color',S.boiler.p<9.5?'#ff3b3b':''); + setT($('kLoad'),f0(S.energy.load)+' kW');setT($('kPV'),f0(S.energy.pv)+' kW'); } /* ================================================================ log */ @@ -960,7 +979,7 @@ document.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target===document const SV=$('svg'),BASE={w:2100,h:820},ZMAX=5,VB={x:0,y:0,w:BASE.w}; function applyVB(){VB.w=Math.min(BASE.w,Math.max(BASE.w/ZMAX,VB.w));const h=VB.w*BASE.h/BASE.w; VB.x=Math.min(BASE.w-VB.w,Math.max(0,VB.x));VB.y=Math.min(BASE.h-h,Math.max(0,VB.y)); - SV.setAttribute('viewBox',VB.x.toFixed(1)+' '+VB.y.toFixed(1)+' '+VB.w.toFixed(1)+' '+h.toFixed(1));SV.classList.toggle('zoomed',VB.wshowPanel(document.body.dataset.panel| const HDR=document.querySelector('header'); new ResizeObserver(()=>document.documentElement.style.setProperty('--hdrH',HDR.offsetHeight+'px')).observe(HDR); +/* ================================================================ effects + Two canvases sized to the SVG box and drawn in viewBox units with the same xMidYMid-meet transform as + the SVG, so pan and zoom need nothing extra. Under the SVG: pipes, flow dashes and the conveyor (under + the machines, as before). Over it: tokens, records, extruder screw, plating bubbles. Nothing here + touches the DOM per frame, which is what made the page lag (forced layouts of the whole SVG). */ +const CU=$('fxUnder'),CO=$('fxOver'),XU=CU.getContext('2d'),XO=CO.getContext('2d'),BOX={w:1,h:1,d:1}; +let fxT=0,screwPh=0,COL={}; +function fxColors(){const de=document.documentElement,cs=getComputedStyle(de),lt=de.getAttribute('data-theme')==='light'; + COL={plat:lt?{b:[199,209,223],h:[224,112,79],c:[111,163,216],s:'#8e9db4'}:{b:[42,58,86],h:[179,64,44],c:[44,91,143],s:'#4a5f85'},pipeA:lt?0.32:0.28,conv:lt?'#dfe5ee':'#1c2740',convd:lt?'#b5c1d4':'#2e4066',screw:lt?'#7d8aa0':'#6d7d99',bub:lt?'#3f97cc':'#8fd3ff'}; + ['steam','cond','cw','cwr','hyd','chw','di','mat','reg','col','metal','scrap','route','lbl'].forEach(k=>{COL[k]=cs.getPropertyValue('--p-'+k).trim();});fxDirty=true;} +function fxResize(){const mr=SV.parentNode.getBoundingClientRect(),sr=SV.getBoundingClientRect(); + BOX.w=Math.max(1,sr.width);BOX.h=Math.max(1,sr.height);BOX.d=Math.min(2,window.devicePixelRatio||1); + for(const c of [CU,CO]){c.style.left=(sr.left-mr.left)+'px';c.style.top=(sr.top-mr.top)+'px';c.style.width=BOX.w+'px';c.style.height=BOX.h+'px'; + c.width=Math.round(BOX.w*BOX.d);c.height=Math.round(BOX.h*BOX.d);}fxDirty=true;} +function fxBegin(x){const vh=VB.w*BASE.h/BASE.w,k=Math.min(BOX.w/VB.w,BOX.h/vh),d=BOX.d; + x.setTransform(1,0,0,1,0,0);x.clearRect(0,0,x.canvas.width,x.canvas.height); + x.setTransform(d*k,0,0,d*k,d*((BOX.w-VB.w*k)/2-VB.x*k),d*((BOX.h-vh*k)/2-VB.y*k));} +function circ(x,cx,cy,r,fill,stroke,sw){x.beginPath();x.arc(cx,cy,r,0,6.2832);if(fill){x.fillStyle=fill;x.fill();}if(stroke){x.lineWidth=sw||1;x.strokeStyle=stroke;x.stroke();}} +const SHAPES={L:[[7,'#1a1408','#c9a24a'],[2.4,'#c9a24a']],Ls:[[7,'#d9dee6','#fff'],[2.4,'#8a97b0']], + S:[[7,'#b9c2cc','#e6ecf7'],[4.5,null,'#7d8794'],[1.6,'#3a4658']],M:[[7,'#8e98a6','#c8d2e4'],[2.4,'#c8d2e4']]}; +function drawFx(dt){fxT+=dt;screwPh+=dt*(S.vex.rpm>1?17.8:0); + const u=XU;fxBegin(u);u.lineJoin='round'; + for(const f of FX.flows){ + if(f.k==='pipe'){u.globalAlpha=COL.pipeA;u.strokeStyle=COL[f.c];u.lineWidth=5;u.lineCap='round';u.setLineDash([]);} + else if(f.k==='conv'){u.globalAlpha=1;u.strokeStyle=COL.conv;u.lineWidth=14;u.lineCap='butt';u.setLineDash([]);} + else if(f.k==='convd'){u.globalAlpha=1;u.strokeStyle=COL.convd;u.lineWidth=14;u.lineCap='butt';u.setLineDash([3,9]);u.lineDashOffset=-(fxT*10%12);} + else{u.globalAlpha=1;u.strokeStyle=COL[f.c];u.lineCap='round'; + if(f.thin){u.lineWidth=1.4;u.setLineDash([3,7]);u.lineDashOffset=-(fxT*13.3%10);}else{u.lineWidth=2.2;u.setLineDash([7,9]);u.lineDashOffset=-(fxT*13.3%16);}} + u.stroke(f.p2);} + u.globalAlpha=1; + const o=XO;fxBegin(o);o.setLineDash([6,6]);o.lineDashOffset=-(screwPh%12);o.lineWidth=2;o.strokeStyle=COL.screw;o.lineCap='butt'; + for(const w of FX.screws){o.beginPath();o.moveTo(w.x1,w.y);o.lineTo(w.x2,w.y);o.stroke();} + o.setLineDash([]);o.globalAlpha=0.7; + for(const b of FX.bubbles){const c=CELLS[b.cell];if(!c||c.ph!=='plate')continue;const k=(((fxT-b.d)%2.2)+2.2)%2.2/2.2;circ(o,b.x,b.y-30*k,1.6,COL.bub);} + o.globalAlpha=1; + // presses: platen colour eases to steam red / cooling blue like the old 0.5 s CSS transition + for(const P of PRESS){const m=M[P.id],q=m.plat;if(!q)continue;const ty=m.pY[0]+(m.pY[1]-m.pY[0])*(1-P.open); + const h=P.__h||0,c=P.__c||0,C=COL.plat,rgb=[0,1,2].map(i=>Math.round(C.b[i]*(1-h-c)+C.h[i]*h+C.c[i]*c)); + o.fillStyle='rgb('+rgb+')';o.strokeStyle=C.s;o.lineWidth=1; + o.fillRect(q.px,q.yb,q.pw,16);o.strokeRect(q.px,q.yb,q.pw,16);o.fillRect(q.px,ty,q.pw,16);o.strokeRect(q.px,ty,q.pw,16); + o.fillStyle='#b9c2cc';o.fillRect(q.px+8,ty+16,q.pw-16,3);if(P.cake)circ(o,q.cx,q.cy,7,'#3d3a33','#8a7a55',1);} + let j=0;for(const t of SPR)if(!t.dead)SPR[j++]=t;SPR.length=j; + for(const t of SPR){ + if(t.kind==='dot')circ(o,t.x,t.y,t.r,t.fill,t.stroke,t.sw); + else if(t.kind==='rect'){o.fillStyle=t.fill;o.fillRect(t.x-t.w/2,t.y-t.h/2,t.w,t.h);o.lineWidth=1;o.strokeStyle=t.stroke;o.strokeRect(t.x-t.w/2,t.y-t.h/2,t.w,t.h);} + else if(t.kind==='rec'){const st=t.cls.rej?['#ff3b3b',2]:t.cls.tp?['#ffbf1f',1.5]:['#3a3f4a',1];circ(o,t.x,t.y,t.r,t.vinyl,st[0],st[1]);circ(o,t.x,t.y,t.lr,t.label);} + else for(const [r,fl,st] of SHAPES[t.k]||SHAPES.M)circ(o,t.x,t.y,r,fl,st,1);}} +new ResizeObserver(fxResize).observe(SV); +new MutationObserver(fxColors).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); +fxResize();fxColors(); + /* ================================================================ seed, then two days of warm-up so the page opens mid-shift */ function seedSets(T,used){T.made={A:T.setsTarget,B:T.setsTarget};T.pairs=T.setsTarget;T.mothers={A:{T,side:'A',gen:'M'},B:{T,side:'B',gen:'M'}}; for(let k=1;k<=T.setsTarget;k++)RACK.push({T,k,used:k===1?used:0});} @@ -1016,9 +1086,10 @@ let last=performance.now(),tagAcc=0; function frame(now){const dt=Math.min(0.1,(now-last)/1000);last=now; if(S.running){const ds=dt*S.speed;const sub=S.speed>=3600?1:S.speed>=600?0.5:0.25;const n=Math.ceil(ds/sub);const h=ds/n;for(let i=0;i=0.2){tagAcc=0;renderTags();} + if(S.running||fxDirty){fxDirty=false;drawFx(S.running?dt:0);} requestAnimationFrame(frame);} requestAnimationFrame(frame); -window.__plant={S,TITLES,RACK,PRESS,CELLS,ST,PLATEQ,M,tick}; +window.__plant={S,TITLES,RACK,PRESS,CELLS,ST,PLATEQ,M,tick,SPR,FX,PATHS,XU,XO}; })(); (function(){ var b=document.getElementById("themeBtn"),de=document.documentElement;