Skip to content

pointeur_main

pointeur_main

Transformer une main pointée en commandes de pointeur, sans toucher à l'OS.

Le moteur de transfert reconnaît une paume et un poing. Le pointeur a un vocabulaire différent et vit donc dans une machine séparée : l'index déplace, le pincement clique, et un pincement maintenu défile. Mélanger les deux ferait d'un clic raté un dépôt de fichier — une ambiguïté qu'aucun seuil ne peut rendre acceptable.

Comme gestes_main, ce module ne voit ni caméra ni écran. Il reçoit les points de Vision et rend une intention pure, testable sans autorisation Accessibilité et sans déplacer le vrai curseur pendant les tests.

Classes

ActionPointeur

Bases: str, Enum

L'action sur le fil — en anglais comme tout contrat inter-processus.

SeuilsPointeur dataclass

SeuilsPointeur(
    confiance_index_minimale: float = 0.45,
    confiance_pose_minimale: float = 0.25,
    confiance_pince_minimale: float = 0.2,
    index_tendu_min: float = 1.3,
    index_maintenu_min: float = 1.05,
    index_pince_maintenu_min: float = 0.75,
    avantage_index_min: float = 0.18,
    autres_replies_requises: int = 2,
    autres_ouvertes_min: float = 1.5,
    images_pointeur_stable: int = 3,
    trou_de_suivi_s: float = 0.28,
    images_pince_stable: int = 2,
    images_relache_stable: int = 2,
    pince_entree: float = 0.6,
    pince_sortie: float = 0.78,
    pince_lointaine: float = 1.1,
    trou_de_pince_s: float = 0.14,
    double_clic_max_s: float = 0.52,
    maintien_defilement_s: float = 0.5,
    seuil_armement_defilement: float = 0.055,
    dominance_defilement: float = 2.0,
    zone_gauche: float = 0.14,
    zone_droite: float = 0.86,
    zone_haute: float = 0.1,
    zone_basse: float = 0.86,
    bande_basse_bureau: float = 0.86,
    bande_coin_bureau: float = 0.28,
    maintien_bureau_s: float = 0.85,
    seuil_armement_apps: float = 0.08,
    bord_space: float = 0.14,
    repos_bureau_s: float = 0.55,
    repos_apps_s: float = 0.18,
    images_face_stables: int = 1,
    maintien_shaka_s: float = 0.35,
    filtre_coupure_min: float = 1.35,
    filtre_beta: float = 0.65,
    filtre_derivee_coupure: float = 1.0,
    zone_morte: float = 0.0015,
    seuil_defilement: float = 0.012,
    lignes_par_image: float = 120.0,
    lignes_max: int = 10,
)

Les nombres qui distinguent pointer, pincer et faire défiler.

LecturePointeur dataclass

LecturePointeur(
    actif: bool,
    action: ActionPointeur = AUCUNE,
    x: Optional[float] = None,
    y: Optional[float] = None,
    defilement_y: int = 0,
    pince: bool = False,
    proximite_pince: float = 0.0,
    shaka: bool = False,
    shaka_hold: float = 0.0,
)

Ce que l'interface doit appliquer — ou ne surtout pas appliquer.

MoteurDePointeur

MoteurDePointeur(seuils: Optional[SeuilsPointeur] = None)

Suivre un index et confirmer les actions dans le temps.

Source code in src/diapason/desktop/pointeur_main.py
def __init__(self, seuils: Optional[SeuilsPointeur] = None) -> None:
    self.seuils = seuils or SeuilsPointeur()
    self._images_pointeur = 0
    self._x_lisse: Optional[float] = None
    self._y_lisse: Optional[float] = None
    self._filtre_x = _FiltreAdaptatif()
    self._filtre_y = _FiltreAdaptatif()
    self._pince = False
    self._images_pince = 0
    self._images_relache = 0
    self._pince_a = 0.0
    self._pince_vue_a = 0.0
    self._defile = False
    self._apps = False
    self._ancre_x_pince: Optional[float] = None
    self._ancre_y_pince: Optional[float] = None
    self._dernier_y_defilement: Optional[float] = None
    self._dernier_clic_a: Optional[float] = None
    self._perdu_depuis: Optional[float] = None
    self._dernier_bureau_a: float = float("-inf")
    self._face: Optional[bool] = None
    self._face_candidat: Optional[bool] = None
    self._face_images: int = 0
    self._shaka_depuis: Optional[float] = None
    self._visee_x: Optional[float] = None
    self._visee_y: Optional[float] = None
    self._pince_ancre_verrouillee = False
Methods:
reinitialiser
reinitialiser() -> None

Oublier une main perdue sans transformer sa disparition en clic.

Source code in src/diapason/desktop/pointeur_main.py
def reinitialiser(self) -> None:
    """Oublier une main perdue sans transformer sa disparition en clic."""
    self._images_pointeur = 0
    self._x_lisse = None
    self._y_lisse = None
    self._filtre_x = _FiltreAdaptatif()
    self._filtre_y = _FiltreAdaptatif()
    self._pince = False
    self._images_pince = 0
    self._images_relache = 0
    self._pince_a = 0.0
    self._pince_vue_a = 0.0
    self._defile = False
    self._apps = False
    self._ancre_x_pince = None
    self._ancre_y_pince = None
    self._dernier_y_defilement = None
    self._visee_x = None
    self._visee_y = None
    self._pince_ancre_verrouillee = False
    self._dernier_clic_a = None
    self._perdu_depuis = None
    self._oublier_la_face()
observer
observer(
    points: Optional[Sequence[Point]],
    *,
    maintenant: Optional[float] = None,
    lateralite: str = "unknown",
) -> LecturePointeur

Rendre l'intention de cette image après confirmation temporelle.

Source code in src/diapason/desktop/pointeur_main.py
def observer(
    self,
    points: Optional[Sequence[Point]],
    *,
    maintenant: Optional[float] = None,
    lateralite: str = "unknown",
) -> LecturePointeur:
    """Rendre l'intention de cette image après confirmation temporelle."""
    maintenant = time.monotonic() if maintenant is None else maintenant
    mesure = _mesurer(points or ())
    s = self.seuils
    acquis = self._images_pointeur >= s.images_pointeur_stable
    retournement = mesure is not None and self._pose_de_retournement(mesure)
    shaka = mesure is not None and shaka_geste(points or ())
    pointe = mesure is not None and (
        self._pose_d_acquisition(mesure)
        or (acquis and self._pose_de_maintien(mesure))
        or (acquis and retournement)
        or shaka
    )
    if not pointe or mesure is None:
        # Pendant un pincement déjà confirmé, une pose « index tendu »
        # qui flanche 140 ms ne doit pas annuler le clic en cours.
        if (
            self._pince
            and mesure is not None
            and maintenant - self._pince_vue_a <= s.trou_de_pince_s
        ):
            pointe = True
        else:
            self._oublier_la_face()
            return self._sans_pointeur(maintenant)

    self._perdu_depuis = None
    if not acquis:
        self._images_pointeur += 1
    x, y = self._position(mesure, maintenant)
    proximite = self._proximite_pince(mesure)
    brut_x, brut_y = self._position_brute(mesure)
    entree_pince = (
        not self._pince
        and mesure.confiance_pince >= s.confiance_pince_minimale
        and mesure.pince <= s.pince_entree
    )
    if not self._pince and not entree_pince:
        self._enregistrer_visee(brut_x, brut_y, x, y, proximite)
    if self._images_pointeur < s.images_pointeur_stable:
        return LecturePointeur(
            actif=False,
            x=x,
            y=y,
            proximite_pince=proximite,
        )

    flip = self._lire_flip_apps(
        points or (),
        mesure,
        retournement=retournement,
        lateralite=lateralite,
        maintenant=maintenant,
        x=x,
        y=y,
        proximite=proximite,
    )
    if flip is not None:
        return flip

    if not self._pince and shaka:
        capture = self._lire_shaka_capture(points or (), maintenant, x=x, y=y)
        hold = self._progression_shaka(maintenant)
        if capture is not None:
            return capture
        return LecturePointeur(
            actif=True,
            action=ActionPointeur.AUCUNE,
            x=self._x_lisse,
            y=self._y_lisse,
            proximite_pince=proximite,
            shaka=True,
            shaka_hold=hold,
        )

    if mesure.confiance_pince < s.confiance_pince_minimale:
        # Au contact, le pouce masque précisément le bout de l'index que
        # Vision doit lire. Une baisse de confiance de 140 ms conserve le
        # contact confirmé, mais ne peut ni le créer ni le relâcher.
        if self._pince and maintenant - self._pince_vue_a <= s.trou_de_pince_s:
            return self._lecture_pince_figee(proximite=1.0)
        self._annuler_pincement()
        return LecturePointeur(
            actif=True,
            action=ActionPointeur.DEPLACER,
            x=x,
            y=y,
            proximite_pince=proximite,
        )

    pince_maintenant = mesure.pince <= (
        s.pince_sortie if self._pince else s.pince_entree
    )
    if pince_maintenant:
        if not self._pince:
            self._pince = True
            self._images_pince = 1
            self._pince_a = maintenant
            self._pince_vue_a = maintenant
            self._images_relache = 0
            self._pince_ancre_verrouillee = False
            self._ancre_x_pince = self._visee_x if self._visee_x is not None else x
            self._ancre_y_pince = self._visee_y if self._visee_y is not None else y
            self._dernier_y_defilement = y
        else:
            self._images_pince += 1
            self._pince_vue_a = maintenant
            self._images_relache = 0

        if not self._pince_ancre_verrouillee:
            if self._images_pince >= s.images_pince_stable:
                self._verrouiller_ancre_pince()
            else:
                return LecturePointeur(
                    actif=True,
                    action=ActionPointeur.DEPLACER,
                    x=x,
                    y=y,
                    pince=True,
                    proximite_pince=proximite,
                )

        if self._apps:
            return self._lecture_pince_figee()

        bureau = self._bureau_bas_imobile(x=x, y=y, maintenant=maintenant)
        if bureau is not None:
            return bureau

        if maintenant - self._pince_a >= s.maintien_defilement_s:
            if (
                not self._defile
                and self._ancre_x_pince is not None
                and self._ancre_y_pince is not None
            ):
                dx = x - self._ancre_x_pince
                dy = y - self._ancre_y_pince
                # Horizontal dominant depuis un bord → Spaces. Le centre
                # n'arme plus les apps : c'est le retournement paume/dos.
                au_bord = (
                    self._ancre_x_pince <= s.bord_space
                    or self._ancre_x_pince >= 1.0 - s.bord_space
                )
                if (
                    au_bord
                    and abs(dx) >= s.seuil_armement_apps
                    and abs(dx) >= s.dominance_defilement * abs(dy)
                    and maintenant - self._dernier_bureau_a >= s.repos_bureau_s
                ):
                    self._apps = True
                    vers_droite = dx > 0
                    action = (
                        ActionPointeur.SPACE_SUIVANT
                        if vers_droite
                        else ActionPointeur.SPACE_PRECEDENT
                    )
                    return self._action_bureau(
                        action,
                        maintenant,
                        x=self._ancre_x_pince,
                        y=self._ancre_y_pince,
                    )
                if abs(dy) >= s.seuil_armement_defilement and abs(
                    dy
                ) >= s.dominance_defilement * abs(dx):
                    self._defile = True
                    self._dernier_y_defilement = self._ancre_y_pince
            if self._defile:
                ancien_y = self._dernier_y_defilement
                self._dernier_y_defilement = y
                if ancien_y is None:
                    return self._lecture_pince_figee()
                deplacement = y - ancien_y
                if abs(deplacement) >= s.seuil_defilement:
                    lignes = round(-deplacement * s.lignes_par_image)
                    if lignes == 0:
                        lignes = -1 if deplacement > 0 else 1
                    lignes = max(-s.lignes_max, min(s.lignes_max, lignes))
                    return LecturePointeur(
                        actif=True,
                        action=ActionPointeur.DEFILER,
                        x=self._ancre_x_pince,
                        y=y,
                        defilement_y=lignes,
                        pince=True,
                        proximite_pince=1.0,
                    )

        # Tant que le scroll n'est pas armé : curseur figé à l'ancre.
        return self._lecture_pince_figee()

    if self._pince:
        if self._images_pince < s.images_pince_stable:
            self._annuler_pincement()
            return LecturePointeur(
                actif=True,
                action=ActionPointeur.DEPLACER,
                x=x,
                y=y,
                proximite_pince=proximite,
            )
        self._images_relache += 1
        if self._images_relache < s.images_relache_stable:
            return self._lecture_pince_figee(proximite=max(0.5, proximite))
        images = self._images_pince
        defilait = self._defile or self._apps
        clic_x = self._ancre_x_pince if self._ancre_x_pince is not None else x
        clic_y = self._ancre_y_pince if self._ancre_y_pince is not None else y
        self._annuler_pincement()
        if not defilait and images >= s.images_pince_stable:
            double = bool(
                self._dernier_clic_a is not None
                and maintenant - self._dernier_clic_a <= s.double_clic_max_s
            )
            self._dernier_clic_a = None if double else maintenant
            return LecturePointeur(
                actif=True,
                action=(
                    ActionPointeur.DOUBLE_CLIQUER
                    if double
                    else ActionPointeur.CLIQUER
                ),
                x=clic_x,
                y=clic_y,
                proximite_pince=proximite,
            )

    return LecturePointeur(
        actif=True,
        action=ActionPointeur.DEPLACER,
        x=x,
        y=y,
        proximite_pince=proximite,
    )

Functions: