Exemple #1
0
class Drawer:
    def __init__(self, draw_area: Gtk.Widget, ssa: Ssa,
                 cb_trigger_label: Callable[[int], str],
                 sprite_provider: SpriteProvider):
        self.draw_area = draw_area

        self.ssa = ssa
        self.map_bg = None
        self.position_marks: List[SourceMapPositionMark] = []

        self.draw_tile_grid = False

        # Interaction
        self.interaction_mode = InteractionMode.SELECT
        self.mouse_x = 99999
        self.mouse_y = 99999

        self.sprite_provider = sprite_provider

        self._cb_trigger_label = cb_trigger_label
        self._sectors_visible = [
            True for _ in range(0, len(self.ssa.layer_list))
        ]
        self._sectors_solo = [
            False for _ in range(0, len(self.ssa.layer_list))
        ]
        self._sector_highlighted = None
        self._selected = None
        # If not None, drag is active and value is coordinate
        self._selected__drag: Optional[Tuple[int, int]] = None
        self._edit_pos_marks = False

        self.selection_plugin = SelectionDrawerPlugin(
            BPC_TILE_DIM, BPC_TILE_DIM, self.selection_draw_callback)
        self.tile_grid_plugin = GridDrawerPlugin(BPC_TILE_DIM,
                                                 BPC_TILE_DIM,
                                                 offset_x=-BPC_TILE_DIM / 2,
                                                 offset_y=-BPC_TILE_DIM / 2)

        self.scale = 1

        self.drawing_is_active = False

    def start(self):
        """Start drawing on the DrawingArea"""
        self.drawing_is_active = True
        if isinstance(self.draw_area, Gtk.DrawingArea):
            self.draw_area.connect('draw', self.draw)
        self.draw_area.queue_draw()

    def draw(self, wdg, ctx: cairo.Context):
        ctx.set_antialias(cairo.Antialias.NONE)
        ctx.scale(self.scale, self.scale)
        # Background
        if self.map_bg is not None:
            ctx.set_source_surface(self.map_bg, 0, 0)
            ctx.get_source().set_filter(cairo.Filter.NEAREST)
            ctx.paint()

        size_w, size_h = self.draw_area.get_size_request()
        size_w /= self.scale
        size_h /= self.scale

        # Black out bg a bit
        if not self._edit_pos_marks:
            ctx.set_source_rgba(0, 0, 0, 0.5)
            ctx.rectangle(0, 0, size_w, size_h)
            ctx.fill()

        # Tile Grid
        if self.draw_tile_grid:
            self.tile_grid_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                       self.mouse_y)

        # RENDER ENTITIES
        for layer_i, layer in enumerate(self.ssa.layer_list):
            if not self._is_layer_visible(layer_i):
                continue

            for actor in layer.actors:
                if not self._is_dragged(actor):
                    bb = self.get_bb_actor(actor)
                    if actor != self._selected:
                        self._handle_layer_highlight(ctx, layer_i, *bb)
                    self._draw_actor(ctx, actor, *bb)
                    self._draw_hitbox_actor(ctx, actor)
            for obj in layer.objects:
                if not self._is_dragged(obj):
                    bb = self.get_bb_object(obj)
                    if obj != self._selected:
                        self._handle_layer_highlight(ctx, layer_i, *bb)
                    self._draw_object(ctx, obj, *bb)
                    self._draw_hitbox_object(ctx, obj)
            for trigger in layer.events:
                if not self._is_dragged(trigger):
                    bb = self.get_bb_trigger(trigger)
                    if trigger != self._selected:
                        self._handle_layer_highlight(ctx, layer_i, *bb)
                    self._draw_trigger(ctx, trigger, *bb)
            for performer in layer.performers:
                if not self._is_dragged(performer):
                    bb = self.get_bb_performer(performer)
                    if performer != self._selected:
                        self._handle_layer_highlight(ctx, layer_i, *bb)
                    self._draw_hitbox_performer(ctx, performer)
                    self._draw_performer(ctx, performer, *bb)

        # Black out bg a bit
        if self._edit_pos_marks:
            ctx.set_source_rgba(0, 0, 0, 0.5)
            ctx.rectangle(0, 0, size_w, size_h)
            ctx.fill()

        # RENDER POSITION MARKS
        for pos_mark in self.position_marks:
            bb = self.get_bb_pos_mark(pos_mark)
            self._draw_pos_mark(ctx, pos_mark, *bb)

        # Cursor / Active selected / Place mode
        self._handle_selection(ctx)
        x, y, w, h = self._handle_drag_and_place_modes()
        self.selection_plugin.set_size(w, h)
        self.selection_plugin.draw(ctx, size_w, size_h, x, y, ignore_obb=True)

        # Position
        ctx.scale(1 / self.scale, 1 / self.scale)
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_source_rgb(*COLOR_WHITE)
        ctx.set_font_size(18)
        try:
            sw: Gtk.ScrolledWindow = self.draw_area.get_parent().get_parent(
            ).get_parent()
            s = sw.get_allocated_size()
            ctx.move_to(sw.get_hadjustment().get_value() + 30,
                        s[0].height + sw.get_vadjustment().get_value() - 30)
            if self._selected__drag is not None:
                sx, sy = self.get_current_drag_entity_pos()
            else:
                sx, sy = self._snap_pos(self.mouse_x, self.mouse_y)
            sx /= BPC_TILE_DIM
            sy /= BPC_TILE_DIM
            ctx.text_path(f"X: {sx}, Y: {sy}")
            ctx.set_source_rgb(*COLOR_BLACK)
            ctx.set_line_width(0.3)
            ctx.set_source_rgb(*COLOR_WHITE)
            ctx.fill_preserve()
            ctx.stroke()
        except BaseException:
            pass

        return True

    def selection_draw_callback(self, ctx: cairo.Context, x: int, y: int):
        if self.interaction_mode == InteractionMode.SELECT:
            if self._selected is not None and self._selected__drag is not None:
                # Draw dragged:
                x, y = self.get_current_drag_entity_pos()
                if isinstance(self._selected, SsaActor):
                    x, y, w, h = self.get_bb_actor(self._selected, x=x, y=y)
                    self._draw_actor(ctx, self._selected, x, y, w, h)
                elif isinstance(self._selected, SsaObject):
                    x, y, w, h = self.get_bb_object(self._selected, x=x, y=y)
                    self._draw_object(ctx, self._selected, x, y, w, h)
                elif isinstance(self._selected, SsaPerformer):
                    x, y, w, h = self.get_bb_performer(self._selected,
                                                       x=x,
                                                       y=y)
                    self._draw_performer(ctx, self._selected, x, y, w, h)
                elif isinstance(self._selected, SsaEvent):
                    x, y, w, h = self.get_bb_trigger(self._selected, x=x, y=y)
                    self._draw_trigger(ctx, self._selected, x, y, w, h)
                elif isinstance(self._selected, SourceMapPositionMark):
                    x, y, w, h = self.get_bb_pos_mark(self._selected, x=x, y=y)
                    self._draw_pos_mark(ctx, self._selected, x, y, w, h)
            return
        # Tool modes
        elif self.interaction_mode == InteractionMode.PLACE_ACTOR:
            self._surface_place_actor(ctx, x, y, BPC_TILE_DIM * 3,
                                      BPC_TILE_DIM * 3)
        elif self.interaction_mode == InteractionMode.PLACE_OBJECT:
            self._surface_place_object(ctx, x, y, BPC_TILE_DIM * 3,
                                       BPC_TILE_DIM * 3)
        elif self.interaction_mode == InteractionMode.PLACE_PERFORMER:
            self._surface_place_performer(ctx, x, y, BPC_TILE_DIM * 3,
                                          BPC_TILE_DIM * 3)
        elif self.interaction_mode == InteractionMode.PLACE_TRIGGER:
            self._surface_place_trigger(ctx, x, y, BPC_TILE_DIM * 3,
                                        BPC_TILE_DIM * 3)

    def set_mouse_position(self, x, y):
        self.mouse_x = x
        self.mouse_y = y

    def get_under_mouse(
        self
    ) -> Tuple[Optional[int], Optional[Union[SsaActor, SsaObject, SsaPerformer,
                                             SsaEvent]]]:
        """
        Returns the first entity under the mouse position, if any, and it's layer number.
        Not visible layers are not searched.
        Elements are searched in reversed drawing order (so what's drawn on top is also taken).
        Does not return positon marks under the mouse.
        """
        for layer_i, layer in enumerate(reversed(self.ssa.layer_list)):
            layer_i = len(self.ssa.layer_list) - layer_i - 1
            if not self._is_layer_visible(layer_i):
                continue

            for performer in reversed(layer.performers):
                bb = self.get_bb_performer(performer)
                if self._is_in_bb(*bb, self.mouse_x, self.mouse_y):
                    return layer_i, performer
            for trigger in reversed(layer.events):
                bb = self.get_bb_trigger(trigger)
                if self._is_in_bb(*bb, self.mouse_x, self.mouse_y):
                    return layer_i, trigger
            for obj in reversed(layer.objects):
                bb = self.get_bb_object(obj)
                if self._is_in_bb(*bb, self.mouse_x, self.mouse_y):
                    return layer_i, obj
            for actor in reversed(layer.actors):
                bb = self.get_bb_actor(actor)
                if self._is_in_bb(*bb, self.mouse_x, self.mouse_y):
                    return layer_i, actor
        return None, None

    def get_pos_mark_under_mouse(self) -> Optional[SourceMapPositionMark]:
        """
        Returns the first position mark under the mouse position, if any.
        Elements are searched in reversed drawing order (so what's drawn on top is also taken).
        """
        for pos_mark in reversed(self.position_marks):
            bb = self.get_bb_pos_mark(pos_mark)
            if self._is_in_bb(*bb, self.mouse_x, self.mouse_y):
                return pos_mark
        return None

    def set_draw_tile_grid(self, v):
        self.draw_tile_grid = v

    def set_scale(self, v):
        self.scale = v

    def get_bb_actor(self,
                     actor: SsaActor,
                     x=None,
                     y=None) -> Tuple[int, int, int, int]:
        if x is None:
            x = actor.pos.x_absolute
        if y is None:
            y = actor.pos.y_absolute
        if actor.actor.entid <= 0:
            _, cx, cy, w, h = self.sprite_provider.get_actor_placeholder(
                actor.actor.id, actor.pos.direction.id,
                lambda: GLib.idle_add(self._redraw))
        else:
            _, cx, cy, w, h = self.sprite_provider.get_monster(
                actor.actor.entid, actor.pos.direction.id,
                lambda: GLib.idle_add(self._redraw))
        return x - cx, y - cy, w, h

    def _draw_hitbox_actor(self, ctx: cairo.Context, actor: SsaActor):
        coords_hitbox = self._get_pmd_bounding_box(actor.pos.x_absolute,
                                                   actor.pos.y_absolute,
                                                   ACTOR_DEFAULT_HITBOX_W,
                                                   ACTOR_DEFAULT_HITBOX_H)
        self._draw_hitbox(ctx, COLOR_ACTORS, *coords_hitbox)

    def _draw_actor(self, ctx: cairo.Context, actor: SsaActor, *sprite_coords):
        self._draw_actor_sprite(ctx, actor, sprite_coords[0], sprite_coords[1])
        self._draw_name(ctx, COLOR_ACTORS, actor.actor.name, sprite_coords[0],
                        sprite_coords[1])

    def get_bb_object(self,
                      object: SsaObject,
                      x=None,
                      y=None) -> Tuple[int, int, int, int]:
        if x is None:
            x = object.pos.x_absolute
        if y is None:
            y = object.pos.y_absolute
        if object.object.name != 'NULL':
            # Load sprite to get dims.
            _, cx, cy, w, h = self.sprite_provider.get_for_object(
                object.object.name, lambda: GLib.idle_add(self._redraw))
            return x - cx, y - cy, w, h
        return self._get_pmd_bounding_box(x, y, object.hitbox_w * BPC_TILE_DIM,
                                          object.hitbox_h * BPC_TILE_DIM)

    def _draw_hitbox_object(self, ctx: cairo.Context, object: SsaObject):
        coords_hitbox = self._get_pmd_bounding_box(
            object.pos.x_absolute, object.pos.y_absolute,
            object.hitbox_w * BPC_TILE_DIM, object.hitbox_h * BPC_TILE_DIM)
        self._draw_hitbox(ctx, COLOR_OBJECTS, *coords_hitbox)

    def _draw_object(self, ctx: cairo.Context, object: SsaObject,
                     *sprite_coords):
        # Draw sprite representation
        if object.object.name != 'NULL':
            self._draw_object_sprite(ctx, object, sprite_coords[0],
                                     sprite_coords[1])
            self._draw_name(ctx, COLOR_OBJECTS, object.object.name,
                            sprite_coords[0], sprite_coords[1])
            return
        self._draw_generic_placeholder(ctx, COLOR_OBJECTS,
                                       object.object.unique_name,
                                       *sprite_coords, object.pos.direction)

    def get_bb_performer(self,
                         performer: SsaPerformer,
                         x=None,
                         y=None) -> Tuple[int, int, int, int]:
        if x is None:
            x = performer.pos.x_absolute
        if y is None:
            y = performer.pos.y_absolute
        return self._get_pmd_bounding_box(x, y,
                                          performer.hitbox_w * BPC_TILE_DIM,
                                          performer.hitbox_h * BPC_TILE_DIM)

    def _draw_hitbox_performer(self, ctx: cairo.Context,
                               performer: SsaPerformer):
        self._draw_hitbox(ctx, COLOR_PERFORMER,
                          *self.get_bb_performer(performer))

    def _draw_performer(self, ctx: cairo.Context, performer: SsaPerformer, x,
                        y, w, h):
        # Label
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_source_rgb(*COLOR_PERFORMER)
        ctx.set_font_size(12)
        ctx.move_to(x - 4, y - 8)
        ctx.show_text(f'{performer.type}')
        # Direction arrow
        self._triangle(ctx, x, y, BPC_TILE_DIM, COLOR_PERFORMER,
                       performer.pos.direction.id)

    def get_bb_trigger(self,
                       trigger: SsaEvent,
                       x=None,
                       y=None) -> Tuple[int, int, int, int]:
        if x is None:
            x = trigger.pos.x_absolute
        if y is None:
            y = trigger.pos.y_absolute
        return (x, y, trigger.trigger_width * BPC_TILE_DIM,
                trigger.trigger_height * BPC_TILE_DIM)

    def _draw_trigger(self, ctx: cairo.Context, trigger: SsaEvent,
                      *coords_hitbox):
        # Draw hitbox
        self._draw_hitbox(ctx, COLOR_PERFORMER, *coords_hitbox)
        # Label
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_source_rgb(1, 1, 1)
        ctx.set_font_size(12)
        ctx.move_to(coords_hitbox[0] + 4, coords_hitbox[1] + 14)
        ctx.show_text(f'{self._cb_trigger_label(trigger.trigger_id)}')

        return coords_hitbox

    def get_bb_pos_mark(self,
                        pos_mark: SourceMapPositionMark,
                        x=None,
                        y=None) -> Tuple[int, int, int, int]:
        if x is None:
            x = pos_mark.x_with_offset * BPC_TILE_DIM
        if y is None:
            y = pos_mark.y_with_offset * BPC_TILE_DIM
        return x - BPC_TILE_DIM, y - BPC_TILE_DIM, BPC_TILE_DIM * 3, BPC_TILE_DIM * 3

    def _draw_pos_mark(self, ctx: cairo.Context,
                       pos_mark: SourceMapPositionMark, *bb_cords):
        # Outline
        ctx.set_source_rgba(*COLOR_POS_MARKS, 0.8)
        ctx.rectangle(*bb_cords)
        ctx.set_line_width(4.0)
        ctx.set_dash([1.0])
        ctx.stroke()
        ctx.set_dash([])
        # Inner
        ctx.rectangle(bb_cords[0] + BPC_TILE_DIM, bb_cords[1] + BPC_TILE_DIM,
                      BPC_TILE_DIM, BPC_TILE_DIM)
        ctx.fill()
        # Label
        self._draw_name(ctx,
                        COLOR_POS_MARKS,
                        pos_mark.name,
                        bb_cords[0],
                        bb_cords[1],
                        scale=2)

    def _is_layer_visible(self, layer_i: int) -> bool:
        return self._sectors_solo[layer_i] or (not any(self._sectors_solo) and
                                               self._sectors_visible[layer_i])

    def _is_dragged(self, entity: Union[SsaActor, SsaObject, SsaPerformer,
                                        SsaEvent, SourceMapPositionMark]):
        return entity == self._selected and self._selected__drag is not None

    def _handle_layer_highlight(self, ctx: cairo.Context, layer: int, x: int,
                                y: int, w: int, h: int):
        if layer == self._sector_highlighted:
            padding = 2
            x -= padding
            y -= padding
            w += padding * 2
            h += padding * 2
            ctx.set_source_rgba(*COLOR_LAYER_HIGHLIGHT)
            ctx.set_line_width(1.5)
            ctx.rectangle(x, y, w, h)
            ctx.set_dash([1.0])
            ctx.stroke()
            ctx.set_dash([])

    def _handle_selection(self, ctx: cairo.Context):
        if self._selected is None:
            return
        if isinstance(self._selected, SsaActor):
            x, y, w, h = self.get_bb_actor(self._selected)
        elif isinstance(self._selected, SsaObject):
            x, y, w, h = self.get_bb_object(self._selected)
        elif isinstance(self._selected, SsaPerformer):
            x, y, w, h = self.get_bb_performer(self._selected)
        elif isinstance(self._selected, SsaEvent):
            x, y, w, h = self.get_bb_trigger(self._selected)
        elif isinstance(self._selected, SourceMapPositionMark):
            x, y, w, h = self.get_bb_pos_mark(self._selected)
        else:
            return
        padding = 2
        x -= padding
        y -= padding
        w += padding * 2
        h += padding * 2
        ctx.set_source_rgba(*COLOR_LAYER_HIGHLIGHT)
        ctx.set_line_width(3)
        ctx.rectangle(x, y, w, h)
        ctx.set_dash([1.0])
        ctx.stroke()
        ctx.set_dash([])

    def _handle_drag_and_place_modes(self):
        if self.interaction_mode == InteractionMode.SELECT:
            # IF DRAGGED
            if self._selected is not None and self._selected__drag is not None:
                # Draw dragged:
                x, y = self.get_current_drag_entity_pos()
                if isinstance(self._selected, SsaActor):
                    x, y, w, h = self.get_bb_actor(self._selected, x=x, y=y)
                elif isinstance(self._selected, SsaObject):
                    x, y, w, h = self.get_bb_object(self._selected, x=x, y=y)
                elif isinstance(self._selected, SsaPerformer):
                    x, y, w, h = self.get_bb_performer(self._selected,
                                                       x=x,
                                                       y=y)
                elif isinstance(self._selected, SsaEvent):
                    x, y, w, h = self.get_bb_trigger(self._selected, x=x, y=y)
                elif isinstance(self._selected, SourceMapPositionMark):
                    x, y, w, h = self.get_bb_pos_mark(self._selected, x=x, y=y)
                return x, y, w, h
            # DEFAULT
            return self.mouse_x, self.mouse_y, BPC_TILE_DIM, BPC_TILE_DIM
        # Tool modes
        x = self.mouse_x - self.mouse_x % (BPC_TILE_DIM / 2)
        y = self.mouse_y - self.mouse_y % (BPC_TILE_DIM / 2)
        return x - BPC_TILE_DIM * 1.5, y - BPC_TILE_DIM * 1.5, BPC_TILE_DIM * 3, BPC_TILE_DIM * 3

    def _get_pmd_bounding_box(self,
                              x_center: int,
                              y_center: int,
                              w: int,
                              h: int,
                              y_offset=0.0) -> Tuple[int, int, int, int]:
        left = x_center - int(w / 2)
        top = y_center - int(h / 2) - int(y_offset)
        return left, top, w, h

    def _draw_hitbox(self, ctx: cairo.Context, color: Color, x: int, y: int,
                     w: int, h: int):
        ctx.set_source_rgba(*color, ALPHA_T)
        ctx.rectangle(x, y, w, h)
        ctx.fill()

    def _draw_generic_placeholder(self, ctx: cairo.Context, color: Color,
                                  label: str, x: int, y: int, w: int, h: int,
                                  direction: Pmd2ScriptDirection):
        # Rectangle
        ctx.set_source_rgb(*color)
        ctx.set_line_width(1)
        ctx.rectangle(x, y, w, h)
        ctx.stroke()
        # Label
        self._draw_name(ctx, color, label, x, y)
        # Direction arrow
        a_sz = int(BPC_TILE_DIM / 2)
        self._triangle(ctx, x + int(w / 2) - int(a_sz / 2),
                       y + h - a_sz - int(a_sz / 2), a_sz, (1, 1, 1),
                       direction.id)

    def _draw_name(self,
                   ctx: cairo.Context,
                   color: Color,
                   label: str,
                   x: int,
                   y: int,
                   scale=1):
        ctx.set_source_rgb(*color)
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(6 * scale)
        ctx.move_to(x, y - 4 * scale)
        ctx.show_text(label)

    def _triangle(self, ctx: cairo.Context, x: int, y: int, a_sz: int,
                  color: Color, direction_id: int):
        if direction_id == 1 or direction_id == 0:
            # Down
            self._polygon(ctx, [(x, y), (x + a_sz, y),
                                (x + int(a_sz / 2), y + a_sz)],
                          color=color)
        elif direction_id == 2:
            # DownRight
            self._polygon(ctx, [(x + a_sz, y), (x + a_sz, y + a_sz),
                                (x, y + a_sz)],
                          color=color)
        elif direction_id == 3:
            # Right
            self._polygon(ctx, [(x, y), (x + a_sz, y + int(a_sz / 2)),
                                (x, y + a_sz)],
                          color=color)
        elif direction_id == 4:
            # UpRight
            self._polygon(ctx, [(x, y), (x + a_sz, y), (x + a_sz, y + a_sz)],
                          color=color)
        elif direction_id == 5:
            # Up
            self._polygon(ctx, [(x, y + a_sz), (x + a_sz, y + a_sz),
                                (x + int(a_sz / 2), y)],
                          color=color)
        elif direction_id == 6:
            # UpLeft
            self._polygon(ctx, [(x, y + a_sz), (x, y), (x + a_sz, y)],
                          color=color)
        elif direction_id == 7:
            # Left
            self._polygon(ctx, [(x + a_sz, y), (x, y + int(a_sz / 2)),
                                (x + a_sz, y + a_sz)],
                          color=color)
        elif direction_id == 8:
            # DownLeft
            self._polygon(ctx, [(x, y), (x, y + a_sz), (x + a_sz, y + a_sz)],
                          color=color)

    def _polygon(self, ctx: cairo.Context, points, color, outline=None):
        ctx.new_path()
        for point in points:
            ctx.line_to(*point)
        ctx.close_path()
        ctx.set_source_rgba(*color)
        if outline is not None:
            ctx.fill_preserve()
            ctx.set_source_rgba(*outline)
            ctx.set_line_width(1)
            ctx.stroke()
        else:
            ctx.fill()

    def _draw_actor_sprite(self, ctx: cairo.Context, actor: SsaActor, x, y):
        """Draws the sprite for an actor"""
        if actor.actor.entid == 0:
            sprite = self.sprite_provider.get_actor_placeholder(
                actor.actor.id, actor.pos.direction.id, self._redraw)[0]
        else:
            sprite = self.sprite_provider.get_monster(
                actor.actor.entid, actor.pos.direction.id,
                lambda: GLib.idle_add(self._redraw))[0]
        ctx.translate(x, y)
        ctx.set_source_surface(sprite)
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()
        ctx.translate(-x, -y)

    def _draw_object_sprite(self, ctx: cairo.Context, obj: SsaObject, x, y):
        """Draws the sprite for an object"""
        sprite = self.sprite_provider.get_for_object(
            obj.object.name, lambda: GLib.idle_add(self._redraw))[0]
        ctx.translate(x, y)
        ctx.set_source_surface(sprite)
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()
        ctx.translate(-x, -y)

    def _surface_place_actor(self, ctx: cairo.Context, x, y, w, h):
        ctx.set_line_width(1)
        sprite_surface = self.sprite_provider.get_monster_outline(1, 1)[0]

        ctx.translate(x, y)
        ctx.set_source_surface(sprite_surface)
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()
        self._draw_plus(ctx)
        ctx.translate(-x, -y)

    def get_pos_place_actor(self) -> Tuple[int, int]:
        """Get the X and Y position on the grid to place the actor on, in PLACE_ACTOR mode."""
        return self._snap_pos(self.mouse_x, self.mouse_y + BPC_TILE_DIM)

    def _surface_place_object(self, ctx: cairo.Context, x, y, w, h):
        ctx.set_line_width(1)
        cx, cy = x + w / 2, y + h / 2
        rect_dim = BPC_TILE_DIM * 2
        ctx.set_source_rgb(1, 1, 1)
        ctx.rectangle(cx - rect_dim / 2, cy - rect_dim / 2, rect_dim, rect_dim)
        ctx.stroke()
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(6)
        ctx.move_to(cx - 5, cy - 2)
        ctx.show_text(f'OBJ')
        ctx.translate(x, y)
        self._draw_plus(ctx)
        ctx.translate(-x, -y)

    def get_pos_place_object(self) -> Tuple[int, int]:
        """Get the X and Y position on the grid to place the object on, in PLACE_OBJECT mode."""
        return self._snap_pos(self.mouse_x, self.mouse_y)

    def _surface_place_performer(self, ctx: cairo.Context, x, y, w, h):
        ctx.set_line_width(1)
        cx, cy = x + w / 2, y + h / 2
        self._triangle(ctx, cx - BPC_TILE_DIM / 2, cy - BPC_TILE_DIM / 2,
                       BPC_TILE_DIM, (255, 255, 255), 1)
        ctx.translate(x, y)
        self._draw_plus(ctx)
        ctx.translate(-x, -y)

    def get_pos_place_performer(self) -> Tuple[int, int]:
        """Get the X and Y position on the grid to place the performer on, in PLACE_PERFORMER mode."""
        return self._snap_pos(self.mouse_x, self.mouse_y)

    def _surface_place_trigger(self, ctx: cairo.Context, x, y, w, h):
        ctx.set_line_width(1)
        cx, cy = x + w / 2, y + h / 2
        rect_dim = BPC_TILE_DIM * 2
        ctx.set_source_rgb(1, 1, 1)
        ctx.rectangle(cx - rect_dim / 2, cy - rect_dim / 2, rect_dim, rect_dim)
        ctx.stroke()
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                             cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(6)
        ctx.move_to(cx - 5, cy - 2)
        ctx.show_text(f'TRG')
        ctx.translate(x, y)
        self._draw_plus(ctx)
        ctx.translate(-x, -y)

    def get_pos_place_trigger(self) -> Tuple[int, int]:
        """Get the X and Y position on the grid to place the trigger on, in PLACE_TRIGGER mode."""
        return self._snap_pos(self.mouse_x - BPC_TILE_DIM,
                              self.mouse_y - BPC_TILE_DIM)

    def _draw_plus(self, ctx: cairo.Context):
        arrow_len = BPC_TILE_DIM / 4
        ctx.set_source_rgb(1, 1, 1)
        ctx.translate(-arrow_len, 0)
        ctx.move_to(0, 0)
        ctx.rel_line_to(arrow_len * 2, 0)
        ctx.stroke()
        ctx.translate(arrow_len, -arrow_len)
        ctx.move_to(0, 0)
        ctx.rel_line_to(0, arrow_len * 2)
        ctx.stroke()
        ctx.translate(0, arrow_len)

    def set_sector_visible(self, sector_id, value):
        self._sectors_visible[sector_id] = value
        self.draw_area.queue_draw()

    def set_sector_solo(self, sector_id, value):
        self._sectors_solo[sector_id] = value
        self.draw_area.queue_draw()

    def set_sector_highlighted(self, sector_id):
        self._sector_highlighted = sector_id
        self.draw_area.queue_draw()

    def get_sector_highlighted(self):
        return self._sector_highlighted

    def set_selected(self,
                     entity: Optional[Union[SsaActor, SsaObject, SsaPerformer,
                                            SsaEvent, SourceMapPositionMark]]):
        self._selected = entity
        self.draw_area.queue_draw()

    def add_position_marks(self, pos_marks):
        self.position_marks += pos_marks

    def set_drag_position(self, x: int, y: int):
        """Start dragging. x/y is the offset on the entity, where the dragging was started."""
        self._selected__drag = (x, y)

    def end_drag(self):
        self._selected__drag = None

    def sector_added(self):
        self._sectors_solo.append(False)
        self._sectors_visible.append(True)

    def sector_removed(self, id):
        del self._sectors_solo[id]
        del self._sectors_visible[id]
        if self._sector_highlighted == id:
            self._sector_highlighted = None
        elif self._sector_highlighted > id:
            self._sector_highlighted -= 1

    def get_current_drag_entity_pos(self) -> Tuple[int, int]:
        return self._snap_pos(self.mouse_x - self._selected__drag[0],
                              self.mouse_y - self._selected__drag[1])

    def _redraw(self):
        if self.draw_area is None or self.draw_area.get_parent() is None:
            return
        self.draw_area.queue_draw()

    def edit_position_marks(self):
        self._edit_pos_marks = True

    @staticmethod
    def _is_in_bb(bb_x, bb_y, bb_w, bb_h, mouse_x, mouse_y):
        return bb_x <= mouse_x < bb_x + bb_w and bb_y <= mouse_y < bb_y + bb_h

    @staticmethod
    def _snap_pos(x, y):
        x = x - x % (BPC_TILE_DIM / 2)
        y = y - y % (BPC_TILE_DIM / 2)
        return x, y
Exemple #2
0
class Drawer:
    def __init__(
        self,
        draw_area: Widget,
        bma: Union[BmaProtocol, None],
        bpa_durations: int,
        pal_ani_durations: int,
        # chunks_surfaces[layer_number][chunk_idx][palette_animation_frame][frame]
        chunks_surfaces: Iterable[Iterable[Iterable[Iterable[cairo.Surface]]]]
    ):
        """
        Initialize a drawer...
        :param draw_area:  Widget to draw on.
        :param bma: Either a BMA with tile indexes or None, has to be set manually then for drawing
        :param bpa_durations: How many frames to hold a BPA animation tile
        :param chunks_surfaces: Bg controller format chunk surfaces
        """
        self.draw_area = draw_area

        self.reset(bma, bpa_durations, pal_ani_durations, chunks_surfaces)

        self.draw_chunk_grid = False
        self.draw_tile_grid = False
        self.use_pink_bg = False

        # Interaction
        self.interaction_mode = DrawerInteraction.NONE
        self.interaction_chunks_selected_id = 0
        self.interaction_col_solid = False
        self.interaction_dat_value = 0

        self.mouse_x = 99999
        self.mouse_y = 99999
        self.edited_layer = -1
        self.edited_collision = -1
        self.show_only_edited_layer = False
        self.dim_layers = False
        self.draw_collision1 = False
        self.draw_collision2 = False
        self.draw_data_layer = False

        self.selection_plugin = SelectionDrawerPlugin(
            BPC_TILE_DIM, BPC_TILE_DIM, self.selection_draw_callback)
        self.tile_grid_plugin = GridDrawerPlugin(BPC_TILE_DIM, BPC_TILE_DIM)
        self.chunk_grid_plugin = GridDrawerPlugin(
            BPC_TILE_DIM * self.tiling_width,
            BPC_TILE_DIM * self.tiling_height,
            color=(0.15, 0.15, 0.15, 0.25))

        self.scale = 1

        self.drawing_is_active = False

    def reset_bma(self, bma):
        if isinstance(bma, BmaProtocol):
            self.tiling_width = bma.tiling_width
            self.tiling_height = bma.tiling_height
            self.mappings: List[Sequence[int]] = [bma.layer0,
                                                  bma.layer1]  # type: ignore
            self.width_in_chunks = bma.map_width_chunks
            self.height_in_chunks = bma.map_height_chunks
            self.width_in_tiles: Optional[u8] = bma.map_width_camera
            self.height_in_tiles: Optional[u8] = bma.map_height_camera
            self.collision1 = bma.collision
            self.collision2 = bma.collision2
            self.data_layer = bma.unknown_data_block
        else:
            self.tiling_width = u8(3)
            self.tiling_height = u8(3)
            self.mappings = [[], []]
            self.width_in_chunks = u8(1)
            self.height_in_chunks = u8(1)
            self.width_in_tiles = None
            self.height_in_tiles = None
            self.collision1 = None
            self.collision2 = None
            self.data_layer = None

    # noinspection PyAttributeOutsideInit
    def reset(self, bma, bpa_durations, pal_ani_durations, chunks_surfaces):
        self.reset_bma(bma)

        self.animation_context = AnimationContext(chunks_surfaces,
                                                  bpa_durations,
                                                  pal_ani_durations)
        self._tileset_drawer_overlay: Optional[MapTilesetOverlay] = None

    def start(self):
        """Start drawing on the DrawingArea"""
        self.drawing_is_active = True
        if isinstance(self.draw_area, Gtk.DrawingArea):
            self.draw_area.connect('draw', self.draw)
        self.draw_area.queue_draw()
        GLib.timeout_add(int(1000 / FPS), self._tick)

    def stop(self):
        self.drawing_is_active = False

    def _tick(self):
        if self.draw_area is None:
            return False
        if self.draw_area is not None and self.draw_area.get_parent() is None:
            # XXX: Gtk doesn't remove the widget on switch sometimes...
            self.draw_area.destroy()
            return False
        self.animation_context.advance()
        if EventManager.instance().get_if_main_window_has_fous():
            self.draw_area.queue_draw()
        return self.drawing_is_active

    def draw(self, wdg, ctx: cairo.Context, do_translates=True):
        ctx.set_antialias(cairo.Antialias.NONE)
        ctx.scale(self.scale, self.scale)
        chunk_width = self.tiling_width * BPC_TILE_DIM
        chunk_height = self.tiling_height * BPC_TILE_DIM
        # Background
        if not self.use_pink_bg:
            ctx.set_source_rgb(0, 0, 0)
        else:
            ctx.set_source_rgb(1.0, 0, 1.0)
        ctx.rectangle(
            0, 0, self.width_in_chunks * self.tiling_width * BPC_TILE_DIM,
            self.height_in_chunks * self.tiling_height * BPC_TILE_DIM)
        ctx.fill()

        if self._tileset_drawer_overlay is not None and self._tileset_drawer_overlay.enabled:
            self._tileset_drawer_overlay.draw_full(ctx, self.mappings[0],
                                                   self.width_in_chunks,
                                                   self.height_in_chunks)
        else:
            # Layers
            for layer_idx, chunks_at_frame in enumerate(
                    self.animation_context.current()):
                if self.show_only_edited_layer and layer_idx != self.edited_layer:
                    continue
                current_layer_mappings = self.mappings[layer_idx]
                for i, chunk_at_pos in enumerate(current_layer_mappings):
                    if 0 < chunk_at_pos < len(chunks_at_frame):
                        chunk = chunks_at_frame[chunk_at_pos]
                        ctx.set_source_surface(chunk, 0, 0)
                        ctx.get_source().set_filter(cairo.Filter.NEAREST)
                        if self.edited_layer != -1 and layer_idx > 0 and layer_idx != self.edited_layer:
                            # For Layer 1 if not the current edited: Set an alpha mask
                            ctx.paint_with_alpha(0.7)
                        else:
                            ctx.paint()
                    if (i + 1) % self.width_in_chunks == 0:
                        # Move to beginning of next line
                        if do_translates:
                            ctx.translate(
                                -chunk_width * (self.width_in_chunks - 1),
                                chunk_height)
                    else:
                        # Move to next tile in line
                        if do_translates:
                            ctx.translate(chunk_width, 0)

                # Move back to beginning
                if do_translates:
                    ctx.translate(0, -chunk_height * self.height_in_chunks)

                if (self.edited_layer != -1 and layer_idx < 1 and layer_idx != self.edited_layer) \
                    or (layer_idx == 1 and self.dim_layers) \
                    or (layer_idx == 0 and self.animation_context.num_layers < 2 and self.dim_layers):
                    # For Layer 0 if not the current edited: Draw dark rectangle
                    # or for layer 1 if dim layers
                    # ...or for layer 0 if dim layers and no second layer
                    ctx.set_source_rgba(0, 0, 0, 0.5)
                    ctx.rectangle(
                        0, 0, self.width_in_chunks * self.tiling_width *
                        BPC_TILE_DIM, self.height_in_chunks *
                        self.tiling_height * BPC_TILE_DIM)
                    ctx.fill()

        # Col 1 and 2
        for col_index, should_draw in enumerate(
            [self.draw_collision1, self.draw_collision2]):
            if should_draw:
                if col_index == 0:
                    ctx.set_source_rgba(1, 0, 0, 0.4)
                    col: Sequence[bool] = self.collision1  # type: ignore
                else:
                    ctx.set_source_rgba(0, 1, 0, 0.4)
                    col = self.collision2  # type: ignore

                for i, c in enumerate(col):
                    if c:
                        ctx.rectangle(0, 0, BPC_TILE_DIM, BPC_TILE_DIM)
                        ctx.fill()
                    if (i + 1) % self.width_in_tiles == 0:  # type: ignore
                        # Move to beginning of next line
                        if do_translates:
                            ctx.translate(-BPC_TILE_DIM *
                                          (self.width_in_tiles - 1),
                                          BPC_TILE_DIM)  # type: ignore
                    else:
                        # Move to next tile in line
                        if do_translates:
                            ctx.translate(BPC_TILE_DIM, 0)
                # Move back to beginning
                if do_translates:
                    ctx.translate(0, -BPC_TILE_DIM *
                                  self.height_in_tiles)  # type: ignore

        # Data
        if self.draw_data_layer:
            ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                                 cairo.FONT_WEIGHT_NORMAL)
            ctx.set_font_size(6)
            ctx.set_source_rgb(0, 0, 1)
            assert self.data_layer is not None
            for i, dat in enumerate(self.data_layer):
                if dat > 0:
                    ctx.move_to(0, BPC_TILE_DIM - 2)
                    ctx.show_text(f"{dat:02x}")
                if (i + 1) % self.width_in_tiles == 0:  # type: ignore
                    # Move to beginning of next line
                    if do_translates:
                        ctx.translate(-BPC_TILE_DIM *
                                      (self.width_in_tiles - 1),
                                      BPC_TILE_DIM)  # type: ignore
                else:
                    # Move to next tile in line
                    if do_translates:
                        ctx.translate(BPC_TILE_DIM, 0)
            # Move back to beginning
            if do_translates:
                ctx.translate(0, -BPC_TILE_DIM *
                              self.height_in_tiles)  # type: ignore

        size_w, size_h = self.draw_area.get_size_request()
        size_w /= self.scale
        size_h /= self.scale
        # Selection
        if self.interaction_mode == DrawerInteraction.CHUNKS:
            self.selection_plugin.set_size(self.tiling_width * BPC_TILE_DIM,
                                           self.tiling_height * BPC_TILE_DIM)
        else:
            self.selection_plugin.set_size(BPC_TILE_DIM, BPC_TILE_DIM)
        self.selection_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                   self.mouse_y)

        # Tile Grid
        if self.draw_tile_grid:
            self.tile_grid_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                       self.mouse_y)

        # Chunk Grid
        if self.draw_chunk_grid:
            self.chunk_grid_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                        self.mouse_y)
        return True

    def selection_draw_callback(self, ctx: cairo.Context, x: int, y: int):
        if self.interaction_mode == DrawerInteraction.CHUNKS:
            # Draw a chunk
            chunks_at_frame = self.animation_context.current()[
                self.edited_layer]
            ctx.set_source_surface(
                chunks_at_frame[self.interaction_chunks_selected_id], x, y)
            ctx.get_source().set_filter(cairo.Filter.NEAREST)
            ctx.paint()
        elif self.interaction_mode == DrawerInteraction.COL:
            # Draw collision
            if self.interaction_col_solid:
                ctx.set_source_rgba(1, 0, 0, 1)
                ctx.rectangle(x, y, BPC_TILE_DIM, BPC_TILE_DIM)
                ctx.fill()
        elif self.interaction_mode == DrawerInteraction.DAT:
            # Draw data
            if self.interaction_dat_value > 0:
                ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL,
                                     cairo.FONT_WEIGHT_NORMAL)
                ctx.set_font_size(6)
                ctx.set_source_rgb(1, 1, 1)
                ctx.move_to(x, y + BPC_TILE_DIM - 2)
                ctx.show_text(f"{self.interaction_dat_value:02x}")

    def set_mouse_position(self, x, y):
        self.mouse_x = x
        self.mouse_y = y

    def set_selected_chunk(self, chunk_id):
        self.interaction_chunks_selected_id = chunk_id

    def get_selected_chunk_id(self):
        return self.interaction_chunks_selected_id

    def set_interaction_col_solid(self, v):
        self.interaction_col_solid = v

    def get_interaction_col_solid(self):
        return self.interaction_col_solid

    def set_interaction_dat_value(self, v):
        self.interaction_dat_value = v

    def get_interaction_dat_value(self):
        return self.interaction_dat_value

    def set_edited_layer(self, layer_id):
        # The layer that is not edited will be drawn with a bit of transparency or darker
        # Default is -1, which shows all at full opacity
        self.dim_layers = False
        self.edited_layer = layer_id
        self.draw_collision1 = False
        self.draw_collision2 = False
        self.draw_data_layer = False
        self.edited_collision = -1
        self.interaction_mode = DrawerInteraction.CHUNKS

    def set_show_only_edited_layer(self, v):
        self.show_only_edited_layer = v

    def set_edited_collision(self, collision_id):
        self.dim_layers = True
        self.edited_layer = -1
        self.draw_collision1 = False
        self.draw_collision2 = False
        self.draw_data_layer = False
        if collision_id == 0:
            self.draw_collision1 = True
        elif collision_id == 1:
            self.draw_collision2 = True
        self.edited_collision = collision_id
        self.interaction_mode = DrawerInteraction.COL

    def get_edited_collision(self):
        return self.edited_collision

    def set_edit_data_layer(self):
        self.dim_layers = True
        self.edited_layer = -1
        self.edited_collision = -1
        self.draw_collision1 = False
        self.draw_collision2 = False
        self.draw_data_layer = True
        self.interaction_mode = DrawerInteraction.DAT

    def get_interaction_mode(self):
        return self.interaction_mode

    def set_draw_chunk_grid(self, v):
        self.draw_chunk_grid = v

    def set_draw_tile_grid(self, v):
        self.draw_tile_grid = v

    def set_pink_bg(self, v):
        self.use_pink_bg = v

    def set_scale(self, v):
        self.scale = v

    def add_overlay(self, tileset_drawer_overlay):
        self._tileset_drawer_overlay = tileset_drawer_overlay

    @typing.no_type_check
    def unload(self):
        self.draw_area = None
        self.reset(None, None, None, None)
        self.draw_chunk_grid = False
        self.draw_tile_grid = False
        self.use_pink_bg = False
        self.interaction_mode = DrawerInteraction.NONE
        self.interaction_chunks_selected_id = 0
        self.interaction_col_solid = False
        self.interaction_dat_value = 0
        self.mouse_x = 99999
        self.mouse_y = 99999
        self.edited_layer = -1
        self.edited_collision = -1
        self.show_only_edited_layer = False
        self.dim_layers = False
        self.draw_collision1 = False
        self.draw_collision2 = False
        self.draw_data_layer = False
        self.selection_plugin = None
        self.tile_grid_plugin = None
        self.chunk_grid_plugin = None
        self.scale = 1
        self.drawing_is_active = False
class Drawer:
    def __init__(
        self,
        draw_area: Widget,
        dbg: Union[Dbg, None],
        pal_ani_durations: int,
        # chunks_surfaces[chunk_idx][palette_animation_frame][frame]
        chunks_surfaces: Iterable[Iterable[List[cairo.Surface]]]):
        """
        Initialize a drawer...
        :param draw_area:  Widget to draw on.
        :param dbg: Either a DBG with chunk indexes or None, has to be set manually then for drawing
        :param chunks_surfaces: Bg controller format chunk surfaces
        """
        self.draw_area = draw_area

        self.reset(dbg, pal_ani_durations, chunks_surfaces)

        self.draw_chunk_grid = True
        self.draw_tile_grid = True
        self.use_pink_bg = False

        # Interaction
        self.interaction_chunks_selected_id = 0

        self.mouse_x = 99999
        self.mouse_y = 99999

        self.tiling_width = DBG_TILING_DIM
        self.tiling_height = DBG_TILING_DIM
        self.width_in_chunks = DBG_WIDTH_AND_HEIGHT
        self.height_in_chunks = DBG_WIDTH_AND_HEIGHT
        self.width_in_tiles = DBG_WIDTH_AND_HEIGHT * 3
        self.height_in_tiles = DBG_WIDTH_AND_HEIGHT * 3

        self.selection_plugin = SelectionDrawerPlugin(
            DPCI_TILE_DIM, DPCI_TILE_DIM, self.selection_draw_callback)
        self.tile_grid_plugin = GridDrawerPlugin(DPCI_TILE_DIM, DPCI_TILE_DIM)
        self.chunk_grid_plugin = GridDrawerPlugin(
            DPCI_TILE_DIM * self.tiling_width,
            DPCI_TILE_DIM * self.tiling_height,
            color=(0.15, 0.15, 0.15, 0.25))

        self.scale = 1

        self.drawing_is_active = False

    # noinspection PyAttributeOutsideInit
    def reset(self, dbg, pal_ani_durations, chunks_surfaces):
        if isinstance(dbg, Dbg):
            self.mappings = dbg.mappings
        else:
            self.mappings = []

        self.animation_context = AnimationContext([chunks_surfaces], 0,
                                                  pal_ani_durations)

    def start(self):
        """Start drawing on the DrawingArea"""
        self.drawing_is_active = True
        if isinstance(self.draw_area, Gtk.DrawingArea):
            self.draw_area.connect('draw', self.draw)
        self.draw_area.queue_draw()
        GLib.timeout_add(int(1000 / FPS), self._tick)

    def stop(self):
        self.drawing_is_active = False

    def _tick(self):
        if self.draw_area is None:
            return False
        if self.draw_area is not None and self.draw_area.get_parent() is None:
            # XXX: Gtk doesn't remove the widget on switch sometimes...
            self.draw_area.destroy()
            return False
        self.animation_context.advance()
        if EventManager.instance().get_if_main_window_has_fous():
            self.draw_area.queue_draw()
        return self.drawing_is_active

    def draw(self, wdg, ctx: cairo.Context, do_translates=True):
        ctx.set_antialias(cairo.Antialias.NONE)
        ctx.scale(self.scale, self.scale)
        chunk_width = self.tiling_width * DPCI_TILE_DIM
        chunk_height = self.tiling_height * DPCI_TILE_DIM
        # Background
        if not self.use_pink_bg:
            ctx.set_source_rgb(0, 0, 0)
        else:
            ctx.set_source_rgb(1.0, 0, 1.0)
        ctx.rectangle(
            0, 0, self.width_in_chunks * self.tiling_width * DPCI_TILE_DIM,
            self.height_in_chunks * self.tiling_height * DPCI_TILE_DIM)
        ctx.fill()

        # Layers
        for chunks_at_frame in self.animation_context.current():
            for i, chunk_at_pos in enumerate(self.mappings):
                if 0 < chunk_at_pos < len(chunks_at_frame):
                    chunk = chunks_at_frame[chunk_at_pos]
                    ctx.set_source_surface(chunk, 0, 0)
                    ctx.get_source().set_filter(cairo.Filter.NEAREST)
                    ctx.paint()
                if (i + 1) % self.width_in_chunks == 0:
                    # Move to beginning of next line
                    if do_translates:
                        ctx.translate(
                            -chunk_width * (self.width_in_chunks - 1),
                            chunk_height)
                else:
                    # Move to next tile in line
                    if do_translates:
                        ctx.translate(chunk_width, 0)

            # Move back to beginning
            if do_translates:
                ctx.translate(0, -chunk_height * self.height_in_chunks)
            break

        size_w, size_h = self.draw_area.get_size_request()
        size_w /= self.scale
        size_h /= self.scale
        # Selection
        self.selection_plugin.set_size(self.tiling_width * DPCI_TILE_DIM,
                                       self.tiling_height * DPCI_TILE_DIM)
        self.selection_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                   self.mouse_y)

        # Tile Grid
        if self.draw_tile_grid:
            self.tile_grid_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                       self.mouse_y)

        # Chunk Grid
        if self.draw_chunk_grid:
            self.chunk_grid_plugin.draw(ctx, size_w, size_h, self.mouse_x,
                                        self.mouse_y)
        return True

    def selection_draw_callback(self, ctx: cairo.Context, x: int, y: int):
        # Draw a chunk
        chunks_at_frame = self.animation_context.current()[0]
        ctx.set_source_surface(
            chunks_at_frame[self.interaction_chunks_selected_id], x, y)
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()

    def set_mouse_position(self, x, y):
        self.mouse_x = x
        self.mouse_y = y

    def set_selected_chunk(self, chunk_id):
        self.interaction_chunks_selected_id = chunk_id

    def get_selected_chunk_id(self):
        return self.interaction_chunks_selected_id

    def set_draw_chunk_grid(self, v):
        self.draw_chunk_grid = v

    def set_draw_tile_grid(self, v):
        self.draw_tile_grid = v

    def set_pink_bg(self, v):
        self.use_pink_bg = v

    def set_scale(self, v):
        self.scale = v
class FixedRoomDrawer:
    def __init__(
            self, draw_area: Gtk.Widget, fixed_floor: FixedFloor,
            sprite_provider: SpriteProvider, entity_rule_container: EntityRuleContainer,
            string_provider: StringProvider
    ):
        self.draw_area = draw_area

        self.fixed_floor = fixed_floor
        self.tileset_renderer: Optional[AbstractTilesetRenderer] = None

        self.draw_tile_grid = False
        self.info_layer_active = None
        self.entity_rule_container = entity_rule_container

        # Interaction
        self.interaction_mode = InteractionMode.SELECT
        self.mouse_x = 99999
        self.mouse_y = 99999

        self.sprite_provider = sprite_provider
        self.string_provider = string_provider

        # Depending on the mode this is either a coordinate tuple or a FixedFloorActionRule to place.
        self._selected: Optional[Union[Tuple[int, int], FixedFloorActionRule]] = None
        self._selected__drag = None

        self.selection_plugin = SelectionDrawerPlugin(
            DPCI_TILE_DIM * DPC_TILING_DIM, DPCI_TILE_DIM * DPC_TILING_DIM, self.selection_draw_callback
        )
        self.tile_grid_plugin = GridDrawerPlugin(
            DPCI_TILE_DIM * DPC_TILING_DIM, DPCI_TILE_DIM * DPC_TILING_DIM,
            offset_x=OFFSET, offset_y=OFFSET
        )

        self.scale = 1

        self.drawing_is_active = False

    def start(self):
        """Start drawing on the DrawingArea"""
        self.drawing_is_active = True
        if isinstance(self.draw_area, Gtk.DrawingArea):
            self.draw_area.connect('draw', self.draw)
        self.draw_area.queue_draw()

    def draw(self, wdg, ctx: cairo.Context):
        ctx.set_antialias(cairo.Antialias.NONE)
        ctx.scale(self.scale, self.scale)
        # Background
        if self.tileset_renderer is not None:
            bg = self.tileset_renderer.get_background()
            if bg is not None:
                ctx.set_source_surface(bg, 0, 0)
                ctx.get_source().set_filter(cairo.Filter.NEAREST)
                ctx.paint()

        size_w, size_h = self.draw_area.get_size_request()
        size_w /= self.scale
        size_h /= self.scale

        # Black out bg a bit
        ctx.set_source_rgba(0, 0, 0, 0.5)
        ctx.rectangle(0, 0, size_w, size_h)
        ctx.fill()

        # Iterate over floor and render it
        draw_outside_as_second_terrain = any(action.tr_type == TileRuleType.SECONDARY_HALLWAY_VOID_ALL
                                             for action in self.fixed_floor.actions if isinstance(action, TileRule))
        outside = DmaType.WATER if draw_outside_as_second_terrain else DmaType.WALL
        rules = []
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        ridx = 0
        for y in range(0, self.fixed_floor.height):
            row = [outside, outside, outside, outside, outside]
            rules.append(row)
            for x in range(0, self.fixed_floor.width):
                action = self.fixed_floor.actions[ridx]
                if isinstance(action, TileRule):
                    if action.tr_type.floor_type == FloorType.FLOOR:
                        row.append(DmaType.FLOOR)
                    elif action.tr_type.floor_type == FloorType.WALL:
                        row.append(DmaType.WALL)
                    elif action.tr_type.floor_type == FloorType.SECONDARY:
                        row.append(DmaType.WATER)
                    elif action.tr_type.floor_type == FloorType.FLOOR_OR_WALL:
                        row.append(DmaType.WALL)
                else:
                    item, monster, tile, stats = self.entity_rule_container.get(action.entity_rule_id)
                    if tile.is_secondary_terrain():
                        row.append(DmaType.WATER)
                    else:
                        row.append(DmaType.FLOOR)
                ridx += 1
            row += [outside, outside, outside, outside, outside]
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))
        rules.append([outside] * (self.fixed_floor.width + 10))

        dungeon = self.tileset_renderer.get_dungeon(rules)
        ctx.set_source_surface(dungeon, 0, 0)
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()

        # Tile Grid
        if self.draw_tile_grid:
            self.tile_grid_plugin.draw(ctx, size_w - OFFSET, size_h - OFFSET, self.mouse_x, self.mouse_y)

        # Black out non-editable area
        ctx.set_source_rgba(0, 0, 0, 0.5)
        ctx.rectangle(0, 0,
                      size_w, DPCI_TILE_DIM * DPC_TILING_DIM * 5)
        ctx.fill()
        ctx.set_source_rgba(0, 0, 0, 0.5)
        ctx.rectangle(0, DPCI_TILE_DIM * DPC_TILING_DIM * (self.fixed_floor.height + 5),
                      size_w, DPCI_TILE_DIM * DPC_TILING_DIM * 5)
        ctx.fill()
        ctx.set_source_rgba(0, 0, 0, 0.5)
        ctx.rectangle(0, DPCI_TILE_DIM * DPC_TILING_DIM * 5,
                      DPCI_TILE_DIM * DPC_TILING_DIM * 5, DPCI_TILE_DIM * DPC_TILING_DIM * self.fixed_floor.height)
        ctx.fill()
        ctx.set_source_rgba(0, 0, 0, 0.5)
        ctx.rectangle(DPCI_TILE_DIM * DPC_TILING_DIM * (self.fixed_floor.width + 5), DPCI_TILE_DIM * DPC_TILING_DIM * 5,
                      DPCI_TILE_DIM * DPC_TILING_DIM * 5, DPCI_TILE_DIM * DPC_TILING_DIM * self.fixed_floor.height)
        ctx.fill()

        # Draw Pokémon, items, traps, etc.
        ridx = 0
        for y in range(0, self.fixed_floor.height):
            y += 5
            for x in range(0, self.fixed_floor.width):
                x += 5
                action = self.fixed_floor.actions[ridx]
                sx = DPCI_TILE_DIM * DPC_TILING_DIM * x
                sy = DPCI_TILE_DIM * DPC_TILING_DIM * y
                self._draw_action(ctx, action, sx, sy)
                ridx += 1

        # Draw info layer
        if self.info_layer_active:
            # Black out bg a bit
            ctx.set_source_rgba(0, 0, 0, 0.5)
            ctx.rectangle(0, 0, size_w, size_h)
            ctx.fill()
            ridx = 0
            for y in range(0, self.fixed_floor.height):
                y += 5
                for x in range(0, self.fixed_floor.width):
                    x += 5
                    action = self.fixed_floor.actions[ridx]
                    sx = DPCI_TILE_DIM * DPC_TILING_DIM * x
                    sy = DPCI_TILE_DIM * DPC_TILING_DIM * y
                    if isinstance(action, EntityRule):
                        item, monster, tile, stats = self.entity_rule_container.get(action.entity_rule_id)
                        # Has trap?
                        if tile.trap_id < 25 and self.info_layer_active == InfoLayer.TRAP:
                            self._draw_info_trap(sx, sy, ctx, self._trap_name(tile.trap_id),
                                                 tile.can_be_broken(), tile.trap_is_visible())
                        # Has item?
                        if item.item_id > 0 and self.info_layer_active == InfoLayer.ITEM:
                            self._draw_info_item(sx, sy, ctx, self._item_name(item.item_id))
                        # Has Pokémon?
                        if monster.md_idx > 0 and self.info_layer_active == InfoLayer.MONSTER:
                            self._draw_info_monster(sx, sy, ctx, monster.enemy_settings)
                        if self.info_layer_active == InfoLayer.TILE:
                            self._draw_info_tile(sx, sy, ctx, tile.room_id, False, False)
                    elif self.info_layer_active == InfoLayer.TILE:
                        self._draw_info_tile(sx, sy, ctx, 
                                             0 if action.tr_type.room_type == RoomType.ROOM else -1, 
                                             action.tr_type.impassable, action.tr_type.absolute_mover)
                    ridx += 1

        # Cursor / Active selected / Place mode
        x, y, w, h = self.mouse_x, self.mouse_y, DPCI_TILE_DIM * DPC_TILING_DIM, DPCI_TILE_DIM * DPC_TILING_DIM
        self.selection_plugin.set_size(w, h)
        xg, yg = self.get_cursor_pos_in_grid()
        xg *= DPCI_TILE_DIM * DPC_TILING_DIM
        yg *= DPCI_TILE_DIM * DPC_TILING_DIM
        self.selection_plugin.draw(ctx, size_w, size_h, xg, yg, ignore_obb=True)
        return True

    def selection_draw_callback(self, ctx: cairo.Context, x: int, y: int):
        sx = DPCI_TILE_DIM * DPC_TILING_DIM * x
        sy = DPCI_TILE_DIM * DPC_TILING_DIM * y
        if self.interaction_mode == InteractionMode.SELECT:
            if self._selected is not None and self._selected__drag is not None:
                # Draw dragged:
                selected_x, selected_y = self._selected
                selected = self.fixed_floor.actions[self.fixed_floor.width * selected_y + selected_x]
                self._draw_single_tile(ctx, selected, x, y)
                self._draw_action(ctx, selected, x, y)
        # Tool modes
        elif self.interaction_mode == InteractionMode.PLACE_TILE or self.interaction_mode == InteractionMode.PLACE_ENTITY:
            self._draw_single_tile(ctx, self._selected, x, y)
            self._draw_action(ctx, self._selected, x, y)

    def set_mouse_position(self, x, y):
        self.mouse_x = x
        self.mouse_y = y

    def end_drag(self):
        self._selected__drag = None

    def set_draw_tile_grid(self, v):
        self.draw_tile_grid = v
        self._redraw()

    def set_info_layer(self, v: Optional[InfoLayer]):
        self.info_layer_active = v
        self.draw_area.queue_draw()

    def set_scale(self, v):
        self.scale = v

    def set_tileset_renderer(self, renderer: AbstractTilesetRenderer):
        self.tileset_renderer = renderer

    def set_selected(self, selected):
        self._selected = selected
        self._redraw()

    def get_selected(self):
        return self._selected

    def set_drag_position(self, x: int, y: int):
        """Start dragging. x/y is the offset on the entity, where the dragging was started."""
        self._selected__drag = (x, y)

    def get_cursor_is_in_bounds(self, w, h, real_offset=False):
        return self.get_pos_is_in_bounds(self.mouse_x, self.mouse_y, w, h, real_offset)

    def get_cursor_pos_in_grid(self, real_offset=False):
        return self.get_pos_in_grid(self.mouse_x, self.mouse_y, real_offset)

    def get_pos_is_in_bounds(self, x, y, w, h, real_offset=False):
        x, y = self.get_pos_in_grid(x, y, real_offset)
        return x > -1 and y > - 1 and x < w and y < h

    def get_pos_in_grid(self, x, y, real_offset=False):
        x = int(x / (DPC_TILING_DIM * DPCI_TILE_DIM))
        y = int(y / (DPC_TILING_DIM * DPCI_TILE_DIM))
        if real_offset:
            x -= 5
            y -= 5
        return x, y

    def _redraw(self):
        if self.draw_area is None or self.draw_area.get_parent() is None:
            return
        self.draw_area.queue_draw()

    def _draw_placeholder(self, actor_id, sx, sy, direction, ctx):
        sprite, cx, cy, w, h = self.sprite_provider.get_actor_placeholder(
            actor_id,
            direction.ssa_id if direction is not None else 0,
            lambda: GLib.idle_add(self._redraw)
        )
        ctx.translate(sx, sy)
        ctx.set_source_surface(
            sprite,
            -cx + DPCI_TILE_DIM * DPC_TILING_DIM / 2,
            -cy + DPCI_TILE_DIM * DPC_TILING_DIM * 0.75
        )
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()
        ctx.translate(-sx, -sy)

    def _draw_info_trap(self, sx, sy, ctx, name, can_be_broken, visible):
        self._draw_name(ctx, sx, sy, name, COLOR_WHITE)
        if can_be_broken:
            self._draw_bottom_left(ctx, sx, sy, COLOR_RED, 'B')
        if visible:
            self._draw_bottom_right(ctx, sx, sy, COLOR_YELLOW, 'V')

    def _draw_info_item(self, sx, sy, ctx, name):
        self._draw_name(ctx, sx, sy, name, COLOR_WHITE)

    def _draw_info_monster(self, sx, sy, ctx, enemy_settings):
        if enemy_settings == MonsterSpawnType.ALLY_HELP:
            self._draw_bottom_left(ctx, sx, sy, COLOR_GREEN, 'A')
        elif enemy_settings == MonsterSpawnType.ENEMY_STRONG:
            self._draw_bottom_left(ctx, sx, sy, COLOR_RED, 'E')
        else:
            self._draw_bottom_right(ctx, sx, sy, COLOR_YELLOW, 'O')

    def _draw_info_tile(self, sx, sy, ctx, room_id, impassable, absolute_mover):
        if room_id > -1:
            self._draw_top_right(ctx, sx, sy, COLOR_RED, str(room_id))
        if impassable:
            self._draw_bottom_left(ctx, sx, sy, COLOR_YELLOW, 'I')
        if absolute_mover:
            self._draw_bottom_right(ctx, sx, sy, COLOR_GREEN, 'A')

    def _trap_name(self, trap_id):
        return MappaTrapType(trap_id).name

    def _item_name(self, item_id):
        return self.string_provider.get_value(StringType.ITEM_NAMES, item_id) if item_id < MAX_ITEMS else _("(Special?)")

    def _draw_name(self, ctx, sx, sy, name, color):
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        ctx.set_source_rgba(*color)
        ctx.set_font_size(12)
        ctx.move_to(sx, sy - 10)
        ctx.show_text(name)

    def _draw_top_right(self, ctx, sx, sy, color, text):
        self._draw_little_text(ctx, sx + 20, sy + 8, color, text)

    def _draw_bottom_left(self, ctx, sx, sy, color, text):
        self._draw_little_text(ctx, sx + 2, sy + 22, color, text)

    def _draw_bottom_right(self, ctx, sx, sy, color, text):
        self._draw_little_text(ctx, sx + 20, sy + 22, color, text)

    def _draw_little_text(self, ctx, sx, sy, color, text):
        ctx.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        ctx.set_source_rgba(*color)
        ctx.set_font_size(8)
        ctx.move_to(sx, sy)
        ctx.show_text(text)

    def _draw_action(self, ctx, action, sx, sy):
        if isinstance(action, EntityRule):
            item, monster, tile, stats = self.entity_rule_container.get(action.entity_rule_id)
            # Has trap?
            if tile.trap_id < 25:
                ctx.rectangle(sx + 5, sy + 5, DPCI_TILE_DIM * DPC_TILING_DIM - 10, DPCI_TILE_DIM * DPC_TILING_DIM - 10)
                ctx.set_source_rgba(*COLOR_TRAP)
                ctx.fill_preserve()
                ctx.set_source_rgba(*COLOR_OUTLINE)
                ctx.set_line_width(1)
                ctx.stroke()
            # Has item?
            if item.item_id > 0:
                ctx.arc(sx + DPCI_TILE_DIM * DPC_TILING_DIM / 2, sy + DPCI_TILE_DIM * DPC_TILING_DIM / 2,
                        DPCI_TILE_DIM * DPC_TILING_DIM / 2, 0, 2 * math.pi)
                ctx.set_source_rgba(*COLOR_ITEM)
                ctx.fill_preserve()
                ctx.set_source_rgba(*COLOR_OUTLINE)
                ctx.set_line_width(1)
                ctx.stroke()
            # Has Pokémon?
            if monster.md_idx > 0:
                sprite, cx, cy, w, h = self.sprite_provider.get_monster(
                    monster.md_idx,
                    action.direction.ssa_id if action.direction is not None else 0,
                    lambda: GLib.idle_add(self._redraw)
                )
                ctx.translate(sx, sy)
                ctx.set_source_surface(
                    sprite,
                    -cx + DPCI_TILE_DIM * DPC_TILING_DIM / 2,
                    -cy + DPCI_TILE_DIM * DPC_TILING_DIM * 0.75
                )
                ctx.get_source().set_filter(cairo.Filter.NEAREST)
                ctx.paint()
                ctx.translate(-sx, -sy)
        else:
            # Leader spawn tile
            if action.tr_type == TileRuleType.LEADER_SPAWN:
                self._draw_placeholder(0, sx, sy, action.direction, ctx)
            # Attendant1 spawn tile
            if action.tr_type == TileRuleType.ATTENDANT1_SPAWN:
                self._draw_placeholder(10, sx, sy, action.direction, ctx)
            # Attendant2 spawn tile
            if action.tr_type == TileRuleType.ATTENDANT2_SPAWN:
                self._draw_placeholder(11, sx, sy, action.direction, ctx)
            # Attendant3 spawn tile
            if action.tr_type == TileRuleType.ATTENDANT3_SPAWN:
                self._draw_placeholder(15, sx, sy, action.direction, ctx)
            # Key walls
            if action.tr_type == TileRuleType.FL_WA_ROOM_FLAG_0C or action.tr_type == TileRuleType.FL_WA_ROOM_FLAG_0D:
                ctx.rectangle(sx + 5, sy + 5, DPCI_TILE_DIM * DPC_TILING_DIM - 10, DPCI_TILE_DIM * DPC_TILING_DIM - 10)
                ctx.set_source_rgba(*COLOR_KEY_WALL)
                ctx.fill_preserve()
                ctx.set_source_rgba(*COLOR_OUTLINE)
                ctx.set_line_width(1)
                ctx.stroke()
            # Warp zone
            if action.tr_type == TileRuleType.WARP_ZONE or action.tr_type == TileRuleType.WARP_ZONE_2:
                ctx.rectangle(sx + 5, sy + 5, DPCI_TILE_DIM * DPC_TILING_DIM - 10, DPCI_TILE_DIM * DPC_TILING_DIM - 10)
                ctx.set_source_rgba(*COLOR_WARP_ZONE)
                ctx.fill_preserve()
                ctx.set_source_rgba(*COLOR_OUTLINE)
                ctx.set_line_width(1)
                ctx.stroke()

    def _draw_single_tile(self, ctx, action, x, y):
        type = DmaType.FLOOR
        if isinstance(action, TileRule):
            if action.tr_type.floor_type == FloorType.WALL:
                type = DmaType.WALL
            elif action.tr_type.floor_type == FloorType.SECONDARY:
                type = DmaType.WATER
            elif action.tr_type.floor_type == FloorType.FLOOR_OR_WALL:
                type = DmaType.WALL
        else:
            item, monster, tile, stats = self.entity_rule_container.get(action.entity_rule_id)
            if tile.is_secondary_terrain():
                type = DmaType.WATER

        surf = self.tileset_renderer.get_single_tile(type)
        ctx.translate(x, y)
        ctx.set_source_surface(
            surf, 0, 0
        )
        ctx.get_source().set_filter(cairo.Filter.NEAREST)
        ctx.paint()
        ctx.translate(-x, -y)