Fleet calculator: machine park schematic with the stamper line feeding it
Deploy test site / deploy (push) Successful in 34s

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.
This commit is contained in:
grabowski committed 2026-10-10 19:30:05 +07:00
1 parent 6df1301fb9
commit 7d7a378c8b
2 files changed
+216 -9

No files matched your search

+211 -7
View File
@@ -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);})();
</div>
<div class="wrap">
<section class="card">
<h2>Machine park and stamper production</h2>
<p class="note" style="margin-top:0">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.</p>
<div class="tscroll"><svg id="park" viewBox="0 0 1200 600" role="img" aria-label="Machine park and stamper production schematic"></svg></div>
</section>
<section class="card">
<h2>From small to fully equipped</h2>
<p class="note" style="margin-top:0">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.</p>
<p>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).</p>
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).</p>
</section>
</div>
<footer>CNX Vinyl planning repository · docs/fleet-calculator.html · figures are planning estimates, October 2026.</footer>
@@ -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 = `<tbody>${rows.join("")}</tbody>`;
$("ladT").innerHTML =
@@ -544,6 +615,8 @@ function render() {
return `<tr class="lad${cur}" data-i="${i}"><td>${n}</td><td class="n">${d}</td><td class="n">${c}</td><td class="n">${q.P}</td><td class="n">${big(x)}</td><td class="n">${big(q.sp)}</td><td class="n">${q.perShift}</td><td class="n">${f0(q.steam[1])}</td><td class="n">${f0(q.coolPeak)}</td><td class="n">${f0(q.kva)}</td><td class="n">${f0(q.floor)}</td></tr>`;
}).join("") + "</tbody>";
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(`<text x="${x}" y="${y}" class="${c}"${a ? ` text-anchor="${a}"` : ""}>${t}</text>`);
const box = (x, y, w, h, c, rx) => o.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx == null ? 6 : rx}" class="${c}"/>`);
const arrow = (x1, y1, x2, y2) => o.push(`<path d="M${x1},${y1} L${x2},${y2}" class="pk-arrow" marker-end="url(#pkA)"/>`);
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(`<defs><marker id="pkA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="pk-head"/></marker></defs>`);
const glyph = (k, x, y) => {
if (k === "lathe") o.push(`<circle cx="${x + 10}" cy="${y + 11}" r="9" class="pk-glyph"/><circle cx="${x + 10}" cy="${y + 11}" r="2" class="pk-head"/>`);
else if (k === "pdpu") o.push(`<rect x="${x}" y="${y}" width="9" height="22" rx="2" class="pk-glyph"/><rect x="${x + 11}" y="${y}" width="9" height="22" rx="2" class="pk-glyph"/>`);
else if (k === "pppu") o.push(`<rect x="${x}" y="${y}" width="20" height="22" rx="2" class="pk-glyph"/>` + [0, 1, 2, 3].map(i => `<circle cx="${x + 5 + (i % 2) * 10}" cy="${y + 6 + Math.floor(i / 2) * 10}" r="2.4" class="pk-head"/>`).join(""));
else if (k === "pmpb") o.push(`<rect x="${x}" y="${y + 7}" width="20" height="15" rx="2" class="pk-glyph"/>`);
else o.push(`<rect x="${x}" y="${y}" width="20" height="22" rx="2" class="pk-glyph"/><circle cx="${x + 10}" cy="${y + 11}" r="6" class="pk-head" opacity=".55"/>`);
};
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(`<g transform="translate(${hx + hw - 300},${y1 + 10})"><rect width="10" height="10" class="pk-ex50"/><text x="14" y="9" class="pk-s">EX 50</text>` +
`<rect x="62" width="10" height="10" class="pk-ex35"/><text x="76" y="9" class="pk-s">EX 35 colour</text>` +
`<circle cx="160" cy="5" r="5" class="pk-op"/><text x="169" y="9" class="pk-s">operator</text>` +
`<rect x="225" width="10" height="10" class="pk-trim"/><text x="239" y="9" class="pk-s">trimmer</text></g>`);
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(`<g transform="translate(${tx},${ty}) scale(${sc})">` +
`<rect width="150" height="104" rx="6" class="pk-cell"/>` +
`<path d="M60,24 L42,36 M74,26 L108,36" class="pk-feed"/>` +
`<rect x="-15" y="-15" width="30" height="30" class="pk-press" transform="translate(32,46) rotate(45)"/>` +
`<rect x="-15" y="-15" width="30" height="30" class="pk-press" transform="translate(118,46) rotate(45)"/>` +
`<rect x="57" y="10" width="14" height="30" rx="2" class="pk-ex50"/>` +
`<rect x="77" y="14" width="11" height="24" rx="2" class="pk-ex35"/>` +
`<circle cx="73" cy="57" r="5.5" class="pk-op"/>` +
`<rect x="48" y="66" width="13" height="13" rx="1" class="pk-trim"/>` +
`<text x="75" y="96" class="pk-s" text-anchor="middle">Duplex ${n} · ${pct(r.dUse)}</text></g>`);
else o.push(`<g transform="translate(${tx},${ty}) scale(${sc})">` +
`<rect width="84" height="104" rx="6" class="pk-cell"/>` +
`<rect x="9" y="16" width="11" height="30" rx="2" class="pk-ex50"/>` +
`<rect x="28" y="22" width="36" height="36" rx="3" class="pk-press"/>` +
`<circle cx="33" cy="27" r="3.5" class="pk-col"/><circle cx="59" cy="27" r="3.5" class="pk-col"/><circle cx="46" cy="53" r="3.5" class="pk-col"/>` +
`<circle cx="66" cy="73" r="7" class="pk-disc"/>` +
`<text x="42" y="96" class="pk-s" text-anchor="middle">Classic ${n} · ${pct(r.cUse)}</text></g>`);
});
// 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(`<path d="M${x + uw / 2},${y3} L${x + uw / 2},${y1 + hh}" class="pk-pipe ${cls}"/>`);
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();