From 9fdf324b0e525cc1080a034896fe9f8d750e9f88 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 27 May 2026 09:59:28 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20math=20de=20molette=20infinie=20pour?= =?UTF-8?q?=20le=20s=C3=A9lecteur=20d'heure=20(MUI-39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../time/composables/useInfiniteWheel.test.ts | 45 +++++++++++++++++++ .../time/composables/useInfiniteWheel.ts | 23 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 app/components/malio/time/composables/useInfiniteWheel.test.ts create mode 100644 app/components/malio/time/composables/useInfiniteWheel.ts diff --git a/app/components/malio/time/composables/useInfiniteWheel.test.ts b/app/components/malio/time/composables/useInfiniteWheel.test.ts new file mode 100644 index 0000000..94d78ad --- /dev/null +++ b/app/components/malio/time/composables/useInfiniteWheel.test.ts @@ -0,0 +1,45 @@ +import {describe, expect, it} from 'vitest' +import { + CENTER_OFFSET, + VISIBLE_ROWS, + loopCorrection, + scrollTopForValueIndex, + valueIndexFromScroll, +} from './useInfiniteWheel' + +const H = 40 // itemHeight +const LEN = 24 // ex. heures + +describe('useInfiniteWheel — math pure', () => { + it('expose 5 lignes visibles et un offset central de 2', () => { + expect(VISIBLE_ROWS).toBe(5) + expect(CENTER_OFFSET).toBe(2) + }) + + it('scrollTopForValueIndex et valueIndexFromScroll font un aller-retour', () => { + for (const index of [0, 1, 9, 23]) { + const top = scrollTopForValueIndex(index, H, LEN) + expect(valueIndexFromScroll(top, H, LEN)).toBe(index) + } + }) + + it('valueIndexFromScroll boucle en modulo', () => { + const top = scrollTopForValueIndex(0, H, LEN) + expect(valueIndexFromScroll(top + LEN * H, H, LEN)).toBe(0) + }) + + it('loopCorrection laisse le scroll de la copie du milieu inchangé', () => { + const top = scrollTopForValueIndex(12, H, LEN) + expect(loopCorrection(top, H, LEN)).toBe(top) + }) + + it('loopCorrection ramène vers le milieu quand on dérive vers le haut', () => { + const drifted = scrollTopForValueIndex(0, H, LEN) - LEN * H + expect(loopCorrection(drifted, H, LEN)).toBe(drifted + LEN * H) + }) + + it('loopCorrection ramène vers le milieu quand on dérive vers le bas', () => { + const drifted = scrollTopForValueIndex(0, H, LEN) + LEN * H + expect(loopCorrection(drifted, H, LEN)).toBe(drifted - LEN * H) + }) +}) diff --git a/app/components/malio/time/composables/useInfiniteWheel.ts b/app/components/malio/time/composables/useInfiniteWheel.ts new file mode 100644 index 0000000..0951e3e --- /dev/null +++ b/app/components/malio/time/composables/useInfiniteWheel.ts @@ -0,0 +1,23 @@ +export const VISIBLE_ROWS = 5 +export const CENTER_OFFSET = (VISIBLE_ROWS - 1) / 2 // 2 + +/** Index de valeur logique (0..length-1) centré pour un scrollTop donné. */ +export function valueIndexFromScroll(scrollTop: number, itemHeight: number, length: number): number { + const flat = Math.round(scrollTop / itemHeight) + CENTER_OFFSET + return ((flat % length) + length) % length +} + +/** scrollTop qui centre l'index donné dans la copie du milieu (buffer à 3 copies). */ +export function scrollTopForValueIndex(valueIndex: number, itemHeight: number, length: number): number { + const flat = length + valueIndex - CENTER_OFFSET + return flat * itemHeight +} + +/** Recentre le scrollTop dans la copie du milieu [length, 2*length) si on a dérivé. */ +export function loopCorrection(scrollTop: number, itemHeight: number, length: number): number { + const block = length * itemHeight + const centeredFlat = Math.round(scrollTop / itemHeight) + CENTER_OFFSET + if (centeredFlat < length) return scrollTop + block + if (centeredFlat >= 2 * length) return scrollTop - block + return scrollTop +}