diff --git a/README.md b/README.md index 3c47da0..cea1b25 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,11 @@ The detail lives in dedicated documents: records a minute each) and Classics (2 a minute) from the start fleet of two Duplex and one Classic up to a full press hall, with the people, power, central hydraulics, steam, cooling, air, floor, compound and - stampers that fleet needs; every assumption is editable, with its - source, and a scenario can be shared by link + stampers that fleet needs, drawn as a live schematic of the machine + park with the stamper line (lathe, Pheenix galvanics, Sibert prep) + feeding it, each station counted from the galvanics cycle times; + every assumption is editable, with its source, and a scenario can be + shared by link - [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/fleet-calculator.html b/docs/fleet-calculator.html index cd6c215..ef13eb9 100644 --- a/docs/fleet-calculator.html +++ b/docs/fleet-calculator.html @@ -27,6 +27,31 @@ document.documentElement.setAttribute("data-theme",t);})(); --input: #ffffff; --flagw: #fff6e0; --flago: #e8f6ee; --flagb: #fdeaea; --cur: #e1f3e8; --bv: #1f5f9e; --bvb: #9cc0e6; --bd: #1e7a45; --bdb: #9fd3b3; --be: #8a5a00; --beb: #e3c27a; --bu: #6b3fa0; --bub: #c9a8ec; } + :root { --pk-press: #2a3a56; --pk-pressS: #6d86b3; --pk-glyph: #3a4d70; } + html[data-theme=light] { --pk-press: #c7d1df; --pk-pressS: #8e9db4; --pk-glyph: #b8c4d6; } + #park { display: block; width: 100%; height: auto; min-width: 900px; } + #park text { font-family: inherit; } + .pk-box { fill: var(--panel2); stroke: var(--stroke); } + .pk-box.warn { stroke: var(--warn); stroke-width: 2; } + .pk-hall { fill: none; stroke: var(--stroke); stroke-width: 1.4; stroke-dasharray: 7 5; } + .pk-cell { fill: var(--panel); stroke: var(--stroke); } + .pk-press { fill: var(--pk-press); stroke: var(--pk-pressS); stroke-width: 1.2; } + .pk-col { fill: var(--pk-pressS); } + .pk-ex50 { fill: var(--dstd); } .pk-ex35 { fill: var(--dspec); } + .pk-op { fill: #e8b400; } .pk-trim { fill: var(--mute); } .pk-disc { fill: #111; stroke: var(--mute); } + .pk-glyph { fill: var(--pk-glyph); stroke: var(--pk-pressS); } + .pk-feed { stroke: var(--mute); stroke-width: 1; fill: none; stroke-dasharray: 2 3; } + .pk-t { fill: var(--fg); font-size: 12px; font-weight: 600; } + .pk-s { fill: var(--mute); font-size: 10.5px; } + .pk-v { fill: var(--fg); font-size: 15px; font-weight: 700; } + .pk-n { fill: var(--fg); font-size: 19px; font-weight: 700; } + .pk-lane { fill: var(--mute); font-size: 11px; letter-spacing: 0.12em; font-weight: 600; } + .pk-arrow { stroke: var(--mute); stroke-width: 1.6; fill: none; } + .pk-head { fill: var(--mute); } + .pk-util { fill: var(--line); } .pk-utilv { fill: var(--accent); } .pk-utilv.hi { fill: var(--warn); } + .pk-pipe { fill: none; stroke-width: 3; stroke-dasharray: 8 5; opacity: 0.85; } + .pk-steam { stroke: #e2462a; } .pk-cool { stroke: #2f7de1; } .pk-hyd { stroke: #d48600; } + .pk-air { stroke: #8a97b0; } .pk-pow { stroke: #c9a800; } .themebtn { position: absolute; top: 24px; right: 20px; background: var(--panel); color: var(--fg); border: 1px solid var(--stroke); border-radius: 5px; padding: 6px 12px; font: inherit; font-size: 14px; cursor: pointer; @@ -221,6 +246,16 @@ document.documentElement.setAttribute("data-theme",t);})();
+
+

Machine park and stamper production

+

The fleet above as a schematic, not to scale: each Duplex cell as + in the cell sketch (presses either side of the operator, EX 50 and EX 35 behind him, trimmer in + front), each Classic with its built-in extruder; above it the stamper line that feeds the presses, + sized from the cycle times in galvanics.md; below it the central utilities. Bars show how busy + each station is; an amber box exceeds the plant as planned today.

+
+
+

From small to fully equipped

Fleets from the start fleet to a full Payap press hall (about 870 m²: @@ -260,7 +295,8 @@ document.documentElement.setAttribute("data-theme",t);})(); share of the power.

Sources in the CNX Vinyl planning repository: equipment/pressing-line.md, equipment/newbilt/ (Duplex site requirements V14, offer 240620-04, 2026 flyers), - equipment/galvanics.md, scale.md, finance/revenue-model.md, site-candidates.md (Payap scenario model).

+ equipment/galvanics.md (cycle times per step; the stamper line counts each station from them, + before remakes and test pressings), scale.md, finance/revenue-model.md, site-candidates.md (Payap scenario model).

@@ -324,8 +360,20 @@ grp("Material", [ grp("Stampers", [ ["runLen", "Records per title", 3000, "", "doc", "Revenue model: 2,500 to 3,300 records per title"], ["stLife", "Pressings per stamper", 1500, "", "doc", "galvanics.md: 1,000 to 2,000"], - ["pdpuLo", "Parts per plating unit a day, low", 20, "", "doc", "galvanics.md: 20 to 32 per Pheenix PDPU on two shifts"], - ["pdpuHi", "Parts per plating unit a day, high", 32, "", "doc", "galvanics.md"], +]); +grp("Mastering, galvanics and stamper prep", [ + ["lathe", "Lacquer sides one lathe cuts a day", 8, "sides", "est", "Estimate: about 20 min cutting per side plus setup and checks, one shift"], + ["galvEff", "Usable share of galvanics and prep hours", 80, "%", "est", "Estimate; OMS: 6 to 8 runs on 4 cells in a 7.5-hour day (galvanics.md). Galvanics runs the plant's shifts"], + ["silverMin", "Silvering per lacquer (PASU)", 15, "min", "est", "galvanics.md: ~15 min, estimate"], + ["preMin", "Pre-plate per master (PPPU, four at once)", 25, "min", "doc", "galvanics.md: 15 to 30 min"], + ["fatherMin", "Father in a PDPU cell", 75, "min", "doc", "galvanics.md: ~1 h 15 min"], + ["motherMin", "Mother in a PDPU cell", 70, "min", "doc", "galvanics.md: ~1 to 1 1/4 h"], + ["stamperMin", "Stamper in a PDPU cell", 45, "min", "doc", "galvanics.md: ~45 min"], + ["handleMin", "Loading, rinsing, unloading per part", 15, "min", "doc", "galvanics.md: ~15 min on top of deposition"], + ["pmpbMin", "Separation, passivation, cleaning per part (PMPB)", 12, "min", "est", "galvanics.md: ~10 to 15 min, estimate"], + ["vsfMin", "Back-finish per stamper (Sibert VSF)", 8, "min", "est", "galvanics.md: 5 to 10 min, estimate; Sibert publishes none"], + ["vspMin", "Centre punch per stamper (Sibert VSP)", 4, "min", "est", "galvanics.md: 3 to 5 min, estimate"], + ["vscMin", "Coin per stamper (Sibert VSC)", 2, "min", "est", "galvanics.md: 1 to 2 min, estimate"], ]); grp("Planned plant, for the checks", [ ["boiler", "Steam boiler", 400, "kg/h", "doc", "scale.md: sized for one Duplex system"], @@ -390,6 +438,24 @@ function compute(D, C, X, b) { const partsTitle = 2 * (2 + perSide); const titles = X / Math.max(1, A.runLen); const partsYr = titles * partsTitle, partsDay = partsYr / Math.max(1, S.days); + // stamper production per working day, stations sized from galvanics.md cycle times + const tDay = titles / Math.max(1, S.days); + const flow = { titlesDay: tDay, lacq: 2 * tDay, fathers: 2 * tDay, mothers: 2 * tDay, stampers: 2 * perSide * tDay }; + flow.parts = flow.fathers + flow.mothers + flow.stampers; + const avail = S.shifts * S.hrs * 60 * Math.max(1, A.galvEff) / 100; + const need = (dem, cap) => dem <= 0 || cap <= 0 ? { n: 0, u: 0 } + : (n => ({ n, u: dem / (n * cap) }))(Math.max(1, Math.ceil(dem / cap - 1e-9))); + const galv = { + lathe: need(flow.lacq, A.lathe), + pasu: need(flow.lacq * A.silverMin, avail), + pppu: need(flow.lacq * (A.preMin + A.handleMin) / 4, avail), + pdpu: need(flow.fathers * (A.fatherMin + A.handleMin) + flow.mothers * (A.motherMin + A.handleMin) + + flow.stampers * (A.stamperMin + A.handleMin), 2 * avail), + pmpb: need(flow.parts * A.pmpbMin, avail), + vsf: need(flow.stampers * A.vsfMin, avail), + vsp: need(flow.stampers * A.vspMin, avail), + vsc: need(flow.stampers * A.vscMin, avail), + }; const coolPeak = P * A.coolPress + E * A.extCool + P * A.hydCool; const air = P * A.airPress + E * A.extAir; const kvaMach = P * A.pressKVA + E * A.extKVA, kwMach = P * A.pressKW + E * A.extKW; @@ -397,8 +463,7 @@ function compute(D, C, X, b) { return { D, C, X, P, E, sp, st, stC, stD, dUse, cUse, perHour, att, fin, perShift, staff: perShift * S.shifts, galv: partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.galvRate)) : 0, - perSide, partsTitle, titles, partsYr, partsDay, - pdpu: [partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.pdpuHi)) : 0, partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.pdpuLo)) : 0], + perSide, partsTitle, titles, partsYr, partsDay, flow, gst: galv, kvaMach, kwMach, kvaHyd, kwHyd, kva: kvaMach + kvaHyd, kw: kwMach + kwHyd, mwh: (kwMach + kwHyd) * b.hoursYr / 1000, chillerKW: coolPeak / Math.max(0.5, A.cop), compKW: air * A.compSpec, @@ -532,8 +597,14 @@ function render() { G("Stampers and galvanics"); R("Titles a year", f0(r.titles), `${f0(A.runLen)} records a title`); R("Electroformed parts a year", f0(r.partsYr), `${r.partsTitle} a title: father, mother and ${r.perSide} stamper${r.perSide > 1 ? "s" : ""} per side`); - R("Parts per working day", f1(r.partsDay), ""); - R("Plating units (Pheenix PDPU)", r.pdpu[0] === r.pdpu[1] ? r.pdpu[0] : `${r.pdpu[0]} to ${r.pdpu[1]}`, `${f0(A.pdpuLo)} to ${f0(A.pdpuHi)} parts a day each, two shifts; plus one for redundancy`); + R("Per working day", `${f1(r.flow.lacq)} / ${f1(r.flow.fathers)} / ${f1(r.flow.mothers)} / ${f1(r.flow.stampers)}`, "lacquers / fathers / mothers / stampers, before remakes"); + const GS = r.gst, su = k => `${GS[k].n} · ${pct(GS[k].u)}`; + R("Lathes", su("lathe"), `units · busy; ${f0(A.lathe)} lacquer sides a day each`); + R("Silvering, Pheenix PASU", su("pasu"), ""); + R("Pre-plating, Pheenix PPPU", su("pppu"), "four masters at once"); + R("Plating, Pheenix PDPU (2 cells)", su("pdpu"), "a second unit adds redundancy, not capacity, at low load"); + R("Passivation, Pheenix PMPB", su("pmpb"), ""); + R("Sibert VSF / VSP / VSC", `${GS.vsf.n} / ${GS.vsp.n} / ${GS.vsc.n}`, `busiest ${pct(Math.max(GS.vsf.u, GS.vsp.u, GS.vsc.u))}`); $("reqT").innerHTML = `${rows.join("")}`; $("ladT").innerHTML = @@ -544,6 +615,8 @@ function render() { return `${n}${d}${c}${q.P}${big(x)}${big(q.sp)}${q.perShift}${f0(q.steam[1])}${f0(q.coolPeak)}${f0(q.kva)}${f0(q.floor)}`; }).join("") + ""; + drawPark(r, b); + for (const [, id] of AS) { const tr = document.querySelector(`#asT tr[data-id="${id}"]`); if (tr) tr.classList.toggle("chg", A[id] !== A0[id]); @@ -551,6 +624,136 @@ function render() { writeHash(); } +// ---------------------------------------------------------------- machine park schematic +function drawPark(r, b) { + const o = []; + const T = (x, y, c, t, a) => o.push(`${t}`); + const box = (x, y, w, h, c, rx) => o.push(``); + const arrow = (x1, y1, x2, y2) => o.push(``); + const bar = (x, y, w, u) => { box(x, y, w, 5, "pk-util", 2); box(x, y, Math.max(0, w * Math.min(u, 1)), 5, "pk-utilv" + (u > 0.9 ? " hi" : ""), 2); }; + o.push(``); + const glyph = (k, x, y) => { + if (k === "lathe") o.push(``); + else if (k === "pdpu") o.push(``); + else if (k === "pppu") o.push(`` + [0, 1, 2, 3].map(i => ``).join("")); + else if (k === "pmpb") o.push(``); + else o.push(``); + }; + const f = r.flow, g = r.gst, days = Math.max(1, S.days); + + // 1. stamper production + T(17, 18, "pk-lane", `STAMPER PRODUCTION · ${f1(f.titlesDay)} TITLES A WORKING DAY (${f0(r.titles)} A YEAR) · ${f1(f.parts)} ELECTROFORMED PARTS A DAY`); + const ST = [ + ["Mastering", "lathe", g.lathe, `${f1(f.lacq)} lacquer sides`, "lathe"], + ["Silvering", "Pheenix PASU", g.pasu, `${f1(f.lacq)} lacquers`, "pasu"], + ["Pre-plating", "Pheenix PPPU", g.pppu, `${f1(f.lacq)} masters`, "pppu"], + ["Plating", "Pheenix PDPU, 2 cells", g.pdpu, `${f1(f.parts)} parts`, "pdpu"], + ["Passivation", "Pheenix PMPB", g.pmpb, `${f1(f.parts)} parts`, "pmpb"], + ["Back-finish", "Sibert VSF", g.vsf, `${f1(f.stampers)} stampers`, "vsf"], + ["Centre punch", "Sibert VSP", g.vsp, `${f1(f.stampers)} stampers`, "vsp"], + ["Coin", "Sibert VSC", g.vsc, `${f1(f.stampers)} stampers`, "vsc"], + ]; + const bw = 130, gp = 18, y0 = 28, bh = 112; + ST.forEach(([t, sub, u, fl, k], i) => { + const x = 17 + i * (bw + gp); + box(x, y0, bw, bh, "pk-box"); + T(x + 8, y0 + 17, "pk-t", t); T(x + 8, y0 + 33, "pk-s", sub); + for (let j = 0; j < Math.min(u.n, 3); j++) glyph(k, x + 8 + j * 24, y0 + 38); + T(x + bw - 8, y0 + 58, "pk-n", u.n + "×", "end"); + T(x + 8, y0 + 80, "pk-s", fl + " a day"); + bar(x + 8, y0 + 88, bw - 16, u.u); + T(x + 8, y0 + 106, "pk-s", pct(u.u) + " busy"); + if (i < ST.length - 1) arrow(x + bw + 2, y0 + bh / 2, x + bw + gp - 2, y0 + bh / 2); + }); + + // 2. press hall, with compound in (left), stampers in and records out (right) + const items = []; + for (let i = 0; i < r.D; i++) items.push(["D", i + 1]); + for (let i = 0; i < r.C; i++) items.push(["C", i + 1]); + const hx = 165, hw = 870, iw = { D: 150, C: 84 }, ih = 104, innerW = hw - 30; + const lay = sc => { let x = 0, y = 0; const pos = []; + for (const it of items) { const w = iw[it[0]] * sc; if (x > 0 && x + w > innerW + 0.5) { x = 0; y += (ih + 12) * sc; } pos.push([x, y]); x += w + 12 * sc; } + return { pos, h: items.length ? y + ih * sc : 0 }; }; + let sc = 1, L = lay(1); + while (L.h > 480 && sc > 0.3) { sc *= 0.85; L = lay(sc); } + const y1 = y0 + bh + 44, hh = Math.max(230, 46 + L.h + 18); + box(hx, y1, hw, hh, "pk-hall", 8); + T(hx + 14, y1 + 20, "pk-lane", `PRESS HALL · ${r.D} DUPLEX SYSTEM${r.D === 1 ? "" : "S"} + ${r.C} CLASSIC${r.C === 1 ? "" : "S"} · ${r.P} PRESSES · ~${f0(r.floor)} M² PRESS FLOOR`); + o.push(`EX 50` + + `EX 35 colour` + + `operator` + + `trimmer`); + if (!items.length) T(hx + hw / 2, y1 + hh / 2, "pk-s", "no presses", "middle"); + items.forEach(([k, n], i) => { + const [px, py] = L.pos[i], tx = hx + 15 + px, ty = y1 + 34 + py; + if (k === "D") o.push(`` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `Duplex ${n} · ${pct(r.dUse)}`); + else o.push(`` + + `` + + `` + + `` + + `` + + `` + + `Classic ${n} · ${pct(r.cUse)}`); + }); + // compound in + const cy = y1 + 20; + box(17, cy, 130, 112, "pk-box"); + T(25, cy + 17, "pk-t", "Compound"); T(25, cy + 33, "pk-s", "colour store and silos"); + T(25, cy + 56, "pk-v", `${f0(r.compound)} t a year`); + T(25, cy + 74, "pk-s", `${f0(r.extKgH)} kg/h extruded`); + T(25, cy + 90, "pk-s", `trim back: ${f0(r.trimT)} t a year`); + arrow(149, cy + 56, hx - 2, cy + 56); + // stamper rack, fed from the coin press above, into the hall + const rx0 = 1053, ry = y1 + 6; + arrow(17 + 7 * (bw + gp) + bw / 2, y0 + bh + 2, 17 + 7 * (bw + gp) + bw / 2, ry - 2); + box(rx0, ry, 130, 88, "pk-box"); + T(rx0 + 8, ry + 17, "pk-t", "Stamper rack"); T(rx0 + 8, ry + 33, "pk-s", "sets to the presses"); + T(rx0 + 8, ry + 54, "pk-v", `${f1(f.stampers)} a day`); + T(rx0 + 8, ry + 72, "pk-s", `${r.perSide} per side a title`); + arrow(rx0 - 2, ry + 44, hx + hw + 2, ry + 44); + // finished records out to finishing + const fy = ry + 108; + box(rx0, fy, 130, 104, "pk-box"); + T(rx0 + 8, fy + 17, "pk-t", "Finishing"); T(rx0 + 8, fy + 33, "pk-s", "QC, sleeves, packing"); + T(rx0 + 8, fy + 54, "pk-v", `${f0(r.X / days)} a day`); + T(rx0 + 8, fy + 72, "pk-s", `${r.fin} people per shift`); + T(rx0 + 8, fy + 88, "pk-s", `${f0(r.perHour)} records an hour`); + arrow(hx + hw + 2, fy + 52, rx0 - 2, fy + 52); + + // 3. utilities, piped up into the hall + const y3 = y1 + hh + 46, uw = 158, ug = (hw - 5 * uw) / 4; + T(hx, y3 - 12, "pk-lane", "CENTRAL UTILITIES"); + const U = [ + ["Central hydraulics", `${f0(r.hyd.flow)} l/min`, `${f1(r.hyd.motor)} kW motors`, `${f0(r.hyd.oil)} l oil, up to 200 bar`, "pk-hyd", false], + ["Steam boiler", `${f0(r.steam[1])} kg/h peak`, `from ${f0(r.steam[0])} kg/h, 10 bar`, `planned ${f0(A.boiler)} kg/h`, "pk-steam", r.steam[1] > A.boiler], + ["Cooling", `${f0(r.coolPeak)} kW peak`, `${f0(r.water[0])} to ${f0(r.water[1])} l/min`, `planned ${f0(A.chiller)} kW chiller`, "pk-cool", r.coolPeak > A.chiller], + ["Compressed air", `${f0(r.air)} m³/h`, "6 to 10 bar, dry", `${f1(r.compKW)} kW compressors`, "pk-air", false], + ["Electrical", `${f0(r.kva)} kVA`, `${f0(r.kw)} kW average`, `${f0(r.mwh)} MWh a year`, "pk-pow", false], + ]; + U.forEach(([t, v, s1, s2, cls, warn], i) => { + const x = hx + i * (uw + ug); + o.push(``); + box(x, y3, uw, 96, "pk-box" + (warn ? " warn" : "")); + T(x + 8, y3 + 17, "pk-t", t); + T(x + 8, y3 + 42, "pk-v", v); + T(x + 8, y3 + 62, "pk-s", s1); + T(x + 8, y3 + 78, "pk-s", s2); + }); + const H = y3 + 96 + 12; + const svg = $("park"); + svg.setAttribute("viewBox", `0 0 1200 ${H}`); + svg.innerHTML = o.join(""); +} + // ---------------------------------------------------------------- inputs and state const NUM = ["target", "dMin", "cMin", "dFleet", "cFleet", "shifts", "hrs", "days", "eff", "rateD", "rateC", "specF"]; function syncInputs() { @@ -608,6 +811,7 @@ $("ladT").addEventListener("click", e => { S.mode = "fleet"; S.dFleet = d; S.cFleet = c; syncInputs(); render(); }); $("reset").addEventListener("click", () => { S = { ...S0 }; A = { ...A0 }; syncInputs(); render(); }); +addEventListener("hashchange", () => { S = { ...S0 }; A = { ...A0 }; readHash(); syncInputs(); render(); }); $("copy").addEventListener("click", () => { const done = () => { $("copy").textContent = "Link copied"; setTimeout(() => { $("copy").textContent = "Copy link to this scenario"; }, 1600); }; if (navigator.clipboard) navigator.clipboard.writeText(location.href).then(done, done); else done();