/* The free-variable sliders both calculators wear: one value per variable in a scope the compile pass reads, one slider per variable anchored under the row that claims it — the row declaring it ("k", "k = 2") when one asks, else the first row speaking it — and the values kept between visits per calculator. A name the scope does not carry yet reads as 1, where every slider starts. */ export interface VarSlider { name: string value: number min: number max: number } /* one row's reading, as far as sliders care: the variables it speaks and the declaration its text claims (a null start says "k", not "k = 2") */ export interface RowVarDecls { rowId: number vars: string[] declared: { name: string; value: number | null } | null } /* how far a slider reaches either side of zero; a value set past an end — by "k = 40" or by typing — pulls that end out so the thumb can reach it */ const SLIDER_MIN = -10 const SLIDER_MAX = 10 export function useVarSliders( varValues: Record, decls: () => RowVarDecls[], storageKey: string, ) { /* every variable spoken, with the value its "k = 2" row asks to start at — or 1, where sliders start. One variable wears one slider however many rows speak it. */ const varStarts = computed(() => { const starts = new Map() for (const d of decls()) { if (d.declared && !starts.has(d.declared.name)) starts.set(d.declared.name, d.declared.value ?? 1) for (const name of d.vars) if (!starts.has(name)) starts.set(name, 1) } return starts }) // a variable the rows only now started speaking takes its start value; // one the reader already moved keeps the value they left it at. Filling // from the first pass keeps what the server draws and what the reader // finds after mounting the same function fillDefaults() { for (const [name, value] of varStarts.value) { if (!(name in varValues)) varValues[name] = value } } watch(varStarts, fillDefaults, { immediate: true }) /* one slider per variable, filed under the row it sits beneath: the row claiming it ("k", "k = 2") wins, else the first row that speaks it */ const slidersByRow = computed(() => { const anchor = new Map() for (const d of decls()) { if (d.declared && !anchor.has(d.declared.name)) anchor.set(d.declared.name, d.rowId) } for (const d of decls()) { for (const name of d.vars) { if (!anchor.has(name)) anchor.set(name, d.rowId) } } const byRow = new Map() for (const d of decls()) { for (const name of d.vars) { if (anchor.get(name) !== d.rowId) continue const value = varValues[name] ?? 1 const list = byRow.get(d.rowId) ?? [] list.push({ name, value, min: Math.min(SLIDER_MIN, value), max: Math.max(SLIDER_MAX, value) }) byRow.set(d.rowId, list) } } return byRow }) function setVar(name: string, raw: number) { if (Number.isFinite(raw)) varValues[name] = raw } /* the values keep themselves — only the ones a row still speaks, so a variable nobody asks for anymore is forgotten on the next save rather than haunting the store */ function save() { try { const kept: Record = {} for (const [name, value] of Object.entries(varValues)) { if (varStarts.value.has(name) && Number.isFinite(value)) kept[name] = value } localStorage.setItem(storageKey, JSON.stringify(kept)) } catch { /* private mode etc. — the slider values just won't persist */ } } watch([varValues, varStarts], save) onMounted(() => { try { const raw = localStorage.getItem(storageKey) if (raw) { const stored: unknown = JSON.parse(raw) if (typeof stored === 'object' && stored !== null) { for (const [name, value] of Object.entries(stored)) { if (typeof value === 'number' && Number.isFinite(value)) varValues[name] = value } } } } catch { /* storage unreadable — the sliders start where their rows ask */ } fillDefaults() }) return { slidersByRow, setVar } }