если загрузить изображение и создать точку то приближение сломано
Python:
import os
import threading
import time
import customtkinter
from pynput import mouse
from pynput.mouse import Controller
import tkinter as tk
from tkinter import messagebox, filedialog, simpledialog
import json
from PIL import Image, ImageTk
from PIL import ImageChops
reference_drag_start = None
drag_start_editor = None
selected_editor_point = None
reference_image = None
reference_tk = None
reference_path = None
ref_pos = [0, 0]
ref_scale = 1.0
ref_angle = 0
offset_x = 0
offset_y = 0
drag_start = None
path_points = []
MACRO_FOLDER = "list"
mouse_controller = Controller()
macros_enabled = True
active_macro = {"weapon": None,"macro": None,"actions": [],"running": False,"filepath": None}
selected_point_index = None
def on_right_button_press_with_shift(event):
global drag_start
if event.state & 0x0001:
drag_start = (event.x, event.y)
canvas.config(cursor="fleur")
else: drag_start = None
def on_right_button_release_with_shift(event):
global drag_start
drag_start = None
canvas.config(cursor="")
def on_mouse_drag_with_shift(event):
global offset_x, offset_y, drag_start
if drag_start is None: return
if event.state & 0x0001:
dx, dy = event.x - drag_start[0], event.y - drag_start[1]
offset_x += dx
offset_y += dy
drag_start = (event.x, event.y)
draw_grid_and_path(active_macro["actions"])
else:
drag_start = None
canvas.config(cursor="")
def update_reference_image():
global reference_tk
if not reference_image: return
img = reference_image.resize((int(reference_image.width * ref_scale), int(reference_image.height * ref_scale)))
img = img.rotate(ref_angle, expand=True)
reference_tk = ImageTk.PhotoImage(img)
redraw_editor()
def load_reference_image():
global reference_image, reference_tk, reference_path
path = filedialog.askopenfilename(filetypes=[("Images", "*.png;*.jpg;*.jpeg")])
if not path: return
reference_path = path
reference_image = Image.open(reference_path)
update_reference_image()
def on_editor_mouse_wheel(event):
global editor_scale, editor_offset_x, editor_offset_y, ref_pos, ref_scale
old_scale = editor_scale
delta = 1 if event.delta > 0 else -1
new_scale = editor_scale + delta
if not (1 <= new_scale <= 100):
return
canvas_width = editor_canvas.winfo_width()
canvas_height = editor_canvas.winfo_height()
# Координаты мыши относительно центра холста с учётом смещения камеры (editor_offset)
mouse_x = event.x - canvas_width // 2 - editor_offset_x
mouse_y = event.y - canvas_height // 2 - editor_offset_y
scale_ratio = new_scale / old_scale
# Смещаем позицию референса так, чтобы точка под мышью не сдвигалась
ref_pos[0] = int(ref_pos[0] - mouse_x * (scale_ratio - 1))
ref_pos[1] = int(ref_pos[1] - mouse_y * (scale_ratio - 1))
# Обновляем масштаб
editor_scale = new_scale
ref_scale *= scale_ratio
redraw_editor()
def parse_macro_file(filepath):
actions = []
if not os.path.exists(filepath): return actions
with open(filepath, 'r') as f:
try:
data = json.load(f)
for entry in data:
dx = float(entry.get("dx", 0))
dy = float(entry.get("dy", 0))
delay = float(entry.get("delay", 20)) / 1000.0
actions.append((dx, dy, delay))
except Exception:
f.seek(0)
for line in f.readlines():
if ":" in line:
try:
dx, dy, delay = map(lambda x: float(x.strip()), line.strip().split(":"))
actions.append((dx, dy, delay / 1000.0))
except: continue
return actions
def save_macro_file(filepath, actions):
data = [{"dx": dx, "dy": dy, "delay": int(delay * 1000)} for dx, dy, delay in actions]
with open(filepath, "w") as f:
json.dump(data, f, indent=2)
def change_delay():
global selected_editor_point
if selected_editor_point is None: return
old_dx, old_dy, old_delay = active_macro["actions"][selected_editor_point]
new_delay = simpledialog.askfloat("Изменить задержку", "Введите новую задержку (сек):", initialvalue=old_delay, minvalue=0.001)
if new_delay is not None:
active_macro["actions"][selected_editor_point] = (old_dx, old_dy, new_delay)
draw_editor_macro(active_macro["actions"])
def macro_runner():
step_time = 0.01
while active_macro["running"] and macros_enabled:
for dx, dy, delay in active_macro["actions"]:
if not active_macro["running"] or not macros_enabled:
break
start_x, start_y = mouse_controller.position
end_x, end_y = start_x + dx, start_y + dy
steps = max(int(delay / step_time), 1)
step_dx = dx / steps
step_dy = dy / steps
for _ in range(steps):
if not active_macro["running"] or not macros_enabled:
break
cur_x = mouse_controller.position[0] + step_dx
cur_y = mouse_controller.position[1] + step_dy
mouse_controller.position = (cur_x, cur_y)
time.sleep(step_time)
def on_click(x, y, button, pressed):
if button == mouse.Button.left:
if pressed and active_macro["actions"] and macros_enabled:
active_macro["running"] = True
threading.Thread(target=macro_runner, daemon=True).start()
else:
active_macro["running"] = False
mouse.Listener(on_click=on_click).start()
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("green")
root = customtkinter.CTk()
root.geometry("1000x600")
root.title("Rust Macro Selector")
tabs = customtkinter.CTkTabview(root)
tabs.pack(fill="both", expand=True, padx=10, pady=10)
visual_tab = tabs.add("Визуализация")
editor_tab = tabs.add("Редактор макросов")
main_container = customtkinter.CTkFrame(visual_tab)
main_container.pack(fill="both", expand=True)
left_frame = customtkinter.CTkFrame(main_container, width=150)
left_frame.pack(side="left", fill="y", padx=(0, 10))
scroll_frame = customtkinter.CTkScrollableFrame(left_frame)
scroll_frame.pack(fill="both", expand=True)
def toggle_enable():
global macros_enabled
macros_enabled = not macros_enabled
toggle_switch.configure(text="ВКЛ" if macros_enabled else "ВЫКЛ")
if not macros_enabled:
active_macro["running"] = False
toggle_switch = customtkinter.CTkSwitch(master=left_frame, text="ВКЛ", command=toggle_enable)
toggle_switch.select()
toggle_switch.pack(pady=10)
right_frame = tk.Frame(main_container, bg="black")
right_frame.pack(side="left", fill="both", expand=True)
canvas = tk.Canvas(right_frame, bg="black", highlightthickness=0)
canvas.pack(fill="both", expand=True)
scale = 10
macro_coords = []
def draw_grid_and_path(actions):
canvas.delete("all")
if not actions: return
abs_coords = []
x, y = 0, 0
for dx, dy, _ in actions:
x += dx
y += dy
abs_coords.append((x, y))
min_x = min(c[0] for c in abs_coords)
max_x = max(c[0] for c in abs_coords)
min_y = min(c[1] for c in abs_coords)
max_y = max(c[1] for c in abs_coords)
center_macro_x = (min_x + max_x) / 2
center_macro_y = (min_y + max_y) / 2
canvas_width = canvas.winfo_width()
canvas_height = canvas.winfo_height()
center_x = canvas_width // 2 + offset_x
center_y = canvas_height // 2 + offset_y
for gx in range(int(min_x) - 50, int(max_x) + 50):
screen_x = center_x + (gx - center_macro_x) * scale
color = "gray" if gx % 5 == 0 else "#262626"
canvas.create_line(screen_x, 0, screen_x, canvas_height, fill=color)
for gy in range(int(min_y) - 50, int(max_y) + 50):
screen_y = center_y + (gy - center_macro_y) * scale
color = "gray" if gy % 5 == 0 else "#262626"
canvas.create_line(0, screen_y, canvas_width, screen_y, fill=color)
path_points.clear()
x, y = 0, 0
prev_x = center_x + (x - center_macro_x) * scale
prev_y = center_y + (y - center_macro_y) * scale
canvas.create_oval(prev_x - 3, prev_y - 3, prev_x + 3, prev_y + 3, fill="white")
path_points.append((prev_x, prev_y))
for dx, dy, _ in actions:
x += dx
y += dy
screen_x = center_x + (x - center_macro_x) * scale
screen_y = center_y + (y - center_macro_y) * scale
canvas.create_line(prev_x, prev_y, screen_x, screen_y, fill="white", width=2)
canvas.create_oval(screen_x - 2, screen_y - 2, screen_x + 2, screen_y + 2, fill="white")
prev_x, prev_y = screen_x, screen_y
path_points.append((screen_x, screen_y))
def on_mouse_wheel(event):
global scale
delta = 1 if event.delta > 0 else -1
new_scale = scale + delta
if 1 <= new_scale <= 100:
scale = new_scale
draw_grid_and_path(active_macro["actions"])
canvas.bind("<MouseWheel>", on_mouse_wheel)
editor_container = customtkinter.CTkFrame(editor_tab)
editor_container.pack(fill="both", expand=True)
editor_top_panel = customtkinter.CTkFrame(editor_container)
editor_top_panel.pack(fill="x", pady=5)
panel_visible = tk.BooleanVar(value=True)
show_grid_var = tk.BooleanVar(value=True)
show_speed_var = tk.BooleanVar(value=False)
show_coords_var = tk.BooleanVar(value=False)
free_move_mode = tk.BooleanVar(value=False)
def redraw_editor(_=None):
draw_editor_macro(active_macro["actions"])
def toggle_panel_visibility():
if panel_visible.get():
control_frame.pack_forget()
toggle_button.configure(text="Показать панель")
else:
control_frame.pack(side="left", padx=5)
toggle_button.configure(text="Скрыть панель")
panel_visible.set(not panel_visible.get())
toggle_button = customtkinter.CTkButton(editor_top_panel, text="Скрыть панель", command=toggle_panel_visibility)
toggle_button.pack(side="left", padx=5)
control_frame = customtkinter.CTkFrame(editor_top_panel)
control_frame.pack(side="left", padx=5)
customtkinter.CTkButton(control_frame, text="Загрузить фон", command=load_reference_image).pack(side="left", padx=5)
customtkinter.CTkButton(control_frame, text="Удалить фон", command=lambda: clear_reference()).pack(side="left", padx=5)
grid_switch = customtkinter.CTkCheckBox(control_frame, text="Сетка", variable=show_grid_var, command=redraw_editor)
grid_switch.select()
grid_switch.pack(side="left", padx=5)
speed_switch = customtkinter.CTkCheckBox(control_frame, text="Скорость", variable=show_speed_var, command=redraw_editor)
speed_switch.pack(side="left", padx=5)
coords_switch = customtkinter.CTkCheckBox(control_frame, text="Координаты", variable=show_coords_var, command=redraw_editor)
coords_switch.pack(side="left", padx=5)
free_move_switch = customtkinter.CTkSwitch(control_frame, text="Free Move", variable=free_move_mode)
free_move_switch.pack(side="left", padx=5)
def save_macro():
if active_macro["filepath"] is None:
f = filedialog.asksaveasfilename(defaultextension=".mbt", filetypes=[("JSON files", "*.mbt")])
if not f: return
active_macro["filepath"] = f
save_macro_file(active_macro["filepath"], active_macro["actions"])
messagebox.showinfo("Сохранено", f"Макрос сохранён в:\n{active_macro['filepath']}")
def load_macro():
f = filedialog.askopenfilename(filetypes=[("JSON files", "*.mbt")])
if not f: return
active_macro["actions"] = parse_macro_file(f)
active_macro["filepath"] = f
active_macro["running"] = False
global selected_editor_point
selected_editor_point = None
redraw_editor()
draw_grid_and_path(active_macro["actions"])
def clear_macro():
if messagebox.askyesno("Очистить макрос", "Вы уверены?"):
active_macro["actions"].clear()
active_macro["running"] = False
global selected_editor_point
selected_editor_point = None
redraw_editor()
draw_grid_and_path(active_macro["actions"])
def duplicate_selected_point():
global selected_editor_point
if selected_editor_point is None:
messagebox.showwarning("Нет точки", "Выберите точку для дублирования")
return
point = active_macro["actions"][selected_editor_point]
active_macro["actions"].insert(selected_editor_point + 1, point)
selected_editor_point += 1
redraw_editor()
def add_point_after():
global selected_editor_point
if selected_editor_point is None:
active_macro["actions"].append((0.0, 0.0, 0.02))
selected_editor_point = len(active_macro["actions"]) - 1
else:
active_macro["actions"].insert(selected_editor_point + 1, (0.0, 0.0, 0.02))
selected_editor_point += 1
redraw_editor()
save_button = customtkinter.CTkButton(control_frame, text="Сохранить", command=save_macro)
save_button.pack(side="left", padx=5)
load_button = customtkinter.CTkButton(control_frame, text="Загрузить", command=load_macro)
load_button.pack(side="left", padx=5)
clear_button = customtkinter.CTkButton(control_frame, text="Очистить", command=clear_macro)
clear_button.pack(side="left", padx=5)
duplicate_button = customtkinter.CTkButton(control_frame, text="Дублировать точку", command=duplicate_selected_point)
duplicate_button.pack(side="left", padx=5)
add_point_button = customtkinter.CTkButton(control_frame, text="Добавить точку", command=add_point_after)
add_point_button.pack(side="left", padx=5)
editor_canvas = tk.Canvas(editor_container, bg="black", highlightthickness=0)
def on_reference_drag_start(event):
global ref_drag_start
if event.state & 0x0004 and event.num == 1: # Ctrl + ЛКМ
ref_drag_start = (event.x, event.y)
def on_reference_drag_stop(event):
global ref_drag_start
if event.num == 1:
ref_drag_start = None
def on_reference_drag_motion(event):
global ref_drag_start
if ref_drag_start is None:
return
dx = event.x - ref_drag_start[0]
dy = event.y - ref_drag_start[1]
ref_pos[0] += dx
ref_pos[1] += dy
ref_drag_start = (event.x, event.y)
redraw_editor()
def editor_on_mouse_down(event):
global drag_start_editor
# Проверяем, зажат ли Shift (0x0001)
if event.state & 0x0001 and event.num == 1: # Shift + ЛКМ
drag_start_editor = (event.x, event.y)
def editor_on_mouse_up(event):
global drag_start_editor
if event.num == 1:
drag_start_editor = None
def editor_on_mouse_move(event):
global drag_start_editor, editor_offset_x, editor_offset_y
if drag_start_editor is None:
return
dx = event.x - drag_start_editor[0]
dy = event.y - drag_start_editor[1]
editor_offset_x += dx
editor_offset_y += dy
drag_start_editor = (event.x, event.y)
redraw_editor()
def editor_show_context_menu(event):
if event.state & 0x0001:
return # если зажат shift — не показывать меню
menu = tk.Menu(editor_canvas, tearoff=0)
menu.add_command(label="Изменить задержку", command=change_delay)
menu.add_command(label="Удалить точку", command=delete_selected_point)
menu.add_command(label="Дублировать точку", command=duplicate_selected_point)
menu.tk_popup(event.x_root, event.y_root)
def delete_selected_point():
global selected_editor_point
if selected_editor_point is None:
messagebox.showwarning("Нет точки", "Выберите точку для удаления")
return
del active_macro["actions"][selected_editor_point]
selected_editor_point = None
redraw_editor()
editor_canvas.pack(fill="both", expand=True)
editor_scale = 10
editor_offset_x = 0
editor_offset_y = 0
editor_path_points = []
selected_editor_point = None
editor_context_menu = None
editor_point_data = []
def draw_editor_macro(actions):
editor_canvas.delete("all")
global reference_tk
if reference_image:
if reference_image:
try:
canvas_width = editor_canvas.winfo_width()
canvas_height = editor_canvas.winfo_height()
center_x = canvas_width // 2 + editor_offset_x + ref_pos[0]
center_y = canvas_height // 2 + editor_offset_y + ref_pos[1]
final_scale = editor_scale / 10 * ref_scale
scaled_width = int(reference_image.width * final_scale)
scaled_height = int(reference_image.height * final_scale)
img = reference_image.resize((scaled_width, scaled_height), Image.Resampling.LANCZOS)
img = img.rotate(ref_angle, expand=True)
# Центр зсунутий, треба компенсувати
dx = (img.width - scaled_width) // 2
dy = (img.height - scaled_height) // 2
reference_tk = ImageTk.PhotoImage(img)
editor_canvas.create_image(center_x + dx, center_y + dy, image=reference_tk, anchor="center")
except Exception as e:
print("Ошибка отрисовки изображения:", e)
if not actions: return
abs_coords = []
x, y = 0, 0
for dx, dy, _ in actions:
x += dx
y += dy
abs_coords.append((x, y))
min_x = min(c[0] for c in abs_coords)
max_x = max(c[0] for c in abs_coords)
min_y = min(c[1] for c in abs_coords)
max_y = max(c[1] for c in abs_coords)
center_macro_x = (min_x + max_x) / 2
center_macro_y = (min_y + max_y) / 2
canvas_width = editor_canvas.winfo_width()
canvas_height = editor_canvas.winfo_height()
center_x = canvas_width // 2 + editor_offset_x
center_y = canvas_height // 2 + editor_offset_y
if show_grid_var.get():
for gx in range(int(min_x) - 50, int(max_x) + 50):
screen_x = center_x + (gx - center_macro_x) * editor_scale
color = "gray" if gx % 5 == 0 else "#262626"
editor_canvas.create_line(screen_x, 0, screen_x, canvas_height, fill=color)
for gy in range(int(min_y) - 50, int(max_y) + 50):
screen_y = center_y + (gy - center_macro_y) * editor_scale
color = "gray" if gy % 5 == 0 else "#262626"
editor_canvas.create_line(0, screen_y, canvas_width, screen_y, fill=color)
editor_path_points.clear()
editor_point_data.clear()
x, y = 0, 0
prev_x = center_x + (x - center_macro_x) * editor_scale
prev_y = center_y + (y - center_macro_y) * editor_scale
editor_canvas.create_oval(prev_x - 3, prev_y - 3, prev_x + 3, prev_y + 3, fill="white")
editor_path_points.append((prev_x, prev_y))
editor_point_data.append((0, prev_x, prev_y))
for i, (dx, dy, delay) in enumerate(actions):
x += dx
y += dy
screen_x = center_x + (x - center_macro_x) * editor_scale
screen_y = center_y + (y - center_macro_y) * editor_scale
editor_canvas.create_line(prev_x, prev_y, screen_x, screen_y, fill="white", width=2)
editor_canvas.create_oval(screen_x - 4, screen_y - 4, screen_x + 4, screen_y + 4,
fill="red" if selected_editor_point == i else "white", outline="")
editor_path_points.append((screen_x, screen_y))
editor_point_data.append((i + 1, screen_x, screen_y))
if show_speed_var.get():
speed_text = f"{delay:.3f}s"
editor_canvas.create_text(screen_x + 10, screen_y - 10, text=speed_text, fill="lime", font=("Arial", 8))
if show_coords_var.get():
coord_text = f"({int(x)}, {int(y)})"
editor_canvas.create_text(screen_x + 10, screen_y + 10, text=coord_text, fill="yellow", font=("Arial", 8))
prev_x, prev_y = screen_x, screen_y
def on_editor_mouse_down(event):
global selected_editor_point
closest_point = None
min_dist = 10
for idx, (i, px, py) in enumerate(editor_point_data):
dist = ((px - event.x) ** 2 + (py - event.y) ** 2) ** 0.5
if dist < min_dist:
min_dist = dist
closest_point = i - 1 # because i starts at 1 for second point
if closest_point < 0: closest_point = 0
selected_editor_point = closest_point
redraw_editor()
def move_selected_point(dx, dy):
global selected_editor_point
if selected_editor_point is None: return
dx_old, dy_old, delay = active_macro["actions"][selected_editor_point]
active_macro["actions"][selected_editor_point] = (dx_old + dx, dy_old + dy, delay)
redraw_editor()
def on_key_press(event):
if selected_editor_point is None: return
step = 1
if event.keysym == 'Up':
move_selected_point(0, -step)
elif event.keysym == 'Down':
move_selected_point(0, step)
elif event.keysym == 'Left':
move_selected_point(-step, 0)
elif event.keysym == 'Right':
move_selected_point(step, 0)
editor_canvas.bind("<Button-1>", on_editor_mouse_down)
root.bind("<Key>", on_key_press)
def clear_reference():
global reference_image, reference_tk, reference_path
reference_image = None
reference_tk = None
reference_path = None
redraw_editor()
def redraw_editor():
draw_editor_macro(active_macro["actions"])
# При запуске загружаем макросы в левый список (упрощённо)
def load_macros():
for widget in scroll_frame.winfo_children():
widget.destroy()
if not os.path.exists(MACRO_FOLDER):
os.makedirs(MACRO_FOLDER)
switches = []
for weapon in sorted(os.listdir(MACRO_FOLDER)):
wp = os.path.join(MACRO_FOLDER, weapon)
if not os.path.isdir(wp):
continue
label = customtkinter.CTkLabel(scroll_frame, text=f"{weapon}>", font=("Arial", 14, "bold"))
label.pack(anchor="w", padx=10, pady=5)
for macro_file in sorted(os.listdir(wp)):
if macro_file.endswith(".mbt"):
path = os.path.join(wp, macro_file)
var = tk.IntVar()
def toggle_macro(v=var, sw=None, w=weapon, m=macro_file, p=path):
if v.get():
for wgt in switches:
if wgt != sw:
wgt.deselect()
active_macro["weapon"] = w
active_macro["macro"] = m
active_macro["actions"] = parse_macro_file(p)
active_macro["filepath"] = p
active_macro["running"] = False
redraw_editor()
draw_grid_and_path(active_macro["actions"])
else:
active_macro["actions"] = []
active_macro["filepath"] = None
active_macro["running"] = False
redraw_editor()
draw_grid_and_path([])
sw = customtkinter.CTkSwitch(scroll_frame, text=macro_file, variable=var)
sw.configure(command=lambda v=var, sw=sw, w=weapon, m=macro_file, p=path: toggle_macro(v, sw, w, m, p))
sw.pack(anchor="w", padx=20, pady=2)
switches.append(sw)
load_macros()
canvas.bind("<ButtonPress-3>", on_right_button_press_with_shift)
canvas.bind("<ButtonRelease-3>", on_right_button_release_with_shift, add="+")
canvas.bind("<B3-Motion>", on_mouse_drag_with_shift)
canvas.bind("<ButtonRelease-3>", lambda e: show_context_menu(e) if not (e.state & 0x0001) else None, add="+")
canvas.bind("<Configure>", lambda e: draw_grid_and_path(active_macro["actions"]))
editor_canvas.bind("<Button-3>", editor_show_context_menu)
editor_canvas.bind("<ButtonPress-1>", editor_on_mouse_down)
editor_canvas.bind("<ButtonRelease-1>", editor_on_mouse_up)
editor_canvas.bind("<B1-Motion>", editor_on_mouse_move)
editor_canvas.bind("<ButtonPress-1>", on_reference_drag_start, add="+")
editor_canvas.bind("<ButtonRelease-1>", on_reference_drag_stop, add="+")
editor_canvas.bind("<B1-Motion>", on_reference_drag_motion, add="+")
editor_canvas.bind("<MouseWheel>", on_editor_mouse_wheel)
def show_context_menu(event):
if event.state & 0x0001: return
menu = tk.Menu(canvas, tearoff=0)
menu.add_command(label="Очистить макрос", command=clear_macro)
menu.add_command(label="Загрузить макрос", command=load_macro)
menu.add_command(label="Сохранить макрос", command=save_macro)
menu.tk_popup(event.x_root, event.y_root)
root.mainloop()