Add grid.py
This commit is contained in:
516
grid.py
Normal file
516
grid.py
Normal file
@@ -0,0 +1,516 @@
|
||||
# v4.1
|
||||
import json
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
import tkinter.colorchooser as colorchooser
|
||||
|
||||
import customtkinter as ctk
|
||||
import cv2
|
||||
import dxcam
|
||||
import numpy as np
|
||||
|
||||
from pathlib import Path
|
||||
from queue import Queue, Empty
|
||||
|
||||
from pynput import keyboard, mouse
|
||||
from pystray import Icon, Menu, MenuItem
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from analyze import CompositionAnalyzer
|
||||
from helper import GridOverlay
|
||||
|
||||
CONFIG_PATH = Path("config.json")
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"grid_color": "#00ff66",
|
||||
"line_scale": 1.0,
|
||||
"mouse_toggle": True,
|
||||
"grid_hotkeys": ["<ctrl>+9"],
|
||||
"mode": "thirds",
|
||||
"analysis_enabled": True,
|
||||
"feedback_enabled": True,
|
||||
"target_fps": 144,
|
||||
}
|
||||
|
||||
ctk.set_appearance_mode("dark")
|
||||
ctk.set_default_color_theme("dark-blue")
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self):
|
||||
self.config = self.load()
|
||||
|
||||
def load(self):
|
||||
if not CONFIG_PATH.exists():
|
||||
self.save(DEFAULT_CONFIG)
|
||||
return DEFAULT_CONFIG.copy()
|
||||
|
||||
try:
|
||||
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
merged = DEFAULT_CONFIG.copy()
|
||||
merged.update(data)
|
||||
|
||||
return merged
|
||||
|
||||
except Exception:
|
||||
return DEFAULT_CONFIG.copy()
|
||||
|
||||
def save(self, data):
|
||||
CONFIG_PATH.write_text(
|
||||
json.dumps(data, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class HotkeyManager:
|
||||
def __init__(self, bindings):
|
||||
self.bindings = bindings
|
||||
self.listener = None
|
||||
|
||||
def start(self):
|
||||
self.stop()
|
||||
|
||||
self.listener = keyboard.GlobalHotKeys(self.bindings)
|
||||
|
||||
self.listener.start()
|
||||
|
||||
def stop(self):
|
||||
if self.listener:
|
||||
try:
|
||||
self.listener.stop()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class CaptureWorker:
|
||||
def __init__(self):
|
||||
self.camera = dxcam.create(output_idx=0)
|
||||
|
||||
self.frame_queue = Queue(maxsize=1)
|
||||
|
||||
self.running = False
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
threading.Thread(
|
||||
target=self.capture_loop,
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
def capture_loop(self):
|
||||
self.camera.start(
|
||||
target_fps=144,
|
||||
video_mode=True,
|
||||
)
|
||||
|
||||
while self.running:
|
||||
frame = self.camera.get_latest_frame()
|
||||
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
if self.frame_queue.full():
|
||||
self.frame_queue.get_nowait()
|
||||
|
||||
self.frame_queue.put_nowait(frame)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
time.sleep(0.001)
|
||||
|
||||
|
||||
class ValidationWorker:
|
||||
def __init__(
|
||||
self,
|
||||
overlay,
|
||||
capture,
|
||||
):
|
||||
self.overlay = overlay
|
||||
self.capture = capture
|
||||
|
||||
self.analyzer = CompositionAnalyzer()
|
||||
|
||||
self.running = False
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
threading.Thread(
|
||||
target=self.validation_loop,
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
def validation_loop(self):
|
||||
while self.running:
|
||||
try:
|
||||
frame = self.capture.frame_queue.get(timeout=0.1)
|
||||
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
result = self.analyzer.analyze(frame)
|
||||
|
||||
self.overlay.update_feedback(result)
|
||||
|
||||
|
||||
class GridApplication:
|
||||
def __init__(self):
|
||||
self.running = True
|
||||
|
||||
self.queue = Queue(maxsize=256)
|
||||
|
||||
self.config_manager = ConfigManager()
|
||||
|
||||
self.config = self.config_manager.config
|
||||
|
||||
self.overlay = GridOverlay(
|
||||
color=self.config["grid_color"],
|
||||
line_width=max(
|
||||
1,
|
||||
round(self.config["line_scale"] * 2),
|
||||
),
|
||||
mode=self.config["mode"],
|
||||
)
|
||||
|
||||
self.capture = CaptureWorker()
|
||||
|
||||
self.validator = ValidationWorker(
|
||||
self.overlay,
|
||||
self.capture,
|
||||
)
|
||||
|
||||
self.mouse_listener = mouse.Listener(on_click=self.on_click)
|
||||
|
||||
self.hotkeys = HotkeyManager(self.build_hotkeys())
|
||||
|
||||
self.mouse_listener.start()
|
||||
|
||||
self.hotkeys.start()
|
||||
|
||||
self.capture.start()
|
||||
|
||||
self.validator.start()
|
||||
|
||||
self.create_tray()
|
||||
|
||||
self.overlay.schedule(
|
||||
self.process_queue,
|
||||
8,
|
||||
)
|
||||
|
||||
self.overlay.schedule(
|
||||
self.overlay.schedule_monitor_refresh,
|
||||
1000,
|
||||
)
|
||||
|
||||
signal.signal(
|
||||
signal.SIGINT,
|
||||
self.shutdown,
|
||||
)
|
||||
|
||||
def build_hotkeys(self):
|
||||
bindings = {}
|
||||
|
||||
for hotkey in self.config["grid_hotkeys"]:
|
||||
bindings[hotkey] = self.queue_toggle
|
||||
|
||||
bindings["<shift>+1"] = lambda: self.queue_color("#ff0000")
|
||||
|
||||
bindings["<shift>+2"] = lambda: self.queue_color("#00ff66")
|
||||
|
||||
bindings["<shift>+3"] = lambda: self.queue_color("#ffffff")
|
||||
|
||||
bindings["<shift>+4"] = lambda: self.queue_color("#ffff00")
|
||||
|
||||
bindings["<shift>+5"] = self.queue_custom_color
|
||||
|
||||
bindings["<ctrl>+2"] = lambda: self.queue_mode("crosshair")
|
||||
|
||||
bindings["<ctrl>+3"] = lambda: self.queue_mode("circle")
|
||||
|
||||
bindings["<ctrl>+4"] = lambda: self.queue_mode("circle_dot")
|
||||
|
||||
bindings["<ctrl>+5"] = lambda: self.queue_mode("9:16")
|
||||
|
||||
bindings["<ctrl>+6"] = lambda: self.queue_mode("4:3")
|
||||
|
||||
bindings["<ctrl>+7"] = lambda: self.queue_mode("1:1")
|
||||
|
||||
bindings["<ctrl>+8"] = lambda: self.queue_mode("thirds")
|
||||
|
||||
bindings["<ctrl>+1"] = self.quit
|
||||
|
||||
return bindings
|
||||
|
||||
def create_tray_icon(self):
|
||||
image = Image.new(
|
||||
"RGBA",
|
||||
(64, 64),
|
||||
(0, 0, 0, 0),
|
||||
)
|
||||
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
draw.rectangle(
|
||||
(10, 10, 54, 54),
|
||||
outline=(
|
||||
0,
|
||||
255,
|
||||
100,
|
||||
255,
|
||||
),
|
||||
width=5,
|
||||
)
|
||||
|
||||
return image
|
||||
|
||||
def create_tray(self):
|
||||
self.tray = Icon(
|
||||
"overlay-grid",
|
||||
self.create_tray_icon(),
|
||||
"Overlay Grid",
|
||||
menu=Menu(
|
||||
MenuItem(
|
||||
"Toggle",
|
||||
self.tray_toggle,
|
||||
),
|
||||
MenuItem(
|
||||
"Settings",
|
||||
self.open_menu,
|
||||
),
|
||||
MenuItem(
|
||||
"Quit",
|
||||
self.quit,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
threading.Thread(
|
||||
target=self.tray.run,
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def tray_toggle(
|
||||
self,
|
||||
icon=None,
|
||||
item=None,
|
||||
):
|
||||
self.queue_toggle()
|
||||
|
||||
def open_menu(
|
||||
self,
|
||||
icon=None,
|
||||
item=None,
|
||||
):
|
||||
if hasattr(
|
||||
self,
|
||||
"window",
|
||||
):
|
||||
try:
|
||||
self.window.focus()
|
||||
|
||||
return
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.window = ctk.CTkToplevel()
|
||||
|
||||
self.window.geometry("420x320")
|
||||
|
||||
self.window.title("Overlay Grid")
|
||||
|
||||
self.window.attributes(
|
||||
"-topmost",
|
||||
True,
|
||||
)
|
||||
|
||||
frame = ctk.CTkFrame(self.window)
|
||||
|
||||
frame.pack(
|
||||
fill="both",
|
||||
expand=True,
|
||||
padx=12,
|
||||
pady=12,
|
||||
)
|
||||
|
||||
self.hotkey_entry = ctk.CTkEntry(
|
||||
frame,
|
||||
width=360,
|
||||
)
|
||||
|
||||
self.hotkey_entry.insert(
|
||||
0,
|
||||
", ".join(self.config["grid_hotkeys"]),
|
||||
)
|
||||
|
||||
self.hotkey_entry.pack(pady=12)
|
||||
|
||||
ctk.CTkButton(
|
||||
frame,
|
||||
text="Save Hotkeys",
|
||||
command=self.save_hotkeys,
|
||||
).pack(
|
||||
fill="x",
|
||||
pady=6,
|
||||
)
|
||||
|
||||
ctk.CTkButton(
|
||||
frame,
|
||||
text="Pick Color",
|
||||
command=self.queue_custom_color,
|
||||
).pack(
|
||||
fill="x",
|
||||
pady=6,
|
||||
)
|
||||
|
||||
def save_hotkeys(self):
|
||||
keys = [x.strip() for x in (self.hotkey_entry.get().split(",")) if x.strip()]
|
||||
|
||||
if not keys:
|
||||
return
|
||||
|
||||
self.config["grid_hotkeys"] = keys
|
||||
|
||||
self.config_manager.save(self.config)
|
||||
|
||||
self.hotkeys.bindings = self.build_hotkeys()
|
||||
|
||||
self.hotkeys.start()
|
||||
|
||||
def on_click(
|
||||
self,
|
||||
x,
|
||||
y,
|
||||
button,
|
||||
pressed,
|
||||
):
|
||||
if pressed and button == mouse.Button.middle and self.config["mouse_toggle"]:
|
||||
self.queue_toggle()
|
||||
|
||||
def safe_queue_put(
|
||||
self,
|
||||
event,
|
||||
):
|
||||
try:
|
||||
if self.queue.full():
|
||||
self.queue.get_nowait()
|
||||
|
||||
self.queue.put_nowait(event)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def queue_toggle(self):
|
||||
self.safe_queue_put(("toggle", None))
|
||||
|
||||
def queue_color(
|
||||
self,
|
||||
color,
|
||||
):
|
||||
self.safe_queue_put(("color", color))
|
||||
|
||||
def queue_mode(
|
||||
self,
|
||||
mode,
|
||||
):
|
||||
self.safe_queue_put(("mode", mode))
|
||||
|
||||
def queue_custom_color(
|
||||
self,
|
||||
):
|
||||
self.safe_queue_put(("custom", None))
|
||||
|
||||
def process_queue(self):
|
||||
processed = 0
|
||||
|
||||
try:
|
||||
while processed < 32:
|
||||
action, value = self.queue.get_nowait()
|
||||
|
||||
if action == "toggle":
|
||||
self.overlay.toggle()
|
||||
|
||||
elif action == "color":
|
||||
self.overlay.set_color(value)
|
||||
|
||||
self.config["grid_color"] = value
|
||||
|
||||
self.config_manager.save(self.config)
|
||||
|
||||
elif action == "mode":
|
||||
self.overlay.set_mode(value)
|
||||
|
||||
self.config["mode"] = value
|
||||
|
||||
self.config_manager.save(self.config)
|
||||
|
||||
elif action == "custom":
|
||||
color = colorchooser.askcolor(
|
||||
initialcolor=(self.overlay.color),
|
||||
)[1]
|
||||
|
||||
if color:
|
||||
self.overlay.set_color(color)
|
||||
|
||||
self.config["grid_color"] = color
|
||||
|
||||
self.config_manager.save(self.config)
|
||||
|
||||
processed += 1
|
||||
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
if self.running:
|
||||
self.overlay.schedule(
|
||||
self.process_queue,
|
||||
8,
|
||||
)
|
||||
|
||||
def shutdown(
|
||||
self,
|
||||
*args,
|
||||
):
|
||||
self.quit()
|
||||
|
||||
def quit(
|
||||
self,
|
||||
*args,
|
||||
):
|
||||
self.running = False
|
||||
|
||||
self.hotkeys.stop()
|
||||
|
||||
self.capture.stop()
|
||||
|
||||
self.validator.stop()
|
||||
|
||||
try:
|
||||
self.mouse_listener.stop()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.tray.stop()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.overlay.destroy()
|
||||
|
||||
def run(self):
|
||||
self.overlay.run()
|
||||
Reference in New Issue
Block a user