Files
cnx_vinyl/docs/plant-operation.html
T
grabowski 122d908862 Labels are pressed into the record, not applied at finishing
The docs and the plant page had labelling as a finishing step after the
press. On a record press the bottom label, the cake and the top label go
in between the stampers every cycle and the mold bonds the paper into the
vinyl; the oven only pre-dries them. Corrected everywhere the process is
described:

- process-flow.md: labels are a parallel input to pressing with their own
  row (print inspection, oven, lot per cycle); stage 6 loads label, cake,
  label; stage 7 is trim, sleeve, shrink
- process-flow.drawio (+ regenerated png/pdf): label input now feeds
  stage 6, not stage 7
- README vision list and the business plan source (regenerated docx/pdf):
  pressing paragraph explains the pressed-in label, finishing no longer
  claims to label
- plant-operation.html: label dry oven station feeds two label discs
  into the press with every cake, the cake in the mold shows its labels,
  the cycle log records the label lot, finishing is sleeve + shrink
2026-09-21 16:16:18 +02:00

759 lines
57 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CNX Vinyl · Plant overview (simulated SCADA)</title>
<style>
:root{
--bg:#0a0f1a;--panel:#0f1626;--box:#121a2b;--stroke:#2a3a56;--head:#1a2640;
--fg:#d6deeb;--mute:#8a97b0;--run:#3ddc84;--idle:#6b7a90;--warn:#ffbf1f;--alarm:#ff3b3b;
--mono:ui-monospace,"Cascadia Mono",Consolas,"Liberation Mono",monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{background:var(--bg);color:var(--fg);font:13px/1.4 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;display:flex;flex-direction:column;height:100vh;overflow:hidden}
header{display:flex;align-items:center;gap:16px;padding:7px 14px;background:var(--panel);border-bottom:1px solid var(--stroke);flex:0 0 auto}
header h1{font-size:15px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
header h1 span{color:var(--mute);font-weight:400}
.kpis{display:flex;gap:14px;margin-left:auto}
.kpi{display:flex;flex-direction:column;min-width:70px}
.kpi b{font:600 15px var(--mono);white-space:nowrap}
.kpi small{color:var(--mute);font-size:10px;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}
.ctl{display:flex;gap:6px;align-items:center;margin-left:16px}
button{background:#182238;color:var(--fg);border:1px solid var(--stroke);border-radius:4px;padding:4px 9px;font-size:12px;cursor:pointer;white-space:nowrap}
button:hover{background:#1f2c47}
button.on{background:#244a7a;border-color:#3b74b8}
button.demo{border-color:#5a4a1f;color:#ffd777}
main{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;padding:4px 10px}
svg{width:100%;height:100%}
footer{flex:0 0 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}
.panel h2{font-size:13px;margin-bottom:4px}
.panel h3{font-size:11px;color:var(--mute);text-transform:uppercase;letter-spacing:.06em;margin:6px 0 2px}
.panel p{font-size:12px;color:#c8d2e4}
.panel ul{font-size:12px;padding-left:16px;color:#c8d2e4}
.panel li{margin:1px 0}
.log{font:11px/1.45 var(--mono)}
.log div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.log .t{color:var(--mute)}
.log .ok{color:var(--run)}
.log .warn{color:var(--warn)}
.log .alarm{color:var(--alarm)}
.legend{display:grid;grid-template-columns:1fr 1fr;gap:2px 12px;font-size:11px;color:#c8d2e4}
.legend i{display:inline-block;width:22px;height:0;border-top:3px solid;margin-right:6px;vertical-align:middle}
.legend i.d{border-top-style:dashed}
.legend .note{grid-column:1/3;color:var(--mute);margin-top:6px;font-size:10.5px}
/* SVG */
.mbox{fill:#121a2b;stroke:#2a3a56;stroke-width:1}
.machine{cursor:pointer}
.machine:hover .mbox{stroke:#4f7ac7}
.machine.sel .mbox{stroke:#7fb2ff;stroke-width:1.5}
.mhead{fill:#1a2640}
.mtitle{fill:#e6ecf7;font-size:12px;font-weight:600}
.msub{fill:#8a97b0;font-size:9.5px}
.mtag{fill:#c8d2e4;font-family:var(--mono);font-size:10.5px}
.small{fill:#8a97b0;font-size:9px}
.lane{fill:#5f6f8c;font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:600}
.lamp{fill:#6b7a90}
.lamp.run{fill:#3ddc84}
.lamp.warn{fill:#ffbf1f}
.lamp.alarm{fill:#ff3b3b;animation:blink .6s steps(2,start) infinite}
@keyframes blink{to{visibility:hidden}}
.pipe{fill:none;stroke-width:5;stroke-linejoin:round;stroke-linecap:round;opacity:.28}
.flow{fill:none;stroke-width:2.2;stroke-linejoin:round;stroke-linecap:round;stroke-dasharray:7 9;animation:dash 1.2s linear infinite}
.thin{stroke-width:1.4;stroke-dasharray:3 7}
@keyframes dash{to{stroke-dashoffset:-16}}
.paused .flow,.paused .spin,.paused .bub,.paused .convd,.paused .screw{animation-play-state:paused}
.steam{stroke:#ff5a3c}.cond{stroke:#c96a4e}.cw{stroke:#3aa0ff}.cwr{stroke:#7fc0ff}.air{stroke:#59d9c9}.hyd{stroke:#f5a623}
.mat{stroke:#b9c2d0}.reg{stroke:#8ea36f}.metal{stroke:#c0c8d6}.scrap{stroke:#c2b280}.route{stroke:#3a4d70}
.rail{stroke:#2a3a56;stroke-width:1.5;fill:none}
.conv{stroke:#1c2740;stroke-width:14;fill:none}
.convd{stroke:#2e4066;stroke-width:14;stroke-dasharray:3 9;fill:none;animation:dash 1.6s linear infinite}
.gate polygon{fill:#1a2640;stroke:#ffbf1f;stroke-width:1.5}
.gate.ok polygon{stroke:#3ddc84;fill:#143324}
.gate text{fill:#8a97b0;font-size:9px;text-anchor:middle}
.disc{fill:#0b0b0d;stroke:#3a3f4a;stroke-width:1}
.rec.rej .disc{stroke:#ff3b3b;stroke-width:2}
.rec.tp .disc{stroke:#ffbf1f;stroke-width:1.5}
.tank{fill:#0e1a2a;stroke:#3a5a7a}
.tank.on{stroke:#3ddc84}
.liquid{fill:#12405a;opacity:.9}
.tank.on .liquid{fill:#1a6a4a}
.bub{fill:#8fd3ff;opacity:.7;animation:rise 2.2s linear infinite}
@keyframes rise{from{transform:translateY(0);opacity:.7}to{transform:translateY(-38px);opacity:0}}
.spin{transform-box:fill-box;transform-origin:center;animation:spin 1.4s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.screw{stroke:#6d7d99;stroke-width:2;stroke-dasharray:6 6;animation:dash 0.9s linear infinite;fill:none}
.platen{fill:#2a3a56;stroke:#4a5f85;transition:fill .5s}
.platen.heat{fill:#b3402c}
.platen.cool{fill:#2c5b8f}
.frame{fill:#0f1626;stroke:#3d5077;stroke-width:2}
.silo{fill:#121a2b;stroke:#2a3a56}
.fill{fill:#3b4658}
.fill.reg{fill:#4e5a3a}
.tok{font-family:var(--mono);font-size:10px;fill:#c8d2e4}
.hopper{fill:#121a2b;stroke:#2a3a56}
.zone{stroke:#2a3a56;stroke-width:1}
.bar{fill:#1a2640}
.barv{fill:#3aa0ff}
.warntxt{fill:#ffbf1f}
</style>
</head>
<body>
<header>
<h1>CNX Vinyl <span>· Poy Records · Plant overview · P1 semi-automatic line</span></h1>
<div class="ctl">
<button id="btnPlay" class="on">Pause</button>
<button data-speed="1">1×</button>
<button data-speed="4" class="on">4×</button>
<button data-speed="10">10×</button>
<button id="btnSteam" class="demo" title="Demonstrate a steam pressure dip: the press holds in STEAM until the boiler recovers">Steam dip</button>
<button id="btnLot" class="demo" title="Demonstrate a new PVC lot arriving: the lab gate holds it until a test pressing releases it">New PVC lot</button>
</div>
<div class="kpis">
<div class="kpi"><b id="kClock">08:00:00</b><small>sim clock</small></div>
<div class="kpi"><b id="kState">IDLE</b><small>press P1</small></div>
<div class="kpi"><b id="kCycle">-- s</b><small>last cycle</small></div>
<div class="kpi"><b id="kGood">0</b><small>good today</small></div>
<div class="kpi"><b id="kRej">0</b><small>rejects</small></div>
<div class="kpi"><b id="kYield">--</b><small>yield</small></div>
<div class="kpi"><b id="kSteam">-- bar</b><small>steam</small></div>
<div class="kpi"><b id="kLoad">-- kW</b><small>plant load</small></div>
<div class="kpi"><b id="kPV">-- kW</b><small>solar</small></div>
</div>
</header>
<main>
<svg id="svg" viewBox="0 0 1800 740" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
<g id="lPipes"></g>
<g id="lMach"></g>
<g id="lTok"></g>
<g id="lTop"></g>
</svg>
</main>
<footer>
<div class="panel" id="detail">
<h2>Click any station</h2>
<p>Every box is a stage of the plant. Click it for what happens there, the checks that gate it, and the planning figures behind the numbers on screen. Values are simulated from the figures in this repository (Newbilt site requirements, scale.md, quality-automation.md, process-flow.md); nothing here is live. Run lengths and approval waits are shortened so stamper changes and gates are visible within minutes.</p>
</div>
<div class="panel"><h2>Event log</h2><div class="log" id="log"></div></div>
<div class="panel"><h2>Legend</h2>
<div class="legend">
<div><i style="border-color:#ff5a3c"></i>Saturated steam 10 bar</div>
<div><i class="d" style="border-color:#c96a4e"></i>Condensate return</div>
<div><i style="border-color:#3aa0ff"></i>Cooling water supply</div>
<div><i class="d" style="border-color:#7fc0ff"></i>Cooling water return</div>
<div><i style="border-color:#f5a623"></i>Hydraulic oil 180 bar</div>
<div><i style="border-color:#59d9c9"></i>Compressed air</div>
<div><i style="border-color:#b9c2d0"></i>PVC granulate</div>
<div><i style="border-color:#8ea36f"></i>Regrind (20 % share)</div>
<div><i style="border-color:#c0c8d6"></i>Metal path (lacquer to stamper)</div>
<div><i class="d" style="border-color:#c2b280"></i>Trim scrap and rejects</div>
<div class="note">Lamps: green run · grey idle · amber hold or under test · red alarm. Yellow diamonds are the three artist approval gates. Metal path is shown compressed (about one minute of sim time per day of real work).</div>
</div>
</div>
</footer>
<script>
(function(){
'use strict';
const NS='http://www.w3.org/2000/svg';
const $=id=>document.getElementById(id);
const L={pipes:$('lPipes'),mach:$('lMach'),tok:$('lTok'),top:$('lTop')};
function el(tag,attrs,parent,text){const e=document.createElementNS(NS,tag);for(const k in attrs)e.setAttribute(k,attrs[k]);if(text!=null)e.textContent=text;(parent||L.mach).appendChild(e);return e;}
const rnd=(a,b)=>a+Math.random()*(b-a);
const jit=(v,a)=>v+(Math.random()-.5)*a;
const f1=v=>v.toFixed(1), f2=v=>v.toFixed(2), f0=v=>Math.round(v).toString();
/* ------------------------------------------------------------------ state */
const S={
t:0, speed:4, running:true, clockStart:8*3600,
good:0, rej:0, tp:0, samples:0, trimmed:0, scrapKg:0, regrindKg:0, assembled:0, pallet:0, parcels:0, mismatches:0,
logged:0, events:0,
boiler:{p:10.05,pTarget:10.05,flow:35,alarmUntil:-1},
chiller:{ret:24,load:25,flow:128},
comp:{p:7.6},
hyd:{p:30,oil:44},
ext:{z:[132,146,156],rpm:0,melt:0,cake:0},
lab:{lot:'26-0912',testingUntil:-1,nextLot:'26-0913'},
hvac:{t:23.2,rh:52,pm:6},
energy:{pv:0,load:0,grid:0,soc:78},
galv:{active:null,cur:0,temp:55,ni:0.25},
press:{state:'IDLE',idx:0,tIn:0,cycle:0,cycleStart:0,lastCycle:0,force:0,moldT:58,job:null,run:0,runQty:0,tpLeft:0,waiting:false,changeT:0,rack:[],topY:300},
jobN:41, stamperN:2261, gates:{g1:false,g2:false,g3:false},
opticalLast:'-', audioLast:'-', lastMoldT:58, labelPairs:480,
};
const PALETTE=['#e0552f','#3aa0ff','#ffbf1f','#3ddc84','#c86bff','#ff7ab6','#59d9c9'];
/* ------------------------------------------------------------------ machines */
const MACH=[
// metal path, row 1
{id:'audio',x:40,y:70,w:140,h:90,t:'Audio intake',s:'checksums · format · tech QA',tags:[()=>'queue 3 titles',()=>'QA levels · clipping']},
{id:'master',x:240,y:70,w:140,h:90,t:'Vinyl mastering',s:'engineer QA of the master',tags:[()=>'room in-house',()=>'session 2 to 4 days']},
{id:'cut',x:440,y:70,w:140,h:90,t:'Lathe cutting',s:'lacquer · microscope · scribe',tags:[()=>'scribe mechanical',()=>'plate in < 72 h']},
{id:'silver',x:620,y:70,w:120,h:90,t:'Silvering',s:'Ag spray on lacquer',tags:[()=>'batch SV-'+f0(1180+S.jobN),()=>'log bath chem']},
{id:'galv',x:780,y:70,w:330,h:90,t:'Electroforming',s:'',custom:'galv'},
{id:'prep',x:1150,y:70,w:140,h:90,t:'Stamper prep',s:'punch · back-finish · coin',tags:[()=>'concentr. '+f0(jit(18,4))+' µm',()=>'rack '+S.press.rack.length+' ready']},
{id:'archive',x:1330,y:70,w:140,h:90,t:'Mother archive',s:'repress path, no gates',tags:[()=>'mothers 128 stored',()=>'repress 7 to 11 days']},
// side boxes
{id:'labels',x:560,y:195,w:150,h:60,t:'Label dry oven',s:'print inspected · pre-dried',tags:[()=>'60 °C · L-77 · '+S.labelPairs+' pr']},
{id:'audioqc',x:1050,y:195,w:130,h:60,t:'Audio compare',s:'needle-drop · 1 in 50',tags:[()=>'n '+S.samples+' · last '+S.audioLast]},
{id:'carton',x:1390,y:195,w:150,h:60,t:'Carton partner',s:'incoming QC per delivery',tags:[()=>'lot CP-0311 · ΔE 0.8 OK']},
// material + record path, row 2
{id:'silo_v',x:40,y:280,w:70,h:130,t:'',custom:'silo',lbl:'Virgin',lvl:.72},
{id:'silo_r',x:130,y:280,w:70,h:130,t:'',custom:'silo',lbl:'Regrind',lvl:.35,reg:true},
{id:'lab',x:240,y:300,w:120,h:120,t:'Material lab',s:'per-lot release gate',tags:[()=>'lot '+S.lab.lot,()=>'H2O '+f2(jit(0.03,0.004))+' % ok',()=>'ΔE 0.3 · dens ok',()=>S.t<S.lab.testingUntil?'TEST PRESSING':'released']},
{id:'ext',x:400,y:290,w:240,h:130,t:'',custom:'ext'},
{id:'press',x:720,y:270,w:140,h:210,t:'',custom:'press'},
{id:'trim',x:900,y:345,w:110,h:100,t:'Trimmer',s:'Vario 12" · flash cut',tags:[()=>'trimmed '+S.trimmed,()=>'scrap '+f1(S.scrapKg)+' kg',()=>'to grinder']},
{id:'optical',x:1050,y:345,w:130,h:100,t:'Optical inspection',s:'100 % · darkfield line-scan',tags:[()=>'pass '+yieldStr(),()=>'rejects '+S.rej,()=>'last '+S.opticalLast]},
{id:'label',x:1220,y:345,w:130,h:100,t:'Finishing',s:'inner sleeve · shrink wrap',tags:[()=>'sleeved '+S.assembled,()=>'sleeve lot IS-204',()=>'shrink sampled QC']},
{id:'pack',x:1390,y:345,w:150,h:100,t:'Packaging',s:'matrix code vs jacket barcode',tags:[()=>'assembled '+S.assembled,()=>'mismatch '+S.mismatches,()=>'barcode 100 % verified']},
{id:'ship',x:1580,y:345,w:180,h:100,t:'Fulfillment',s:'bulk pallet · dropship',tags:[()=>'pallet '+S.pallet+' / 300',()=>'dropship '+S.parcels+' parcels',()=>'tracking per shipment']},
// utilities, row 3
{id:'boiler',x:40,y:560,w:170,h:120,t:'Steam boiler',s:'400 kg/h · 10 bar · 180 °C',tags:[()=>'P '+f1(S.boiler.p)+' bar',()=>'T '+f0(jit(180,1))+' °C',()=>'flow '+f0(S.boiler.flow)+' kg/h',()=>'fuel biomass (open)']},
{id:'chiller',x:250,y:560,w:200,h:120,t:'Process cooling',s:'chiller 250 kW · cooling tower',tags:[()=>'supply '+f1(jit(22,.3))+' °C',()=>'return '+f1(S.chiller.ret)+' °C',()=>'flow '+f0(S.chiller.flow)+' l/min',()=>'load '+f0(S.chiller.load)+' kW']},
{id:'comp',x:490,y:560,w:130,h:120,t:'Compressed air',s:'6 to 10 bar, dry',tags:[()=>'P '+f1(S.comp.p)+' bar',()=>'dew '+f1(jit(2.5,.4))+' °C',()=>'use 14 m³/h']},
{id:'hyd',x:660,y:560,w:140,h:120,t:'Hydraulics',s:'aggregate, 180 bar',tags:[()=>'P '+f0(S.hyd.p)+' bar',()=>'oil '+f1(S.hyd.oil)+' °C',()=>'HLP 46 · 200 l']},
{id:'grind',x:850,y:560,w:150,h:120,t:'PVC grinder',s:'trims + rejects → regrind',tags:[()=>'ground '+f1(S.regrindKg)+' kg',()=>'share 20 % at extruder',()=>'contam. gate OK']},
{id:'energy',x:1040,y:560,w:220,h:120,t:'Energy',s:'247 kWp PV · 271 kWh BESS · PEA MV',tags:[()=>'PV '+f0(S.energy.pv)+' kW',()=>'load '+f0(S.energy.load)+' kW',()=>'grid '+f0(S.energy.grid)+' kW',()=>'BESS '+f0(S.energy.soc)+' % SoC']},
{id:'trace',x:1300,y:560,w:200,h:120,t:'Traceability',s:'log at source, append-only',tags:[()=>'records '+S.logged,()=>'events '+S.events,()=>'key stamper+press+cycle',()=>'retain life of customer']},
{id:'hvac',x:1540,y:560,w:220,h:120,t:'Plant HVAC',s:'20 to 26 °C · 35 to 60 % RH',tags:[()=>'T '+f1(S.hvac.t)+' °C',()=>'RH '+f0(S.hvac.rh)+' %',()=>'PM2.5 '+f0(S.hvac.pm)+' µg/m³',()=>'zones press · galv · archive']},
];
const M={}; MACH.forEach(m=>M[m.id]=m);
function yieldStr(){const n=S.good+S.rej;return n?f1(100*S.good/n)+' %':'--';}
/* ------------------------------------------------------------------ drawing */
function lane(x,y,txt){el('text',{x,y,class:'lane'},L.mach,txt);}
lane(40,40,'Metal path · audio file to stamper');
lane(40,272,'Material and record path');
lane(40,552,'Utilities and plant systems');
function drawBox(m){
const g=el('g',{class:'machine',id:'m_'+m.id});
el('rect',{x:m.x,y:m.y,width:m.w,height:m.h,rx:4,class:'mbox'},g);
if(m.t){
el('rect',{x:m.x+1,y:m.y+1,width:m.w-2,height:18,class:'mhead'},g);
el('text',{x:m.x+7,y:m.y+14,class:'mtitle'},g,m.t);
m.lamp=el('circle',{cx:m.x+m.w-10,cy:m.y+10,r:4,class:'lamp'},g);
if(m.s) el('text',{x:m.x+7,y:m.y+31,class:'msub'},g,m.s);
m.tagEls=(m.tags||[]).map((t,i)=>el('text',{x:m.x+7,y:m.y+46+i*13,class:'mtag'},g,''));
}
m.g=g;
g.addEventListener('click',()=>select(m.id));
return g;
}
MACH.forEach(m=>{
if(m.custom==='silo') drawSilo(m);
else if(m.custom==='ext') drawExt(m);
else if(m.custom==='press') drawPress(m);
else if(m.custom==='galv') drawGalv(m);
else drawBox(m);
});
function drawSilo(m){
const g=el('g',{class:'machine',id:'m_'+m.id});
const {x,y,w,h}=m;
el('path',{d:`M${x},${y} H${x+w} V${y+h-30} L${x+w/2+8},${y+h} H${x+w/2-8} L${x},${y+h-30} Z`,class:'silo'},g);
const fh=(h-34)*m.lvl;
m.fillEl=el('rect',{x:x+2,y:y+h-32-fh,width:w-4,height:fh,class:'fill'+(m.reg?' reg':'')},g);
el('text',{x:x+w/2,y:y+14,class:'msub','text-anchor':'middle'},g,m.lbl);
el('text',{x:x+w/2,y:y+26,class:'small','text-anchor':'middle'},g,m.reg?'20 % share':'80 % share');
m.lvlEl=el('text',{x:x+w/2,y:y+h-40,class:'mtag','text-anchor':'middle'},g,'');
m.lamp=el('circle',{cx:x+w-8,cy:y+8,r:3.5,class:'lamp run'},g);
m.g=g; g.addEventListener('click',()=>select(m.id));
}
function drawExt(m){
const g=el('g',{class:'machine',id:'m_'+m.id});
el('rect',{x:m.x,y:m.y,width:m.w,height:m.h,rx:4,class:'mbox'},g);
el('rect',{x:m.x+1,y:m.y+1,width:m.w-2,height:18,class:'mhead'},g);
el('text',{x:m.x+7,y:m.y+14,class:'mtitle'},g,'Extruder VEX 50');
m.lamp=el('circle',{cx:m.x+m.w-10,cy:m.y+10,r:4,class:'lamp'},g);
// hopper
el('path',{d:'M480,312 H540 V336 L522,370 H498 L480,336 Z',class:'hopper'},g);
el('text',{x:510,y:330,class:'small','text-anchor':'middle'},g,'hopper');
// barrel with 3 heater zones
const zx=[404,482,560];
m.zoneEls=zx.map((zx0,i)=>{
const r=el('rect',{x:zx0,y:372,width:76,height:40,class:'zone',fill:'#1b2438'},g);
el('text',{x:zx0+38,y:407,class:'small','text-anchor':'middle'},g,'Z'+(i+1));
return r;
});
m.zoneT=zx.map((zx0)=>el('text',{x:zx0+38,y:386,class:'mtag','text-anchor':'middle'},g,''));
el('line',{x1:404,y1:392,x2:636,y2:392,class:'screw'},g);
el('path',{d:'M636,384 H648 V400 H636 Z',fill:'#3a4658',stroke:'#5a6a86'},g);
m.tagEls=[el('text',{x:m.x+7,y:m.y+31,class:'msub'},g,''),el('text',{x:m.x+7,y:m.y+44,class:'msub'},g,'')];
m.tags=[()=>'screw '+f0(S.ext.rpm)+' rpm · melt '+f0(S.ext.melt)+' °C',()=>'mix 80/20 · cake '+f0(S.ext.cake)+' g · 8 kW avg'];
m.g=g; g.addEventListener('click',()=>select(m.id));
}
function drawPress(m){
const g=el('g',{class:'machine',id:'m_'+m.id});
el('rect',{x:720,y:270,width:140,height:210,rx:4,class:'frame'},g);
el('rect',{x:721,y:271,width:138,height:18,class:'mhead'},g);
el('text',{x:727,y:284,class:'mtitle'},g,'Press HRP 12');
m.lamp=el('circle',{cx:850,cy:280,r:4,class:'lamp'},g);
el('text',{x:727,y:299,class:'msub'},g,'150 t · steam heated');
// columns
el('rect',{x:728,y:300,width:6,height:150,fill:'#2a3a56'},g);
el('rect',{x:846,y:300,width:6,height:150,fill:'#2a3a56'},g);
// bottom platen fixed
m.botPl=el('rect',{x:738,y:404,width:104,height:18,class:'platen'},g);
el('rect',{x:748,y:401,width:84,height:3,fill:'#b9c2cc'},g); // stamper B
// top platen moving
m.topPl=el('rect',{x:738,y:300,width:104,height:18,class:'platen'},g);
m.topSt=el('rect',{x:748,y:318,width:84,height:3,fill:'#b9c2cc'},g); // stamper A
m.cake=el('g',{visibility:'hidden'},g);el('circle',{cx:790,cy:395,r:9,fill:'#3d3a33',stroke:'#8a7a55'},m.cake);m.cakeLbl=[el('rect',{x:781,y:383,width:18,height:2.5,rx:1,fill:'#e6ecf7'},m.cake),el('rect',{x:781,y:404,width:18,height:2.5,rx:1,fill:'#e6ecf7'},m.cake)];
m.stateEl=el('text',{x:790,y:440,class:'mtag','text-anchor':'middle',style:'font-weight:700;fill:#e6ecf7'},g,'IDLE');
m.tagEls=[el('text',{x:727,y:456,class:'mtag'},g,''),el('text',{x:727,y:469,class:'mtag'},g,'')];
m.tags=[()=>'F '+f0(S.press.force)+' t · mold '+f0(S.press.moldT)+' °C',()=>S.press.job?('run '+S.press.run+'/'+S.press.runQty+' · '+S.press.job.stamper):'no stamper mounted'];
m.jobEl=el('text',{x:790,y:262,class:'tok','text-anchor':'middle'},L.top,'');
m.g=g; g.addEventListener('click',()=>select(m.id));
}
function drawGalv(m){
const g=drawBox(m);
const names=['Father','Mother','Stamper'];
m.tanks=names.map((n,i)=>{
const x=795+i*105, y=94, w=95, h=60;
const tg=el('g',{class:'tank',id:'tank_'+n.toLowerCase()},g);
el('rect',{x,y,width:w,height:h,rx:2,class:'tank'},tg);
el('rect',{x:x+3,y:y+22,width:w-6,height:h-25,class:'liquid'},tg);
for(let b=0;b<4;b++) el('circle',{cx:x+12+b*22+rnd(-4,4),cy:y+h-6,r:1.6,class:'bub',style:'animation-delay:'+rnd(0,2.2).toFixed(2)+'s'},tg);
el('text',{x:x+w/2,y:y+13,class:'msub','text-anchor':'middle',style:'fill:#c8d2e4'},tg,n);
const val=el('text',{x:x+w/2,y:y+h-9,class:'mtag','text-anchor':'middle',style:'font-size:9.5px'},tg,'');
return {g:tg,val,name:n.toLowerCase()};
});
el('text',{x:m.x+7,y:m.y+31,class:'msub'},g,'Ni 3-step · XRF thickness per part');
m.tagEls=[];m.tags=[];
}
/* silo levels */
function siloLvl(m,lvl){const h=(m.h-34)*lvl;m.fillEl.setAttribute('y',m.y+m.h-32-h);m.fillEl.setAttribute('height',h);m.lvlEl.textContent=f0(lvl*100)+' %';}
/* gates */
function gate(id,x,y,l1,l2){
const g=el('g',{class:'gate',id:'gate_'+id},L.top);
el('polygon',{points:`${x},${y-13} ${x+13},${y} ${x},${y+13} ${x-13},${y}`},g);
el('text',{x,y:y+26},g,l1); el('text',{x,y:y+36},g,l2);
return g;
}
const G={g1:gate('g1',210,115,'artist gate 1','master plan'),g2:gate('g2',410,115,'artist gate 2','master OK'),g3:gate('g3',960,178,'artist gate 3','test pressing')};
/* rails, routes, pipes */
el('path',{d:'M40,52 H1310',class:'rail'},L.pipes);
el('path',{d:'M1310,52 V178 H790 V270',class:'pipe metal'},L.pipes);
el('path',{d:'M1310,52 V178 H790 V270',class:'flow metal thin'},L.pipes);
// record conveyor
el('path',{d:'M860,395 H1700',class:'conv'},L.pipes);
el('path',{d:'M860,395 H1700',class:'convd'},L.pipes);
const PATHS={};
function route(id,d,cls){PATHS[id]=el('path',{d,class:'flow '+cls},L.pipes);PATHS[id].len=PATHS[id].getTotalLength();}
route('feedv','M75,410 V440 H300 V420','mat thin');
route('feedr','M165,410 V440 H300 V420','reg thin');
route('feed2','M300,300 V250 H510 V312','mat thin');
route('cake','M648,392 H790','mat thin');
route('scrap','M955,445 V470 H925 V560','scrap thin');
route('reject','M1115,445 V470 H925 V560','scrap thin');
route('sample','M1115,345 V255','route thin');
route('labels','M690,255 V262 H760 V300','mat thin');
route('carton','M1465,255 V345','route thin');
route('regrind','M925,680 V715 H20 V262 H165 V280','reg thin');
PATHS.route=el('path',{d:'M860,395 H1700',fill:'none',stroke:'none'},L.pipes);PATHS.route.len=PATHS.route.getTotalLength();
function pipe(d,cls,thin){el('path',{d,class:'pipe '+cls},L.pipes);el('path',{d,class:'flow '+cls+(thin?' thin':'')},L.pipes);}
pipe('M125,560 V515 H750 V480','steam');
pipe('M765,480 V530 H145 V560','cond',true);
pipe('M340,560 V500 H790 V480','cw');
pipe('M620,500 V420','cw');
pipe('M805,480 V545 H360 V560','cwr',true);
pipe('M555,560 V490 H830 V480','air',true);
pipe('M730,560 V480','hyd');
// small labels on the bus
el('text',{x:430,y:511,class:'small'},L.top,'steam 10 bar');
el('text',{x:430,y:496,class:'small'},L.top,'cooling water 22 °C');
el('text',{x:180,y:713,class:'small'},L.top,'regrind return to silo');
el('text',{x:1122,y:300,class:'small'},L.top,'1 in 50 + all TPs');
el('text',{x:1472,y:300,class:'small'},L.top,'jackets · sleeves');
el('text',{x:1520,y:395,class:'small'},L.top,'');
el('text',{x:660,y:378,class:'small'},L.top,'cake');
el('text',{x:700,y:268,class:'small'},L.top,'labels');
el('text',{x:1265,y:178,class:'small'},L.top,'stamper to press');
/* ------------------------------------------------------------------ tokens */
const movers=[]; // generic path followers
function mover(pathId,node,v,onEnd,dwellAt){const p=PATHS[pathId];const m={p,node,pos:0,v,onEnd,dwellAt,dwell:0};movers.push(m);return m;}
function place(node,p,pos){const pt=p.getPointAtLength(Math.max(0,Math.min(p.len,pos)));node.setAttribute('transform',`translate(${pt.x.toFixed(1)},${pt.y.toFixed(1)})`);}
function dot(cls,r){return el('circle',{r:r||2.6,class:cls},L.tok);}
const RECS=[]; // records on the main conveyor
const STATIONS=[
{id:'trim',at:955,dwell:2},
{id:'optical',at:1115,dwell:2.2},
{id:'label',at:1285,dwell:2.5},
{id:'pack',at:1465,dwell:3},
{id:'ship',at:1670,dwell:0.5,end:true},
];
function mkRecord(job,tp){
const g=el('g',{class:'rec'+(tp?' tp':'')},L.tok);
el('circle',{r:9,class:'disc'},g);
el('circle',{r:3.2,fill:job.color},g);
const r={node:g,pos:0,v:32,visited:{},dwell:0,job,tp,n:S.press.cycle};
RECS.push(r);return r;
}
function removeRec(r){const i=RECS.indexOf(r);if(i>=0)RECS.splice(i,1);r.node.remove();}
/* metal path job token */
const MSTAGES=[
{id:'audio',x:110,dwell:45,shape:'file',lbl:'audio files in'},
{id:'g1',x:210,dwell:8,shape:'file',lbl:'gate 1 · master plan'},
{id:'master',x:310,dwell:45,shape:'file',lbl:'vinyl master'},
{id:'g2',x:410,dwell:8,shape:'file',lbl:'gate 2 · master approved'},
{id:'cut',x:510,dwell:40,shape:'lacquer',lbl:'lacquer cut + scribed'},
{id:'silver',x:680,dwell:20,shape:'silver',lbl:'silvered'},
{id:'father',x:842,dwell:32,shape:'nickel',lbl:'father plating'},
{id:'mother',x:947,dwell:32,shape:'nickel',lbl:'mother plating'},
{id:'stamper',x:1052,dwell:32,shape:'stamper',lbl:'stamper plating'},
{id:'prep',x:1220,dwell:22,shape:'stamper',lbl:'stamper prep'},
];
const EXIT=[[1310,52],[1310,178],[790,178],[790,258]];
let job=null;
function newJob(){
S.jobN++;S.stamperN++;
const j={id:'PR-26-0'+S.jobN,stamper:'S-'+S.stamperN,color:PALETTE[S.jobN%PALETTE.length],stage:-1,x:40,y:52,dwell:0,exit:0,phase:'rail',node:el('g',{},L.top),shape:null,tpDone:0};
j.lbl=el('text',{x:14,y:-2,class:'tok'},j.node,'');
j.body=el('g',{},j.node);
setShape(j,'file');
S.gates.g1=S.gates.g2=false;G.g1.classList.remove('ok');G.g2.classList.remove('ok');
log('New title '+j.id+' received: audio intake, checksums verified','info');
return j;
}
function setShape(j,shape){
if(j.shape===shape)return;j.shape=shape;j.body.innerHTML='';
if(shape==='file'){el('rect',{x:-6,y:-8,width:12,height:16,rx:1,fill:'#1f2a44',stroke:'#7fb2ff'},j.body);for(let i=0;i<3;i++)el('line',{x1:-3,y1:-4+i*4,x2:3,y2:-4+i*4,stroke:'#7fb2ff','stroke-width':1},j.body);}
else if(shape==='lacquer'){el('circle',{r:9,fill:'#1a1408',stroke:'#c9a24a'},j.body);el('circle',{r:3,fill:'#c9a24a'},j.body);}
else if(shape==='silver'){el('circle',{r:9,fill:'#d9dee6',stroke:'#fff'},j.body);el('circle',{r:3,fill:'#8a97b0'},j.body);}
else if(shape==='nickel'){el('circle',{r:9,fill:'#8e98a6',stroke:'#c8d2e4'},j.body);el('circle',{r:3,fill:'#c8d2e4'},j.body);}
else if(shape==='stamper'){el('circle',{r:9,fill:'#b9c2cc',stroke:'#e6ecf7'},j.body);el('circle',{r:6,fill:'none',stroke:'#7d8794'},j.body);el('circle',{r:2,fill:'#3a4658'},j.body);}
}
function jobMove(ds){
if(!job){job=newJob();return;}
const j=job;
if(j.phase==='rail'){
const st=MSTAGES[j.stage+1];
if(j.stage>=0&&j.dwell>0){
j.dwell-=ds;
const cur=MSTAGES[j.stage];
if(cur.id==='prep'&&j.dwell<=0&&S.press.rack.length>=3){j.dwell=1;j.lbl.textContent=j.id+' · rack full, waiting';}
return;
}
if(!st){j.phase='exit';j.exit=0;return;}
// travel toward st.x
const dx=st.x-j.x;const step=60*ds;
if(Math.abs(dx)<=step){j.x=st.x;j.stage++;j.dwell=st.dwell;onStage(j,st);}else j.x+=Math.sign(dx)*step;
}else{
const tgt=EXIT[j.exit];const dx=tgt[0]-j.x,dy=tgt[1]-j.y;const d=Math.hypot(dx,dy);const step=90*ds;
if(d<=step){j.x=tgt[0];j.y=tgt[1];j.exit++;if(j.exit>=EXIT.length){S.press.rack.push({id:j.id,stamper:j.stamper,color:j.color});log(j.stamper+' ('+j.id+') delivered to press rack','ok');j.node.remove();job=null;return;}}
else{j.x+=dx/d*step;j.y+=dy/d*step;}
}
j.node.setAttribute('transform',`translate(${j.x.toFixed(1)},${j.y.toFixed(1)})`);
}
function onStage(j,st){
setShape(j,st.shape);j.lbl.textContent=j.id+' · '+st.lbl;
S.galv.active=null;
switch(st.id){
case 'g1':S.gates.g1=true;G.g1.classList.add('ok');log(j.id+': master plan approved by artist (gate 1)','ok');break;
case 'g2':S.gates.g2=true;G.g2.classList.add('ok');log(j.id+': master approved by artist (gate 2)','ok');break;
case 'cut':log(j.id+': lacquer cut, groove microscope OK, dead-wax matrix scribed','info');break;
case 'silver':log(j.id+': lacquer silvered, batch SV-'+(1180+S.jobN),'info');break;
case 'father':case 'mother':case 'stamper':S.galv.active=st.id;log(j.id+': '+st.id+' in nickel bath, '+f0(jit(180,10))+' A','info');break;
case 'prep':log(j.stamper+': XRF Ni '+f1(jit(0.25,0.02))+' mm, TTV '+f0(jit(14,4))+' µm, back marked, punched and coined','ok');break;
}
}
/* ------------------------------------------------------------------ press */
const SEQ=[['LOAD',3],['CLOSE',2],['STEAM',7],['COOL',11],['OPEN',2],['EJECT',3]];
function pressEnter(state){
const P=S.press;P.state=state;P.tIn=0;
if(state==='LOAD'){
P.cake.node=dot('',9);P.cake.node.setAttribute('fill','#3d3a33');P.cake.node.setAttribute('stroke','#8a7a55');
mover('cake',P.cake.node,142/2.6,m=>{m.node.remove();M.press.cake.setAttribute('visibility','visible');M.press.cakeLbl.forEach(r=>r.setAttribute('fill',P.job?P.job.color:'#e6ecf7'));});
S.ext.cake=jit(163,4);
for(let k=0;k<2;k++){const lb=el('circle',{r:3.2,fill:P.job?P.job.color:'#fff',stroke:'#e6ecf7','stroke-width':1},L.tok);mover('labels',lb,60,m=>{m.node.remove();});}
S.labelPairs--;if(S.labelPairs<=0){S.labelPairs=480;log('Labels: new pre-dried stack loaded at P1, lot L-77','info');}
}
if(state==='CLOSE'){}
if(state==='STEAM'){M.press.cake.setAttribute('visibility','hidden');}
if(state==='EJECT'){
const tp=P.tpLeft>0;if(tp)P.tpLeft--;
P.awaitGate=tp&&P.tpLeft===0;
P.cycle++;P.run++;
P.lastCycle=S.t-P.cycleStart;P.cycleStart=S.t;
const r=mkRecord(P.job,tp);
S.logged++;
const msg='P1 cycle #'+P.cycle+(tp?' TEST PRESSING':'')+' · '+f1(P.lastCycle)+' s · cake '+f0(S.ext.cake)+' g · labels L-77 · mold '+f0(S.lastMoldT)+' °C';
log(msg,tp?'warn':'info');
}
}
S.press.cake={};
function pressTick(ds){
const P=S.press;
if(P.state==='IDLE'){
if(P.rack.length){P.state='CHANGE';P.tIn=0;const j=P.rack[0];log('P1: mold change, mounting '+j.stamper+' for '+j.id,'warn');G.g3.classList.remove('ok');}
else{P.force+= (0-P.force)*ds;return;}
}
P.tIn+=ds;
if(P.state==='CHANGE'){
P.topY+= (300-P.topY)*ds*3;
if(P.tIn>=10){P.job=P.rack.shift();P.run=0;P.runQty=Math.round(rnd(12,20));P.tpLeft=3;S.press.cycleStart=S.t;log('P1: '+P.job.stamper+' mounted, warm-up done, 3 test pressings first','ok');pressEnter('LOAD');}
return;
}
if(P.state==='TPHOLD'){P.force+=(0-P.force)*ds*2;P.moldT+=(58-P.moldT)*ds*0.2;if(G.g3.classList.contains('ok')||S.t-P.holdSince>60){pressEnter('LOAD');}return;}
const idx=SEQ.findIndex(s=>s[0]===P.state);const dur=SEQ[idx][1];
// steam hold
P.waiting=false;
if(P.state==='STEAM'&&S.boiler.p<9.5){P.waiting=true;P.tIn=Math.min(P.tIn,dur-0.01);}
// physics
if(P.state==='CLOSE')P.topY=300+(384-300)*Math.min(1,P.tIn/dur);
if(P.state==='OPEN')P.topY=384-(384-300)*Math.min(1,P.tIn/dur);
if(P.state==='STEAM'){P.force+=(150-P.force)*ds*1.5;P.moldT+=(162-P.moldT)*ds*0.45;S.lastMoldT=Math.max(S.lastMoldT,P.moldT);}
else if(P.state==='COOL'){P.force+=(150-P.force)*ds;P.moldT+=(42-P.moldT)*ds*0.35;}
else {P.force+=(0-P.force)*ds*2;P.moldT+=(58-P.moldT)*ds*0.2;}
if(P.state==='LOAD'&&P.tIn<0.1)S.lastMoldT=P.moldT;
if(P.tIn>=dur&&!P.waiting){
if(P.state==='EJECT'){
if(P.awaitGate){P.awaitGate=false;P.state='TPHOLD';P.tIn=0;P.holdSince=S.t;log('P1: 3 test pressings out, press holds for artist approval (gate 3)','warn');return;}
if(P.run>=P.runQty){P.state='IDLE';P.job=null;log('P1: run complete, waiting for next stamper','warn');M.press.jobEl.textContent='';return;}
pressEnter('LOAD');
} else pressEnter(SEQ[idx+1][0]);
}
}
/* ------------------------------------------------------------------ conveyor */
function recTick(ds){
RECS.sort((a,b)=>b.pos-a.pos);
for(let i=0;i<RECS.length;i++){
const r=RECS[i];
if(r.dwell>0){r.dwell-=ds;continue;}
let maxPos=PATHS.route.len;
if(i>0){const ahead=RECS[i-1];maxPos=Math.min(maxPos,ahead.pos-24);}
let np=Math.min(r.pos+r.v*ds,Math.max(r.pos,maxPos));
// station arrival
for(const st of STATIONS){
const at=st.at-860;
if(!r.visited[st.id]&&np>=at){np=at;r.visited[st.id]=true;r.dwell=st.dwell;station(st,r);break;}
}
r.pos=np;
}
RECS.forEach(r=>{if(r.node.isConnected)place(r.node,PATHS.route,r.pos);});
}
function station(st,r){
if(st.id==='trim'){S.trimmed++;S.scrapKg+=0.018;const d=dot('',2.6);d.setAttribute('fill','#c2b280');mover('scrap',d,70,m=>{m.node.remove();S.regrindKg+=0.018;});}
if(st.id==='optical'){
S.logged++;
const bad=!r.tp&&Math.random()<0.035;
if(bad){
const defects=['non-fill','stitching','off-center hole','debris','label off-center','warp'];
const d=defects[Math.floor(Math.random()*defects.length)];
S.rej++;S.opticalLast='REJECT '+d;r.node.classList.add('rej');
log('Optical: record #'+r.n+' rejected ('+d+'), to grinder','alarm');
removeRec(r);L.tok.appendChild(r.node);
mover('reject',r.node,70,m=>{m.node.remove();S.regrindKg+=0.16;});
return;
}
S.opticalLast='pass #'+r.n;
if(r.tp||r.n%50===0){
const g=el('g',{class:'rec'+(r.tp?' tp':'')},L.tok);el('circle',{r:7,class:'disc'},g);el('circle',{r:2.4,fill:r.job.color},g);
mover('sample',g,45,m=>{m.node.remove();S.samples++;S.logged++;const sc=jit(98.2,1.2);S.audioLast=f1(sc);
log('Audio compare: record #'+r.n+(r.tp?' (TP)':'')+' score '+f1(sc)+', clicks '+f0(rnd(0,3))+'/side, noise floor OK',r.tp?'warn':'ok');
if(r.tp){r.job.tpDone=(r.job.tpDone||0)+1;if(r.job.tpDone===3){G.g3.classList.add('ok');log('Artist gate 3: test pressing approved for '+r.job.id+' (5 to 15 days in reality), production run released','ok');}}
},3);
}
}
if(st.id==='pack'){S.assembled++;S.logged++;S.good++;}
if(st.id==='ship'){removeRec(r);if(r.n%3===0)S.parcels++;else{S.pallet++;if(S.pallet>=300){S.pallet=0;log('Fulfillment: pallet of 300 closed and booked with carrier','ok');}}}
}
/* ------------------------------------------------------------------ misc flows */
let feedAcc=0,cartonAcc=0;
function flowsTick(ds){
feedAcc+=ds;
if(feedAcc>0.5){feedAcc=0;
const reg=Math.random()<0.2;const d=dot('',2.4);d.setAttribute('fill',reg?'#8ea36f':'#b9c2d0');
mover(reg?'feedr':'feedv',d,80,m=>{m.node.remove();const d2=dot('',2.4);d2.setAttribute('fill',reg?'#8ea36f':'#b9c2d0');
const hold=S.t<S.lab.testingUntil;mover('feed2',d2,80,m2=>m2.node.remove(),hold?2.5:0.6);});
}
cartonAcc+=ds;
if(cartonAcc>6){cartonAcc=0;const d=el('rect',{x:-6,y:-4,width:12,height:8,fill:'#8a6d3b',stroke:'#c9a24a'},L.tok);mover('carton',d,40,m=>m.node.remove());}
// regrind return: one dot per kg ground
if(S.regrindKg-flowsTick.lastReg>0.5){flowsTick.lastReg=S.regrindKg;const d=dot('',2.4);d.setAttribute('fill','#8ea36f');mover('regrind',d,120,m=>m.node.remove());}
// movers
for(let i=movers.length-1;i>=0;i--){
const m=movers[i];
if(m.dwellAt!=null&&m.pos>=m.p.len*0.99&&m.dwellAt>0){m.dwellAt-=ds;if(m.dwellAt>0){place(m.node,m.p,m.p.len);continue;}}
m.pos+=m.v*ds;
if(m.pos>=m.p.len){place(m.node,m.p,m.p.len);if(m.dwellAt>0)continue;movers.splice(i,1);m.onEnd&&m.onEnd(m);}
else place(m.node,m.p,m.pos);
}
}
flowsTick.lastReg=0;
/* ------------------------------------------------------------------ utilities */
function utilTick(ds){
const P=S.press;const B=S.boiler;
const steaming=P.state==='STEAM'&&!P.waiting, cooling=P.state==='COOL';
B.pTarget=(S.t<B.alarmUntil)?8.8:10.05;
const draw=steaming?0.3:0;
B.p+=(B.pTarget-draw-B.p)*ds*0.6;
B.flow+=((steaming?172:38)-B.flow)*ds*1.2;
S.chiller.ret+=((cooling?30.5:24)-S.chiller.ret)*ds*0.5;
S.chiller.load+=((cooling?96:26)-S.chiller.load)*ds*0.8;
S.chiller.flow+=((cooling?128:96)-S.chiller.flow)*ds*0.8;
S.comp.p+=((P.state==='LOAD'||P.state==='EJECT'?7.1:7.7)-S.comp.p)*ds*0.9;
S.hyd.p+=(((P.state==='CLOSE'||steaming||cooling)?180:30)-S.hyd.p)*ds*2;
S.hyd.oil+=((P.state==='IDLE'?40:46)-S.hyd.oil)*ds*0.05;
S.ext.rpm+=((P.state==='IDLE'||P.state==='CHANGE'?0:P.state==='LOAD'?42:28)-S.ext.rpm)*ds*1.5;
S.ext.melt+=((P.state==='IDLE'?90:158)-S.ext.melt)*ds*0.1;
S.hvac.t+=(23.2-S.hvac.t)*ds*0.02+(Math.random()-.5)*0.01;
S.hvac.rh+=(52-S.hvac.rh)*ds*0.02+(Math.random()-.5)*0.05;
// energy
const h=(S.clockStart+S.t)/3600;
const sun=Math.max(0,Math.sin(Math.PI*(h-6.5)/12));
S.energy.pv+=((247*sun*0.85)-S.energy.pv)*ds*0.2;
const load=150+(P.state==='IDLE'?0:30)+(cooling?70:20)+(steaming?12:0);
S.energy.load+=(load-S.energy.load)*ds*0.5;
const surplus=S.energy.pv-S.energy.load;
if(surplus>0){S.energy.soc=Math.min(100,S.energy.soc+surplus/271*ds/36);S.energy.grid+=(0-S.energy.grid)*ds;}
else{if(S.energy.soc>20){S.energy.soc=Math.max(20,S.energy.soc+surplus/271*ds/36);S.energy.grid+=(0-S.energy.grid)*ds;}else S.energy.grid+=((-surplus)-S.energy.grid)*ds;}
// silo levels
siloLvl(M.silo_v,0.72-((S.t/3600)%1)*0.02);
siloLvl(M.silo_r,0.35+Math.min(0.3,S.regrindKg/60));
// lab release
if(S.lab.testingUntil>0&&S.t>=S.lab.testingUntil){S.lab.testingUntil=-1;S.lab.lot=S.lab.nextLot;S.lab.nextLot='26-09'+(parseInt(S.lab.nextLot.slice(-2))+1);log('Material lab: lot '+S.lab.lot+' released by test pressing (optical + audio pass), retained sample archived','ok');}
if(B.alarmUntil>0&&S.t>=B.alarmUntil&&B.p>9.5){B.alarmUntil=-1;log('Boiler: steam pressure restored, '+f1(B.p)+' bar, press resumes','ok');}
S.galv.cur+=((S.galv.active?182:0)-S.galv.cur)*ds*2;
}
/* ------------------------------------------------------------------ render */
let renderAcc=0;
function render(dt){
const P=S.press,m=M.press;
m.topPl.setAttribute('y',P.topY);m.topSt.setAttribute('y',P.topY+18);
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);
renderAcc+=dt;if(renderAcc<0.2)return;renderAcc=0;
m.stateEl.textContent=P.waiting?'STEAM · WAIT':P.state==='TPHOLD'?'GATE 3 HOLD':P.state;
m.stateEl.style.fill=(P.waiting||P.state==='TPHOLD')?'#ffbf1f':(P.state==='IDLE'?'#8a97b0':'#e6ecf7');
m.jobEl.textContent=P.job?(P.job.id+' · '+P.job.stamper+(P.tpLeft>0?' · TP':'')):(P.rack.length?'rack: '+P.rack.map(r=>r.stamper).join(' '):'waiting for stamper');
MACH.forEach(mm=>{if(mm.tags&&mm.tagEls)mm.tags.forEach((fn,i)=>{mm.tagEls[i].textContent=fn();});});
// extruder zones
const zt=[jit(132,1),jit(146,1),jit(156,1)];
M.ext.zoneT.forEach((t,i)=>t.textContent=f0(P.state==='IDLE'?zt[i]-40:zt[i])+' °C');
M.ext.zoneEls.forEach((z,i)=>z.setAttribute('fill',P.state==='IDLE'?'#1b2438':['#3a2a1c','#4a2e1a','#5a301a'][i]));
// galv tanks
M.galv.tanks.forEach(t=>{const on=S.galv.active===t.name;t.g.classList.toggle('on',on);t.val.textContent=on?f0(jit(S.galv.cur,3))+' A · '+f0(jit(55,.6))+' °C':'idle · '+f0(jit(55,.6))+' °C';});
// lamps
const lamp=(id,cls)=>{const mm=M[id];if(mm&&mm.lamp)mm.lamp.setAttribute('class','lamp '+cls);};
const running=P.state!=='IDLE';
lamp('press',(P.waiting||P.state==='CHANGE'||P.state==='TPHOLD')?'warn':running?'run':'idle');
lamp('ext',running?'run':'idle');lamp('trim',running?'run':'idle');lamp('optical',running?'run':'idle');lamp('label',running?'run':'idle');lamp('pack',running?'run':'idle');lamp('ship','run');
lamp('boiler',S.boiler.p<9.5?'alarm':S.boiler.p<9.8?'warn':'run');
lamp('chiller','run');lamp('comp','run');lamp('hyd',running?'run':'idle');lamp('grind','run');lamp('energy','run');lamp('trace','run');lamp('hvac','run');
lamp('lab',S.t<S.lab.testingUntil?'warn':'run');
lamp('audioqc',movers.some(mv=>mv.p===PATHS.sample)?'run':'idle');lamp('labels',running?'run':'idle');lamp('carton','run');
const js=job?MSTAGES[job.stage]&&MSTAGES[job.stage].id:null;
['audio','master','cut','silver','prep'].forEach(id=>lamp(id,js===id?'run':'idle'));
lamp('galv',S.galv.active?'run':'idle');lamp('archive','idle');
// header
const c=S.clockStart+S.t;const hh=Math.floor(c/3600)%24,mi=Math.floor(c/60)%60,ss=Math.floor(c)%60;
$('kClock').textContent=[hh,mi,ss].map(v=>String(v).padStart(2,'0')).join(':');
$('kState').textContent=P.waiting?'HOLD':P.state==='TPHOLD'?'GATE 3':P.state;$('kState').style.color=(P.waiting||P.state==='TPHOLD')?'#ffbf1f':'';
$('kCycle').textContent=P.lastCycle?f1(P.lastCycle)+' s':'-- s';
$('kGood').textContent=S.good;$('kRej').textContent=S.rej;$('kYield').textContent=yieldStr();
$('kSteam').textContent=f1(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';
}
/* ------------------------------------------------------------------ log */
const logEl=$('log');
function log(msg,lvl){
S.events++;
const c=S.clockStart+S.t;const hh=Math.floor(c/3600)%24,mi=Math.floor(c/60)%60,ss=Math.floor(c)%60;
const d=document.createElement('div');d.className=lvl||'info';
d.innerHTML='<span class="t">'+[hh,mi,ss].map(v=>String(v).padStart(2,'0')).join(':')+'</span> '+msg;
logEl.prepend(d);while(logEl.children.length>60)logEl.lastChild.remove();
}
/* ------------------------------------------------------------------ details */
const DETAILS={
audio:{t:'1 · Audio intake',w:'Files received from the label or artist. Checksums and format are verified and a technical QA pass checks levels, clipping and side length against the chosen format. Nothing is cut until the artist confirms the master plan (gate 1).',c:['File checksums and format verification','Technical QA: levels, clipping, side length vs format','Artist gate 1: master plan approved, recorded in traceability'],f:['1 to 2 working days','Rejects here are cheap; rejects later are not']},
master:{t:'2 · Vinyl mastering',w:'The audio is mastered for the physical limits of the groove. Launch configuration uses an external engineer; the mature plant has its own room. The artist approves the vinyl master (gate 2) before anything is cut.',c:['Engineer QA of the vinyl master','Artist gate 2: master approved'],f:['2 to 4 days mature, 3 to 7 at launch','Mastering room ~40 to 60 m²']},
cut:{t:'3 · Lathe cutting',w:'The master is cut into a nitrocellulose lacquer on the lathe. Grooves are 40 to 80 µm wide, ~28 µm deep, 200 to 350 per inch. The dead-wax matrix code and an angular fiducial are scribed cold; the lacquer is highly flammable, so never with a laser. This scribe propagates through every metal generation onto every record.',c:['Groove inspection under microscope','Test cut evaluation','Dead-wax matrix scribe and angular fiducial (mechanical cold scribe)'],f:['1 to 2 days','Lacquer must be plated within roughly 24 to 72 h, so cutting and plating stay in the same building']},
silver:{t:'4a · Silvering',w:'The lacquer is sensitised and sprayed with silver to make its surface conductive, the first step of the metal path. Bath chemistry is logged per batch.',c:['Bath chemistry log per batch','Handled in the galvanics clean room with decontamination and PPE'],f:['Part of the 2 to 4 day galvanics block']},
galv:{t:'4b · Electroforming: father, mother, stamper',w:'Nickel is electroformed onto the silvered lacquer to grow a father (negative), from which a mother (playable positive) and then stampers (negatives) are grown. In-house galvanics removes the overseas stamper dependency that bottlenecks most plants.',c:['Bath chemistry logs per batch','XRF nickel thickness per part (0.2 to 0.3 mm, TTV target 10 to 20 µm)','Confocal or profiler groove metrology on sampled radial zones','Mother listening check (mothers are playable)','Fiber-laser Data Matrix on every part back'],f:['2 to 4 days','Clean room + water treatment + chemical store ~80 to 150 m²','Zero contaminated water commitment']},
prep:{t:'5 · Stamper prep',w:'The stamper is centre-punched, back-finished and coined to fit the press mold. Concentricity of the hole against the groove spiral is the critical check; an off-centre stamper makes every record wow.',c:['Concentricity and dimensional checks after punch, back-finish, coin','Visual inspection','Genealogy: stamper knows its mother, father and lacquer'],f:['1 day','Stamper prep room ~30 m²','Stampers last roughly 1,000 to 2,000 records']},
archive:{t:'Mother archive',w:'Archived mothers make a repress the fastest and highest-margin job the plant takes: pull a fresh stamper, prep, press. No artist gates, 7 to 11 working days.',c:['Storage conditions and genealogy per traceability spec','Retention policy to be stated in the customer contract'],f:['Choose 3-step plating so a mother exists to go back to']},
audioqc:{t:'QC layer 3 · Automated audio comparison, sampled',w:'A needle-drop capture station plays a sample and compares it to the source master by spectral envelope, counts clicks and pops, reads the noise floor from lead-in silence and channel balance from program RMS. It runs on every test pressing plus about one record in fifty, densifying as a stamper ages.',c:['Spectral-envelope comparison against the master (whole-program FFT fingerprint)','Click/pop detection and count','Noise floor and channel balance','Numeric score and pass/fail to traceability'],f:['Only this layer catches a wrong master, EQ errors, or pitch problems','100 % audio QC is impossible: one press outruns 80 to 95 listening stations']},
carton:{t:'Cartons and sleeves from local partners',w:'Jackets and sleeves come from local carton partners (Ploy Printing as intended partner). Every delivery passes an incoming gate before it reaches assembly, and the packaging path often becomes the critical path when artwork lands late.',c:['Dimensions (records must fit without force, spine per title)','Print Delta E against the approved proof','Seam strength, board grammage, lamination','Inner sleeve scuff check'],f:['10 to 20 working days from approved artwork','Artwork deadline is effectively a fourth approval gate']},
silo_v:{t:'Virgin PVC compound',w:'Record-grade compound, classically a vinyl chloride / vinyl acetate copolymer, makes up 80 % of the mix. Every lot is released through the material lab before it reaches the extruder and its ID attaches to every record pressed from it.',c:['Certificate of analysis per lot','Retained sample archived per lot'],f:['Qualifying Thai and regional compound against European references is an explicit goal']},
silo_r:{t:'Regrind stream',w:'Trims and rejected records are ground in-house and fed back as roughly 20 % of the feedstock. It is a material source in its own right and gets its own gate.',c:['No labels, paper or foreign material into the grinder','Contamination check per regrind batch','Virgin/regrind ratio verified at the extruder','Regrind batch IDs logged like any other lot'],f:['Newbilt process schematic: 80 % new granulate, 20 % recycled']},
lab:{t:'M · Material lab gate',w:'Every incoming compound lot passes a benchtop gate before release. New lots, and especially new suppliers, are released only after test pressings from that lot pass optical and audio QC.',c:['Moisture (halogen analyzer or Karl Fischer): the most common cause of noisy surfaces','Heat stability (oven strips, ISO 182): PVC degradation releases HCl','Density, color L*a*b* and gloss, granulate sieve analysis, ash','Release by test pressing'],f:['Later: torque rheometry, DSC (degree of gelation), FTIR','External per supplier: K-value, XRF stabilizer system, TGA, resistivity']},
ext:{t:'6a · Extruder VEX 50',w:'Granulate from the hopper is melted through three electrically heated zones and extruded as a cake (puck) of about 160 g for a 140 g record, the surplus becoming flash. The cake is sandwiched between the bottom and top label and loaded into the press.',c:['Zone temperatures and screw speed logged per cycle','Cake weight per cycle (press telemetry)','Regrind ratio verified here'],f:['15 kVA connected, 8 kW average','660 kg on ~0.5 m²','Cooling water ~4 l/min, air 2 m³/h']},
press:{t:'6b · Hydraulic press HRP 12',w:'The stamper pair is mounted on the platens. Each cycle: bottom label, cake and top label go in between the stampers, close, steam at 10 bar / 180 °C heats the molds and the ~150 t force flows the vinyl into the grooves, cooling water sets it, open, eject. About 28 to 30 s per record. Telemetry is logged every cycle, and the artist approves the test pressing (gate 3) before the production run.',c:['Per cycle: nozzle pressure, mold temperatures, cycle time, cake weight, label lot, yields, drift alerts','Artist gate 3: test pressing approved','Record identity = stamper ID + press ID + cycle timestamp + side'],f:['1,350 kg on 0.9 m²: slab loading is a building criterion','Steam 150 to 180 kg/h at 9.5 to 10.5 bar','Cooling up to 100 l/min, up to 125 kW','Fleet plan: 50/50 automated (Pheenix Alpha class) and semi-automatic (Newbilt) presses']},
trim:{t:'7a · Trimmer',w:'The Vario trimmer cuts the flash from the pressed record. Trimmings go straight to the grinder and back into the regrind stream.',c:['Trim quality on every record'],f:['12" now, 7"/10" upgrade path']},
optical:{t:'QC layer 1 · 100 % optical inspection, inline',w:'Machine vision at line speed inspects every record with darkfield lighting and line-scan cameras. Non-fill and stitching correlate with audible defects, so this is a strong proxy for audio quality without playing the record. No vinyl-specific product exists; this is a custom Keyence/Cognex-class integration.',c:['Non-fill (dull patches), stitching and flow lines','Warp via structured light','Off-centre spindle hole vs groove spiral','Surface debris, label errors'],f:['Rejects go to the grinder; the cycle stays in the traceability record','The dead-wax fiducial gives the camera a fixed angular anchor']},
label:{t:'7b · Finishing',w:'The trimmed record goes into its inner sleeve and, once assembled, through the shrink wrapper. The labels are already on: they were pressed into the record in the mold, not applied here.',c:['Inner sleeve scuff check per sleeve lot','Shrink wrap on a sampling basis'],f:['Shrink wrapper and buffer spindles in the Newbilt scope']},
labels:{t:'L · Labels',w:'Center labels are pressed into the record, not glued on afterwards. Each cycle the operator (or the automatic label feed on an automated press) places the bottom label, the cake and the top label between the stampers; the steam-heated mold bonds the paper into the vinyl. Labels are pre-dried in the oven before they reach the press because moisture in the paper causes bubbles, and the label lot is logged per press cycle.',c:['Print inspection of each label lot against the approved proof','Pre-dried in the oven (Newbilt quotes a France Etuves XU 112 class oven)','Label identity and centering checked by the optical inspection on every record'],f:['Label dry oven and 7" center label punch in the Newbilt scope','Vision-guided label rotation at the press is feasible but deprioritized; the dead-wax fiducial keeps it measurable']},
pack:{t:'8 · Packaging and assembly',w:'The highest-value check in the plant: the runout matrix code is scanned against the jacket barcode at the packing station, so the wrong record in the right jacket is systematically caught. Inserts and extras are checked against the order bill of materials.',c:['Matrix code vs jacket barcode on every unit','Barcode verification (a misprinted barcode is a rejected delivery at the distributor)','Inserts per bill of materials','Final visual on a sampling basis'],f:['Packaging + finished goods ~300 to 500 m²']},
ship:{t:'9 · Fulfillment',w:'Consignments are assembled against the order, shipped as bulk pallets or dropshipped parcel by parcel to end customers on behalf of the label. Carrier tracking is recorded per shipment or per end customer and joins the traceability record.',c:['Consignment vs bill of materials','Tracking per shipment or per dropshipped customer'],f:['Quoted lead time: 7 to 9 weeks mature, 2 to 3 weeks repress, from test-pressing approval']},
boiler:{t:'Steam boiler',w:'Saturated steam at 10 bar / 180 °C heats the press molds. Sized at 400 kg/h (~300 kW thermal) for the Duplex path; one press draws 150 to 180 kg/h. Sourced and installed locally with water treatment, in a detached boiler hall with a 14 m flue.',c:['Pressure held between 9.5 and 10.5 bar at the press','Feedwater treatment, condensate return'],f:['Fuel decision open: biomass (own 60 rai coppice + northern agricultural waste), LPG, or electric (~300 kW)','Boiler hall 66 m², fuel store 42 m²']},
chiller:{t:'Process cooling',w:'Cooling water at 20 to 25 °C sets the vinyl after the steam phase. 98 % of the 250 kW cooling load is the molds. Open cooling tower with a heat exchanger, chiller in its own 60 m² room.',c:['Supply 20 to 25 °C, 10 bar, 7 to 8 °dH, max 200 µm particles','Flow 128 l/min typical, 220 l/min peak (Duplex)'],f:['~80 to 125 kW electrical','Outlet water reaches about 60 °C from the press']},
comp:{t:'Compressed air',w:'Dry, filtered air at 6 to 10 bar for press and extruder actuators.',c:['Dew point max 3 °C, impurities max 5 µm'],f:['14 m³/h for a Duplex system']},
hyd:{t:'Hydraulic aggregate',w:'Supplies the ~150 t press force at 180 bar from a gas-loaded accumulator. Sized for a second press from day one, which is the Duplex upgrade path.',c:['Oil temperature and pressure logged','200 l HLP 46 mineral oil, customer supplied'],f:['15 kVA, 12 kW average, 600 kg','Preferably outside the pressing room']},
grind:{t:'PVC grinder',w:'Trims and rejected records are ground to regrind, which feeds back to the regrind silo as the ~20 % recycled share of the feedstock.',c:['No labels or paper into the grinder','Contamination check per regrind batch'],f:['In the Newbilt scope']},
energy:{t:'Energy',w:'247 kWp of roof PV on the phase 1 building (~498 kWp with phase 2), a 271 kWh Gobel Power battery stack for peak shaving and ride-through, and a PEA medium-voltage connection with a 500 to 1,000 kVA transformer sized by the boiler decision.',c:['Plant peak 300 to 450 kW without electric boiler, 600 to 750 kW with it'],f:['Northern Thailand yield ~1,400 to 1,500 kWh/kWp/yr','PV covers roughly 28 to 45 % of consumption without electric steam']},
trace:{t:'Traceability',w:'Every step logs at the source: press and rectifier PLCs, lab instruments and vision stations write directly, manual steps get operator check-ins. Logs are append-only and retained for the life of the customer relationship. Customers get a per-order view of the whole chain.',c:['Record key: stamper + press + cycle timestamp + side','Every artifact has an ID and knows its parents','Environment (HVAC per zone) logged on the same timeline'],f:['Machine data access is a purchasing requirement','A complaint about one record resolves to stamper, press, shift, operator, compound lot, packaging lot']},
hvac:{t:'Whole-factory HVAC',w:'The press room is held at 20 to 26 °C and 35 to 60 % RH per the Newbilt site requirements; galvanics, archive and audio rooms have their own zones. Temperature, humidity and particulate per zone are logged with production.',c:['Temperature, RH and PM per zone','Correlate any production window with plant conditions'],f:['~100 to 200 kW peak (estimate)','~1,500 m² conditioned']},
};
let selected=null;
function select(id){
if(selected)selected.g.classList.remove('sel');selected=M[id];selected.g.classList.add('sel');
const d=DETAILS[id];if(!d)return;
const p=$('detail');p.innerHTML='';
const h=document.createElement('h2');h.textContent=d.t;p.appendChild(h);
const w=document.createElement('p');w.textContent=d.w;p.appendChild(w);
if(d.c){const h3=document.createElement('h3');h3.textContent='Checks and gates';p.appendChild(h3);const ul=document.createElement('ul');d.c.forEach(x=>{const li=document.createElement('li');li.textContent=x;ul.appendChild(li);});p.appendChild(ul);}
if(d.f){const h3=document.createElement('h3');h3.textContent='Planning figures';p.appendChild(h3);const ul=document.createElement('ul');d.f.forEach(x=>{const li=document.createElement('li');li.textContent=x;ul.appendChild(li);});p.appendChild(ul);}
}
/* ------------------------------------------------------------------ controls */
$('btnPlay').addEventListener('click',()=>{S.running=!S.running;$('btnPlay').textContent=S.running?'Pause':'Play';$('btnPlay').classList.toggle('on',S.running);$('svg').classList.toggle('paused',!S.running);});
document.querySelectorAll('[data-speed]').forEach(b=>b.addEventListener('click',()=>{S.speed=+b.dataset.speed;document.querySelectorAll('[data-speed]').forEach(x=>x.classList.toggle('on',x===b));}));
$('btnSteam').addEventListener('click',()=>{S.boiler.alarmUntil=S.t+22;log('Boiler: steam pressure falling, feedwater pump fault (demo)','alarm');});
$('btnLot').addEventListener('click',()=>{if(S.t<S.lab.testingUntil)return;S.lab.testingUntil=S.t+40;log('Material lab: new PVC lot '+S.lab.nextLot+' received, CoA checked, moisture '+f2(jit(0.04,.01))+' %, held for test pressing','warn');});
document.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target===document.body){e.preventDefault();$('btnPlay').click();}});
/* ------------------------------------------------------------------ bootstrap */
S.press.rack.push({id:'PR-26-041',stamper:'S-2261',color:PALETTE[41%PALETTE.length]});
log('Shift start 08:00. Boiler at pressure, chiller online, HVAC in range.','ok');
log('Material lab: lot 26-0912 released (moisture 0.03 %, heat stability OK, ΔE 0.3)','ok');
select('press');
let last=performance.now();
function frame(now){
const dt=Math.min(0.1,(now-last)/1000);last=now;
if(S.running){
let ds=dt*S.speed;
// sub-step for stability at high speed
const n=Math.ceil(ds/0.25);const sub=ds/n;
for(let i=0;i<n;i++){S.t+=sub;pressTick(sub);recTick(sub);flowsTick(sub);utilTick(sub);jobMove(sub);}
}
render(dt);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>