From b2dbd890d9b9b723b33edfea7d2b56adeb56f082 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 30 Jul 2026 10:24:21 +0200 Subject: [PATCH 1/3] chore(perf): la sonde distingue le scrub timeline du scrub barre de progression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les deux chemins n'ont rien à voir, et la sonde les confondait — ou plutôt, elle ne voyait que le premier : `setUiProbeScrubbing` n'était appelé que depuis `V4Timeline.startScrub`. Un scrub via la barre de progression atterrissait donc dans les buckets `repos` ou `preview`, ce qui pollue silencieusement toute mesure où l'utilisateur touche la barre — y compris le `repos@N` qui a servi à juger l'accumulation au fil des bascules de clip. Les états deviennent `scrub-tl` et `scrub-bar` (et leurs variantes `+preview`). Les mélanger reviendrait à moyenner deux populations différentes, exactement le biais que cette sonde existe pour éviter — c'est déjà arrivé une fois avec le franchissement de clip, où l'écart avait été attribué à un changement de code qui n'y était pour rien. Ce commit ne change AUCUN comportement : il ne fait qu'étiqueter, pour que le correctif d'alignement qui suit soit jugeable sur un avant/après plutôt que sur une impression. --- src/components/ai-edition/TransportBar.tsx | 7 +++ src/lib/ai-edition/perf/uiFrameProbe.ts | 52 ++++++++++++++++------ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/components/ai-edition/TransportBar.tsx b/src/components/ai-edition/TransportBar.tsx index fa017ff13..b8c1047fa 100644 --- a/src/components/ai-edition/TransportBar.tsx +++ b/src/components/ai-edition/TransportBar.tsx @@ -1,6 +1,7 @@ import { Pause, Play, SkipBack, SkipForward } from "lucide-react"; import { memo } from "react"; import { useScopedT } from "@/contexts/I18nContext"; +import { setUiProbeScrubbing } from "@/lib/ai-edition/perf/uiFrameProbe"; import type { AxcutClip } from "@/lib/ai-edition/schema"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; import styles from "./NewEditorShell.module.css"; @@ -97,6 +98,12 @@ export const TransportBar = memo(function TransportBar({ step={0.01} value={inputValue} onChange={(e) => onSeek(Number(e.target.value))} + // Sonde de fluidité (diagnostic) : marque la fenêtre de drag comme + // venant de la BARRE, pour ne pas la confondre avec un drag de timeline — + // les deux empruntent des chemins de code différents. + onPointerDown={() => setUiProbeScrubbing(true, "bar")} + onPointerUp={() => setUiProbeScrubbing(false, "bar")} + onPointerCancel={() => setUiProbeScrubbing(false, "bar")} className={styles.scrubInput} aria-label={te("transport.seekVideo")} /> diff --git a/src/lib/ai-edition/perf/uiFrameProbe.ts b/src/lib/ai-edition/perf/uiFrameProbe.ts index abfc69c94..8fb2adb31 100644 --- a/src/lib/ai-edition/perf/uiFrameProbe.ts +++ b/src/lib/ai-edition/perf/uiFrameProbe.ts @@ -21,7 +21,13 @@ * qu'on ne le demande pas — aucun coût en usage normal. */ -type BaseState = "repos" | "preview" | "scrub" | "scrub+preview"; +type BaseState = + | "repos" + | "preview" + | "scrub-tl" + | "scrub-tl+preview" + | "scrub-bar" + | "scrub-bar+preview"; /** L'état porte le suffixe `@N` = nombre de bascules de clip déjà vues. Voir * `noteUiProbeClipSwitch` : sans cette séparation, deux mesures ne sont pas comparables. */ type ProbeState = string; @@ -36,7 +42,19 @@ let rafHandle = 0; let lastTs = 0; /** Frames de preview peintes depuis le dernier tick — dit si la preview travaille. */ let previewFramesSinceTick = 0; -let scrubbing = false; +/** + * D'OÙ vient le scrub en cours. + * + * Les deux chemins n'ont rien à voir : la timeline écrit la tête de lecture directement + * dans le DOM et coalesce son écriture au store en rAF ; la barre de progression, elle, + * écrit au store à chaque événement ET repose un `seekTarget` — un état de la RACINE de + * l'éditeur — ce qui re-render tout et déclenche un seek du `