From b672c58e34360f5323627f067f39c222285ecdb0 Mon Sep 17 00:00:00 2001 From: grabowski Date: Sat, 3 Oct 2026 22:47:57 +0200 Subject: [PATCH] Plant page: mature fleet at real cycle times Rebuild docs/plant-operation.html from the single P1 press into the mature plant: Newbilt Duplex (two HRP 12 on a VEX 50, EX 35 for splatter), two Newbilt Classic automatic presses with edge trimmers, label punch and Granulator 400; Pheenix Alpha PASU, PPPU, two PDPU and PMPB; Sibert VSF, VSP and VSC in that order; father and mother archive and the stamper rack. Time runs at 1x real time with speeds up to 3600x. Press cycles are Newbilt's (Classic 28 s, Duplex ~30 s), plating follows Faraday's law at the PDPU current, and the operator steps use the galvanics.md table, with unpublished times marked as estimates. Titles flow through the three artist gates in days; test pressings unmount and the presses run approved work meanwhile; worn stampers and represses come from the archived mothers. Two shifts, unplanned press stops sized to scale.md's 60 to 70 % effectiveness, and demos for a steam dip, a new PVC lot and a repress. The boiler and chiller are shown at 800 kg/h and 500 kW (estimates) and the detail panels flag that scale.md's 400 kg/h and 250 kW are Duplex figures; scale.md itself is unchanged. Also commits the galvanics.md cycle-time table the page runs on (one PDPU covers demand, the second is redundancy) and updates the README and the test-site descriptions. --- README.md | 14 +- docs/equipment/galvanics.md | 57 +- docs/plant-operation.html | 1335 +++++++++++++++++++---------------- site/public/index.html | 12 +- 4 files changed, 764 insertions(+), 654 deletions(-) diff --git a/README.md b/README.md index deaf32f..2a35476 100644 --- a/README.md +++ b/README.md @@ -131,12 +131,14 @@ The detail lives in dedicated documents: fan-funded campaign that the plant presses and ships to each fan once enough have pre-ordered, with refunds if not - [Plant operation page](docs/plant-operation.html): a single static page - animating the whole plant as a simulated SCADA overview, from PVC intake - and the material lab through extruder, press, trim, optical inspection, - finishing, packaging and fulfillment, with the metal path, utilities and - energy alongside; open it in a browser, every station is clickable; - on phones and tablets held sideways it pinch-zooms and opens station - details in a side sheet + animating the mature plant as a simulated SCADA overview at real cycle + times: the Newbilt Duplex and two Classic presses, the Pheenix Alpha + electroforming line and Sibert stamper prep, from PVC intake and the + material lab through pressing, inspection, packaging and fulfillment, + with the three artist gates, the mother archive, utilities and energy; + 1× is real time, up to 3600× (a day in 24 s); every station is + clickable and names the source of its figures; on phones and tablets + held sideways it pinch-zooms and opens station details in a side sheet - [Test site](site/README.md): `site/` builds https://cnxvinyl.lol/, the internal test site with demo pages from `docs/` (the plant operation page first) and any static site we want to share by link; Gitea Actions diff --git a/docs/equipment/galvanics.md b/docs/equipment/galvanics.md index 6f9ebfc..0622cc6 100644 --- a/docs/equipment/galvanics.md +++ b/docs/equipment/galvanics.md @@ -102,10 +102,24 @@ What this changes in the plan: no vendor cloud. The maintenance alerts are the bath events the galvanics record in [traceability](../traceability.md) needs anyway. -### How many cells (estimate) +### Cycle times and cell count -The cell count is the number that sizes the line, and no vendor has stated -a cycle time. A rough count from this repo's own figures: +Pheenix publishes no cycle times. Published practice from other plating +rooms agrees with Faraday's law at the PDPU's 200 A ceiling (1.095 g of +nickel per ampere-hour), and these are the times the +[plant operation page](../plant-operation.html) runs on: + +| Step | Time | Source | +|---|---|---| +| Silvering (clean, sensitise, spray, rinse) | ~15 min | estimate; no published figure | +| Pre-plate | 15 to 30 min at 10 to 20 A | ~15 min at 35 °C (flight 13 duplication, Freiburg, in Leuze Verlag's Galvanotechnik report "Of fathers, mothers and sons"); ~20 min at ~20 A (Welcome to 1979, Nashville, on a Digital Matrix line, in Finishing and Coating); 30 min at 10 to 20 A (US patent 4,363,705) | +| Father | ~1 h 15 min | ~0.2 mm and ~200 g at 150 A and 11 V (flight 13); 1¼ h with the current raised from 20 to 200 A (US 4,363,705) | +| Mother | ~1 to 1¼ h | 1¼ h at 200 A (US 4,363,705); "as little as one hour" (Wikipedia, Production of phonograph records) | +| Stamper | ~45 min | a 145 g stamper in 45 min (Wikipedia, same article) | +| Separation, passivation, cleaning and drying (PMPB) | ~10 to 15 min | estimate | +| Back-finish (VSF), centre punch (VSP), coin (VSC) | ~5 to 10, 3 to 5 and 1 to 2 min | estimate; Sibert publishes none | + +The count, from this repo's own demand figures: - At full capacity the [revenue model](../finance/revenue-model.md) assumes 300 to 600 titles a year across 1.0 to 1.5 million records, so 2,500 to @@ -114,18 +128,18 @@ a cycle time. A rough count from this repo's own figures: (at 1,000 to 2,000 pressings per stamper): 8 to 10 electroformed parts per title, **~2,400 to 6,000 parts a year**, 8 to 20 per working day at 300 days, before remakes. -- A 12" part of ~0.25 mm nickel is roughly 150 to 200 g. At the PDPU's - 200 A ceiling (1.095 g of nickel per ampere-hour) that is under an hour of - deposition; with the current ramp, loading, rinsing and separation, - **~2 to 3 hours per part per cell** (estimate). -- Two shifts give 5 to 8 parts per cell per day, 10 to 16 per PDPU. +- With ~15 minutes of loading, rinsing and unloading on top of 45 to 75 + minutes of deposition, a cell turns a part in ~1 to 1.5 hours: 10 to 16 + parts per cell per two-shift day, **20 to 32 per PDPU**. -So **one PDPU covers the low end and the high end needs two** (or a third -shift). One PASU, one PPPU (pre-plate is for the masters, 2 to 4 a day at -full capacity, and the unit takes four at once) and one PMPB cover the -rest. Thicker fathers and mothers scale the time up in proportion. Ask -Pheenix for the measured cycle per part at the plant's thickness before -this goes into a budget. +So **one PDPU covers full-capacity demand** with headroom for remakes, and +a second PDPU buys redundancy rather than capacity, which is how Pheenix +itself sells it ("more capacity as well as enhanced redundancy"). The +first version of this page assumed 2 to 3 hours per part and concluded +that the high end needed two. One PASU, one PPPU (pre-plate is for the +masters, 2 to 4 a day at full capacity, and the unit takes four at once) +and one PMPB cover the rest. Ask Pheenix for measured cycles at the +plant's thickness before this goes into a budget. ## Stamper prep: the Sibert VS line @@ -149,10 +163,13 @@ What this changes in the plan: the air network has to reach the stamper prep room. - **Weights 200, 250 and 600 kg**, on castors: pallet-truck moves, not crane lifts ([crane table](../crane-workshop-tours.md) updated). -- **Order of operations.** The VSC locates on the punched centre hole, so - the punch comes before the coiner. The - [process flow](../process-flow.md) already lists punch, back-finish, - coin. +- **Order of operations: finish, punch, coin.** Gotta Groove's + Electromastering Lab in Cleveland, which runs a Sibert VSF and VSP, + back-sands first, then centres and punches on the VSP, then trims and + coins the edge and forms the centre hole for the press type + (recordplating.com). The VSC locates on the punched hole, which fits the + same order. The [process flow](../process-flow.md)'s "punch, back-finish, + coin" names the operations, not their sequence. - **Centring is manual.** The VSP pre-centres pneumatically, then the operator centres the lead-out groove under ×100 optics against the graticule. Concentricity depends on the operator, so the stage 5 check @@ -163,7 +180,9 @@ What this changes in the plan: them: the answer decides whether stamper prep stays a manual check-in station in the [digital factory](../digital-factory.md). - **Mould fit.** The standard punch is Ø25.4 mm, other sizes on request, - and the VSC claims tooling for every vinyl mould. Which centre hole and + and the VSC claims tooling for every vinyl mould. Gotta Groove keeps nine + centre-hole forming tools to fit the presses its customers run, which + shows the scale of the question. Which centre hole and edge form each mould needs (Newbilt book mould, Pheenix/Toolex, M-Tech) has to come from the press vendors, and the tooling sets per mould type are a quote line. Whether the VSC duplicates anything a press vendor diff --git a/docs/plant-operation.html b/docs/plant-operation.html index d5d8167..24d36c9 100644 --- a/docs/plant-operation.html +++ b/docs/plant-operation.html @@ -19,10 +19,10 @@ 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:12px;margin-left:auto;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:none} .kpis::-webkit-scrollbar{display:none} -.kpi{display:flex;flex-direction:column;min-width:70px;flex:0 0 auto} +.kpi{display:flex;flex-direction:column;min-width:62px;flex:0 0 auto} .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;flex:0 0 auto} +.ctl{display:flex;flex-wrap:wrap;gap:6px;align-items:center;flex:0 1 auto} 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} @@ -60,7 +60,7 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr header{flex-wrap:nowrap;gap:8px;padding:4px 8px} header h1{font-size:13px} header h1 span{display:none} - .ctl{gap:4px} + .ctl{gap:4px;flex-wrap:nowrap} button{padding:4px 7px;font-size:11px;min-height:28px} .kpis{flex:1 1 0;gap:10px} .kpi{min-width:56px} @@ -86,14 +86,17 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr /* 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} +.machine:hover .mbox,.machine:hover .frame{stroke:#4f7ac7} +.machine.sel .mbox,.machine.sel .frame{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} +.ctxt{fill:#c8d2e4;font-family:var(--mono);font-size:9.5px} .small{fill:#8a97b0;font-size:9px} .lane{fill:#5f6f8c;font-size:10px;letter-spacing:.14em;text-transform:uppercase;font-weight:600} +.roomlbl{fill:#6f84a8;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:600} +.room{fill:rgba(40,60,90,.08);stroke:#2f4566;stroke-dasharray:6 5;stroke-width:1.2} .lamp{fill:#6b7a90} .lamp.run{fill:#3ddc84} .lamp.warn{fill:#ffbf1f} @@ -103,10 +106,9 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr .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} +.paused .flow,.paused .bub,.paused .convd,.paused .screw{animation-play-state:paused} +.steam{stroke:#ff5a3c}.cond{stroke:#c96a4e}.cw{stroke:#3aa0ff}.cwr{stroke:#7fc0ff}.hyd{stroke:#f5a623}.chw{stroke:#2e7fd0}.di{stroke:#9ad7ff} +.mat{stroke:#b9c2d0}.reg{stroke:#8ea36f}.col{stroke:#c86bff}.metal{stroke:#c0c8d6}.scrap{stroke:#c2b280}.route{stroke:#3a4d70}.lbl{stroke:#e6ecf7} .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} @@ -115,15 +117,20 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr .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} +.cellr{fill:#0e1a2a;stroke:#3a5a7a} +.cell.on .cellr{stroke:#3ddc84} +.cell.done .cellr{stroke:#ffbf1f} .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} +.cell.on .liquid{fill:#1a6a4a} +.bub{fill:#8fd3ff;opacity:0;animation:rise 2.2s linear infinite} +.cell.on .bub{opacity:.7} +@keyframes rise{from{transform:translateY(0)}to{transform:translateY(-30px)}} +.pos{fill:#0e1a2a;stroke:#3a5a7a} +.pos.on{stroke:#3ddc84;fill:#123326} +.thk{fill:#1a2640} +.thkv{fill:#9fb3c8} +.pbar{fill:#1a2640} +.pbarv{fill:#3ddc84} .platen{fill:#2a3a56;stroke:#4a5f85;transition:fill .5s} .platen.heat{fill:#b3402c} .platen.cool{fill:#2c5b8f} @@ -131,33 +138,37 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr .silo{fill:#121a2b;stroke:#2a3a56} .fill{fill:#3b4658} .fill.reg{fill:#4e5a3a} -.tok{font-family:var(--mono);font-size:10px;fill:#c8d2e4} +.fill.col{fill:#5a3b6a} .hopper{fill:#121a2b;stroke:#2a3a56} .zone{stroke:#2a3a56;stroke-width:1} -.bar{fill:#1a2640} -.barv{fill:#3aa0ff} -.warntxt{fill:#ffbf1f} +.screw{stroke:#6d7d99;stroke-width:2;stroke-dasharray:6 6;animation:dash 0.9s linear infinite;fill:none}
-

CNX Vinyl · Poy Records · Plant overview · P1 semi-automatic line

+

CNX Vinyl · Poy Records · Plant overview · mature fleet

- - + - - + + + + + +
-
08:00:00sim clock
-
IDLEpress P1
-
-- slast cycle
+
D1 08:00:00sim clock
+
--shift
+
0/4presses
+
--cycles/h
0good today
0rejects
--yield
+
0metal in work
+
0sets ready
-- barsteam
-- kWplant load
-- kWsolar
@@ -165,7 +176,7 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr
- + @@ -188,22 +199,24 @@ footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr

Click any station

-

Every box is a stage of the plant. Click it (tap on a phone or tablet; pinch or use the zoom buttons to get close) 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.

+

The mature plant: the Newbilt Duplex (two HRP 12 presses on one VEX 50 extruder, with an EX 35 for colour effects), two Newbilt Classic automatic presses, the Pheenix Alpha electroforming line and Sibert stamper prep. At 1× everything runs at its real duration: press cycles of 28 s (Classic) and about 30 s (Duplex), 45 to 75 minutes of plating per part, artist approvals in days. Speed up to watch whole days (3600× runs a day in 24 s). Click a station for what happens there, its checks, and where its figures come from; durations without a published source are marked as estimates. Simulated from this repository (equipment/galvanics.md, equipment/pressing-line.md, production-timeline.md, scale.md); nothing here is live.

Event log

Legend

Saturated steam 10 bar
Condensate return
-
Cooling water supply
+
Press cooling water
Cooling water return
Hydraulic oil 180 bar
-
Compressed air
-
PVC granulate
-
Regrind (20 % share)
-
Metal path (lacquer to stamper)
-
Trim scrap and rejects
-
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).
+
Galvanics cooling 12 to 20 °C
+
DI water
+
PVC granulate
+
Regrind
+
Colour compound
+
Stamper sets to presses
+
Edge trim and rejects
+
Lamps: green run · grey idle · amber hold, change or waiting · red alarm. Yellow diamonds are the three artist approval gates, with the number of titles waiting at each. Two shifts, 06:00 to 22:00: presses and operator steps stop at night, plating already in the bath finishes. Discs on the metal path: gold lacquer, silver silvered lacquer, grey father or mother, ringed stamper.
@@ -218,657 +231,731 @@ 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 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)); +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']; +const TP_N=10; // test pressings per new title +const STEAM_KG=1.3; // steam per press cycle: 150 to 180 kg/h at ~30 s -/* ------------------------------------------------------------------ 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'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)+' %':'--';} +/* ================================================================ state */ +const S={t:0,speed:1,running:true,quiet:true,clock0:8*HOUR-2*DAY, + day:{n:null,good:0,rej:0,tp:0,stampers:0},cyc:[],events:0,logged:0,samples:0,audioLast:'-',opticalLast:'-',lastXRF:'-',lastConc:0, + trimmed:0,sleeved:0,assembled:0,pallet:0,parcels:0,mismatch:0,punched:0,punchUntil:0,tpToArtist:0,pulled:0, + boiler:{p:10.05,F:0.17,faultUntil:-1}, + chiller:{sup:22,ret:30,load:280,flow:230}, + comp:{p:7.6},hyd:{p:30,oil:44},vex:{rpm:0,melt:150},ex35:false, + lab:{lot:'26-0912',test:null,queue:[],nextLot:913}, + silo:{v:2900,vcap:4000,r:240,rcap:600,c:700,ccap:1000}, + gran:{bin:4,run:0,end:0,total:0}, + labels:{pairs:1800,lot:'L-77'}, + hvac:{t:23.2,rh:52,pm:8},energy:{pv:0,load:0,grid:0,soc:70}, + diro:{tank:0.72,cond:0.6},zld:{tank:0.35},galvKW:0, + archive:{F:96,M:104},nextArrival:0, +}; +const clk=t=>S.clock0+(t==null?S.t:t); +function hourOf(t){const c=clk(t);return (((c%DAY)+DAY)%DAY)/HOUR;} +function shiftOn(t){const h=hourOf(t);return h>=6&&h<22;} +const dayN=t=>Math.floor(clk(t)/DAY)+1; +function clockStr(){const c=clk();const s=((c%DAY)+DAY)%DAY;const hh=Math.floor(s/3600),mi=Math.floor(s/60)%60,ss=Math.floor(s)%60;return 'D'+dayN()+' '+[hh,mi,ss].map(v=>String(v).padStart(2,'0')).join(':');} +// per-record tokens only up to 10x; part hops and feed dots up to 600x +const vis=()=>!S.quiet&&S.speed<=10; +const visHop=()=>!S.quiet&&S.speed<=600; -/* ------------------------------------------------------------------ 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,'')); +/* ================================================================ titles and upstream stages */ +const UP=[['intake',1,2],['gate1',2,10],['master',2,4],['gate2',2,10],['cut',1,2]]; // days, min to max +const UPI={};UP.forEach((u,i)=>UPI[u[0]]=i); +const QTY=[300,500,1000,1000,2000,3000,3000,5000,5000,10000]; // mean ~3,100 +const TITLES=[];let titleN=40; +function mkTitle(o){o=o||{};titleN++;const r=Math.random(); + const kind=o.kind||(r<0.6?'black':r<0.85?'colour':'splatter'); + const qty=o.qty||pick(QTY);const sid=String(titleN).padStart(3,'0'); + const T={id:'PR-26-'+sid,sid,kind,qty,wt:Math.random()<0.55?180:140, + label:PALETTE[titleN%PALETTE.length],vinyl:kind==='black'?'#0b0b0d':PALETTE[(titleN+3)%PALETTE.length], + stage:o.stage||'intake',tEnd:0,setsTarget:Math.max(1,Math.ceil(qty/1500)),made:{A:0,B:0},ready:{},pairs:0, + cycles:0,good:0,target:Math.ceil(qty*1.04),approved:!!o.approved,tp:o.tp||'none',tpBusy:false,g3due:0, + repress:!!o.repress,faDone:false,mothers:{A:null,B:null},mBusy:{A:false,B:false},done:false,remakes:0}; + TITLES.push(T);return T;} +const cntStage=st=>TITLES.reduce((n,T)=>n+(T.stage===st?1:0),0); +function nextUp(st){let m=Infinity;for(const T of TITLES)if(T.stage===st&&T.tEnd=S.nextArrival){const T=mkTitle();T.tEnd=S.t+rnd(1,2)*DAY;S.nextArrival=S.t+rnd(0.55,0.85)*DAY; + log(T.id+' received: '+T.qty+' × 12" '+T.wt+' g '+T.kind+', checksums verified','info');} + for(const T of TITLES){ + const i=UPI[T.stage]; + if(i!==undefined&&S.t>=T.tEnd){ + if(i=T.g3due){T.tp='ok';T.approved=true;log(T.id+': artist approved the test pressing (gate 3), production released','ok');flash('g3');} } - 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)); +/* ================================================================ metal path: operator stations */ +const ST={}; +function mkStation(id,cap,durFn,next){ST[id]={id,cap,slots:Array(cap).fill(null),q:[],durFn,next,done:0};} +function enq(id,it,from){ST[id].q.push(it);if(from)hop(from,id,it);} +const pl=it=>it.gen+' '+it.T.sid+it.side+(it.gen==='S'?'-'+it.n:''); +const jl=j=>j.grow==='S'&&j.st?pl(j.st):j.grow+' '+j.T.sid+j.side; +function startGalv(T){T.inGalv=true;['A','B'].forEach(s=>enq('pasu',{T,side:s,gen:'L'},'cut'));} +mkStation('pasu',1,()=>15*MIN,it=>{it.silvered=true;enq('pppu',it,'pasu');}); +mkStation('pppu',4,()=>20*MIN,it=>plateEnq({mand:it,grow:'F',T:it.T,side:it.side},'pppu')); +mkStation('pmpb',1,j=>j.grow==='S'?10*MIN:12*MIN,j=>sepDone(j)); +mkStation('qc',1,it=>it.gen==='M'?25*MIN:3*MIN,it=>qcDone(it)); +mkStation('vsf',1,()=>8*MIN,it=>enq('vsp',it,'vsf')); +mkStation('vsp',1,()=>4*MIN,it=>{it.conc=Math.max(3,jit(11,10));S.lastConc=it.conc;enq('vsc',it,'vsp');}); +mkStation('vsc',1,()=>2*MIN,it=>stamperReady(it)); +function sepDone(j){const T=j.T,s=j.side;S.diro.tank=Math.max(0.05,S.diro.tank-0.03);S.zld.tank=Math.min(1,S.zld.tank+0.01); + if(j.grow==='F')enq('qc',{T,side:s,gen:'F'},'pmpb'); + else if(j.grow==='M'){S.archive.F++;enq('qc',{T,side:s,gen:'M'},'pmpb');log(T.sid+s+': mother separated, father '+T.sid+s+' to the archive','info');} + else{enq('qc',j.st,'pmpb'); + if(T.made[s]=w.end){st.slots[i]=null;st.done++;st.next(w.it);} + if(!st.slots[i]&&st.q.length&&shiftOn()){const it=st.q.shift();st.slots[i]={it,t0:S.t,end:S.t+st.durFn(it)};}} +} + +/* ================================================================ metal path: PDPU plating cells + Nickel is deposited by Faraday's law, so each part's time follows from its current. */ +const GROW={F:{th:0.25,ramp:25*MIN,Imax:185},M:{th:0.25,ramp:8*MIN,Imax:200},S:{th:0.20,ramp:5*MIN,Imax:200}}; +const AREA=9.6,GPAH=1.095*0.97; // dm² for a ~Ø350 mm part; g Ni per A·h at 97 % efficiency +const massFor=th=>th*AREA*8.9*10; // g for a thickness in mm +const NOM={};for(const g in GROW){const G=GROW[g];let m=0,t=0;const tgt=massFor(G.th);while(ma.prio-b.prio||a.tq-b.tq);} +const CELLS=[0,1,2,3].map(i=>({i,u:i>>1,c:i&1,job:null,ph:'idle',tIn:0,I:0,V:0,mass:0,target:1,tPlate:0})); +function cellTick(c,ds){ + if(c.ph==='idle'){ + if(shiftOn()&&PLATEQ.length){const j=PLATEQ.shift();c.job=j;c.ph='load';c.tIn=0;c.mass=0;c.target=massFor(GROW[j.grow].th); + if(j.grow==='S'){j.T.made[j.side]++;j.st={T:j.T,side:j.side,gen:'S',n:j.T.made[j.side]};} + if(j.from)hop(j.from,'pdpu'+(c.u+1),j.mand); + log('PDPU '+(c.u+1)+' cell '+(c.c+1)+': '+(j.grow==='F'?'father on lacquer ':j.grow==='M'?'mother on father ':'stamper on mother ')+j.T.sid+j.side+(j.remake?' (from the archive)':''),'info');} + return;} + c.tIn+=ds; + if(c.ph==='load'){if(c.tIn>=5*MIN){c.ph='plate';c.tIn=0;}return;} + if(c.ph==='plate'){const g=GROW[c.job.grow];c.I=Math.min(g.Imax,20+(g.Imax-20)*c.tIn/g.ramp);c.V=8.5+c.I*0.016;c.mass+=c.I*ds/HOUR*GPAH; + if(c.mass>=c.target){c.ph='done';c.tPlate=c.tIn;c.tIn=0;c.I=0;c.V=0;log(jl(c.job)+': plated to '+f2(g.th)+' mm in '+dur(c.tPlate)+', PDPU '+(c.u+1)+' cell '+(c.c+1),'ok');} + return;} + if(c.ph==='done'){if(shiftOn()){c.ph='unload';c.tIn=0;}return;} + if(c.ph==='unload'&&c.tIn>=10*MIN){const j=c.job;c.job=null;c.ph='idle';c.tIn=0;enq('pmpb',j,'pdpu'+(c.u+1));} +} +function cellLeft(c){if(!c.job)return 0;const g=GROW[c.job.grow]; + if(c.ph==='load')return 5*MIN-c.tIn+NOM[c.job.grow]+10*MIN; + if(c.ph==='plate'){const Ieff=c.tInc.job).length;} + +/* repress and stamper remakes come from the archived mothers */ +function requestRemake(T){T.setsTarget++;T.remakes++; + ['A','B'].forEach(s=>{ + if(T.mothers[s]){const mo=T.mothers[s];T.mothers[s]=null;S.archive.M=Math.max(0,S.archive.M-1);S.pulled++;T.mBusy[s]=true;plateEnq({mand:mo,grow:'S',T,side:s,remake:true},'archive');} + else if(!T.mBusy[s]){T.mBusy[s]=true;S.pulled++;plateEnq({mand:{T,side:s,gen:'M'},grow:'S',T,side:s,remake:true},'archive');}}); + log(T.id+': stampers worn before the run is done, a fresh set from the archived mothers is on its way','warn');} +function repress(){ + const cands=TITLES.filter(T=>T.done&&T.mothers.A&&T.mothers.B);const O=cands.length?pick(cands):null; + const R=mkTitle({qty:pick([1000,2000,3000]),approved:true,tp:'ok',repress:true,kind:O?O.kind:'black',stage:'galv'}); + R.origin=O?O.id:'PR-25-'+f0(rnd(100,180));if(O){R.label=O.label;R.vinyl=O.vinyl;R.wt=O.wt;O.mothers={A:null,B:null};} + ['A','B'].forEach(s=>{S.archive.M=Math.max(0,S.archive.M-1);S.pulled++;R.mBusy[s]=true; + plateEnq({mand:{T:R,side:s,gen:'M'},grow:'S',T:R,side:s,remake:true},'archive');}); + log('Repress '+R.id+': '+R.qty+' more of '+R.origin+', mothers pulled from the archive, no artist gates','warn');} + +/* ================================================================ presses */ +const RACK=[]; +// Duplex ~30 s average (Newbilt), Classic 28 s at 180 g (2024 flyer); split between states is an estimate +const DSEQ=[['LOAD',5],['CLOSE',2],['STEAM',8],['COOL',10],['OPEN',2],['EJECT',3]]; +const CSEQ=[['LOAD',3],['CLOSE',2],['STEAM',7],['COOL',11],['OPEN',2],['EJECT',3]]; +const PRESS=[ + {id:'pDA',name:'Duplex A',type:'duplex',seq:DSEQ}, + {id:'pDB',name:'Duplex B',type:'duplex',seq:DSEQ}, + {id:'pC1',name:'Classic C1',type:'classic',seq:CSEQ}, + {id:'pC2',name:'Classic C2',type:'classic',seq:CSEQ}]; +PRESS.forEach(P=>Object.assign(P,{state:'OFF',tIn:0,set:null,mode:null,run:0,life:0,tpLeft:0,open:1,force:0,moldT:58,peakT:0,lastCycle:0,cycleStart:0,prevStart:0,waiting:false,changeDur:0,lastT:null,cyc:0,after:null,restart:false,cake:false,cakeKg:0})); +const RUNSTATES=['LOAD','CLOSE','STEAM','COOL','OPEN','EJECT']; +// Unplanned stops, about one per 4 running hours, 15 to 45 min each (estimate): with stamper changes and +// test pressings this lands near the 60 to 70 % overall effectiveness scale.md applies to the fleet. +const MTBF=4*HOUR,STOPS={duplex:['cake stuck','mould temp drift','hydraulic alarm','label misplaced'],classic:['label feed jam','cake loader jam','cooling flow low','vacuum fault']}; +const running=P=>RUNSTATES.includes(P.state); +function seqDur(P,st){const s=P.seq.find(x=>x[0]===st);let d=s?s[1]:0; + // Duplex loading is by hand, so it varies a little from cycle to cycle (average stays ~30 s) + if(P.type==='duplex'&&st==='LOAD')d=P.loadD||d; + // Classic: cooling by delivered water volume stretches when the supply water runs warm + if(P.type==='classic'&&st==='COOL')d*=1+0.05*Math.max(0,S.chiller.sup-22);return d;} +function materialOk(T){return T.kind==='black'?S.silo.v+S.silo.r>5:S.silo.c>2;} +function consume(T,kg){if(T.kind==='black'){const rg=Math.min(S.silo.r,kg*0.2);S.silo.r-=rg;S.silo.v=Math.max(0,S.silo.v-(kg-rg));}else S.silo.c=Math.max(0,S.silo.c-kg);} +function pickJob(P){let best=-1,score=-1; + RACK.forEach((r,i)=>{const T=r.T;if(P.type==='classic'&&T.kind==='splatter')return;if(!materialOk(T))return;let s; + if(T.approved)s=10;else if(T.tp==='none'&&!T.tpBusy&&r.k===1)s=14;else return; // test pressings first: the artist is waiting + if(P.type==='duplex'&&T.kind!=='black')s+=2;if(P.type==='classic'&&T.kind==='black')s+=2;if(T.repress)s+=3;if(P.lastT===T)s+=1; + if(s>score){score=s;best=i;}}); + return best<0?null:RACK.splice(best,1)[0];} +function mount(P,r){const T=r.T;P.set=r; + P.mode=T.approved?(T.repress&&!T.faDone?'fa':'prod'):'tp';if(P.mode==='fa')T.faDone=true; + if(P.mode==='tp'){T.tpBusy=true;P.tpLeft=TP_N;} + P.life=r.life||(r.life=Math.round(rnd(1000,2000)));P.changeDur=(P.lastT===T?rnd(18,24):rnd(36,46))*MIN;P.lastT=T;P.state='CHANGE';P.tIn=0;P.run=0; + if(visHop())routeHop('st_'+P.id,'S'); + log(P.name+': mounting set '+T.sid+'-'+r.k+' ('+T.id+', '+T.kind+') for '+(P.mode==='tp'?'test pressings':P.mode==='fa'?'the repress':'production')+', '+dur(P.changeDur)+' change','warn');} +function pressEnter(P,st){P.state=st;P.tIn=0; + if(st==='LOAD'&&Math.random()<30/MTBF){P.state='STOP';P.stopDur=rnd(15,45)*MIN;P.stopWhy=pick(STOPS[P.type]);P.cake=false; + log(P.name+': STOP, '+P.stopWhy+', maintenance called','alarm');return;} + 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(st==='STEAM')P.cake=false; +} +function pressTick(P,ds){ + if(P.state==='OFF'){P.force=lag(P.force,0,2,ds);P.moldT=lag(P.moldT,35,0.0003,ds); + if(shiftOn()){if(P.set){P.state='CHANGE';P.tIn=0;P.changeDur=rnd(13,18)*MIN;P.restart=true;log(P.name+': shift start, moulds heating','info');}else P.state='IDLE';}return;} + if(P.state==='IDLE'){P.force=lag(P.force,0,2,ds);P.moldT=lag(P.moldT,58,0.01,ds); + if(!shiftOn()){P.state='OFF';return;}const r=pickJob(P);if(r)mount(P,r);return;} + P.tIn+=ds; + if(P.state==='CHANGE'){P.force=lag(P.force,0,2,ds);P.moldT=lag(P.moldT,120,0.003,ds);if(P.tIn>=P.changeDur){P.restart=false;P.prevStart=S.t;pressEnter(P,'LOAD');}return;} + if(P.state==='UNMOUNT'){P.force=lag(P.force,0,2,ds);if(P.tIn>=10*MIN)unmountDone(P);return;} + if(P.state==='STOP'){P.force=lag(P.force,0,2,ds);P.open=lag(P.open,1,1,ds);if(P.tIn>=P.stopDur){log(P.name+': '+P.stopWhy+' cleared after '+dur(P.tIn)+', restarting','ok');P.prevStart=S.t;pressEnter(P,'LOAD');}return;} + if(P.state==='NOPVC'){if(materialOk(P.set.T))pressEnter(P,'LOAD');return;} + const d=seqDur(P,P.state);P.waiting=false; + if(P.state==='STEAM'&&S.boiler.p<9.5){P.waiting=true;P.tIn=Math.min(P.tIn,d-0.01);} + const k=Math.min(1,P.tIn/d); + if(P.state==='CLOSE')P.open=1-k;else if(P.state==='OPEN')P.open=k;else if(P.state==='LOAD'||P.state==='EJECT')P.open=1;else P.open=0; + if(P.state==='STEAM'){P.force=lag(P.force,150,1.5,ds);P.moldT=lag(P.moldT,165,0.45,ds);P.peakT=Math.max(P.peakT,P.moldT);} + else if(P.state==='COOL'){P.force=lag(P.force,150,1,ds);P.moldT=lag(P.moldT,42,0.35,ds);} + else{P.force=lag(P.force,P.state==='CLOSE'?120:0,2,ds);P.moldT=lag(P.moldT,58,0.2,ds);} + if(P.tIn>=d&&!P.waiting){ + // carry the overshoot into the next state, so cycle times stay exact at any step size + const extra=P.tIn-d; + if(P.state==='EJECT'){P.cycleStart=S.t-extra;eject(P);afterEject(P);} + else{const i=P.seq.findIndex(x=>x[0]===P.state);pressEnter(P,P.seq[i+1][0]);} + if(running(P))P.tIn=extra;} +} +function eject(P){const T=P.set.T,tp=P.mode==='tp';P.cyc++;P.lastCycle=P.cycleStart-P.prevStart;P.prevStart=P.cycleStart;P.set.used++; + S.gran.bin+=P.cakeKg-T.wt/1000;S.logged++; + if(tp)S.day.tp++;else{T.cycles++;S.cyc.push(S.t);} + if(vis()){mkRec(P,tp); + log(P.name+' #'+P.cyc+(tp?' TEST PRESSING':'')+' · '+f1(P.lastCycle)+' s · set '+T.sid+'-'+P.set.k+' · cake '+f0(P.cakeKg*1000)+' g · mould '+f0(P.peakT)+' °C',tp?'warn':'info');} + else outcome(T,tp);} +function afterEject(P){const T=P.set.T; + if(P.mode==='tp'){P.tpLeft--;if(P.tpLeft<=0){T.tp='sent';T.tpBusy=false;T.g3due=S.t+rnd(5,15)*DAY; + log(P.name+': '+TP_N+' test pressings of '+T.id+' to the artist (gate 3, 5 to 15 days), stampers unmounted','warn');P.after='rack';P.state='UNMOUNT';P.tIn=0;return;}} + else{P.run++;if(P.mode==='fa'&&P.run>=3){P.mode='prod';log(P.name+': first-article check of repress '+T.id+' passed','ok');} + if(T.cycles>=T.target){T.done=true;T.stage='done';log(P.name+': '+T.id+' complete, '+T.target+' pressed, stampers retired','ok');P.after='retire';P.state='UNMOUNT';P.tIn=0; + for(let i=RACK.length-1;i>=0;i--)if(RACK[i].T===T)RACK.splice(i,1);return;} + if(P.set.used>=P.life){log(P.name+': stamper set '+T.sid+'-'+P.set.k+' worn after '+P.set.used+' pressings','warn');P.after='retire';P.state='UNMOUNT';P.tIn=0; + const more=RACK.some(r=>r.T===T)||T.pairsQ!==P&&Q.set&&Q.set.T===T);if(!more)requestRemake(T);return;}} + if(!shiftOn()){P.state='OFF';log(P.name+': shift end, stopped with set '+T.sid+'-'+P.set.k+' mounted','info');return;} + pressEnter(P,'LOAD');} +function unmountDone(P){if(P.after==='rack')RACK.unshift(P.set);P.set=null;P.mode=null;P.state=shiftOn()?'IDLE':'OFF';P.tIn=0;P.cake=false;} + +/* records above 10x: counted, not drawn */ +function outcome(T,tp){ + if(tp){S.samples++;S.tpToArtist++;S.audioLast=f1(jit(98.2,1.2));return;} + if(Math.random()<0.035){S.day.rej++;S.punched++;S.gran.bin+=T.wt/1000;S.punchUntil=S.t+10;return;} + S.day.good++;T.good++;S.trimmed++;S.sleeved++;S.assembled++;S.logged+=2; + if(Math.random()<0.02){S.samples++;S.audioLast=f1(jit(98.2,1.2));} + if(Math.random()<0.33)S.parcels++;else if(++S.pallet>=300)S.pallet=0;} + +/* ================================================================ layout */ +const MACH=[ + // metal path + {id:'audio',x:20,y:56,w:110,h:120,t:'Audio intake',s:'checksums · tech QA',tags:[()=>'titles '+cntStage('intake'),()=>'next '+nextUp('intake'),()=>'~1.4 new/day']}, + {id:'master',x:190,y:56,w:110,h:120,t:'Mastering',s:'vinyl master',tags:[()=>'titles '+cntStage('master'),()=>'next '+nextUp('master'),()=>'2 to 4 days']}, + {id:'cut',x:360,y:56,w:110,h:120,t:'Lathe cutting',s:'lacquer · scribe',tags:[()=>'titles '+cntStage('cut'),()=>'next '+nextUp('cut'),()=>'plate < 72 h']}, + {id:'pasu',x:488,y:46,w:112,h:150,t:'PASU silver',s:'clean · spray Ag',st:'pasu',bar:true,tags:[()=>stTxt('pasu'),()=>stLeft('pasu'),()=>'queue '+ST.pasu.q.length,()=>'15 min (est.)']}, + {id:'pppu',x:610,y:46,w:146,h:150,t:'PPPU pre-plate',s:'4 positions · 20 A',custom:'pppu',tags:[()=>'queue '+ST.pppu.q.length+' · 35 °C',()=>'20 min each']}, + {id:'pdpu1',x:766,y:46,w:158,h:150,t:'PDPU 1 plating',s:'2 cells · up to 200 A',custom:'pdpu',u:0,det:'pdpu',tags:[()=>pdpuT1(0),()=>pdpuT2(0)]}, + {id:'pdpu2',x:934,y:46,w:158,h:150,t:'PDPU 2 plating',s:'2 cells · up to 200 A',custom:'pdpu',u:1,det:'pdpu',tags:[()=>pdpuT1(1),()=>pdpuT2(1)]}, + {id:'pmpb',x:1102,y:46,w:120,h:150,t:'PMPB bench',s:'separate · passivate',st:'pmpb',bar:true,tags:[()=>stTxt('pmpb'),()=>stLeft('pmpb'),()=>'queue '+ST.pmpb.q.length,()=>'DI tank '+f0(S.diro.tank*100)+' %']}, + {id:'qc',x:1232,y:46,w:108,h:150,t:'Part QC',s:'XRF · listening',st:'qc',bar:true,tags:[()=>stTxt('qc'),()=>stLeft('qc'),()=>'queue '+ST.qc.q.length,()=>S.lastXRF]}, + {id:'vsf',x:1368,y:46,w:132,h:150,t:'VSF finisher',s:'Sibert · back-finish',st:'vsf',bar:true,tags:[()=>stTxt('vsf'),()=>stLeft('vsf'),()=>'queue '+ST.vsf.q.length,()=>'8 min (est.)']}, + {id:'vsp',x:1510,y:46,w:132,h:150,t:'VSP punch',s:'Sibert · ×100 optics',st:'vsp',bar:true,tags:[()=>stTxt('vsp'),()=>stLeft('vsp'),()=>'queue '+ST.vsp.q.length,()=>'conc. '+(S.lastConc?f0(S.lastConc)+' µm':'-')]}, + {id:'vsc',x:1652,y:46,w:132,h:150,t:'VSC coiner',s:'Sibert · centre + edge',st:'vsc',bar:true,tags:[()=>stTxt('vsc'),()=>stLeft('vsc'),()=>'queue '+ST.vsc.q.length,()=>'today '+S.day.stampers+' done']}, + {id:'archive',x:1812,y:46,w:130,h:150,t:'Archive',s:'fathers · mothers',tags:[()=>'fathers '+S.archive.F,()=>'mothers '+S.archive.M,()=>'pulled '+S.pulled,()=>'3-step plating']}, + {id:'rack',x:1952,y:46,w:130,h:150,t:'Stamper rack',s:'press-ready sets',tags:[()=>'ready '+rackReady(),()=>'at gate 3 '+rackWait(),()=>'for TP '+rackTP(),()=>'next '+rackNext()]}, + // pressing hall + {id:'silo_v',x:20,y:316,w:54,h:120,custom:'silo',lbl:'Virgin',sub:'black',kind:'v'}, + {id:'silo_r',x:80,y:316,w:54,h:120,custom:'silo',lbl:'Regrind',sub:'≤20 %',kind:'r'}, + {id:'silo_c',x:140,y:316,w:54,h:120,custom:'silo',lbl:'Colour',sub:'compound',kind:'c'}, + {id:'lab',x:208,y:316,w:128,h:112,t:'Material lab',s:'per-lot release gate',tags:[()=>'lot '+S.lab.lot,()=>'H2O '+f2(jit(0.03,0.004))+' % ok',()=>S.lab.test?'test '+S.lab.test.lot+' '+dur(S.lab.test.until-S.t):'no lot in test',()=>'queue '+S.lab.queue.length]}, + {id:'oven',x:20,y:456,w:174,h:52,t:'Label dry oven',s:'pre-dried · lot per cycle',tags:[()=>S.labels.lot+' · '+S.labels.pairs+' pairs']}, + {id:'pDA',x:350,y:316,w:124,h:196,custom:'press',t:'HRP 12 · A',s:'Duplex · semi-auto',det:'duplex'}, + {id:'vex',x:484,y:316,w:140,h:128,custom:'vex'}, + {id:'ex35',x:484,y:452,w:140,h:58,t:'EX 35 colour',s:'auxiliary extruder',tags:[()=>S.ex35?'strands ON':'strands idle']}, + {id:'pDB',x:634,y:316,w:124,h:196,custom:'press',t:'HRP 12 · B',s:'Duplex · semi-auto',det:'duplex'}, + {id:'pC1',x:784,y:316,w:150,h:196,custom:'press',t:'Classic C1',s:'automatic · 3-column',det:'classic',c3:true}, + {id:'pC2',x:946,y:316,w:150,h:196,custom:'press',t:'Classic C2',s:'automatic · 3-column',det:'classic',c3:true}, + {id:'tA',x:350,y:522,w:84,h:44,t:'Trimmer A',det:'trim',tags:[()=>trimTag('pDA')]}, + {id:'tB',x:634,y:522,w:84,h:44,t:'Trimmer B',det:'trim',tags:[()=>trimTag('pDB')]}, + {id:'tC1',x:784,y:522,w:110,h:44,t:'Trimmer C1',det:'trim',tags:[()=>trimTag('pC1')]}, + {id:'tC2',x:946,y:522,w:110,h:44,t:'Trimmer C2',det:'trim',tags:[()=>trimTag('pC2')]}, + {id:'audioqc',x:1120,y:338,w:130,h:70,t:'Audio compare',s:'all TPs + 1 in 50',tags:[()=>'n '+S.samples+' · '+S.audioLast]}, + {id:'punch',x:1264,y:338,w:130,h:70,t:'Label punch',s:'106 mm · rejects',tags:[()=>'punched '+S.punched]}, + {id:'gran',x:1408,y:320,w:164,h:104,t:'Granulator 400',s:'edge trim + rejects',tags:[()=>'bin '+f1(S.gran.bin)+' kg',()=>S.gran.run?'BATCH '+f1(S.gran.run)+' kg':'waits for 12 kg',()=>'ground '+f0(S.gran.total)+' kg',()=>'200 to 250 kg/h']}, + {id:'carton',x:1600,y:338,w:170,h:70,t:'Carton partner',s:'incoming QC per delivery',tags:[()=>'CP-0311 · ΔE 0.8 OK']}, + {id:'optical',x:1120,y:452,w:130,h:96,t:'Optical',s:'100 % · darkfield',tags:[()=>'yield '+yieldStr(),()=>'reject '+S.day.rej,()=>'last '+S.opticalLast]}, + {id:'finish',x:1264,y:452,w:130,h:96,t:'Finishing',s:'sleeve · shrink',tags:[()=>'sleeved '+S.sleeved,()=>'sleeve IS-204',()=>'shrink sampled']}, + {id:'pack',x:1408,y:452,w:150,h:96,t:'Packaging',s:'matrix vs barcode',tags:[()=>'assembled '+S.assembled,()=>'mismatch '+S.mismatch,()=>'barcode 100 %']}, + {id:'ship',x:1572,y:452,w:200,h:96,t:'Fulfillment',s:'bulk pallet · dropship',tags:[()=>'pallet '+S.pallet+' / 300',()=>'dropship '+S.parcels+' parcels',()=>'TPs to artists '+S.tpToArtist]}, + // utilities + {id:'boiler',x:20,y:676,w:160,h:132,t:'Steam boiler',s:'10 bar · 180 °C',tags:[()=>'P '+f2(S.boiler.p)+' bar',()=>'flow '+f0(S.boiler.F*HOUR)+' kg/h',()=>'cap 800 kg/h est.',()=>'press 150-180 kg/h',()=>'fuel: open']}, + {id:'chiller',x:192,y:676,w:176,h:132,t:'Process cooling',s:'chiller · cooling tower',tags:[()=>'supply '+f1(S.chiller.sup)+' °C',()=>'return '+f1(S.chiller.ret)+' °C',()=>'flow '+f0(S.chiller.flow)+' l/min',()=>'load '+f0(S.chiller.load)+' kW',()=>'cap 500 kW est.']}, + {id:'comp',x:380,y:676,w:120,h:132,t:'Compressed air',s:'6 to 10 bar, dry',tags:[()=>'P '+f1(S.comp.p)+' bar',()=>'dew '+f1(jit(2.5,.4))+' °C',()=>'presses',()=>'Sibert, PASU']}, + {id:'hyd',x:512,y:676,w:124,h:132,t:'Hydraulics',s:'Duplex aggregate',tags:[()=>'P '+f0(S.hyd.p)+' bar',()=>'oil '+f1(S.hyd.oil)+' °C',()=>'HLP 46 · 200 l',()=>'presses A + B']}, + {id:'diro',x:648,y:676,w:136,h:132,t:'RO / DI water',s:'galvanics rinse',tags:[()=>'tank '+f0(S.diro.tank*100)+' %',()=>'cond '+f1(S.diro.cond)+' µS/cm',()=>'to PMPB, PASU',()=>'0.2 m³/h est.']}, + {id:'zld',x:796,y:676,w:160,h:132,t:'Water treatment',s:'zero discharge',tags:[()=>'buffer '+f0(S.zld.tank*100)+' %',()=>'rinse + VSF loop',()=>'metal recovery',()=>'discharge 0 l']}, + {id:'energy',x:968,y:676,w:210,h:132,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',()=>'galvanics '+f0(S.galvKW)+' kW']}, + {id:'trace',x:1190,y:676,w:196,h:132,t:'Traceability',s:'log at source, append-only',tags:[()=>'records '+S.logged,()=>'events '+S.events,()=>'stamper+press+cycle',()=>'genealogy per part']}, + {id:'hvac',x:1398,y:676,w:196,h:132,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']}, + {id:'exh',x:1606,y:676,w:160,h:132,t:'Galv extraction',s:'hoods · scrubber',tags:[()=>'fans '+(CELLS.some(c=>c.job)||ST.pasu.slots[0]?'running':'standby'),()=>'Ø63 per tank unit',()=>'PASU Ø125 mm',()=>'interlock OK']}, +]; +const M={};MACH.forEach(m=>M[m.id]=m); +const yieldStr=()=>{const n=S.day.good+S.day.rej;return n?f1(100*S.day.good/n)+' %':'--';}; +function stTxt(id){const w=ST[id].slots[0];if(!w)return 'idle';const fr=(S.t-w.t0)/(w.end-w.t0);const it=w.it; + const lbl=id==='pmpb'?jl(it):pl(it);let ph; + if(id==='pasu')ph=fr<0.2?'clean':fr<0.35?'sens.':fr<0.8?'spray':'rinse'; + else if(id==='pmpb')ph=fr<0.3?'sep.':fr<0.75?'passiv.':'dry'; + else if(id==='qc')ph=it.gen==='M'&&fr>0.12?'listen':'XRF'; + else if(id==='vsf')ph=['P240','P600','P1200','P2500','clean'][Math.min(4,Math.floor(fr*5))]; + else if(id==='vsp')ph=fr<0.8?'centre':'punch'; + else ph='form'; + return lbl.replace(' ','')+' '+ph;} +function stLeft(id){const w=ST[id].slots[0];return w?'left '+dur(w.end-S.t):'left -';} +function stFrac(id){const w=ST[id].slots[0];return w?Math.min(1,(S.t-w.t0)/(w.end-w.t0)):0;} +function pdpuT1(u){const V=Math.max(...CELLS.filter(c=>c.u===u).map(c=>c.V));return 'bath 55 °C · '+(V>0?f1(V)+' V':'0 V');} +function pdpuT2(u){const n=CELLS.filter(c=>c.u===u&&c.ph==='plate').length;return 'coil '+(n?f0(jit(16,0.6)):'15')+' °C · '+(n?'10':'0')+' l/min';} +const rackReady=()=>RACK.filter(r=>r.T.approved).length; +const rackWait=()=>RACK.filter(r=>!r.T.approved&&r.T.tp==='sent').length; +const rackTP=()=>RACK.filter(r=>r.T.tp==='none').length; +function rackNext(){const r=RACK.find(r=>r.T.approved)||RACK.find(r=>r.T.tp==='none');return r?r.T.sid+'-'+r.k:'-';} +function trimTag(pid){return running(PRESS.find(p=>p.id===pid))?'trim · 6 s':'idle';} + +/* ---------------- drawing */ +function lane(x,y,txt){el('text',{x,y,class:'lane'},L.mach,txt);} +lane(20,24,'Metal path · audio file to stamper'); +lane(20,300,'Pressing hall · Newbilt · material and record path'); +lane(20,668,'Utilities and plant systems'); +el('rect',{x:478,y:30,width:870,height:176,rx:6,class:'room'},L.pipes); +el('text',{x:488,y:24,class:'roomlbl'},L.mach,'Galvanics clean room · Pheenix Alpha'); +el('rect',{x:1358,y:30,width:436,height:176,rx:6,class:'room'},L.pipes); +el('text',{x:1368,y:24,class:'roomlbl'},L.mach,'Stamper prep · Sibert'); + +function boxBase(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'); + 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); - // 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)+' %';} + if(m.s)el('text',{x:m.x+7,y:m.y+31,class:'msub'},g,m.s); + m.g=g;g.addEventListener('click',()=>select(m.id));return g;} +function drawBox(m){const g=boxBase(m);const y0=m.tagY!=null?m.tagY:m.y+(m.h<=60&&!m.s?34:46); + m.tagEls=(m.tags||[]).map((t,i)=>el('text',{x:m.x+7,y:y0+i*13,class:'mtag'},g,'')); + if(m.bar){el('rect',{x:m.x+7,y:m.y+m.h-16,width:m.w-14,height:6,rx:2,class:'pbar'},g);m.barEl=el('rect',{x:m.x+7,y:m.y+m.h-16,width:0,height:6,rx:2,class:'pbarv'},g);} + return g;} +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-28} L${x+w/2+7},${y+h} H${x+w/2-7} L${x},${y+h-28} Z`,class:'silo'},g); + m.fillEl=el('rect',{x:x+2,y:y+h-30,width:w-4,height:0,class:'fill'+(m.kind==='r'?' reg':m.kind==='c'?' col':'')},g); + el('text',{x:x+w/2,y:y+13,class:'msub','text-anchor':'middle'},g,m.lbl); + el('text',{x:x+w/2,y:y+25,class:'small','text-anchor':'middle'},g,m.sub); + m.lvlEl=el('text',{x:x+w/2,y:y+h-36,class:'mtag','text-anchor':'middle'},g,''); + m.lamp=el('circle',{cx:x+w-7,cy:y+7,r:3.5,class:'lamp run'},g); + m.g=g;g.addEventListener('click',()=>select(m.id));} +function drawPress(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y,w,h}=m; + el('rect',{x,y,width:w,height:h,rx:4,class:'frame'},g); + el('rect',{x:x+1,y:y+1,width:w-2,height:18,class:'mhead'},g); + el('text',{x:x+7,y:y+14,class:'mtitle'},g,m.t); + m.lamp=el('circle',{cx:x+w-10,cy:y+10,r:4,class:'lamp'},g); + el('text',{x:x+7,y:y+31,class:'msub'},g,m.s); + if(m.c3)el('rect',{x:x+w/2-2,y:y+38,width:4,height:84,fill:'#1d2942'},g); // third column, behind the platens + 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); + 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:#e6ecf7'},g,'OFF'); + m.tagEls=[0,1,2].map(i=>el('text',{x:x+7,y:y+154+i*13,class:'mtag'},g,'')); + m.g=g;g.addEventListener('click',()=>select(m.id));} +function drawVex(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y,w,h}=m; + el('rect',{x,y,width:w,height:h,rx:4,class:'mbox'},g); + el('rect',{x:x+1,y:y+1,width:w-2,height:18,class:'mhead'},g); + el('text',{x:x+7,y:y+14,class:'mtitle'},g,'VEX 50 extruder'); + m.lamp=el('circle',{cx:x+w-10,cy:y+10,r:4,class:'lamp'},g); + m.tagEls=[el('text',{x:x+7,y:y+31,class:'msub'},g,''),el('text',{x:x+7,y:y+43,class:'msub'},g,'')]; + m.tags=[()=>'screw '+f0(S.vex.rpm)+' rpm · melt '+f0(S.vex.melt)+' °C',()=>'50 mm · 3 zones · 7.5 kW']; + el('path',{d:`M${x+56},${y+50} H${x+84} V${y+60} L${x+76},${y+74} H${x+64} L${x+56},${y+60} Z`,class:'hopper'},g); + el('text',{x:x+8,y:y+82,class:'small'},g,'◀ cake A'); + 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',fill:'#1b2438'},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); + 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); + return {r,a:el('text',{x:px+5,y:py+14,class:'ctxt'},g,''),b:el('text',{x:px+5,y:py+28,class:'ctxt'},g,'')};});} +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); + 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,'')); + return {g:cg,tv,t,cy};});} +MACH.forEach(m=>{ + if(m.custom==='silo')drawSilo(m);else if(m.custom==='press')drawPress(m);else if(m.custom==='vex')drawVex(m); + else if(m.custom==='pppu')drawPppu(m);else if(m.custom==='pdpu')drawPdpu(m);else drawBox(m);}); /* gates */ -function gate(id,x,y,l1,l2){ - const g=el('g',{class:'gate',id:'gate_'+id},L.top); +function gate(id,x,y,l1){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')}; + el('text',{x,y:y+26},g,l1);const c=el('text',{x,y:y+36},g,'');return {g,c,flash:0};} +const GATES={g1:gate('g1',160,116,'gate 1'),g2:gate('g2',330,116,'gate 2'),g3:gate('g3',1880,232,'gate 3 · TP')}; +function flash(id){if(S.quiet)return;GATES[id].flash=2;GATES[id].g.classList.add('ok');} -/* 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); +/* pipes and routes */ 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'); +// 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); +pipe('M262,676 V626 H1086','cw');['466','750','924','1086'].forEach(x=>pipe(`M${x},626 V512`,'cw')); +pipe('M1092,631 H282 V676','cwr',true); +pipe('M574,676 V636 H444 V512','hyd');pipe('M574,636 H728 V512','hyd'); +// galvanics services: up the right-hand side to headers over the clean room +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); +// 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'); +route('f_ex35','M272,316 V308 H629 V481 H624','col thin');route('ex2vex','M554,452 V444','col thin'); +route('l_pDA','M194,482 H350','lbl thin');route('l_pDB','M194,500 H340 V517 H646 V512','lbl thin'); +route('l_pC1','M194,500 H340 V517 H796 V512','lbl thin');route('l_pC2','M194,500 H340 V517 H958 V512','lbl thin'); +route('trimcart','M1104,574 V444 H1440 V424','scrap thin'); +route('reject','M1200,590 V570 H1257 V432 H1329 V408','scrap thin');route('sample','M1170,590 V570 H1112 V373 H1120','route thin'); +route('p2g','M1394,371 H1408','scrap thin');route('regrind','M1490,320 V284 H107 V316','reg thin'); +route('carton','M1685,408 V430 H1483 V452','route thin'); +['pDA:412','pDB:696','pC1:859','pC2:1021'].forEach(s=>{const [id,x]=s.split(':');route('st_'+id,`M2017,196 V278 H${x} V316`,'metal thin');}); +[[180,612,'steam 10 bar'],[1250,640,'galvanics services: cooling water 12 to 20 °C · DI water'],[1500,274,'stamper sets to presses'],[130,280,'regrind to silo'], + [1120,438,'edge trim, in batches'],[1262,428,'rejects'],[1116,332,'samples'],[200,478,'labels']].forEach(a=>el('text',{x:a[0],y:a[1],class:'small'},L.top,a[2])); -/* ------------------------------------------------------------------ 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 */ +/* ================================================================ 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;} +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; + const kind=it.grow?it.grow:it.gen==='L'?(it.silvered?'Ls':'L'):it.gen;const n=shape(kind);hops.push({n,a,b,t:0,d:1.1});place(n,a);} +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); + 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){ - RECS.sort((a,b)=>b.pos-a.pos); - for(let i=0;i0){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);}); + const onC=RECS.filter(r=>r.ph==='conv'&&!r.gone).sort((a,b)=>b.pos-a.pos); + for(const r of RECS){ + if(r.ph==='trim'){r.dwell-=ds;if(r.dwell<=0){r.ph='drop';S.trimmed++;}} + else if(r.ph==='drop'){r.y=Math.min(CONVY,r.y+60*ds);if(r.y>=CONVY){const p=r.x-CONV0;if(!onC.some(o=>Math.abs(o.pos-p)<24)){r.ph='conv';r.pos=p;r.dwell=0;onC.push(r);onC.sort((a,b)=>b.pos-a.pos);}}}} + for(let i=0;i0){r.dwell-=ds;continue;} + let max=CONVEND-CONV0;for(let j=i-1;j>=0;j--){if(!onC[j].gone){max=Math.min(max,onC[j].pos-24);break;}} + let np=Math.min(r.pos+32*ds,Math.max(r.pos,max)); + for(const st of STATIONS){const at=st.at-CONV0;if(!r.visited[st.id]&&np>=at){np=at;r.visited[st.id]=true;r.dwell=st.dwell;r.pos=np;recStation(st,r);break;}} + if(!r.gone)r.pos=np;} + for(let i=RECS.length-1;i>=0;i--)if(RECS[i].gone)RECS.splice(i,1); } -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; - } +function recStation(st,r){const T=r.T; + if(st.id==='optical'){S.logged++; + if(!r.tp&&Math.random()<0.035){const d=pick(['non-fill','stitching','off-centre hole','debris','label off-centre','warp']);S.day.rej++;S.opticalLast='REJ '+d.slice(0,8);r.node.classList.add('rej');r.gone=true; + 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||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');}}} + 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); + 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();}} + return;} + if(st.id==='finish')S.sleeved++; + if(st.id==='pack'){S.assembled++;S.day.good++;T.good++;S.logged++;} + if(st.id==='ship'){r.gone=true;r.node.remove();if(Math.random()<0.33)S.parcels++;else if(++S.pallet>=300){S.pallet=0;log('Fulfillment: pallet of 300 closed, booked with the 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.tm2.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); - } +/* ================================================================ materials, lab, granulator */ +function newLot(silo){const lot='26-0'+(S.lab.nextLot++);S.lab.queue.push({lot,silo,kg:silo==='v'?3000:600}); + log('PVC lot '+lot+' ('+(silo==='v'?'black, 3 t':'colour, 600 kg')+') delivered, quarantined at the material lab','warn');} +function matTick(){ + const LB=S.lab,pend=s=>LB.queue.some(q=>q.silo===s)||(LB.test&&LB.test.silo===s); + if(S.silo.v<0.25*S.silo.vcap&&!pend('v'))newLot('v'); + if(S.silo.c<0.4*S.silo.ccap&&!pend('c'))newLot('c'); + if(!LB.test&&LB.queue.length&&shiftOn()){LB.test=LB.queue.shift();LB.test.until=S.t+2*HOUR;log('Material lab: lot '+LB.test.lot+' in test: moisture, heat stability, colour, density, then a release pressing','info');} + if(LB.test&&S.t>=LB.test.until){const t=LB.test;LB.test=null;if(t.silo==='v')S.silo.v=Math.min(S.silo.vcap,S.silo.v+t.kg);else S.silo.c=Math.min(S.silo.ccap,S.silo.c+t.kg);LB.lot=t.lot; + log('Material lab: lot '+t.lot+' released (moisture '+f2(jit(0.03,0.01))+' %, heat stability OK, ΔE 0.3), retained sample archived','ok');} + const G=S.gran; + 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());} } -flowsTick.lastReg=0; -/* ------------------------------------------------------------------ utilities */ +/* ================================================================ 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.t0){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; + let draw=0;PRESS.forEach(P=>{if(P.state==='STEAM'&&!P.waiting)draw+=STEAM_KG/seqDur(P,'STEAM');}); + // header holds ~10 kg of steam per bar; boiler shown at 800 kg/h (estimate, see detail) + const B=S.boiler,fault=S.t0&&!fault&&B.p>9.6){B.faultUntil=-1;log('Boiler: feedwater pump back, '+f2(B.p)+' bar, presses resume','ok');} + let heat=0,flow=20;PRESS.forEach(P=>{if(running(P)){heat+=72;flow+=53;}}); + heat+=CELLS.filter(c=>c.ph==='plate').length*1.5; + const C=S.chiller;C.load=lag(C.load,heat,1/600,ds);C.flow=lag(C.flow,flow,0.05,ds);C.sup=lag(C.sup,22+Math.max(0,C.load-500)/40,1/300,ds);C.ret=C.sup+C.load/(C.flow/60*4.186); + S.comp.p=lag(S.comp.p,PRESS.some(P=>P.state==='LOAD'||P.state==='EJECT')?7.2:7.7,0.9,ds); + const D=PRESS.slice(0,2),dRun=D.some(running); + S.hyd.p=lag(S.hyd.p,D.some(P=>['CLOSE','STEAM','COOL'].includes(P.state))?180:30,2,ds);S.hyd.oil=lag(S.hyd.oil,dRun?46:38,0.002,ds); + S.vex.rpm=lag(S.vex.rpm,D.some(P=>P.state==='LOAD')?42:dRun?28:0,1.5,ds);S.vex.melt=lag(S.vex.melt,dRun?158:90,0.01,ds); + S.ex35=D.some(P=>running(P)&&P.set&&P.set.T.kind==='splatter'); + const w=Math.min(ds,1);S.hvac.t=lag(S.hvac.t,23.2,0.001,ds)+(Math.random()-.5)*0.004*w;S.hvac.rh=lag(S.hvac.rh,52,0.001,ds)+(Math.random()-.5)*0.02*w; + S.diro.tank=Math.min(0.95,S.diro.tank+0.2/HOUR*ds);S.diro.cond=lag(S.diro.cond,0.6+Math.random()*0.3,0.01,ds); + S.zld.tank=Math.max(0.15,S.zld.tank-0.05/HOUR*ds); + const h=hourOf(),sun=Math.max(0,Math.sin(Math.PI*(h-6.5)/12));S.energy.pv=lag(S.energy.pv,247*0.85*sun,0.01,ds); + let load=160+40*sun;PRESS.forEach(P=>{if(running(P))load+=P.type==='duplex'?10:15;});load+=C.load/3.5; + let rect=0;CELLS.forEach(c=>{rect+=c.I*c.V;});rect+=ST.pppu.slots.filter(Boolean).length*20*9; + S.galvKW=rect/1000/0.85+(shiftOn()?6:3);load+=S.galvKW;if(S.gran.run)load+=7.5;load+=shiftOn()?14:3; + const E=S.energy;E.load=lag(E.load,load,0.2,ds);const def=E.load-E.pv; + if(def>0){const dis=E.soc>20?Math.min(def,135):0;E.soc=Math.max(20,E.soc-dis*ds/(271*36));E.grid=lag(E.grid,def-dis,1,ds);} + else{const ch=E.soc<100?Math.min(-def,135):0;E.soc=Math.min(100,E.soc+ch*ds/(271*36));E.grid=lag(E.grid,0,1,ds);} } -/* ------------------------------------------------------------------ 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.tmv.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':''; +/* ================================================================ sim step */ +function tick(ds){S.t+=ds; + const dn=dayN();if(S.day.n!==dn){if(S.day.n!==null)log('Day '+dn+' starts: '+S.day.good+' good records yesterday, '+S.day.rej+' rejects','ok');S.day={n:dn,good:0,rej:0,tp:0,stampers:0};} + upTick();for(const k in ST)stTick(ST[k]);CELLS.forEach(c=>cellTick(c,ds));PRESS.forEach(P=>pressTick(P,ds)); + recTick(ds);matTick();utilTick(ds); + if(S.cyc.length&&S.cyc[0]150){for(let i=0;i120;i++){if(TITLES[i].done){TITLES.splice(i,1);i--;}}} +} + +/* ================================================================ 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=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());}} + 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');}); +} +const lampSet=(id,cls)=>{const m=M[id];if(m&&m.lamp)m.lamp.setAttribute('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));}); + [['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'));}); + 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'?'#ff3b3b':amber?'#ffbf1f':(P.state==='IDLE'||P.state==='OFF')?'#8a97b0':'#e6ecf7'; + 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; + 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.setAttribute('fill',dRun?['#3a2a1c','#4a2e1a','#5a301a'][i]:'#1b2438'));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):'';}); + 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':'')); + 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':'';}); + 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')); + lampSet('archive','run');lampSet('rack',rackReady()?'run':'warn'); + const anyRun=PRESS.some(running);['optical','finish','pack'].forEach(id=>lampSet(id,anyRun||RECS.length?'run':'idle'));lampSet('ship','run'); + lampSet('audioqc',movers.some(mv=>mv.p===PATHS.sample)?'run':'idle');lampSet('punch',S.t24?'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'; } -/* ------------------------------------------------------------------ log */ +/* ================================================================ 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; +function log(msg,lvl){S.events++;if(S.quiet)return; const d=document.createElement('div');d.className=lvl||'info'; - d.innerHTML=''+[hh,mi,ss].map(v=>String(v).padStart(2,'0')).join(':')+' '+msg; - logEl.prepend(d);while(logEl.children.length>60)logEl.lastChild.remove(); -} + d.innerHTML=''+clockStr()+' '+msg;logEl.prepend(d);while(logEl.children.length>80)logEl.lastChild.remove();} -/* ------------------------------------------------------------------ details */ +/* ================================================================ 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','Candidate: Pheenix PASU, 4-nozzle spray (2024 flyer)']}, - 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','Candidates: Digital Matrix, or Pheenix PPPU pre-plate + PDPU, 2 cells at up to 200 A (flyers)']}, - 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','Sibert VSP punch (manual centring at ×100), VSF wet back-finish, VSC coiner (2021 specs)']}, - 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. Rejected records lose their label area in the label punch first; edge trim goes in directly.',c:['No labels or paper into the grinder','Contamination check per regrind batch'],f:['Newbilt Granulator 400 (flyer): 200 to 250 kg/h, 7.5 kW, so it runs in batches','Edge trim ~20 to 40 g per 12" 180 g record (Newbilt flyer)','Label punch: 106 mm, 7"/10"/12"']}, - 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']}, + audio:{t:'1 · Audio intake',w:'Files arrive from the label or artist; checksums and format are verified and a technical QA pass checks levels, clipping and side length against the format. Nothing is cut until the artist confirms the master plan (gate 1, 2 to 10 days on the customer side).',c:['Checksums and format','Technical QA: levels, clipping, side length','Artist gate 1: master plan approved'],f:['1 to 2 days (production timeline)','A new title every 0.55 to 0.85 days here, ~520 a year (revenue model at full capacity: 300 to 600)','Runs of 300 to 10,000, ~3,100 records per title on average (revenue model: 2,500 to 3,300)']}, + master:{t:'2 · Vinyl mastering',w:'The audio is mastered for the physical limits of the groove. The artist approves the vinyl master (gate 2, 2 to 10 days) before anything is cut.',c:['Engineer QA of the vinyl master','Artist gate 2: master approved'],f:['2 to 4 days in-house (production timeline)','Mastering room ~40 to 60 m²','Mastering and lathe cutting are the planned Payap student-programme subjects (payap-partnership.md)']}, + cut:{t:'3 · Lathe cutting',w:'The master is cut into a nitrocellulose lacquer per side: grooves 40 to 80 µm wide, ~28 µm deep. The dead-wax matrix code and an angular fiducial are scribed cold (never with a laser: the lacquer is flammable), and the scribe carries through every metal generation onto every record. Both lacquers go to silvering together and must be plated within roughly 24 to 72 hours.',c:['Groove microscope','Test cut','Dead-wax scribe and fiducial'],f:['1 to 2 days','Sillitoe SDMS or Agnew lathe (vendor landscape)']}, + pasu:{t:'4a · PASU automatic silvering (Pheenix Alpha)',w:'The lacquer is cleaned on the bench station, sensitised, then turned at 45 rpm under four nozzles that spray the silver in an automatic sequence, making the groove conductive for the nickel. Silver is the first metal to touch the groove.',c:['Silvering chemistry logged per batch','Lacquer inspected before silvering'],f:['~15 min per lacquer (estimate; no published figure)','W 2188 × D 715 × H 1835 mm, single-phase 10 A, air 6 bar, exhaust Ø125 mm at 150 m³/h (2024 flyer)']}, + pppu:{t:'4b · PPPU pre-plating (Pheenix Alpha)',w:'A thin first nickel layer over the silver at low current, before production plating. Up to four masters at once, each position with its own control and cathode movement, PID temperature control.',c:['Bath temperature PID-controlled','Current and time logged per position'],f:['15 to 30 min at 10 to 20 A in published practice; 20 min at 20 A and 35 °C here (galvanics.md)','Up to 15 VDC and 30 A per position, 385 L tank, 4.5 kW heater (2024 flyer)']}, + pdpu:{t:'4c · PDPU dual plating unit (Pheenix Alpha)',w:'Production electroforming: father on the pre-plated lacquer, mother on the father, stampers on the mother. Each cell has its own rotating cathode and digitally controlled rectifier; the current ramps up and the part grows to its thickness. The simulation deposits nickel by Faraday\'s law (1.095 g per ampere-hour at ~97 % efficiency, ~9.6 dm² for a Ø350 mm part), so the times follow from the current: father 0.25 mm in ~1 h 15 min on a slow ramp to 185 A, mother 0.25 mm in ~1 h and stamper 0.20 mm in ~50 min at 200 A. Loading takes 5 min, rinsing and unloading 10 min per part. A part that finishes after the shift waits in the cell until morning.',c:['Current, voltage and ampere-hours per cell (PLC with network access)','Bath temperature, filters, nickel top-up and anode basket alerts','Extraction interlock: no extraction, no plating'],f:['Up to 15 VDC and 200 A per cell, 40 L per cell, 385 L tank, parts up to Ø355 mm, cooling water 12 to 20 °C at 10 L/min (2024 flyer)','Published practice: father ~1 h 15 min, mother 1 to 1¼ h, stamper ~45 min (galvanics.md)','One PDPU covers full-capacity demand (20 to 32 parts a day); the second buys redundancy rather than capacity','Bath at 55 °C on screen is an estimate']}, + pmpb:{t:'4d · PMPB preparation bench (Pheenix Alpha)',w:'Between generations the new part is separated, cleaned, passivated anodically so the next generation will part from it, and spun dry. The lacquer is consumed when the father comes off; in 3-step plating the father, and later the mother, go to the archive.',c:['Passivation logged per part','Rinse water to treatment, never the drain'],f:['~10 to 15 min per part (estimate)','Spin table up to 120 rpm, DI water G3/4", single-phase 10 A (2024 flyer)']}, + qc:{t:'4e · Part QC: XRF and mother listening',w:'Every father, mother and stamper gets an XRF thickness reading against its serial; every mother is played before stampers are grown from it, since the mother is the playable positive.',c:['XRF nickel thickness per part, 0.2 to 0.3 mm','Mother listening check'],f:['~3 min per XRF reading, ~25 min for a mother with listening (estimates)']}, + vsf:{t:'5a · VSF back-finish (Sibert)',w:'The stamper back is wet-sanded through a sequence of abrasive grades and cleaned, so it seats evenly on the mould and no back-side defect presses through. First of the three Sibert steps: finish, punch, coin (the order Gotta Groove runs its Sibert machines in).',c:['Back visual check','Closed water loop with filtration; the nickel-bearing water goes to treatment'],f:['~5 to 10 min (estimate; Sibert publishes none), 8 min here','P120 to P2500 in nine grades, 5-station disc head, 150 to 1500 rpm, 1200 VA, 250 kg (2021 spec)']}, + vsp:{t:'5b · VSP centre punch (Sibert)',w:'After a pneumatic pre-centring the operator centres the lead-out groove, the one true circle on the stamper, under ×100 optics; then the punch cuts the centre hole. An off-centre hole makes every record wow, so concentricity is the critical stamper check, logged per stamper.',c:['Concentricity of hole to groove per stamper','Hardness on the punched centre slug (quality-automation.md)','Punch-and-die shot count against the service interval'],f:['~3 to 5 min (estimate), 4 min here','Ø25.4 mm punch standard, ±0.002 mm, turntable ±0.0015 mm, 250 VA, 200 kg (2021 spec)']}, + vsc:{t:'5c · VSC coiner (Sibert)',w:'One hydraulic operation forms the centre and the edge and sizes the outside diameter for the press mould, so no trimming is needed. It locates on the punched hole, which is why it comes last.',c:['Fit against the mould type'],f:['~1 to 2 min (estimate), 2 min here','7", 10" and 12" die sets, three tool stations, 1 kVA, 600 kg (2021 spec)','Tooling per mould type is a quote line']}, + archive:{t:'Father and mother archive',w:'3-step plating keeps the father and the mother of every side. A repress, or a stamper set that wears out before its run is done, pulls a fresh stamper from the archived mother: no lacquer, no artist gates. Press Repress to watch that path.',c:['Storage conditions and genealogy per the traceability spec','Retention policy to be stated in the customer contract'],f:['Repress ~7 to 11 working days door to door (production timeline)']}, + rack:{t:'Stamper rack',w:'Press-ready stamper sets (A and B side). A title gets one set per ~1,500 records, since stampers last 1,000 to 2,000 pressings; a set that wears out early triggers a fresh one from the mothers. A new title\'s first set goes to a press for 10 test pressings, then waits here while the artist listens (gate 3), and the press runs other approved titles meanwhile.',c:['Set identity and pressing count travel with the stampers'],f:['Test pressings with the artist 5 to 15 days at gate 3 (production timeline)']}, + silo_v:{t:'Virgin black compound',w:'Record-grade compound, classically a vinyl chloride / vinyl acetate copolymer. A lot reaches the silo only after the material lab releases it, and its ID attaches to every record pressed from it. A new lot is ordered when the silo falls below a quarter.',c:['Certificate of analysis per lot','Retained sample per lot'],f:['Qualifying Thai and regional compound is an explicit goal']}, + silo_r:{t:'Regrind',w:'Edge trim and punched rejects, ground in-house, feed back as up to 20 % of the black mix. It is a material source with its own gate.',c:['No labels or paper in the grinder','Contamination check per regrind batch','Ratio verified at the extruder'],f:['Edge trim 20 to 40 g per 12" 180 g record (Granulator 400 flyer)','80 % new, 20 % recycled (Newbilt process schematic)']}, + silo_c:{t:'Colour compound',w:'Pre-coloured compound for colour titles and the base colour of splatter; the EX 35 adds the second colour as strands. Colour runs do not take black regrind.',c:['Colour ΔE per lot against the reference'],f:['Masterbatch dosed into a local base compound is the alternative route (Making Vinyl briefing)']}, + lab:{t:'M · Material lab gate',w:'Every incoming lot is quarantined and tested before release; production keeps running on released stock meanwhile. New lots and new suppliers are released only after a release pressing passes optical and audio QC.',c:['Moisture (halogen analyser), heat stability, density, colour','Release pressing'],f:['~2 h from start of tests to release here (estimate)','Instruments and costs: quality-automation.md']}, + oven:{t:'L · Label dry oven',w:'Centre labels are pressed into the record, not glued on. Moisture in the paper causes bubbles, so labels are pre-dried before they reach a press, and the label lot is logged per press cycle.',c:['Print inspection per label lot','Label identity and centring checked by the optical inspection'],f:['France Etuves XU 112 class oven (Newbilt quote)']}, + duplex:{t:'6 · Duplex: HRP 12 presses A and B (Newbilt)',w:'Two semi-automatic presses on one VEX 50 extruder and one hydraulic unit. Each cycle the operator places the bottom label, the cake and the top label; the press closes, steam at 10 bar heats the mould and ~150 t flows the vinyl into the grooves, cooling water sets it, it opens and ejects. Newbilt recommends the Duplex for splatter, so colour and splatter titles come here first; splatter runs the EX 35.',c:['Per cycle: temperatures, pressures, cycle time, cake weight, label lot','10 test pressings for every new title, then gate 3','Record key: stamper + press + cycle timestamp + side'],f:['~30 s average cycle (Newbilt website); here LOAD 4 to 6.5 (by hand), CLOSE 2, STEAM 8, COOL 10, OPEN 2, EJECT 3 s (split is an estimate)','scale.md assumes only 60 to 80 records/h per semi-automatic press with operator handling (estimate); Newbilt\'s 30 s is 120/h','HRP 12: four columns, Ø330 mm cylinder, ~150 t (2024 quote)','Steam 150 to 180 kg/h, cooling up to 100 l/min and 125 kW per press (site requirements)','Stamper change ~20 min, new title ~40 min (estimates)','Unplanned stops about one per 4 running hours, 15 to 45 min each (estimate, sized so overall effectiveness lands near scale.md\'s 60 to 70 %)']}, + classic:{t:'6 · Classic automatic presses C1 and C2 (Newbilt)',w:'Fully automatic, on a 3-column frame derived from the Toolex Alpha: built-in extruder, automatic cake and label loading, cooling-plate handling and spindle stacking. Cooling can end by time or by delivered water volume, and the target volume rises as the cooling water warms, so the cycle stretches when the chiller supply runs above design (modelled at 5 % per °C over 22 °C). Black and solid colour titles; Newbilt sends splatter to the Duplex.',c:['Steam temperature, cooling water temperature and flow monitored','Cycle and trend recording; SCADA interface over OPC UA'],f:['28 s even at 180 g, claimed ≥95 % yield and ≥95 % uptime (2024 flyer); here LOAD 3, CLOSE 2, STEAM 7, COOL 11, OPEN 2, EJECT 3 s','12" at 110 to 180 g, 7" in development, inserts shared with the Duplex (2026 flyer)','Built-in extruder: three zones, 7.5 kW motor (2024 flyer)','Unplanned stops as for the Duplex (estimate)']}, + vex:{t:'6a · VEX 50 extruder (Newbilt)',w:'Feeds both Duplex presses: granulate from the hopper melts through three heated zones and leaves as a cake 20 to 40 g heavier than the record, the surplus becoming edge trim. The two presses draw cakes alternately.',c:['Zone temperatures and screw speed logged','Cake weight per cycle','Regrind ratio verified here'],f:['50 mm screw, three zones, 7.5 kW motor, 45° mounting (2024 quote)','15 kVA connected, 8 kW average (site requirements)']}, + ex35:{t:'6b · EX 35 auxiliary extruder (Newbilt)',w:'Runs beside the VEX 50 and adds a second or third colour as PVC strands for splatter, marble and other effects. Active here whenever a Duplex press runs a splatter title.',c:['Colour lot logged per cycle'],f:['35 mm screw, three zones, two fans, frequency-controlled screw speed (2026 flyer)','Not in the 2024 quote']}, + trim:{t:'7a · Edge trimmers (Newbilt)',w:'A heated blade trims each record to its final diameter straight after the press. The trim drops into bins that go to the granulator in batches.',c:['Trim quality on every record'],f:['~6 s per record (estimate; no published figure)','Heated blade, 7"/10"/12" plates, standalone or in the Duplex emergency-stop circuit (2026 flyer)']}, + audioqc:{t:'QC layer 3 · Automated audio comparison',w:'A needle-drop station plays every test pressing and about one record in fifty and compares it with the source master: spectral envelope, click count, noise floor, channel balance.',c:['Numeric score and pass/fail to traceability'],f:['Only this layer catches a wrong master, EQ or pitch problems','100 % audio QC is impossible: one press outruns 80 to 95 listening stations']}, + punch:{t:'Label punch (Newbilt)',w:'Rejected records lose their label area here so the remaining PVC can be ground without paper.',c:['No label paper into the grinder'],f:['106 mm punch, 7"/10"/12", manual loading then automatic transport and collection (2026 flyer)']}, + gran:{t:'Granulator 400 (Newbilt)',w:'Edge trim and punched rejects are ground to regrind in batches, since the plant makes far less scrap per hour than the granulator takes.',c:['Contamination check per batch','Outlet magnet'],f:['200 to 250 kg/h, 7.5 kW, 12 knives, 8 mm screen, cyclone with ~100 kg storage, 800 kg (2026 flyer)','Batches from 12 kg here (estimate)']}, + carton:{t:'Cartons and sleeves from local partners',w:'Jackets and sleeves come from local carton partners (Ploy Printing as the intended partner); every delivery passes an incoming gate before assembly.',c:['Dimensions, print ΔE against the proof, seam strength, inner sleeve scuff'],f:['10 to 20 working days from approved artwork']}, + optical:{t:'QC layer 1 · 100 % optical inspection',w:'Machine vision inspects every record at line speed: non-fill, stitching, flow lines, warp, off-centre hole, debris, label errors. Rejects go to the label punch and the granulator; test pressings and samples go to the audio station.',c:['Every record, result to traceability'],f:['Custom Keyence/Cognex-class integration','~3.5 % rejects simulated']}, + finish:{t:'7b · Finishing',w:'Inner sleeve and, once assembled, the shrink wrapper. The labels are already on: they were pressed in.',c:['Inner sleeve scuff check per lot','Shrink wrap sampled'],f:['2 to 3 days per order (production timeline)']}, + pack:{t:'8 · Packaging and assembly',w:'The runout matrix code is scanned against the jacket barcode, so the wrong record in the right jacket is caught every time.',c:['Matrix code vs jacket barcode on every unit','Barcode verification'],f:['Packaging and finished goods ~300 to 500 m²']}, + ship:{t:'9 · Fulfillment',w:'Bulk pallets, or per-parcel dropship for the label; carrier tracking joins the traceability record. Test pressings leave here for the artists.',c:['Consignment vs bill of materials','Tracking per shipment or per end customer'],f:['Quoted lead time 7 to 9 weeks mature, 2 to 3 weeks for a repress']}, + boiler:{t:'Steam boiler',w:'Saturated steam at 10 bar / 180 °C heats the moulds; each press draws ~1.3 kg per cycle. Four presses need 600 to 720 kg/h at Newbilt\'s 150 to 180 kg/h per press, so the boiler is shown at 800 kg/h. That is an estimate: scale.md sizes the boiler at 400 kg/h, which covers the Duplex alone. Below 9.5 bar the presses hold in STEAM (Steam dip button).',c:['Pressure held between 9.5 and 10.5 bar at the press','Feedwater treatment, condensate return'],f:['Fuel decision open: biomass, LPG or electric','Detached boiler hall with a 14 m flue','scale.md\'s 400 kg/h is the Duplex figure; to be re-based for the four-press fleet']}, + chiller:{t:'Process cooling',w:'Cooling water sets the vinyl after the steam phase. The four presses here can need up to 125 kW each, so the chiller is shown at 500 kW (estimate; scale.md\'s 250 kW is the Duplex figure). If the supply water runs warm, the Classics stretch their cooling and their cycle. The galvanics plating units take a small branch at 12 to 20 °C.',c:['Supply 20 to 25 °C, closed loop','Up to 100 l/min per press, ~53 l/min average (site requirements)'],f:['~80 to 125 kW electrical per 250 kW of cooling','PDPU coil: 10 L/min at 12 to 20 °C (2024 flyer)']}, + comp:{t:'Compressed air',w:'Dry, filtered air for the press and extruder actuators, the Sibert machines and the silvering unit.',c:['Dew point max 3 °C, impurities max 5 µm'],f:['14 m³/h for a Duplex system (site requirements)','Sibert: 6 to 8 bar, 3.1 to 4.5 L/s per machine in use; PASU 6 bar']}, + hyd:{t:'Hydraulic aggregate (Duplex)',w:'One aggregate supplies ~150 t to both Duplex presses at 180 bar from a gas-loaded accumulator. A larger central supply shared by several systems is possible (Duplex flyer); how the Classics are supplied is a re-quote question.',c:['Oil temperature and pressure logged'],f:['200 l HLP 46, 15 kVA, 12 kW average, 600 kg (site requirements)']}, + diro:{t:'RO / DI water',w:'Plating rinses need deionised water: the PMPB bench has a DI connection and the silvering chemistry is made up with it.',c:['Conductivity logged'],f:['The RO skid in the utilities CSV is an option row sized for boiler feed, not for this (galvanics.md)','0.2 m³/h make-up here (estimate)']}, + zld:{t:'Water treatment, zero discharge',w:'Galvanics rinse water and the VSF\'s nickel-bearing loop water go through closed-loop treatment and metal recovery with an evaporator; nothing goes to the drain. Neither Pheenix nor Digital Matrix supplies this; it is a separate vendor.',c:['Rinse conductivity, evaporator state'],f:['Zero contaminated water commitment (sustainability.md)','USD 90k to 240k (total investment)']}, + energy:{t:'Energy',w:'247 kWp of roof PV, a 271 kWh battery for peak shaving and ride-through, and a medium-voltage PEA connection. Solar follows the clock; the battery covers the evening.',c:['Plant peak 300 to 450 kW without an electric boiler'],f:['Galvanics ~27 kVA connected per line, far less drawn (galvanics.md)','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, the lab, the XRF, the vision station; manual steps check in. Append-only, kept for the life of the customer relationship.',c:['Record key: stamper + press + cycle timestamp + side','Every part knows its parents: stamper, mother, father, lacquer'],f:['The part of the plan the industry responded to at Making Vinyl Europe 2026']}, + hvac:{t:'Plant HVAC',w:'The press hall at 20 to 26 °C and 35 to 60 % RH; galvanics, archive and audio rooms in their own zones, logged on the production timeline.',c:['T, RH and PM per zone'],f:['~100 to 200 kW peak (estimate)']}, + exh:{t:'Galvanics extraction',w:'Every tank unit is extracted (Ø63 mm per plating unit, Ø125 mm at 150 m³/h on the silvering unit) through a scrubber. The interlock is hard-wired: no extraction, no plating.',c:['Fan status logged, interlock hard-wired'],f:['Facility security: fume-extraction interlocks']}, }; let selected=null; -function select(id,quiet){ - if(!quiet&&COMPACT.matches)showPanel('detail',true); +function select(id,quiet){if(!quiet&&COMPACT.matches)showPanel('detail',true); 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 d=DETAILS[M[id].det||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);} -} + [['Checks and gates',d.c],['Figures and sources',d.f]].forEach(([tt,arr])=>{if(!arr)return;const h3=document.createElement('h3');h3.textContent=tt;p.appendChild(h3); + const ul=document.createElement('ul');arr.forEach(x=>{const li=document.createElement('li');li.textContent=x;ul.appendChild(li);});p.appendChild(ul);});} -/* ------------------------------------------------------------------ controls */ +/* ================================================================ 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.boiler.faultUntil=S.t+120;log('Boiler: feedwater pump fault, steam pressure falling (demo)','alarm');}); +$('btnLot').addEventListener('click',()=>newLot(S.silo.v/S.silo.vcaprepress()); document.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target===document.body){e.preventDefault();$('btnPlay').click();}}); -/* ------------------------------------------------------------------ touch: pan, zoom, panel sheet */ -// The drawing zooms by changing the viewBox (same aspect ratio as the full -// plant), so every coordinate in the sim stays as it is. -const SV=$('svg'),BASE={w:1800,h:740},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; +/* ================================================================ touch: pan, zoom, panel sheet + Zoom changes only the viewBox, so sim coordinates never change. */ +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.w{ - if(e.pointerType==='mouse'&&e.button!==0)return; - PTR.set(e.pointerId,{x:e.clientX,y:e.clientY}); - if(PTR.size===1)travel=0; - if(PTR.size===2)pinch=pinchState(); -}); -SV.addEventListener('pointermove',e=>{ - const prev=PTR.get(e.pointerId);if(!prev)return; - const cur={x:e.clientX,y:e.clientY};PTR.set(e.pointerId,cur); - if(PTR.size===1){ - travel+=Math.abs(cur.x-prev.x)+Math.abs(cur.y-prev.y); - // capture only once it is a drag, so a tap still clicks the station - if(travel>8){if(!SV.hasPointerCapture(e.pointerId))SV.setPointerCapture(e.pointerId);SV.classList.add('dragging');panBy(cur.x-prev.x,cur.y-prev.y);} - }else if(PTR.size===2&&pinch){ - travel=99;const n=pinchState(); - panBy(n.x-pinch.x,n.y-pinch.y);if(pinch.d>0)zoomAt(n.x,n.y,n.d/pinch.d);pinch=n; - } -}); +SV.addEventListener('pointerdown',e=>{if(e.pointerType==='mouse'&&e.button!==0)return;PTR.set(e.pointerId,{x:e.clientX,y:e.clientY});if(PTR.size===1)travel=0;if(PTR.size===2)pinch=pinchState();}); +SV.addEventListener('pointermove',e=>{const prev=PTR.get(e.pointerId);if(!prev)return;const cur={x:e.clientX,y:e.clientY};PTR.set(e.pointerId,cur); + // capture only once it is a drag, so a tap still clicks the station + if(PTR.size===1){travel+=Math.abs(cur.x-prev.x)+Math.abs(cur.y-prev.y);if(travel>8){if(!SV.hasPointerCapture(e.pointerId))SV.setPointerCapture(e.pointerId);SV.classList.add('dragging');panBy(cur.x-prev.x,cur.y-prev.y);}} + else if(PTR.size===2&&pinch){travel=99;const n=pinchState();panBy(n.x-pinch.x,n.y-pinch.y);if(pinch.d>0)zoomAt(n.x,n.y,n.d/pinch.d);pinch=n;}}); function ptrUp(e){PTR.delete(e.pointerId);if(PTR.size<2)pinch=null;if(!PTR.size)SV.classList.remove('dragging');} ['pointerup','pointercancel'].forEach(t=>SV.addEventListener(t,ptrUp)); // a drag or pinch must not also select whatever is under the finger SV.addEventListener('click',e=>{if(travel>8){e.stopPropagation();e.preventDefault();}},true); SV.addEventListener('wheel',e=>{e.preventDefault();zoomAt(e.clientX,e.clientY,Math.exp(-e.deltaY*(e.deltaMode?0.05:0.0015)));},{passive:false}); -$('zIn').addEventListener('click',()=>zoomMid(1.5)); -$('zOut').addEventListener('click',()=>zoomMid(1/1.5)); +$('zIn').addEventListener('click',()=>zoomMid(1.5));$('zOut').addEventListener('click',()=>zoomMid(1/1.5)); $('zFit').addEventListener('click',()=>{VB.x=0;VB.y=0;VB.w=BASE.w;applyVB();}); -document.addEventListener('keydown',e=>{ - if(e.target!==document.body)return; - if(e.key==='+'||e.key==='=')zoomMid(1.5);else if(e.key==='-')zoomMid(1/1.5);else if(e.key==='0')$('zFit').click(); -}); -// Short screens: Detail / Log / Legend open one at a time as a sheet. -function showPanel(name,keepOpen){ - // a View button toggles its panel; picking a station always opens Detail - const open=!!name&&(keepOpen||!(document.body.dataset.panel===name&&document.body.classList.contains('sheet'))); - if(open)document.body.dataset.panel=name; - document.body.classList.toggle('sheet',open); - document.querySelectorAll('#views button[data-view]').forEach(b=>b.classList.toggle('on',b.dataset.view===document.body.dataset.panel)); - $('btnInfo').classList.toggle('on',open); -} +document.addEventListener('keydown',e=>{if(e.target!==document.body)return;if(e.key==='+'||e.key==='=')zoomMid(1.5);else if(e.key==='-')zoomMid(1/1.5);else if(e.key==='0')$('zFit').click();}); +// short screens: Detail / Log / Legend open one at a time as a sheet +function showPanel(name,keepOpen){const open=!!name&&(keepOpen||!(document.body.dataset.panel===name&&document.body.classList.contains('sheet'))); + if(open)document.body.dataset.panel=name;document.body.classList.toggle('sheet',open); + document.querySelectorAll('#views button[data-view]').forEach(b=>b.classList.toggle('on',b.dataset.view===document.body.dataset.panel));$('btnInfo').classList.toggle('on',open);} document.querySelectorAll('#views button[data-view]').forEach(b=>b.addEventListener('click',()=>showPanel(b.dataset.view,true))); $('sheetClose').addEventListener('click',()=>showPanel(null)); $('btnInfo').addEventListener('click',()=>showPanel(document.body.dataset.panel||'detail')); const HDR=document.querySelector('header'); new ResizeObserver(()=>document.documentElement.style.setProperty('--hdrH',HDR.offsetHeight+'px')).observe(HDR); -/* ------------------------------------------------------------------ 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',true); +/* ================================================================ 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});} +function seed(){ + for(let i=0;i<6;i++){const T=mkTitle({qty:pick([3000,5000,5000,10000]),approved:true,tp:'ok',stage:'press',kind:i<2?pick(['colour','splatter']):'black'});seedSets(T,TP_N);} + for(let i=0;i<12;i++){const T=mkTitle({tp:'sent',stage:'press'});T.g3due=Math.random()*12*DAY;seedSets(T,TP_N);} + for(let i=0;i<2;i++){const T=mkTitle({stage:'press'});seedSets(T,0);} + for(let i=0;i<3;i++){const T=mkTitle({stage:'galv'});startGalv(T);} + [['cut',2],['gate2',8],['master',4],['gate1',8],['intake',2]].forEach(([st,n])=>{const u=UP[UPI[st]];for(let i=0;i{const h=hourOf();return h>=7&&h<19&&CELLS.some(c=>c.ph==='plate')&&galvCount()>=3&&PRESS.filter(running).length>=3;}; +const t0=performance.now();while(S.t<2*DAY)tick(2); +while(S.t<6*DAY&&!busy())tick(2); +for(let i=0;i<480;i++)tick(0.25); // last two minutes at the 1x step +S.quiet=false; +log('Two shifts, 06:00 to 22:00: Pheenix line, Sibert prep and four Newbilt presses at real duration (warm-up '+f0(performance.now()-t0)+' ms)','ok'); +log('Material lab: lot '+S.lab.lot+' released, moisture 0.03 %, heat stability OK','ok'); +select('pC1',true);renderTags(); -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=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();} + requestAnimationFrame(frame);} requestAnimationFrame(frame); +window.__plant={S,TITLES,RACK,PRESS,CELLS,ST,PLATEQ,M,tick}; })(); diff --git a/site/public/index.html b/site/public/index.html index 09cd2ec..160bc42 100644 --- a/site/public/index.html +++ b/site/public/index.html @@ -54,11 +54,13 @@
  • Plant overview (simulated SCADA) - The whole plant animated, from PVC intake and the material lab - through extruder, press, trim, optical inspection, finishing, - packaging and fulfillment, with the metal path, utilities and energy - alongside. Every station is clickable; on a phone, turn it sideways - and pinch to zoom. + The mature plant animated at real cycle times: Newbilt Duplex + and Classic presses, Pheenix Alpha electroforming and Sibert stamper + prep, from PVC intake and the material lab through pressing, + inspection, packaging and fulfillment, with the artist gates, + utilities and energy alongside. Real time up to a day in 24 s; + every station is clickable; on a phone, turn it sideways and pinch + to zoom.