From 7d7a378c8bff690d0555bbe3e2fb12d9430acf7b Mon Sep 17 00:00:00 2001 From: grabowski Date: Sat, 10 Oct 2026 19:30:05 +0700 Subject: [PATCH] Fleet calculator: machine park schematic with the stamper line feeding it User, October 2026: add a visualization of the machine park and the connected stamper production to the tool. - New card "Machine park and stamper production": one live SVG that follows every input. Top: the stamper line, lathe, Pheenix PASU, PPPU, PDPU (2 cells), PMPB, Sibert VSF, VSP, VSC, each with its count, flow a day and how busy it is. Middle: the press hall, each Duplex cell drawn as in the cell sketch (presses either side of the operator, EX 50 and EX 35 behind, trimmer in front) and each Classic with its built-in extruder, wrapping into rows and scaling down for big fleets; compound in on the left, stamper rack in and finishing out on the right. Bottom: central hydraulics, boiler, cooling, air and electrical piped up into the hall, amber where they exceed the plant as planned today. - Stamper production is now counted per station from the cycle times in galvanics.md (silvering, pre-plate four at a time, father, mother, stamper plus handling per PDPU cell, PMPB, Sibert steps; usable share of the hours 80 %, estimate) instead of a flat 20 to 32 parts per PDPU. At the defaults: 1 PDPU at ~43 % busy, as galvanics.md concludes. New assumptions with sources; the lathe rate (8 sides a day) is an estimate. - Requirements table: per-station counts and load; the URL hash now also reloads in an open tab (hashchange). Checked headless in Edge, both themes: no JS errors, no text overlap or overflow in the schematic at the default, 8 M and 30 M records, no horizontal overflow at 390 px. --- README.md | 7 +- docs/fleet-calculator.html | 218 +++++++++++++++++++++++++++++++++++-- 2 files changed, 216 insertions(+), 9 deletions(-) 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();