Files
mathew/frontend/app/composables/useVarSliders.ts
Aran Roig 6df7950161
Some checks failed
Build and Deploy Nuxt / build (push) Failing after 41s
Ai slop
2026-10-03 14:13:08 +02:00

120 lines
4.1 KiB
TypeScript

/* 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<string, number>,
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<string, number>()
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<string, number>()
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<number, VarSlider[]>()
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<string, number> = {}
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 }
}