Source code for editor.ui.gizmo

import pygame
import math
from core.components import Transform

[docs] class Gizmo: MODE_TRANSLATE = 0 MODE_ROTATE = 1 MODE_SCALE = 2 AXIS_NONE = 0 AXIS_X = 1 AXIS_Y = 2 AXIS_Z = 3 # For rotation (Z-axis in 2D) AXIS_BOTH = 4 def __init__(self): self.mode = self.MODE_TRANSLATE self.active_axis = self.AXIS_NONE self.hover_axis = self.AXIS_NONE self.targets = [] # List of Transform components self.size = 60 self.handle_size = 10 self.last_mouse_pos = None self.start_values = None # Not used for multi-selection simply yet # Colors self.col_x = (200, 50, 50) self.col_y = (50, 200, 50) self.col_z = (50, 50, 200) self.col_hover = (255, 255, 0) self.col_active = (255, 255, 255)
[docs] def set_targets(self, entities): self.targets = [] if entities: if not isinstance(entities, list): entities = [entities] for e in entities: t = e.get_component(Transform) if t: self.targets.append(t)
[docs] def set_target(self, entity): # Backward compatibility self.set_targets([entity] if entity else [])
[docs] def get_center(self): if not self.targets: return 0, 0 sum_x = sum(t.x for t in self.targets) sum_y = sum(t.y for t in self.targets) return sum_x / len(self.targets), sum_y / len(self.targets)
[docs] def set_mode(self, mode): self.mode = mode
[docs] def render(self, surface, camera_x, camera_y, zoom): if not self.targets: return world_cx, world_cy = self.get_center() # Screen position cx = int((world_cx - camera_x) * zoom) cy = int((world_cy - camera_y) * zoom) # Colors c_x = self.col_hover if self.hover_axis == self.AXIS_X or self.active_axis == self.AXIS_X else self.col_x c_y = self.col_hover if self.hover_axis == self.AXIS_Y or self.active_axis == self.AXIS_Y else self.col_y c_z = self.col_hover if self.hover_axis == self.AXIS_Z or self.active_axis == self.AXIS_Z else self.col_z # Center color (common for Translate and Scale) c_center = self.col_hover if self.hover_axis == self.AXIS_BOTH or self.active_axis == self.AXIS_BOTH else (200, 200, 200) if self.mode == self.MODE_TRANSLATE: # X Axis pygame.draw.line(surface, c_x, (cx, cy), (cx + self.size, cy), 3) # Arrow head X pygame.draw.polygon(surface, c_x, [(cx + self.size + 10, cy), (cx + self.size, cy - 5), (cx + self.size, cy + 5)]) # Y Axis pygame.draw.line(surface, c_y, (cx, cy), (cx, cy + self.size), 3) # Arrow head Y pygame.draw.polygon(surface, c_y, [(cx, cy + self.size + 10), (cx - 5, cy + self.size), (cx + 5, cy + self.size)]) # Center square pygame.draw.rect(surface, c_center, (cx - 6, cy - 6, 12, 12)) elif self.mode == self.MODE_ROTATE: # Circle pygame.draw.circle(surface, c_z, (cx, cy), self.size, 3) # Reference line to show rotation (using first target's rotation or 0) base_rot = self.targets[0].rotation if self.targets else 0 rad = math.radians(base_rot) # Positive because +y is down in screen coords end_x = cx + math.cos(rad) * self.size end_y = cy + math.sin(rad) * self.size pygame.draw.line(surface, (255, 255, 255), (cx, cy), (end_x, end_y), 1) elif self.mode == self.MODE_SCALE: # X Axis pygame.draw.line(surface, c_x, (cx, cy), (cx + self.size, cy), 3) # Box X pygame.draw.rect(surface, c_x, (cx + self.size - 6, cy - 6, 12, 12)) # Y Axis pygame.draw.line(surface, c_y, (cx, cy), (cx, cy + self.size), 3) # Box Y pygame.draw.rect(surface, c_y, (cx - 6, cy + self.size - 6, 12, 12)) # Center square (Uniform Scale) pygame.draw.rect(surface, c_center, (cx - 6, cy - 6, 12, 12))
[docs] def update_hover(self, mouse_pos, camera_x, camera_y, zoom): if not self.targets or self.active_axis != self.AXIS_NONE: return mx, my = mouse_pos world_cx, world_cy = self.get_center() cx = (world_cx - camera_x) * zoom cy = (world_cy - camera_y) * zoom self.hover_axis = self.AXIS_NONE # Hit detection logic based on mode if self.mode == self.MODE_TRANSLATE: # Check X axis (rect from center to tip) if cx <= mx <= cx + self.size + 15 and cy - 8 <= my <= cy + 8: self.hover_axis = self.AXIS_X # Check Y axis elif cx - 8 <= mx <= cx + 8 and cy <= my <= cy + self.size + 15: self.hover_axis = self.AXIS_Y # Center if abs(mx - cx) < 8 and abs(my - cy) < 8: self.hover_axis = self.AXIS_BOTH elif self.mode == self.MODE_ROTATE: dist = math.hypot(mx - cx, my - cy) radius = self.size if radius - 8 <= dist <= radius + 8: self.hover_axis = self.AXIS_Z elif self.mode == self.MODE_SCALE: # Center (Uniform Scale) if abs(mx - cx) < 8 and abs(my - cy) < 8: self.hover_axis = self.AXIS_BOTH # Check X axis handle elif cx + self.size - 8 <= mx <= cx + self.size + 8 and cy - 8 <= my <= cy + 8: self.hover_axis = self.AXIS_X # Check Y axis handle elif cx - 8 <= mx <= cx + 8 and cy + self.size - 8 <= my <= cy + self.size + 8: self.hover_axis = self.AXIS_Y
[docs] def handle_event(self, event_type, mouse_pos, camera_x, camera_y, zoom): """ Returns True if the event was consumed by the gizmo """ if not self.targets: return False if event_type == "MOUSEBUTTONDOWN": # Ensure we have the latest hover state before processing click self.update_hover(mouse_pos, camera_x, camera_y, zoom) if self.hover_axis != self.AXIS_NONE: self.active_axis = self.hover_axis self.last_mouse_pos = mouse_pos return True elif event_type == "MOUSEBUTTONUP": if self.active_axis != self.AXIS_NONE: self.active_axis = self.AXIS_NONE self.last_mouse_pos = None return True elif event_type == "MOUSEMOTION": self.update_hover(mouse_pos, camera_x, camera_y, zoom) if self.active_axis != self.AXIS_NONE and self.last_mouse_pos: mx, my = mouse_pos lx, ly = self.last_mouse_pos dx = (mx - lx) / zoom dy = (my - ly) / zoom world_cx, world_cy = self.get_center() cx = (world_cx - camera_x) * zoom cy = (world_cy - camera_y) * zoom if self.mode == self.MODE_TRANSLATE: dx_total = 0 dy_total = 0 if self.active_axis == self.AXIS_X: dx_total = dx elif self.active_axis == self.AXIS_Y: dy_total = dy elif self.active_axis == self.AXIS_BOTH: dx_total = dx dy_total = dy if dx_total != 0 or dy_total != 0: for t in self.targets: t.translate(dx_total, dy_total) elif self.mode == self.MODE_ROTATE: # Calculate angle difference relative to center angle1 = math.atan2(ly - cy, lx - cx) angle2 = math.atan2(my - cy, mx - cx) diff = math.degrees(angle2 - angle1) if diff != 0: for t in self.targets: t.rotate(diff) elif self.mode == self.MODE_SCALE: sensitivity = 0.01 dsx = 0 dsy = 0 if self.active_axis == self.AXIS_X: dsx = dx * sensitivity elif self.active_axis == self.AXIS_Y: dsy = dy * sensitivity elif self.active_axis == self.AXIS_BOTH: dsx = dx * sensitivity dsy = dx * sensitivity if dsx != 0 or dsy != 0: for t in self.targets: t.scale(dsx, dsy) self.last_mouse_pos = mouse_pos return True return False