File size: 12,146 Bytes
2695555 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | """openTARS 2.9.2: mouse preciso.
Lógica pura (sem X, sem pyautogui) dos cliques e arrastos, testável sozinha:
- ancora(): "top-left", "canto superior esquerdo", "oben links"... -> ponto da tela
- caminho(): os pontos de um arrasto "de gente": sai devagar do lugar
(apps como o Chrome/Brave só entendem que é arrasto depois
de uns pixels com o botão apertado), acelera e freia no fim
- recorte_em_volta(): a região em volta de um ponto
- mudanca(): quanto a tela mudou entre duas capturas (o clique pegou?)
"""
import re
import unicodedata
# ------------------------------------------------------------
# Âncoras: lugares da tela por nome, em 5 idiomas
# ------------------------------------------------------------
MARGEM_ANCORA = 48 # px da borda: soltar colado na borda liga o "encaixe" de janela do GNOME/KDE
_VERTICAL = {
"topo": ("top", "upper", "topo", "cima", "alto", "superior", "arriba", "haut", "oben", "obere", "oberen"),
"base": ("bottom", "lower", "baixo", "inferior", "embaixo", "abajo", "bas", "unten", "untere", "unteren"),
}
_HORIZONTAL = {
"esquerda": ("left", "esquerda", "esquerdo", "izquierda", "izquierdo", "gauche", "links", "linke", "linken"),
"direita": ("right", "direita", "direito", "derecha", "derecho", "droite", "rechts", "rechte", "rechten"),
}
_CENTRO = ("center", "centre", "middle", "centro", "meio", "medio", "milieu", "mitte")
def _normalizar(texto):
texto = unicodedata.normalize("NFKD", str(texto or "").lower())
texto = "".join(c for c in texto if not unicodedata.combining(c))
return re.findall(r"[a-z]+", texto)
def ancora(nome, largura, altura, margem=MARGEM_ANCORA):
"""(x, y) do lugar da tela chamado 'nome', ou None se não for um lugar.
'top-left' -> perto do canto superior esquerdo; 'left' -> meio da
borda esquerda; 'center' -> centro."""
palavras = set(_normalizar(nome))
if not palavras:
return None
vertical = next((k for k, v in _VERTICAL.items() if palavras & set(v)), None)
horizontal = next((k for k, v in _HORIZONTAL.items() if palavras & set(v)), None)
centro = bool(palavras & set(_CENTRO))
if not (vertical or horizontal or centro):
return None
# "metade esquerda", "left half": o centro daquela metade
metade = bool(palavras & {"half", "metade", "mitad", "moitie", "halfte", "haelfte"})
margem = max(0, min(margem, largura // 4, altura // 4))
if horizontal == "esquerda":
x = largura // 4 if metade else margem
elif horizontal == "direita":
x = largura - largura // 4 if metade else largura - 1 - margem
else:
x = largura // 2
if vertical == "topo":
y = altura // 4 if metade else margem
elif vertical == "base":
y = altura - altura // 4 if metade else altura - 1 - margem
else:
y = altura // 2
return x, y
# ------------------------------------------------------------
# Caminho do arrasto
# ------------------------------------------------------------
EMPURRAO_PX = 14 # o primeiro pedaço, devagar: vira "arrasto" no app
PASSOS_ARRASTO = 24
def caminho(x0, y0, x1, y1, passos=PASSOS_ARRASTO, empurrao=EMPURRAO_PX):
"""Pontos inteiros de (x0, y0) até (x1, y1), sem o de partida e
terminando exatamente no destino. Os primeiros cobrem só 'empurrao' px
(em 3 passos); o resto vai com aceleração e freada (ease-in-out)."""
dx, dy = x1 - x0, y1 - y0
distancia = (dx * dx + dy * dy) ** 0.5
if distancia < 1:
return [(int(x1), int(y1))]
pontos = []
inicio = min(1.0, empurrao / distancia)
for i in (1, 2, 3):
f = inicio * i / 3
pontos.append((round(x0 + dx * f), round(y0 + dy * f)))
resto = max(2, passos - 3)
for i in range(1, resto + 1):
s = i / resto
suave = s * s * (3 - 2 * s) # smoothstep
f = inicio + (1 - inicio) * suave
pontos.append((round(x0 + dx * f), round(y0 + dy * f)))
pontos[-1] = (int(x1), int(y1))
limpos = []
for p in pontos:
if not limpos or limpos[-1] != p:
limpos.append(p)
return limpos
def recorte_em_volta(x, y, raio, largura, altura):
"""(x0, y0, x1, y1) do quadrado de lado 2*raio em volta de (x, y),
empurrado pra dentro da imagem (sem encolher, se couber)."""
lado = 2 * raio
x0 = min(max(0, x - raio), max(0, largura - lado))
y0 = min(max(0, y - raio), max(0, altura - lado))
return x0, y0, min(largura, x0 + lado), min(altura, y0 + lado)
SEMELHANCA_COR = 30 # soma R+G+B: até isso é "a mesma cor" (antisserrilhado à parte)
def _mesma_cor(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2]) <= SEMELHANCA_COR
def encaixar(imagem, x, y, raio=20):
"""Se o ponto caiu numa área lisa (errou o botão
por pouco), vai pro centro do elemento INTEIRO mais perto (uma mancha
de cor própria que cabe na região). Caiu em cima de algo: fica."""
try:
img = imagem.convert("RGB")
if not (0 <= x < img.width and 0 <= y < img.height):
return x, y
x0, y0, x1, y1 = recorte_em_volta(x, y, 3, img.width, img.height)
cor = img.getpixel((x, y))
if not all(_mesma_cor(cor, img.getpixel((i, j))) for i in range(x0, x1) for j in range(y0, y1)):
return x, y # já está em cima de alguma coisa
x0, y0, x1, y1 = recorte_em_volta(x, y, raio, img.width, img.height)
regiao = img.crop((x0, y0, x1, y1))
largura, altura = regiao.size
px = regiao.load()
visto = [[False] * largura for _ in range(altura)]
lx, ly = x - x0, y - y0
melhor = None
for j in range(altura):
for i in range(largura):
if visto[j][i]:
continue
semente = px[i, j]
pilha, pontos = [(i, j)], []
visto[j][i] = True
while pilha:
a, b = pilha.pop()
pontos.append((a, b))
for na, nb in ((a + 1, b), (a - 1, b), (a, b + 1), (a, b - 1)):
if 0 <= na < largura and 0 <= nb < altura and not visto[nb][na] \
and _mesma_cor(px[na, nb], semente):
visto[nb][na] = True
pilha.append((na, nb))
xs = [a for a, _ in pontos]
ys = [b for _, b in pontos]
bx0, bx1, by0, by1 = min(xs), max(xs), min(ys), max(ys)
# o fundo onde o ponto caiu, pedaços de coisa maior (encostam
# na borda) e ruído não são "o botão"
if (bx0 <= lx <= bx1 and by0 <= ly <= by1 and (lx, ly) in set(pontos)) \
or bx0 == 0 or by0 == 0 or bx1 == largura - 1 or by1 == altura - 1 \
or len(pontos) < 12:
continue
dx = max(bx0 - lx, 0, lx - bx1)
dy = max(by0 - ly, 0, ly - by1)
d = dx * dx + dy * dy
if melhor is None or d < melhor[0]:
melhor = (d, (bx0 + bx1) / 2, (by0 + by1) / 2)
if melhor is None:
return x, y
return round(x0 + melhor[1]), round(y0 + melhor[2])
except Exception:
return x, y
# ------------------------------------------------------------
# A tela mudou?
# ------------------------------------------------------------
LARGURA_COMPARACAO = 320
LIMIAR_PIXEL = 40 # diferença R+G+B pra contar como pixel mudado
FRACAO_MUDOU = 0.002 # 0,2% dos pixels: uma aba fechando já passa disso
def mudanca(antes, depois):
"""Fração (0–1) dos pixels que mudaram entre duas capturas do mesmo
lugar. Compara em tamanho pequeno: rápido e ignora ruído de 1 px."""
if antes is None or depois is None or antes.size != depois.size:
return None
largura = min(LARGURA_COMPARACAO, antes.width)
altura = max(1, round(antes.height * largura / antes.width))
a = antes.convert("RGB").resize((largura, altura))
b = depois.convert("RGB").resize((largura, altura))
pa, pb = a.load(), b.load()
mudados = 0
for j in range(altura):
for i in range(largura):
ca, cb = pa[i, j], pb[i, j]
if abs(ca[0] - cb[0]) + abs(ca[1] - cb[1]) + abs(ca[2] - cb[2]) > LIMIAR_PIXEL:
mudados += 1
return mudados / (largura * altura)
def mudou(fracao):
return fracao is not None and fracao >= FRACAO_MUDOU
# ------------------------------------------------------------
# 3.0 Miller: mover JANELA (não arrastar o que está escrito nela)
# ------------------------------------------------------------
# "arraste a janela do openTARS pro topo esquerdo": antes o arrasto ia
# até o TEXTO "openTARS" dentro da janela. Mover janela é com o
# gerenciador de janelas (EWMH), que sabe onde fica a borda e a barra.
_PALAVRAS_JANELA = {"janela", "window", "ventana", "fenetre", "fenster", "titlebar", "barra de titulo",
"title bar", "barre de titre", "titelleiste", "barra de titulo"}
_PALAVRAS_ABA = {"aba", "tab", "guia", "pestana", "onglet", "reiter", "tabs", "abas"}
_RE_MAXIMIZAR = {"maximize", "maximizar", "maximizada", "maximiser", "maximieren", "tela cheia", "full screen",
"fullscreen", "pantalla completa", "plein ecran", "vollbild", "inteira"}
_RE_RESTAURAR = {"restore", "restaurar", "restaure", "wiederherstellen", "normal", "unmaximize"}
def fala_de_janela(texto):
"""'janela do openTARS', 'barra de título', 'the firefox window' (mas não 'aba do YouTube')."""
n = " ".join(_normalizar(texto))
palavras = set(n.split())
if palavras & _PALAVRAS_ABA:
return False
return bool(palavras & _PALAVRAS_JANELA) or any(p in n for p in _PALAVRAS_JANELA if " " in p)
def sem_palavras_de_janela(texto):
"""'a janela do openTARS' -> 'openTARS'."""
fora = {"a", "o", "the", "la", "le", "die", "der", "da", "do", "de", "of", "del", "du", "des"}
fora |= {p for p in _PALAVRAS_JANELA if " " not in p}
fora |= {"barra", "titulo", "title", "bar", "barre", "titre", "titelleiste", "sua", "your", "minha", "my"}
return " ".join(p for p in str(texto or "").split() if " ".join(_normalizar(p)) not in fora).strip()
def lugar_da_janela(lugar, area, tamanho=None):
"""Onde pôr a janela: (x, y, largura, altura) do retângulo EXTERNO (com a
borda), largura/altura None = não muda o tamanho; ou 'maximizar' /
'restaurar'; ou None se 'lugar' não é um lugar conhecido.
area = (x, y, largura, altura) da área útil (sem painéis, _NET_WORKAREA);
tamanho = (largura, altura) da janela por fora, pra encostar nos cantos
da direita/de baixo e centralizar."""
palavras = set(_normalizar(lugar))
n = " ".join(_normalizar(lugar))
if palavras & _RE_MAXIMIZAR or any(p in n for p in _RE_MAXIMIZAR if " " in p):
return "maximizar"
if palavras & _RE_RESTAURAR:
return "restaurar"
ax, ay, al, aa = area
vertical = next((k for k, v in _VERTICAL.items() if palavras & set(v)), None)
horizontal = next((k for k, v in _HORIZONTAL.items() if palavras & set(v)), None)
centro = bool(palavras & set(_CENTRO))
metade = bool(palavras & {"half", "metade", "mitad", "moitie", "halfte", "haelfte"})
if not (vertical or horizontal or centro):
return None
if metade:
if horizontal and not vertical:
return (ax if horizontal == "esquerda" else ax + al // 2, ay, al // 2, aa)
if vertical and not horizontal:
return (ax, ay if vertical == "topo" else ay + aa // 2, al, aa // 2)
largura, altura = tamanho or (al // 2, aa // 2)
largura, altura = min(largura, al), min(altura, aa)
if horizontal == "esquerda":
x = ax
elif horizontal == "direita":
x = ax + al - largura
else:
x = ax + (al - largura) // 2
if vertical == "topo":
y = ay
elif vertical == "base":
y = ay + aa - altura
else:
y = ay + (aa - altura) // 2
return (x, y, None, None)
|