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();