Esempio n. 1
0
 def redraw(self, color_change=False):
     tdw = self.doc.tdw
     if color_change:
         tdw.queue_draw()
     else:
         prev_trash = self._prev_disable_button_rectangle
         new_trash = self._disable_button_rectangle
         if prev_trash:
             tdw.queue_draw_area(*prev_trash)
         if new_trash:
             tdw.queue_draw_area(*new_trash)
         old_corners = tuple(pairwise(self._prev_display_corners))
         new_corners = tuple(pairwise(self._display_corners))
         prev = self._prev_rectangles
         new = self._new_rectangles
         for i, (r1, r2) in enumerate(zip(prev, new)):
             if r1 == r2:
                 continue  # Skip if unchanged
             if r1 and r2:
                 r1.expand_to_include_rect(r2)
                 tdw.queue_draw_area(*r1)
             elif r1 or r2:
                 r = r1 or r2
                 corners = new_corners if r == r1 else old_corners
                 if corners:
                     (cx0, cy0), (cx1, cy1) = corners[i]
                     r.expand_to_include_point(cx0, cy0)
                     r.expand_to_include_point(cx1, cy1)
                     tdw.queue_draw_area(*r)
                 else:
                     tdw.queue_draw()
                     return
Esempio n. 2
0
    def get_color_at_position(self, x, y, ignore_mask=False):
        """Converts an `x`, `y` position to a color.

        Ordinarily, this implmentation uses any active mask to limit the
        colors which can be clicked on. Set `ignore_mask` to disable this
        added behaviour.

        """
        sup = HueSaturationWheelMixin
        if ignore_mask or not self.mask_toggle.get_active():
            return sup.get_color_at_position(self, x, y)
        voids = self.get_mask_voids()
        if not voids:
            return sup.get_color_at_position(self, x, y)
        isects = []
        for vi, void in enumerate(voids):
            # If we're inside a void, use the unchanged value
            if geom.point_in_convex_poly((x, y), void):
                return sup.get_color_at_position(self, x, y)
            # If outside, find the nearest point on the nearest void's edge
            for p1, p2 in geom.pairwise(void):
                isect = geom.nearest_point_in_segment(p1, p2, (x, y))
                if isect is not None:
                    d = math.sqrt((isect[0] - x)**2 + (isect[1] - y)**2)
                    isects.append((d, isect))
                # Above doesn't include segment ends, so add those
                d = math.sqrt((p1[0] - x)**2 + (p1[1] - y)**2)
                isects.append((d, p1))
        # Determine the closest point.
        if isects:
            isects.sort()
            x, y = isects[0][1]
        return sup.get_color_at_position(self, x, y)
Esempio n. 3
0
    def get_color_at_position(self, x, y, ignore_mask=False):
        """Converts an `x`, `y` position to a color.

        Ordinarily, this implmentation uses any active mask to limit the
        colors which can be clicked on. Set `ignore_mask` to disable this
        added behaviour.

        """
        sup = HueSaturationWheelMixin
        if ignore_mask or not self.mask_toggle.get_active():
            return sup.get_color_at_position(self, x, y)
        voids = self.get_mask_voids()
        if not voids:
            return sup.get_color_at_position(self, x, y)
        isects = []
        for vi, void in enumerate(voids):
            # If we're inside a void, use the unchanged value
            if geom.point_in_convex_poly((x, y), void):
                return sup.get_color_at_position(self, x, y)
            # If outside, find the nearest point on the nearest void's edge
            for p1, p2 in geom.pairwise(void):
                isect = geom.nearest_point_in_segment(p1, p2, (x, y))
                if isect is not None:
                    d = math.sqrt((isect[0]-x)**2 + (isect[1]-y)**2)
                    isects.append((d, isect))
                # Above doesn't include segment ends, so add those
                d = math.sqrt((p1[0]-x)**2 + (p1[1]-y)**2)
                isects.append((d, p1))
        # Determine the closest point.
        if isects:
            isects.sort()
            x, y = isects[0][1]
        return sup.get_color_at_position(self, x, y)
Esempio n. 4
0
 def __dist_to_nearest_shape(self, x, y):
     # Distance from `x`, `y` to the nearest edge or vertex of any shape.
     dists = []
     for hull in self.get_mask_voids():
         # cx, cy = geom.poly_centroid(hull)
         for p1, p2 in geom.pairwise(hull):
             np = geom.nearest_point_in_segment(p1, p2, (x, y))
             if np is not None:
                 nx, ny = np
                 d = math.sqrt((x - nx)**2 + (y - ny)**2)
                 dists.append(d)
         # Segment end too
         d = math.sqrt((p1[0] - x)**2 + (p1[1] - y)**2)
         dists.append(d)
     if not dists:
         return None
     dists.sort()
     return dists[0]
Esempio n. 5
0
 def __dist_to_nearest_shape(self, x, y):
     # Distance from `x`, `y` to the nearest edge or vertex of any shape.
     dists = []
     for hull in self.get_mask_voids():
         # cx, cy = geom.poly_centroid(hull)
         for p1, p2 in geom.pairwise(hull):
             nearest_point = geom.nearest_point_in_segment(p1, p2, (x, y))
             if nearest_point is not None:
                 nx, ny = nearest_point
                 d = math.sqrt((x-nx)**2 + (y-ny)**2)
                 dists.append(d)
         # Segment end too
         d = math.sqrt((p1[0]-x)**2 + (p1[1]-y)**2)
         dists.append(d)
     if not dists:
         return None
     dists.sort()
     return dists[0]
Esempio n. 6
0
    def __update_active_objects(self, x, y):
        # Decides what a click or a drag at (x, y) would do, and updates the
        # mouse cursor and draw state to match.

        assert self.__drag_func is None
        self.__active_shape = None
        self.__active_ctrlpoint = None
        self.__tmp_new_ctrlpoint = None
        self.queue_draw()  # yes, always

        # Possible mask void manipulations
        mask = self.get_mask()
        for mask_idx in xrange(len(mask)):
            colors = mask[mask_idx]
            if len(colors) < 3:
                continue

            # If the pointer is near an existing control point, clicking and
            # dragging will move it.
            void = []
            for col_idx in xrange(len(colors)):
                col = colors[col_idx]
                px, py = self.get_pos_for_color(col)
                dp = math.sqrt((x - px)**2 + (y - py)**2)
                if dp <= self.__ctrlpoint_grab_radius:
                    mask.remove(colors)
                    mask.insert(0, colors)
                    self.__active_shape = colors
                    self.__active_ctrlpoint = col_idx
                    self.__set_cursor(None)
                    return
                void.append((px, py))

            # If within a certain distance of an edge, dragging will create and
            # then move a new control point.
            void = geom.convex_hull(void)
            for p1, p2 in geom.pairwise(void):
                isect = geom.nearest_point_in_segment(p1, p2, (x, y))
                if isect is not None:
                    ix, iy = isect
                    di = math.sqrt((ix - x)**2 + (iy - y)**2)
                    if di <= self.__ctrlpoint_grab_radius:
                        newcol = self.get_color_at_position(ix, iy)
                        self.__tmp_new_ctrlpoint = newcol
                        mask.remove(colors)
                        mask.insert(0, colors)
                        self.__active_shape = colors
                        self.__set_cursor(None)
                        return

            # If the mouse is within a mask void, then dragging would move that
            # shape around within the mask.
            if geom.point_in_convex_poly((x, y), void):
                mask.remove(colors)
                mask.insert(0, colors)
                self.__active_shape = colors
                self.__set_cursor(None)
                return

        # Away from shapes, clicks and drags manipulate the entire mask: adding
        # cutout voids to it, or rotating the whole mask around its central
        # axis.
        alloc = self.get_allocation()
        cx, cy = self.get_center(alloc=alloc)
        radius = self.get_radius(alloc=alloc)
        dx, dy = x - cx, y - cy
        r = math.sqrt(dx**2 + dy**2)
        if r < radius * (1.0 - self.min_shape_size):
            if len(mask) < self.__max_num_shapes:
                d = self.__dist_to_nearest_shape(x, y)
                minsize = radius * self.min_shape_size
                if d is None or d > minsize:
                    # Clicking will result in a new void
                    self.__set_cursor(self.__add_cursor)
        else:
            # Click-drag to rotate the entire mask
            self.__set_cursor(self.__rotate_cursor)
Esempio n. 7
0
    def __update_active_objects(self, x, y):
        # Decides what a click or a drag at (x, y) would do, and updates the
        # mouse cursor and draw state to match.

        assert self.__drag_func is None
        self.__active_shape = None
        self.__active_ctrlpoint = None
        self.__tmp_new_ctrlpoint = None
        self.queue_draw()  # yes, always

        # Possible mask void manipulations
        mask = self.get_mask()
        for mask_idx in xrange(len(mask)):
            colors = mask[mask_idx]
            if len(colors) < 3:
                continue

            # If the pointer is near an existing control point, clicking and
            # dragging will move it.
            void = []
            for col_idx in xrange(len(colors)):
                col = colors[col_idx]
                px, py = self.get_pos_for_color(col)
                dp = math.sqrt((x-px)**2 + (y-py)**2)
                if dp <= self.__ctrlpoint_grab_radius:
                    mask.remove(colors)
                    mask.insert(0, colors)
                    self.__active_shape = colors
                    self.__active_ctrlpoint = col_idx
                    self.__set_cursor(None)
                    return
                void.append((px, py))

            # If within a certain distance of an edge, dragging will create and
            # then move a new control point.
            void = geom.convex_hull(void)
            for p1, p2 in geom.pairwise(void):
                isect = geom.nearest_point_in_segment(p1, p2, (x, y))
                if isect is not None:
                    ix, iy = isect
                    di = math.sqrt((ix-x)**2 + (iy-y)**2)
                    if di <= self.__ctrlpoint_grab_radius:
                        newcol = self.get_color_at_position(ix, iy)
                        self.__tmp_new_ctrlpoint = newcol
                        mask.remove(colors)
                        mask.insert(0, colors)
                        self.__active_shape = colors
                        self.__set_cursor(None)
                        return

            # If the mouse is within a mask void, then dragging would move that
            # shape around within the mask.
            if geom.point_in_convex_poly((x, y), void):
                mask.remove(colors)
                mask.insert(0, colors)
                self.__active_shape = colors
                self.__set_cursor(None)
                return

        # Away from shapes, clicks and drags manipulate the entire mask: adding
        # cutout voids to it, or rotating the whole mask around its central
        # axis.
        alloc = self.get_allocation()
        cx, cy = self.get_center(alloc=alloc)
        radius = self.get_radius(alloc=alloc)
        dx, dy = x-cx, y-cy
        r = math.sqrt(dx**2 + dy**2)
        if r < radius*(1.0-self.min_shape_size):
            if len(mask) < self.__max_num_shapes:
                d = self.__dist_to_nearest_shape(x, y)
                minsize = radius * self.min_shape_size
                if d is None or d > minsize:
                    # Clicking will result in a new void
                    self.__set_cursor(self.__add_cursor)
        else:
            # Click-drag to rotate the entire mask
            self.__set_cursor(self.__rotate_cursor)
Esempio n. 8
0
    def _recalculate_coordinates(self, redraw, *args):
        """Calculates geometric data that does not need updating every time"""
        # Skip calculations when the frame is not enabled (this is important
        # because otherwise all of this would be recalculated on moving,
        # scaling and rotating the canvas.
        if not (self.doc.model.frame_enabled or redraw):
            return
        tdw = self.doc.tdw
        # Canvas rectangle - regular and offset
        self._canvas_rect = Rect.new_from_gdk_rectangle(tdw.get_allocation())
        self._canvas_rect_offset = self._canvas_rect.expanded(
            self.OUTLINE_WIDTH * 4)
        # Frame corners in model coordinates
        x, y, w, h = tuple(self.doc.model.get_frame())
        corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
        # Pixel-aligned frame corners in display space
        d_corners = [tdw.model_to_display(mx, my) for mx, my in corners]
        pxoffs = 0.5 if (self.OUTLINE_WIDTH % 2) else 0.0
        self._prev_display_corners = self._display_corners
        self._display_corners = tuple(
            (int(x) + pxoffs, int(y) + pxoffs) for x, y in d_corners)
        # Position of the button for disabling/deleting the frame
        # Placed near the center of the frame, clamped to the viewport,
        # with an offset so it does not cover visually small frames
        # (when the frame _is_ small, or when zoomed out).
        xs, ys = zip(*d_corners)
        r = gui.style.FLOATING_BUTTON_RADIUS
        tx, ty = self._canvas_rect.expanded(-2 * r).clamped_point(
            sum(xs) / 4.0,
            sum(ys) / 4.0)
        self._trash_btn_pos = tx, ty
        r += 6  # margin for drop shadows
        self._prev_disable_button_rectangle = self._disable_button_rectangle
        self._disable_button_rectangle = (tx - r, ty - r, r * 2, r * 2)
        # Corners
        self._zone_corners = []
        radius = gui.style.DRAGGABLE_POINT_HANDLE_SIZE
        canvas_limit = self._canvas_rect.expanded(radius)
        for i, (cx, cy) in enumerate(d_corners):
            if canvas_limit.contains_pixel(cx, cy):
                self._zone_corners.append((cx, cy, self._ZONE_EDGES[i]))
        # Intersecting frame lines & calculation of rectangles
        l_type = LineType.SEGMENT
        cx, cy, cw, ch = self._canvas_rect
        canvas_corners = ((cx, cy), (cx + cw, cy), (cx + cw, cy + ch),
                          (cx, cy + ch))
        intersections = [
            intersection_of_vector_and_poly(canvas_corners, p1, p2, l_type)
            for p1, p2 in pairwise(d_corners)
        ]

        self._prev_rectangles = self._new_rectangles
        self._new_rectangles = [(), (), (), ()]
        if intersections != [None, None, None, None]:
            self._new_rectangles = []
            m = radius + 6  # margin for handle drop shadows
            for intersection in intersections:
                if not intersection:
                    self._new_rectangles.append(())
                    continue
                (x0, y0), (x1, y1) = intersection
                w = abs(x1 - x0) + 2 * m
                h = abs(y1 - y0) + 2 * m
                x = min(x0, x1) - m
                y = min(y0, y1) - m
                self._new_rectangles.append(Rect(x, y, w, h))
        if redraw:
            self.redraw()
Esempio n. 9
0
class FrameOverlay(Overlay):
    """Overlay showing the frame, and edit boxes if in FrameEditMode

    This is a display-space overlay, since the edit boxes need to be drawn with
    pixel precision at a consistent weight regardless of zoom.

    Only the main TDW is supported."""

    OUTLINE_WIDTH = 1

    # Which edges belong to which corner, CW from top left
    _ZONE_EDGES = [sum(p) for p in pairwise(_SIDES)]

    def __init__(self, doc):
        """Initialize overlay"""
        Overlay.__init__(self)
        self.doc = doc
        self.app = doc.app
        self._trash_icon_pixbuf = None
        # Cached data used for painting and to minimize redraw areas
        self._canvas_rect = None
        self._canvas_rect_offset = None
        self._display_corners = []
        self._prev_display_corners = []
        self._trash_btn_pos = None
        # Stores per-edge invalidation rectangles,
        # indexed canonically: top, right, bottom, left
        self._prev_rectangles = [(), (), (), ()]
        self._new_rectangles = [(), (), (), ()]
        self._prev_disable_button_rectangle = None
        self._disable_button_rectangle = None

        # Calculate initial data - recalculate on frame changes
        # and view changes.
        self._recalculate_coordinates(True)
        self.app.doc.model.frame_updated += self._frame_updated_cb
        self.doc.tdw.transformation_updated += self._transformation_updated_cb

    def _frame_updated_cb(self, *args):
        self._recalculate_coordinates(True, *args)

    def _transformation_updated_cb(self, *args):
        # The redraw is already triggered at this point
        self._recalculate_coordinates(False, *args)

    def _recalculate_coordinates(self, redraw, *args):
        """Calculates geometric data that does not need updating every time"""
        # Skip calculations when the frame is not enabled (this is important
        # because otherwise all of this would be recalculated on moving,
        # scaling and rotating the canvas.
        if not (self.doc.model.frame_enabled or redraw):
            return
        tdw = self.doc.tdw
        # Canvas rectangle - regular and offset
        self._canvas_rect = Rect.new_from_gdk_rectangle(tdw.get_allocation())
        self._canvas_rect_offset = self._canvas_rect.expanded(
            self.OUTLINE_WIDTH * 4)
        # Frame corners in model coordinates
        x, y, w, h = tuple(self.doc.model.get_frame())
        corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
        # Pixel-aligned frame corners in display space
        d_corners = [tdw.model_to_display(mx, my) for mx, my in corners]
        pxoffs = 0.5 if (self.OUTLINE_WIDTH % 2) else 0.0
        self._prev_display_corners = self._display_corners
        self._display_corners = tuple(
            (int(x) + pxoffs, int(y) + pxoffs) for x, y in d_corners)
        # Position of the button for disabling/deleting the frame
        # Placed near the center of the frame, clamped to the viewport,
        # with an offset so it does not cover visually small frames
        # (when the frame _is_ small, or when zoomed out).
        xs, ys = zip(*d_corners)
        r = gui.style.FLOATING_BUTTON_RADIUS
        tx, ty = self._canvas_rect.expanded(-2 * r).clamped_point(
            sum(xs) / 4.0,
            sum(ys) / 4.0)
        self._trash_btn_pos = tx, ty
        r += 6  # margin for drop shadows
        self._prev_disable_button_rectangle = self._disable_button_rectangle
        self._disable_button_rectangle = (tx - r, ty - r, r * 2, r * 2)
        # Corners
        self._zone_corners = []
        radius = gui.style.DRAGGABLE_POINT_HANDLE_SIZE
        canvas_limit = self._canvas_rect.expanded(radius)
        for i, (cx, cy) in enumerate(d_corners):
            if canvas_limit.contains_pixel(cx, cy):
                self._zone_corners.append((cx, cy, self._ZONE_EDGES[i]))
        # Intersecting frame lines & calculation of rectangles
        l_type = LineType.SEGMENT
        cx, cy, cw, ch = self._canvas_rect
        canvas_corners = ((cx, cy), (cx + cw, cy), (cx + cw, cy + ch),
                          (cx, cy + ch))
        intersections = [
            intersection_of_vector_and_poly(canvas_corners, p1, p2, l_type)
            for p1, p2 in pairwise(d_corners)
        ]

        self._prev_rectangles = self._new_rectangles
        self._new_rectangles = [(), (), (), ()]
        if intersections != [None, None, None, None]:
            self._new_rectangles = []
            m = radius + 6  # margin for handle drop shadows
            for intersection in intersections:
                if not intersection:
                    self._new_rectangles.append(())
                    continue
                (x0, y0), (x1, y1) = intersection
                w = abs(x1 - x0) + 2 * m
                h = abs(y1 - y0) + 2 * m
                x = min(x0, x1) - m
                y = min(y0, y1) - m
                self._new_rectangles.append(Rect(x, y, w, h))
        if redraw:
            self.redraw()

    def redraw(self, color_change=False):
        tdw = self.doc.tdw
        if color_change:
            tdw.queue_draw()
        else:
            prev_trash = self._prev_disable_button_rectangle
            new_trash = self._disable_button_rectangle
            if prev_trash:
                tdw.queue_draw_area(*prev_trash)
            if new_trash:
                tdw.queue_draw_area(*new_trash)
            old_corners = tuple(pairwise(self._prev_display_corners))
            new_corners = tuple(pairwise(self._display_corners))
            prev = self._prev_rectangles
            new = self._new_rectangles
            for i, (r1, r2) in enumerate(zip(prev, new)):
                if r1 == r2:
                    continue  # Skip if unchanged
                if r1 and r2:
                    r1.expand_to_include_rect(r2)
                    tdw.queue_draw_area(*r1)
                elif r1 or r2:
                    r = r1 or r2
                    corners = new_corners if r == r1 else old_corners
                    if corners:
                        (cx0, cy0), (cx1, cy1) = corners[i]
                        r.expand_to_include_point(cx0, cy0)
                        r.expand_to_include_point(cx1, cy1)
                        tdw.queue_draw_area(*r)
                    else:
                        tdw.queue_draw()
                        return

    def paint(self, cr):
        """Paints the frame, and the edit boxes if appropriate"""

        if not self.doc.model.frame_enabled:
            return

        # Frame mask: outer closed rectangle just outside the viewport
        cr.rectangle(*self._canvas_rect_offset)

        # Frame mask: inner closed rectangle
        p1, p2, p3, p4 = self._display_corners
        cr.move_to(*p1)
        cr.line_to(*p2)
        cr.line_to(*p3)
        cr.line_to(*p4)
        cr.close_path()

        # Fill the frame mask. We may need the shape again.
        frame_rgba = self.app.preferences["frame.color_rgba"]
        frame_rgba = [lib.helpers.clamp(c, 0, 1) for c in frame_rgba]
        cr.set_source_rgba(*frame_rgba)
        cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
        cr.fill_preserve()

        # If the doc controller is not in a frame-editing mode, no edit
        # controls will be drawn. The frame mask drawn above normally
        # has some alpha, and may be unclear as a result. To make it
        # clearer, double-strike the edges.

        editmode = None
        for m in self.doc.modes:
            if isinstance(m, FrameEditMode):
                editmode = m
                break
        if not editmode:
            cr.set_line_width(self.OUTLINE_WIDTH)
            cr.stroke()
            return

        # Editable frame: shadows for the frame edge lines
        cr.set_line_cap(cairo.LINE_CAP_ROUND)
        zonelines = [
            (_EditZone.TOP, p1, p2),
            (_EditZone.RIGHT, p2, p3),
            (_EditZone.BOTTOM, p3, p4),
            (_EditZone.LEFT, p4, p1),
        ]
        cr.set_line_width(gui.style.DRAGGABLE_EDGE_WIDTH)
        for zone, p, q in zonelines:
            cr.move_to(*p)
            cr.line_to(*q)
        gui.drawutils.render_drop_shadow(cr, z=1)
        cr.new_path()
        for zone, p, q in zonelines:
            cr.move_to(*p)
            cr.line_to(*q)
            if editmode._zone and (editmode._zone == zone):
                rgb = gui.style.ACTIVE_ITEM_COLOR.get_rgb()
            else:
                rgb = gui.style.EDITABLE_ITEM_COLOR.get_rgb()
            cr.set_source_rgb(*rgb)
            cr.stroke()

        # Editable corners: drag handles (with hover)
        for x, y, zonemask in self._zone_corners:
            if editmode._zone and (editmode._zone == zonemask):
                col = gui.style.ACTIVE_ITEM_COLOR
            else:
                col = gui.style.EDITABLE_ITEM_COLOR
            gui.drawutils.render_round_floating_color_chip(
                cr=cr,
                x=x,
                y=y,
                color=col,
                radius=gui.style.DRAGGABLE_POINT_HANDLE_SIZE,
            )

        # Frame remove button position, frame center, constrained to viewport
        if editmode._zone == _EditZone.REMOVE_FRAME:
            button_color = gui.style.ACTIVE_ITEM_COLOR
        else:
            button_color = gui.style.EDITABLE_ITEM_COLOR
        bx, by = self._trash_btn_pos
        gui.drawutils.render_round_floating_button(
            cr=cr,
            x=bx,
            y=by,
            color=button_color,
            radius=gui.style.FLOATING_BUTTON_RADIUS,
            pixbuf=self._trash_icon(),
        )
        editmode.remove_button_pos = (bx, by)

    def _trash_icon(self):
        """Return trash icon, using cached instance if it exists"""
        if not self._trash_icon_pixbuf:
            self._trash_icon_pixbuf = gui.drawutils.load_symbolic_icon(
                icon_name="mypaint-trash-symbolic",
                size=gui.style.FLOATING_BUTTON_ICON_SIZE,
                fg=(0, 0, 0, 1),
            )
        return self._trash_icon_pixbuf