def collidepoint(self, point: Sequence[Number]) -> bool: mask_size = Vector(self._hitbox.get_size()) point = Vector(point[:2]) / self._last_zoom - self._last_camera.flip_y() - self.pos[:2].flip_y() point = ((point + self._center_offset) * HITBOX_RESOLUTION + mask_size / 2).round() if 0 <= point.x < mask_size.x and 0 <= point.y < mask_size.y: return bool(self._hitbox.get_at(point)) return False
def colliderect(self, rect: Sequence[Number], mask: Mask = None) -> bool: mask_size = Vector(self._hitbox.get_size()) point = Vector(rect[:2]) / self._last_zoom - self._last_camera.flip_y() - self.pos[:2].flip_y() point = ((point + self._center_offset) * HITBOX_RESOLUTION + mask_size / 2).round() if mask is None: mask = rect_hitbox_mask(rect, self._last_zoom) return bool(self._hitbox.overlap(mask, point))
def __init__(self, dictionary: dict): super().__init__(dictionary) self.selected = False self._hitbox: Optional[Mask] = None self._center_offset = Vector(0, 0) self._last_zoom: int = 1 self._last_camera = Vector(0, 0)
def pos(self, value: Vector): change = value - self.pos SelectableObject.pos.__set__(self, value) for pin in self.static_pins: (Vector(pin) + change).to_dict(pin) for anchor in self.anchors: anchor.pos += change
def calculate_hitbox(self, align_center=False): points_base = self.points # Calculate bounding rect leftmost, rightmost, topmost, bottommost = 1000, -1000, 1000, -1000 for point in points_base: leftmost = min(leftmost, point.x) rightmost = max(rightmost, point.x) topmost = min(topmost, point.y) bottommost = max(bottommost, point.y) width, height = rightmost - leftmost, bottommost - topmost # Adjust center basepos = self.pos[:2] center = Vector(leftmost + width / 2 + basepos.x, topmost + height / 2 + basepos.y) if align_center: center.to_dict(self._dict["m_Pos"]) self.points = (points_base := [point + basepos - center for point in points_base]) leftmost, rightmost = [x + basepos.x - center.x for x in (leftmost, rightmost)] topmost, bottommost = [y + basepos.y - center.y for y in (topmost, bottommost)] self._center_offset = (0, 0) else: self._center_offset = basepos - center # Create hitbox bitmap offset = (- leftmost, topmost) points_hitbox = [(HITBOX_RESOLUTION * (point + offset).flip_y()).round() for point in points_base] surface = Surface((HITBOX_RESOLUTION * width + 1, HITBOX_RESOLUTION * height + 1), pygame.SRCALPHA, 32) pygame.draw.polygon(surface, BLACK, points_hitbox) self._hitbox = mask_from_surface(surface)
def flipped(self, value: bool): old_flipped = self._dict["m_Flipped"] self._dict["m_Flipped"] = value if old_flipped != value: basepos = self.pos[:2] for pin in self.static_pins: Vector(pin).flip(basepos, self.rotation).to_dict(pin) for anchor in self.anchors: anchor.pos = anchor.pos.flip(basepos, self.rotation)
def scale(self, value: Vector): old_scale = self.scale value.to_dict(self._dict["m_Scale"]) change = (value / old_scale)[:2] if abs(change.x - 1) > 0.000001 or abs(change.y - 1) > 0.000001: basepos, rot = self.pos[:2], self.rotation for pin in self.static_pins: ((Vector(pin).rotate(-rot, basepos) - basepos) * change + basepos).rotate(rot, basepos).to_dict(pin) for anchor in self.anchors: anchor.pos = ((anchor.pos.rotate(-rot, basepos) - basepos) * change + basepos).rotate(rot, basepos)
def rotations(self, values: Vector): old_rotz = self.rotation values.quaternion().to_dict(self._dict["m_Rot"]) self._dict["m_RotationDegrees"] = values[2] change = self.rotation - old_rotz if abs(change) > 0.000001: basepos = self.pos[:2] for pin in self.static_pins: Vector(pin).rotate(change, basepos).to_dict(pin) for anchor in self.anchors: anchor.pos = anchor.pos.rotate(change, basepos)
def __init__(self, dictionary: dict, anchors: Sequence[Anchor] = None): super().__init__(dictionary) self.bounding_box = Rect(0, 0, 0, 0) self.point_hitboxes: List[CustomShapePoint] = [] self.anchors: List[Anchor] = [] self.selected_point_index: Optional[int] = None self.add_point_closest: ClosestPoint = (Vector(), 0, 0) self.add_point_hitbox = Rect(0, 0, 0, 0) if anchors: for dyn_anc_id in self.dynamic_anchor_ids: for anchor in anchors: if anchor.id == dyn_anc_id: self.anchors.append(anchor) self.calculate_hitbox()
def render(self, display: Surface, camera: Vector, zoom: int, args=None): offset = Vector(-self.width / 2, 0) start = (zoom * (self.pos + offset + camera).flip_y()).round() end = start + (round(zoom * self.width), 0) thickness = max(1, round(zoom * PLATFORM_THICKNESS)) # Legs if abs(self.height) > 0.000001: height = round(zoom * self.height) * (1 if self.flipped else -1) offset = round(zoom * PLATFORM_THICKNESS / 2) leg1_start, leg1_end = start + (offset, 0), start - (-offset, height) leg2_start, leg2_end = end + (-offset, 0), end - (offset, height) pygame.draw.line(display, PLATFORM_COLOR_2, leg1_start, leg1_end, thickness) pygame.draw.line(display, PLATFORM_COLOR_2, leg2_start, leg2_end, thickness) # Platform pygame.draw.line(display, PLATFORM_COLOR_1, start, end, thickness)
def render(self, display: Surface, camera: Vector, zoom: int, args: ShapeRenderArgs = None): """Draws the shape on the screen and calculates attributes like bounding_box. It also searches for a single point to be selected, which is saved to the args object.""" super().render(display, camera, zoom) basepos = self.pos[:2] points_pixels = [zoom * (point + basepos + camera).flip_y() for point in self.points] border_color = tuple(self.color[i] * 0.75 for i in range(3)) pygame.gfxdraw.filled_polygon(display, points_pixels, self.color) pygame.gfxdraw.aapolygon(display, points_pixels, border_color) for pin in self.static_pins: p = (zoom * (Vector(pin) + camera).flip_y()).round() pygame.gfxdraw.aacircle(display, p.x, p.y, round(zoom * PIN_RADIUS), STATIC_PIN_COLOR) pygame.gfxdraw.filled_circle(display, p.x, p.y, round(zoom * PIN_RADIUS), STATIC_PIN_COLOR) if self.selected: # We don't know how to make it antialiased pygame.draw.polygon(display, HIGHLIGHT_COLOR, points_pixels, scale(SHAPE_HIGHLIGHTED_WIDTH, zoom, 60)) self.point_hitboxes = [] self.add_point_hitbox = None self.bounding_box = pygame.draw.polygon(DUMMY_SURFACE, WHITE, points_pixels) if args.draw_points: max_radius = round(zoom * POINT_SELECTED_RADIUS) self.bounding_box.left -= max_radius self.bounding_box.top -= max_radius self.bounding_box.width += max_radius * 2 self.bounding_box.height += max_radius * 2 for i, p in enumerate(points_pixels): self.point_hitboxes.append(CustomShapePoint(p, i, round(zoom * POINT_SELECTED_RADIUS))) for i, point in enumerate(self.point_hitboxes): if i == self.selected_point_index: args.selected_point = point break if not args.holding_shift: for i, point in enumerate(self.point_hitboxes): if point.collidepoint(args.mouse_pos): args.moused_over_point = point break if args.draw_hitboxes: pygame.draw.rect(display, HITBOX_COLOR, self.bounding_box, 1) center_width = scale(HITBOX_CENTER_WIDTH, zoom) center_start = (zoom * (self.pos + camera)).flip_y() - (center_width / 2, 0) center_end = center_start + (center_width, 0) pygame.draw.line(display, HITBOX_COLOR, center_start, center_end, center_width)
def render_points(self, display: Surface, camera: Vector, zoom: int, args: ShapeRenderArgs): """Draws dots for the shape's points and performs operations related to selecting and moving them. It also searches for the top point to display, which is saved to the args object.""" if not args.draw_points: return points, basepos = self.points, self.pos[:2] points_pixels = [zoom * (point + basepos + camera).flip_y() for point in self.points] # Move point if a point is selected for i in range(len(points)): if i == self.selected_point_index: newpoints = list(points) newpoints[i] += args.mouse_change points = newpoints self.points = tuple(newpoints) break # Render points for point in self.point_hitboxes: if point == args.selected_point or args.selected_point is None and args.moused_over_point == point: args.top_point = point else: point.render(display, POINT_COLOR, round(zoom * POINT_RADIUS)) # Show overlay of where a point will be added if args.selected_point is None and args.holding_shift and self.bounding_box.collidepoint(*args.mouse_pos): closest: ClosestPoint = (Vector(), zoom / 7, -1) for i in range(len(points)): ni = 0 if i + 1 == len(points) else i + 1 point = args.mouse_pos.closest_point(points_pixels[i], points_pixels[ni]) if not point: continue distance = math.sqrt((point.x - args.mouse_pos.x) ** 2 + (point.y - args.mouse_pos.y) ** 2) if distance < closest[1]: closest = (point.round(), distance, ni) if closest[0]: self.add_point_closest = closest self.add_point_hitbox = pygame.draw.circle( DUMMY_SURFACE, 0, (closest[0].round()), round(zoom * PIN_RADIUS / 1.7), 0) pygame.gfxdraw.aacircle( display, closest[0].x, closest[0].y, round(zoom * PIN_RADIUS / 1.7), ADD_POINT_COLOR) pygame.gfxdraw.filled_circle( display, closest[0].x, closest[0].y, round(zoom * PIN_RADIUS / 1.7), ADD_POINT_COLOR) # Update hitbox and move center to actual center if self.selected_point_index is not None: self.calculate_hitbox(True)
def render(self, display: Surface, camera: Vector, zoom: int, args=None): points = self.points thickness = round(zoom * PLATFORM_THICKNESS) # Legs width = points[-1].x - points[0].x if not self.hide_legs and abs(width) > 0.01: base_y = min(p.y for p in points) - self.leg_height - PLATFORM_THICKNESS leg_separation = width / (width // RAMP_MAX_LEG_SEPARATION) last_leg_x = 1000 for i in range(len(points)): current = points[i] if abs(current.x - last_leg_x) >= leg_separation or i == len(points) - 1: last_leg_x = current.x leg_a = (zoom * (points[i] + camera).flip_y()).round() leg_b = Vector(leg_a.x, -zoom * (base_y + camera.y)).round() if abs(leg_a.y - leg_b.y) > thickness * 1.5: pygame.draw.line(display, PLATFORM_COLOR_2, leg_a, leg_b, thickness) # Ramp for i in range(len(points) - 1): point_a = (zoom * (points[i] + camera).flip_y()).round() point_b = (zoom * (points[i+1] + camera).flip_y()).round() # We don't know how to make it antialiased pygame.draw.line(display, PLATFORM_COLOR_1, point_a, point_b, thickness)
def non_anchor_joints(self): """A dictionary of vertex IDs and their positions""" return {j["m_Guid"]: Vector(j["m_Pos"])[:2] for j in self._dict["m_BridgeJoints"]}
def joints(self) -> Dict[str, Vector]: """A dictionary of vertex IDs and their positions""" return {j["m_Guid"]: Vector(j["m_Pos"])[:2] for j in chain(self._dict["m_BridgeJoints"], self._dict["m_Anchors"])}
def collidepoint(self, point: Sequence[Number]): point = Vector(point) return math.sqrt((point.x - self.pos.x) ** 2 + (point.y - self.pos.y) ** 2) <= self.radius
def points(self) -> Tuple[Vector, ...]: pts_scale = self.scale values = self._dict["m_PointsLocalSpace"] return tuple((Vector(p) * pts_scale).flip_x(only_if=self.flipped).rotate(self.rotation) for p in values)
def color(self) -> Vector: return Vector(round(v*255) for v in self._dict["m_Color"].values())
def points(self) -> Tuple[Vector, ...]: return tuple(Vector(p) for p in self._dict["m_LinePoints"])
def scale(self) -> Vector: return Vector(self._dict["m_Scale"])
def render(self, display: Surface, camera: Vector, zoom: int, color=WHITE): start = Vector(zoom * (self.pos.x - self.width / 2 + camera.x), zoom * -(self.height + camera.y)) end = start + (zoom * self.width, 0) pygame.draw.line(display, color, start, end, scale(WATER_EDGE_WIDTH, zoom))
def pos(self) -> Vector: return Vector(self._dict["m_Pos"])
def rotations(self) -> Vector: """Rotation degrees in the X, Y, and Z axis, calculated from a quaternion""" return Vector(self._dict["m_Rot"]).euler_angles()
def editor(layout: dict, layoutfile: str, jsonfile: str, backupfile: str, events: ev.EventCommunicator): zoom = 20 size = Vector(BASE_SIZE) camera = Vector(0, 0) clock = pygame.time.Clock() paused = False pause_force_render = False draw_points = False draw_hitboxes = False panning = False selecting = False moving = False point_moving = False mouse_pos = Vector(0, 0) old_mouse_pos = Vector(0, 0) old_true_mouse_pos = Vector(0, 0) selecting_pos = Vector(0, 0) dragndrop_pos = Vector(0, 0) object_being_edited: Optional[lay.SelectableObject] = None selected_shape: Optional[lay.CustomShape] = None bg_color = BACKGROUND_BLUE bg_color_2 = BACKGROUND_BLUE_GRID fg_color = WHITE object_lists = [ terrain_stretches := lay.LayoutList(lay.TerrainStretch, layout), water_blocks := lay.LayoutList(lay.WaterBlock, layout), platforms := lay.LayoutList(lay.Platform, layout), ramps := lay.LayoutList(lay.Ramp, layout), custom_shapes := lay.LayoutList(lay.CustomShape, layout), pillars := lay.LayoutList(lay.Pillar, layout), anchors := lay.LayoutList(lay.Anchor, layout) ] objects: Dict[Type[lay.LayoutObject], lay.LayoutList] = {li.cls: li for li in object_lists} bridge = lay.Bridge(layout) selectable_objects = lambda: tuple(chain(custom_shapes, pillars)) holding_shift = lambda: pygame.key.get_mods() & pygame.KMOD_SHIFT true_mouse_pos = lambda: mouse_pos.flip_y() / zoom - camera # Start pygame display = pygame.display.set_mode(size, pygame.RESIZABLE) pygame.display.set_caption("PolyEditor") if ICON: pygame.display.set_icon(pygame.image.load(ICON)) pygame.init() if USER32: # Maximize USER32.ShowWindow(USER32.GetForegroundWindow(), 3) for pyevent in pygame.event.get(): if pyevent.type == pygame.VIDEORESIZE: size = Vector(pyevent.size) display = pygame.display.set_mode(size, pygame.RESIZABLE) lay.DUMMY_SURFACE = pygame.Surface(size, pygame.SRCALPHA, 32) camera = (size / zoom / 2 + (0, 10)).flip_y() menu_button_font = pygame.font.SysFont("Courier", 20, True) menu_button = pygame.Surface( Vector(menu_button_font.size("Menu")) + (10, 6)) menu_button.fill(BACKGROUND_BLUE_DARK) pygame.draw.rect(menu_button, BLACK, menu_button.get_rect(), 1) menu_button.blit(menu_button_font.render("Menu", True, WHITE), (5, 4)) menu_button_rect = None # Editor loop while True: # Process editor events if (event := events.read()) is not None: if event == ev.CLOSE_EDITOR: pygame.quit() events.send(ev.DONE) return elif event == ev.DONE: paused = False elif object_being_edited: if event == ev.EXIT: object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) elif hasattr(event, "values"): values = event.values hl_objs = [o for o in selectable_objects() if o.selected] if len(hl_objs) == 1: obj = hl_objs[0] obj.pos = Vector(values[popup.POS_X], values[popup.POS_Y], values[popup.POS_Z]) if isinstance(obj, lay.CustomShape): obj.scale = Vector(values[popup.SCALE_X], values[popup.SCALE_Y], values[popup.SCALE_Z]) obj.rotations = Vector(values[popup.ROT_X], values[popup.ROT_Y], values[popup.ROT_Z]) obj.color = Vector(values[popup.RGB_R], values[popup.RGB_G], values[popup.RGB_B]) obj.flipped = values[popup.FLIP] obj.calculate_hitbox() elif isinstance(obj, lay.Pillar): obj.height = values[popup.HEIGHT] else: # Multiple objects for obj in hl_objs: if isinstance(obj, lay.CustomShape): obj.color = (values[popup.RGB_R], values[popup.RGB_G], values[popup.RGB_B]) elif paused: if event in (ev.MENU_RETURN, ev.ESCAPE, ev.FOCUS_OUT): events.send(ev.DONE) paused = False elif event == ev.MENU_SAVE: pygame.event.post(pygame.event.Event( SAVE_LAYOUT_EVENT, {})) events.send(ev.DONE) paused = False elif event == ev.MENU_HITBOXES: draw_hitboxes = not draw_hitboxes pause_force_render = True elif event == ev.MENU_COLORS: if bg_color == BACKGROUND_GRAY: bg_color = BACKGROUND_BLUE bg_color_2 = BACKGROUND_BLUE_GRID fg_color = WHITE else: bg_color = BACKGROUND_GRAY bg_color_2 = BACKGROUND_GRAY_GRID fg_color = BLACK pause_force_render = True elif event == ev.MENU_CHANGE_LEVEL: events.send(ev.RESTART_PROGRAM) elif event == ev.MENU_QUIT: events.send(ev.CLOSE_PROGRAM, force=False) # Proccess pygame events for pyevent in pygame.event.get(): if pyevent.type == pygame.QUIT: events.send(ev.CLOSE_PROGRAM, force=True) elif pyevent.type == pygame.ACTIVEEVENT: if pyevent.state == 6 and not pyevent.gain: # Minimized object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) events.send(ev.DONE) elif pyevent.type == pygame.VIDEORESIZE: size = Vector(pyevent.size) display = pygame.display.set_mode(size, pygame.RESIZABLE) lay.DUMMY_SURFACE = pygame.Surface(size, pygame.SRCALPHA, 32) pause_force_render = True elif (paused and pyevent.type == pygame.KEYDOWN and pyevent.key in (pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE)): events.send(ev.DONE) paused = False continue if paused: continue elif pyevent.type == SAVE_LAYOUT_EVENT: jsonstr = json.dumps(layout, indent=2) jsonstr = re.sub(r"(\r\n|\r|\n)( ){6,}", r" ", jsonstr) # limit depth to 3 levels jsonstr = re.sub(r"(\r\n|\r|\n)( ){4,}([}\]])", r" \3", jsonstr) with open(jsonfile, "w") as openfile: openfile.write(jsonstr) program = run(f"{POLYCONVERTER} {jsonfile}", capture_output=True) if program.returncode == SUCCESS_CODE: output = program.stdout.decode().strip() if len(output) == 0: events.send(popup.notif, "Saved! No new changes to apply.") paused = True else: if "backup" in program.stdout.decode(): events.send(popup.notif, f"Saved level as {layoutfile}!", f"(Copied original to {backupfile})") else: events.send( popup.notif, f"Saved level as {layoutfile}!", ) elif program.returncode == FILE_ERROR_CODE: # failed to write file? events.send(popup.notif, "Couldn't save:", program.stdout.decode().strip()) else: outputs = [ program.stdout.decode().strip(), program.stderr.decode().strip() ] events.send(popup.notif, f"Unexpected error while trying to save:", "\n".join([o for o in outputs if len(o) > 0])) elif pyevent.type == pygame.MOUSEBUTTONDOWN: if pyevent.button == 1: # left click if menu_button_rect.collidepoint(pyevent.pos): events.send(popup.open_menu, clicked=True) paused = True continue if draw_points: # Point editing for obj in reversed(custom_shapes): if draw_points and obj.bounding_box.collidepoint( *pyevent.pos): clicked_point = [ p.collidepoint(pyevent.pos) for p in obj.point_hitboxes ] if holding_shift() and obj.add_point_hitbox: if obj.add_point_hitbox.collidepoint( pyevent.pos): obj.add_point(obj.add_point_closest[2], obj.add_point_closest[0]) obj.selected_point_index = obj.add_point_closest[ 2] point_moving = True selected_shape = obj break elif True in clicked_point: point_moving = True obj.selected_point_index = clicked_point.index( True) selected_shape = obj for o in selectable_objects(): o.selected = False object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) break if not point_moving: # Dragging and multiselect for obj in reversed(selectable_objects()): if obj.collidepoint(pyevent.pos): if not holding_shift(): moving = True dragndrop_pos = true_mouse_pos( ) if not obj.selected else Vector() if not obj.selected: if not holding_shift( ): # clear other selections for o in selectable_objects(): o.selected = False obj.selected = True object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) elif holding_shift(): obj.selected = False object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) break if not (moving or point_moving): panning = True dragndrop_pos = true_mouse_pos() old_mouse_pos = Vector(pyevent.pos) if pyevent.button == 3: # right click object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) # Delete point deleted_point = False if draw_points: for obj in reversed(custom_shapes): if obj.bounding_box.collidepoint(*pyevent.pos): for i, point in enumerate(obj.point_hitboxes): if point.collidepoint(pyevent.pos): if len(obj.points) > 3: obj.del_point(i) deleted_point = True break if deleted_point: break if not deleted_point: if not point_moving or moving or holding_shift(): selecting_pos = Vector(pyevent.pos) selecting = True if pyevent.button == 4: # mousewheel up zoom_old_pos = true_mouse_pos() if not holding_shift() and round(zoom * (ZOOM_MULT - 1)) >= 1: zoom = round(zoom * ZOOM_MULT) else: zoom += 1 zoom = min(zoom, ZOOM_MAX) zoom_new_pos = true_mouse_pos() camera += zoom_new_pos - zoom_old_pos if pyevent.button == 5: # mousewheel down zoom_old_pos = true_mouse_pos() if not holding_shift() and round(zoom / (ZOOM_MULT - 1)) >= 1: zoom = round(zoom / ZOOM_MULT) else: zoom -= 1 zoom = max(zoom, ZOOM_MIN) zoom_new_pos = true_mouse_pos() camera += zoom_new_pos - zoom_old_pos elif pyevent.type == pygame.MOUSEBUTTONUP: if pyevent.button == 1: # left click if point_moving: selected_shape.selected_point_index = None selected_shape = None point_moving = False if (not holding_shift() and dragndrop_pos and ((not panning and dragndrop_pos != true_mouse_pos()) or (panning and dragndrop_pos == true_mouse_pos()))): hl_objs = [ o for o in selectable_objects() if o.selected ] for obj in hl_objs: obj.selected = False object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) panning = False moving = False if pyevent.button == 3: # right click selecting = False elif pyevent.type == pygame.MOUSEMOTION: mouse_pos = Vector(pyevent.pos) if panning: camera += (mouse_pos - old_mouse_pos).flip_y() / zoom old_mouse_pos = mouse_pos elif pyevent.type == pygame.KEYDOWN: move_x, move_y = 0, 0 move = False if pyevent.key == pygame.K_ESCAPE: events.send(popup.open_menu, clicked=False) object_being_edited = None paused = True continue elif pyevent.key == pygame.K_LEFT: move_x = -1 move = True elif pyevent.key == pygame.K_RIGHT: move_x = 1 move = True elif pyevent.key == pygame.K_UP: move_y = 1 move = True elif pyevent.key == pygame.K_DOWN: move_y = -1 move = True elif pyevent.key == pygame.K_s: pygame.event.post(pygame.event.Event( SAVE_LAYOUT_EVENT, {})) elif pyevent.key == pygame.K_p: draw_points = not draw_points elif pyevent.key == pygame.K_h: draw_hitboxes = not draw_hitboxes elif pyevent.key == pygame.K_d: # Delete selected for obj in [o for o in selectable_objects() if o.selected]: if isinstance(obj, lay.CustomShape): for dyn_anc_id in obj.dynamic_anchor_ids: for anchor in [a for a in anchors]: if anchor.id == dyn_anc_id: anchors.remove(anchor) objects[type(obj)].remove(obj) elif pyevent.key == pygame.K_c: # Copy Selected for old_obj in [ o for o in selectable_objects() if o.selected ]: new_obj = type(old_obj)(deepcopy(old_obj.dictionary)) old_obj.selected = False new_obj.selected = True if isinstance(old_obj, lay.CustomShape): new_anchors = [] for i in range(len(old_obj.dynamic_anchor_ids)): for anchor in [ a for a in anchors if a.id == old_obj.dynamic_anchor_ids[i] ]: new_anchor = deepcopy(anchor) new_anchor.id = str(uuid4()) new_anchors.append(new_anchor) anchors.extend(new_anchors) new_obj.dynamic_anchor_ids = [ a.id for a in new_anchors ] new_obj.anchors = new_anchors new_obj.pos += (1, -1) objects[type(new_obj)].append(new_obj) elif pyevent.key == pygame.K_e: # Popup window to edit object properties hl_objs = [o for o in selectable_objects() if o.selected] if len(hl_objs) == 0: # under cursor for obj in reversed(selectable_objects()): if obj.collidepoint(mouse_pos): obj.selected = True hl_objs.append(obj) break if object_being_edited: # clear previous for obj in hl_objs: obj.selected = False hl_objs.clear() object_being_edited = None events.send(ev.CLOSE_OBJ_EDIT) elif len(hl_objs) == 1: obj = hl_objs[0] values = { popup.POS_X: obj.pos.x, popup.POS_Y: obj.pos.y, popup.POS_Z: obj.pos.z } if isinstance(obj, lay.CustomShape): rot = obj.rotations values[popup.SCALE_X] = obj.scale.x values[popup.SCALE_Y] = obj.scale.y values[popup.SCALE_Z] = obj.scale.z values[popup.ROT_Z] = rot.z # Z first values[popup.ROT_X] = rot.x values[popup.ROT_Y] = rot.y values[popup.RGB_R] = obj.color[0] values[popup.RGB_G] = obj.color[1] values[popup.RGB_B] = obj.color[2] values[popup.FLIP] = obj.flipped elif isinstance(obj, lay.Pillar): values[popup.HEIGHT] = obj.height object_being_edited = obj events.send(ev.OPEN_OBJ_EDIT, values=values) elif len(hl_objs) > 1: values = {} for i in range(len(hl_objs)): if isinstance(hl_objs[i], lay.CustomShape): values[popup.RGB_R] = hl_objs[i].color[0] values[popup.RGB_G] = hl_objs[i].color[1] values[popup.RGB_B] = hl_objs[i].color[2] object_being_edited = hl_objs[i] events.send(ev.OPEN_OBJ_EDIT, values=values) break # Move selection with keys if move: hl_objs = [o for o in selectable_objects() if o.selected] for obj in hl_objs: obj.pos += (move_x, move_y) if len(hl_objs) == 0: camera -= (move_x, move_y) elif object_being_edited and len( hl_objs ) == 1 and object_being_edited == hl_objs[0]: events.send(ev.UPDATE_OBJ_EDIT, values={ popup.POS_X: hl_objs[0].pos.x, popup.POS_Y: hl_objs[0].pos.y }) # Don't render while paused if paused and not pause_force_render: clock.tick(FPS) continue # Render background display.fill(bg_color) block_size = zoom line_width = lay.scale(1, zoom) shift = (camera * zoom % block_size).round() for x in range(shift.x, size.x, block_size): pygame.draw.line(display, bg_color_2, (x, 0), (x, size.y), line_width) for y in range(-shift.y, size.y, block_size): pygame.draw.line(display, bg_color_2, (0, y), (size.x, y), line_width) # Move selection with mouse if moving: hl_objs = [o for o in selectable_objects() if o.selected] for obj in hl_objs: obj.pos += true_mouse_pos() - old_true_mouse_pos if object_being_edited and len( hl_objs) == 1 and object_being_edited == hl_objs[0]: events.send(ev.UPDATE_OBJ_EDIT, values={ popup.POS_X: hl_objs[0].pos.x, popup.POS_Y: hl_objs[0].pos.y }) true_mouse_change = true_mouse_pos() - old_true_mouse_pos old_true_mouse_pos = true_mouse_pos() # Render Objects for terrain in terrain_stretches: terrain.render(display, camera, zoom, fg_color) for water in water_blocks: water.render(display, camera, zoom, fg_color) for platform in platforms: platform.render(display, camera, zoom) for ramp in ramps: ramp.render(display, camera, zoom) shape_args = lay.ShapeRenderArgs(draw_points, draw_hitboxes, holding_shift(), mouse_pos, true_mouse_change) for shape in custom_shapes: shape.render(display, camera, zoom, shape_args) for shape in custom_shapes: shape.render_points(display, camera, zoom, shape_args) if shape_args.top_point is not None: color = lay.HIGHLIGHT_COLOR if shape_args.selected_point is not None else lay.POINT_COLOR shape_args.top_point.render( display, color, round(zoom * lay.POINT_SELECTED_RADIUS)) for pillar in pillars: pillar.render(display, camera, zoom, draw_hitboxes) bridge.render(display, camera, zoom) dyn_anc_ids = list( chain(*[shape.dynamic_anchor_ids for shape in custom_shapes])) for anchor in anchors: anchor.render(display, camera, zoom, dyn_anc_ids) # Selecting shapes if selecting: rect = (min(selecting_pos.x, mouse_pos.x), min(selecting_pos.y, mouse_pos.y), abs(mouse_pos.x - selecting_pos.x), abs(mouse_pos.y - selecting_pos.y)) pygame.draw.rect(display, lay.SELECT_COLOR, rect, 1) mask = lay.rect_hitbox_mask(rect, zoom) for obj in selectable_objects(): if not holding_shift(): obj.selected = obj.colliderect(rect, mask) elif obj.colliderect(rect, mask): # multiselect obj.selected = True # Display mouse position, zoom and fps font = pygame.font.SysFont("Courier", 20) pos_msg = f"[{round(true_mouse_pos().x, 2):>6},{round(true_mouse_pos().y, 2):>6}]" pos_text = font.render(pos_msg, True, fg_color) display.blit(pos_text, (2, 5)) font = pygame.font.SysFont("Courier", 16) zoom_msg = f"({zoom})" zoom_size = font.size(zoom_msg) zoom_text = font.render(zoom_msg, True, fg_color) display.blit(zoom_text, (round(size[0] / 2 - zoom_size[0] / 2), 5)) fps_msg = str(round(clock.get_fps())).rjust(2) fps_size = font.size(fps_msg) fps_text = font.render(fps_msg, True, fg_color) display.blit(fps_text, (size[0] - fps_size[0] - 5, 5)) # Display buttons menu_button_rect = display.blit( menu_button, (10, size.y - menu_button.get_size()[1] - 10)) pause_force_render = False pygame.display.flip() clock.tick(FPS)