Initial roast planner webapp: fillable worksheet, live ledger, curve, URL prefill, .alog reference curve
- shared/ ports the worksheet's ledger math, time parsing, and reference tables (cultivars/processes/roast-levels/machine bands) as browser-safe ESM, imported by both the server and the browser so the arithmetic can't drift between them. - server/prefill.js runs a zero-tool Pi Coding Agent SDK turn to extract page facts from a bean product URL, then derives worksheet field IDs deterministically from reference-data.js — the model never invents a first-crack anchor or a modifier. - server/alog.js ports the Python alog_parser.py's format handling, including the tokenizer-based Python-dict-literal-to-JSON conversion the real files need. - public/ is the worksheet reproduced as a live HTML form: worksheet.css is a verbatim copy of the paper worksheet's print CSS, print.js bakes values into the print layout so the same DOM renders both on screen and on paper. - Ledger math verified against both of the paper worksheet's worked examples; alog parser verified against all 14 real logs in ref/roasts/. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Browser-safe. Geometry for the roast-curve SVG grid (viewBox 0 0 714 334), matching
|
||||
// manual-roast-planner.html's worksheet box 10 grid exactly.
|
||||
|
||||
import { MACHINE } from "./reference-data.js";
|
||||
|
||||
export const PLOT = { x0: 52, x1: 652, y0: 24, y1: 294, tMaxS: 900 };
|
||||
|
||||
export function tToX(seconds) {
|
||||
const clamped = Math.max(0, Math.min(PLOT.tMaxS, seconds));
|
||||
return PLOT.x0 + (clamped / PLOT.tMaxS) * (PLOT.x1 - PLOT.x0);
|
||||
}
|
||||
|
||||
// 60C -> y294 (bottom axis), 200C label sits at y42 (confirmed by the FC band rect y=65.4,h=19.8 = 176-187C)
|
||||
export function tempToY(degC) {
|
||||
return PLOT.y1 - (degC - 60) * 1.8;
|
||||
}
|
||||
|
||||
export function rorToY(rorCPerMin) {
|
||||
return PLOT.y1 - rorCPerMin * 9;
|
||||
}
|
||||
|
||||
/** Build the five plan-curve points from the ledger + box-8 temps, falling back to machine medians. */
|
||||
export function buildPlanCurve(plan, ledger) {
|
||||
const temps = plan.temps ?? {};
|
||||
const num = (v) => {
|
||||
const n = Number.parseFloat(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
|
||||
const points = [
|
||||
{ key: "charge", label: "Charge", timeS: 0, tempC: num(temps.charge?.tempC) ?? MACHINE.charge.medianC },
|
||||
{ key: "tp", label: "Turning point", timeS: parseTpTime(temps.tp?.time), tempC: num(temps.tp?.tempC) ?? MACHINE.turningPoint.medianC },
|
||||
{ key: "yellow", label: "Yellow", timeS: ledger.yellow, tempC: num(temps.yellow?.tempC) ?? MACHINE.yellow.medianC },
|
||||
{ key: "fc", label: "First crack", timeS: ledger.A, tempC: num(temps.fc?.tempC) ?? MACHINE.firstCrack.medianC },
|
||||
{ key: "drop", label: "Drop", timeS: ledger.D, tempC: num(temps.drop?.tempC) ?? MACHINE.drop.medianC },
|
||||
];
|
||||
|
||||
return points.filter((p) => p.timeS !== null && p.timeS !== undefined);
|
||||
}
|
||||
|
||||
function parseTpTime(str) {
|
||||
if (!str) return 52; // MACHINE.turningPoint.medianTime, "0:52"
|
||||
const m = String(str).match(/^(\d+):(\d{1,2})$/);
|
||||
if (!m) return null;
|
||||
return Number(m[1]) * 60 + Number(m[2]);
|
||||
}
|
||||
|
||||
/** Build an SVG <path> "d" string through points, sorted by time, plain polyline (no kink invented). */
|
||||
export function pointsToPathD(points) {
|
||||
const sorted = [...points].sort((a, b) => a.timeS - b.timeS);
|
||||
if (sorted.length === 0) return "";
|
||||
return sorted
|
||||
.map((p, i) => `${i === 0 ? "M" : "L"}${tToX(p.timeS).toFixed(1)},${tempToY(p.tempC).toFixed(1)}`)
|
||||
.join(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user