This commit is contained in:
119
frontend/app/composables/useVarSliders.ts
Normal file
119
frontend/app/composables/useVarSliders.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/* 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user