From bfab6e1d2ea4b148ba73267aa7e1d2e21169454a Mon Sep 17 00:00:00 2001 From: Melvin Strobl Date: Fri, 16 Oct 2026 09:00:36 +0200 Subject: [PATCH] Counter wrap stays on the step grid: it counts in span = top - lo + the finest step rather than the period, so one counter of step 5 up to 100 wraps 100 + 5 to 0 and 0 - 5 to 100; the time picker is unchanged. Check cases and docs formula --- docs/interface/dashboards.md | 2 +- .../Dashboard/ui/core/counter.check.ts | 4 +++- .../src/components/Dashboard/ui/core/counter.ts | 16 +++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index b2039a8..8fb9fb3 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -154,7 +154,7 @@ which the firmware ports as it stands): | Digit dᵢ | d₀ = ⌊v / w₀⌋; dᵢ = ⌊v / wᵢ⌋ mod sizeᵢ, a remainder that is never negative | | A step | counter i by k steps (k < 0 down): v + k × stepᵢ × wᵢ, so carry and borrow follow | | Range | lo = min₀ × w₀. With a `max`, period = (max₀ − min₀ + 1) × w₀ and top = lo + ⌊(period − 1) / s⌋ × s, s the finest counter's step: 0–23 in 5-minute steps tops out at 1435 (23:55), one counter of step 5 up to 100 at 100 | -| Wrap | v = lo + ((v − lo) mod period): 23:55 + 5 min is 00:00, 00:00 − 1 h is 23:00 | +| Wrap | v = lo + ((v − lo) mod span), span = top − lo + s, so a wrap stays on the step grid; where s divides the period the span is the period. 23:55 + 5 min is 00:00, 00:00 − 1 h is 23:00; one counter of step 5 up to 100 goes from 100 + 5 to 0 and from 0 − 5 to 100 | | Stop | v held to lo … top, as a slider is | | No `max` | no top and nothing to wrap at: v is only held to lo or above | diff --git a/frontend/src/components/Dashboard/ui/core/counter.check.ts b/frontend/src/components/Dashboard/ui/core/counter.check.ts index 3acfec0..25b4bf2 100644 --- a/frontend/src/components/Dashboard/ui/core/counter.check.ts +++ b/frontend/src/components/Dashboard/ui/core/counter.check.ts @@ -47,7 +47,9 @@ assert.equal(stepCounter(time, false, at(0, 30), HOUR, -1), 0) const single: Counter[] = [{ step: 5, max: 100 }] assert.equal(rangeOf(single).top, 100) assert.equal(stepCounter(single, false, 100, 0, 1), 100) -assert.equal(stepCounter(single, true, 100, 0, 1), 4) +// Wrap stays on the step grid: the span is top − lo + 5 = 105, not 101. +assert.equal(stepCounter(single, true, 100, 0, 1), 0) +assert.equal(stepCounter(single, true, 0, 0, -1), 100) assert.deepEqual(writtenOf(single, 5), ["005"]) // No max: no top, nothing to wrap at, still held at min. diff --git a/frontend/src/components/Dashboard/ui/core/counter.ts b/frontend/src/components/Dashboard/ui/core/counter.ts index ccbc4f9..cd14a22 100644 --- a/frontend/src/components/Dashboard/ui/core/counter.ts +++ b/frontend/src/components/Dashboard/ui/core/counter.ts @@ -59,23 +59,25 @@ export function digitsOf(counters: Counter[], value: number): number[] { /** * The value's range: `lo`, the highest value the finest step reaches from it - * (`top`), and the `period` a wrap counts in. Without a `max` there is no top - * and nothing to wrap at. + * (`top`), and the `span` a wrap counts in, top − lo + one finest step, which + * keeps a wrap on the step grid. Without a `max` there is no top and nothing + * to wrap at. */ export function rangeOf(counters: Counter[]) { const coarsest = counters[0] const weight = weightsOf(counters)[0] const min = coarsest.min ?? 0 const lo = min * weight - if (coarsest.max == null) return { lo, top: Infinity, period: null } + if (coarsest.max == null) return { lo, top: Infinity, span: null } const period = (coarsest.max - min + 1) * weight const fine = counters[counters.length - 1].step ?? 1 - return { lo, top: lo + Math.floor((period - 1) / fine) * fine, period } + const top = lo + Math.floor((period - 1) / fine) * fine + return { lo, top, span: top - lo + fine } } /** * `value` with counter `index` stepped `by` steps, carrying into and borrowing - * from the others. At the ends it wraps round the period, or stops at `lo` and + * from the others. At the ends it wraps round the span, or stops at `lo` and * `top`; without a `max` it only stops at `lo`. */ export function stepCounter( @@ -87,8 +89,8 @@ export function stepCounter( ): number { const next = value + by * (counters[index].step ?? 1) * weightsOf(counters)[index] - const { lo, top, period } = rangeOf(counters) - if (wrap && period !== null) return lo + mod(next - lo, period) + const { lo, top, span } = rangeOf(counters) + if (wrap && span !== null) return lo + mod(next - lo, span) return Math.min(top, Math.max(lo, next)) }