2025-09-02 16:49:39 +08:00
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from tkinter import ttk
|
|
|
|
|
|
from PIL import Image, ImageTk
|
|
|
|
|
|
from image_manager import ImageManager
|
|
|
|
|
|
from transform_utils import TransformHelper
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CanvasWidget(tk.Canvas):
|
|
|
|
|
|
"""自定义画布组件,处理绘图和交互"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent, project,main_window, *args, **kwargs):
|
|
|
|
|
|
super().__init__(parent, *args, **kwargs, bg="#f0f0f0", highlightthickness=1, highlightbackground="#ccc")
|
|
|
|
|
|
self.parent = parent # 这是Frame对象
|
|
|
|
|
|
self.main_window = main_window # 这是MainWindow对象
|
|
|
|
|
|
self.project = project
|
|
|
|
|
|
self.selected_item = None
|
|
|
|
|
|
self.dragging = False
|
|
|
|
|
|
self.drag_data = {"x": 0, "y": 0}
|
|
|
|
|
|
self.zoom = project.canvas_zoom
|
|
|
|
|
|
|
|
|
|
|
|
# 绑定事件
|
|
|
|
|
|
self.bind("<Button-1>", self.on_click)
|
|
|
|
|
|
self.bind("<B1-Motion>", self.on_drag)
|
|
|
|
|
|
self.bind("<ButtonRelease-1>", self.on_release)
|
|
|
|
|
|
self.bind("<MouseWheel>", self.on_zoom) # Windows
|
|
|
|
|
|
self.bind("<Button-4>", self.on_zoom) # Linux
|
|
|
|
|
|
self.bind("<Button-5>", self.on_zoom) # Linux
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.active_handle = None # 当前激活的控制点
|
|
|
|
|
|
|
|
|
|
|
|
# 图像缓存
|
|
|
|
|
|
self.image_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 初始绘制
|
|
|
|
|
|
self.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def canvas_to_template(self, x, y):
|
|
|
|
|
|
"""将画布坐标转换为模板坐标"""
|
|
|
|
|
|
scale = self.zoom
|
|
|
|
|
|
template_x = x / scale
|
|
|
|
|
|
template_y = y / scale
|
|
|
|
|
|
return template_x, template_y
|
|
|
|
|
|
|
|
|
|
|
|
def template_to_canvas(self, x, y):
|
|
|
|
|
|
"""将模板坐标转换为画布坐标"""
|
|
|
|
|
|
scale = self.zoom
|
|
|
|
|
|
canvas_x = x * scale
|
|
|
|
|
|
canvas_y = y * scale
|
|
|
|
|
|
return canvas_x, canvas_y
|
|
|
|
|
|
|
|
|
|
|
|
# 修改 canvas_widget.py 中的 get_foreground_at 方法,优化碰撞检测
|
|
|
|
|
|
def get_foreground_at(self, x, y):
|
|
|
|
|
|
"""获取指定坐标处的前景图,优化碰撞检测"""
|
|
|
|
|
|
template_x, template_y = self.canvas_to_template(x, y)
|
|
|
|
|
|
|
|
|
|
|
|
# 按图层顺序倒序检查(顶层优先)
|
|
|
|
|
|
for fg in sorted(self.project.foregrounds, key=lambda f: -f.z_index):
|
|
|
|
|
|
if not fg.visible:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 更精确的碰撞检测(考虑变换后的顶点)
|
|
|
|
|
|
min_x = min(v[0] for v in fg.vertices)
|
|
|
|
|
|
max_x = max(v[0] for v in fg.vertices)
|
|
|
|
|
|
min_y = min(v[1] for v in fg.vertices)
|
|
|
|
|
|
max_y = max(v[1] for v in fg.vertices)
|
|
|
|
|
|
|
|
|
|
|
|
if min_x <= template_x <= max_x and min_y <= template_y <= max_y:
|
|
|
|
|
|
return fg
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 修改 canvas_widget.py 中的 on_click 方法,添加控制点检测
|
|
|
|
|
|
def on_click(self, event):
|
|
|
|
|
|
"""处理鼠标点击事件,添加1. 检查是否点击了控制点
|
|
|
|
|
|
2. 检查是否点击了图片
|
|
|
|
|
|
3. 处理选择状态
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.active_handle = None
|
|
|
|
|
|
# 先检查是否点击了已选中项的控制点
|
|
|
|
|
|
if self.selected_item:
|
|
|
|
|
|
handles = TransformHelper.calculate_handles(self.selected_item, self)
|
|
|
|
|
|
for handle in handles.values():
|
|
|
|
|
|
if handle.hit_test(event.x, event.y):
|
|
|
|
|
|
self.active_handle = handle.position
|
|
|
|
|
|
# 保持选中状态
|
|
|
|
|
|
self.dragging = True
|
|
|
|
|
|
self.drag_data["x"] = event.x
|
|
|
|
|
|
self.drag_data["y"] = event.y
|
|
|
|
|
|
self.draw_preview()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 检查是否点击了图片
|
|
|
|
|
|
fg = self.get_foreground_at(event.x, event.y)
|
|
|
|
|
|
if fg and not fg.locked:
|
|
|
|
|
|
self.selected_item = fg
|
|
|
|
|
|
self.drag_data["x"] = event.x
|
|
|
|
|
|
self.drag_data["y"] = event.y
|
|
|
|
|
|
self.dragging = True
|
|
|
|
|
|
self.main_window.update_property_panel(fg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.selected_item = None
|
|
|
|
|
|
self.main_window.clear_property_panel()
|
|
|
|
|
|
|
|
|
|
|
|
self.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def on_drag(self, event):
|
|
|
|
|
|
"""处理拖拽事件"""
|
|
|
|
|
|
if self.dragging and self.selected_item:
|
|
|
|
|
|
if self.active_handle:
|
|
|
|
|
|
# 拖拽控制点
|
|
|
|
|
|
dx = event.x - self.drag_data["x"]
|
|
|
|
|
|
dy = event.y - self.drag_data["y"]
|
|
|
|
|
|
self.selected_item = TransformHelper.update_vertices(
|
|
|
|
|
|
self.selected_item, self.active_handle, dx, dy, self
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 拖拽整个图片
|
|
|
|
|
|
dx = event.x - self.drag_data["x"]
|
|
|
|
|
|
dy = event.y - self.drag_data["y"]
|
|
|
|
|
|
t_dx, t_dy = self.canvas_to_template(dx, dy)
|
|
|
|
|
|
|
|
|
|
|
|
# 移动所有顶点
|
|
|
|
|
|
new_vertices = []
|
|
|
|
|
|
for x, y in self.selected_item.vertices:
|
|
|
|
|
|
new_vertices.append((x + t_dx, y + t_dy))
|
|
|
|
|
|
self.selected_item.vertices = new_vertices
|
|
|
|
|
|
|
|
|
|
|
|
# 更新拖拽起始位置
|
|
|
|
|
|
self.drag_data["x"] = event.x
|
|
|
|
|
|
self.drag_data["y"] = event.y
|
|
|
|
|
|
|
|
|
|
|
|
# 重绘
|
|
|
|
|
|
self.draw_preview()
|
|
|
|
|
|
# 更新属性面板
|
|
|
|
|
|
self.main_window.update_property_panel(self.selected_item)
|
|
|
|
|
|
|
|
|
|
|
|
def on_release(self, event):
|
|
|
|
|
|
"""处理鼠标释放事件"""
|
|
|
|
|
|
self.dragging = False
|
|
|
|
|
|
self.active_handle = None
|
|
|
|
|
|
|
|
|
|
|
|
def on_zoom(self, event):
|
|
|
|
|
|
"""处理缩放事件"""
|
|
|
|
|
|
# 计算缩放因子
|
|
|
|
|
|
if event.delta > 0 or event.num == 4: # 放大
|
|
|
|
|
|
self.zoom *= 1.1
|
|
|
|
|
|
else: # 缩小
|
|
|
|
|
|
self.zoom /= 1.1
|
|
|
|
|
|
|
|
|
|
|
|
# 限制缩放范围
|
|
|
|
|
|
self.zoom = max(0.1, min(self.zoom, 5.0))
|
|
|
|
|
|
|
|
|
|
|
|
# 更新项目的缩放值
|
|
|
|
|
|
self.project.canvas_zoom = self.zoom
|
|
|
|
|
|
|
|
|
|
|
|
# 重绘
|
|
|
|
|
|
self.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def draw_preview(self):
|
|
|
|
|
|
"""绘制预览内容"""
|
|
|
|
|
|
self.delete("all")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取模板尺寸
|
|
|
|
|
|
template_w = self.project.template.width_px
|
|
|
|
|
|
template_h = self.project.template.height_px
|
|
|
|
|
|
|
|
|
|
|
|
# 计算画布上的模板尺寸
|
|
|
|
|
|
canvas_w = template_w * self.zoom
|
|
|
|
|
|
canvas_h = template_h * self.zoom
|
|
|
|
|
|
|
|
|
|
|
|
# 调整画布滚动区域
|
|
|
|
|
|
self.config(scrollregion=(0, 0, canvas_w, canvas_h))
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制模板背景
|
|
|
|
|
|
self.create_rectangle(0, 0, canvas_w, canvas_h, fill=self.project.template.bg_color)
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制背景图(如果有)
|
|
|
|
|
|
if self.project.template.bg_image:
|
2025-09-03 11:55:31 +08:00
|
|
|
|
bg_key = id(self.project.template.bg_image)
|
2025-09-02 16:49:39 +08:00
|
|
|
|
if bg_key not in self.image_cache or self.image_cache[bg_key][0] != self.zoom:
|
|
|
|
|
|
# 缩放背景图并缓存
|
|
|
|
|
|
scaled_bg = self.project.template.bg_image.resize(
|
|
|
|
|
|
(int(canvas_w), int(canvas_h)),
|
|
|
|
|
|
Image.Resampling.LANCZOS
|
|
|
|
|
|
)
|
|
|
|
|
|
self.image_cache[bg_key] = (self.zoom, ImageTk.PhotoImage(scaled_bg))
|
|
|
|
|
|
|
|
|
|
|
|
bg_img = self.image_cache[bg_key][1]
|
|
|
|
|
|
self.create_image(0, 0, image=bg_img, anchor=tk.NW)
|
|
|
|
|
|
|
|
|
|
|
|
# 按图层顺序绘制前景图
|
|
|
|
|
|
for fg in sorted(self.project.foregrounds, key=lambda f: f.z_index):
|
|
|
|
|
|
# 跳过隐藏的图片
|
|
|
|
|
|
if fg.hidden or not fg.visible:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 检查缓存 (使用顶点元组作为键的一部分)
|
|
|
|
|
|
img_key = (fg.file_path, tuple(tuple(v) for v in fg.vertices), fg.angle, fg.mask_shape, self.zoom)
|
|
|
|
|
|
if img_key not in self.image_cache:
|
|
|
|
|
|
# 加载并处理图像
|
|
|
|
|
|
img = ImageManager.load_image(fg.file_path)
|
|
|
|
|
|
if img:
|
|
|
|
|
|
# 应用遮罩
|
|
|
|
|
|
masked_img = ImageManager.apply_mask(img, fg.mask_shape)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用基于顶点的图像变形方法
|
|
|
|
|
|
transformed_img, (x, y) = ImageManager.transform_with_vertices(
|
|
|
|
|
|
masked_img, fg.vertices
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 确保图像具有透明度
|
|
|
|
|
|
if transformed_img.mode != 'RGBA':
|
|
|
|
|
|
transformed_img = transformed_img.convert('RGBA')
|
|
|
|
|
|
|
|
|
|
|
|
# 缩放到预览尺寸
|
|
|
|
|
|
scaled_img = transformed_img.resize(
|
|
|
|
|
|
(max(1, int(transformed_img.width * self.zoom)),
|
|
|
|
|
|
max(1, int(transformed_img.height * self.zoom))),
|
|
|
|
|
|
Image.Resampling.LANCZOS
|
|
|
|
|
|
)
|
|
|
|
|
|
# 缓存图像
|
|
|
|
|
|
self.image_cache[img_key] = ImageTk.PhotoImage(scaled_img)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取画布坐标
|
|
|
|
|
|
min_x = min(v[0] for v in fg.vertices)
|
|
|
|
|
|
min_y = min(v[1] for v in fg.vertices)
|
|
|
|
|
|
canvas_x, canvas_y = self.template_to_canvas(min_x, min_y)
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制图像
|
|
|
|
|
|
if img_key in self.image_cache:
|
|
|
|
|
|
img = self.image_cache[img_key]
|
|
|
|
|
|
# 使用包围盒左上角坐标绘制图像
|
|
|
|
|
|
self.create_image(canvas_x, canvas_y, image=img, anchor=tk.NW)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果是选中状态,绘制边框
|
|
|
|
|
|
if self.selected_item == fg:
|
|
|
|
|
|
# 绘制变形后的边框
|
|
|
|
|
|
canvas_vertices = [
|
|
|
|
|
|
self.template_to_canvas(x, y)
|
|
|
|
|
|
for x, y in fg.vertices
|
|
|
|
|
|
]
|
|
|
|
|
|
# 绘制边框
|
|
|
|
|
|
self.create_polygon(
|
|
|
|
|
|
canvas_vertices,
|
|
|
|
|
|
outline="blue", width=2, dash=(4, 2)
|
|
|
|
|
|
)
|
|
|
|
|
|
# 绘制控制点
|
|
|
|
|
|
handles = TransformHelper.calculate_handles(fg, self)
|
|
|
|
|
|
for handle in handles.values():
|
|
|
|
|
|
self.create_oval(
|
|
|
|
|
|
handle.x - handle.radius,
|
|
|
|
|
|
handle.y - handle.radius,
|
|
|
|
|
|
handle.x + handle.radius,
|
|
|
|
|
|
handle.y + handle.radius,
|
|
|
|
|
|
fill="white", outline="blue", width=2
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"绘制图像失败: {e}")
|
|
|
|
|
|
# 绘制一个占位矩形
|
|
|
|
|
|
min_x = min(v[0] for v in fg.vertices)
|
|
|
|
|
|
min_y = min(v[1] for v in fg.vertices)
|
|
|
|
|
|
canvas_x, canvas_y = self.template_to_canvas(min_x, min_y)
|
|
|
|
|
|
# 计算宽度和高度
|
|
|
|
|
|
width = max(v[0] for v in fg.vertices) - min_x
|
|
|
|
|
|
height = max(v[1] for v in fg.vertices) - min_y
|
|
|
|
|
|
canvas_w, canvas_h = self.template_to_canvas(width, height)
|
|
|
|
|
|
self.create_rectangle(
|
|
|
|
|
|
canvas_x, canvas_y,
|
|
|
|
|
|
canvas_x + canvas_w, canvas_y + canvas_h,
|
|
|
|
|
|
outline="red", fill="#ffeeee", dash=(2, 2)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.create_text(
|
|
|
|
|
|
canvas_x + canvas_w / 2, canvas_y + canvas_h / 2,
|
|
|
|
|
|
text="无法加载", fill="red"
|
|
|
|
|
|
)
|