예제 #1
0
	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
예제 #2
0
	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))
예제 #3
0
	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)
예제 #4
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
예제 #5
0
	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)
예제 #6
0
	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)
예제 #7
0
	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)
예제 #8
0
	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)
예제 #9
0
	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()
예제 #10
0
	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)
예제 #11
0
	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)
예제 #12
0
	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)
예제 #13
0
	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)
예제 #14
0
	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"]}
예제 #15
0
	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"])}
예제 #16
0
	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
예제 #17
0
	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)
예제 #18
0
	def color(self) -> Vector:
		return Vector(round(v*255) for v in self._dict["m_Color"].values())
예제 #19
0
	def points(self) -> Tuple[Vector, ...]:
		return tuple(Vector(p) for p in self._dict["m_LinePoints"])
예제 #20
0
	def scale(self) -> Vector:
		return Vector(self._dict["m_Scale"])
예제 #21
0
	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))
예제 #22
0
	def pos(self) -> Vector:
		return Vector(self._dict["m_Pos"])
예제 #23
0
	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()
예제 #24
0
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)