Source code for plotpy.items.shape.rectangle

# -*- coding: utf-8 -*-

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from guidata.utils.misc import assert_interfaces_valid

from plotpy.items.shape.polygon import PolygonShape
from plotpy.mathutils.geometry import (
    compute_center,
    vector_norm,
    vector_projection,
    vector_rotation,
)

if TYPE_CHECKING:
    from plotpy.styles.shape import ShapeParam


def _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
    return (
        vector_norm(x0, y0, x1, y1)
        and vector_norm(x0, y0, x2, y2)
        and vector_norm(x0, y0, x3, y3)
        and vector_norm(x1, y1, x2, y2)
        and vector_norm(x1, y1, x3, y3)
        and vector_norm(x2, y2, x3, y3)
    )


[docs] class RectangleShape(PolygonShape): """Rectangle shape Args: x1: X coordinate of the top-left corner y1: Y coordinate of the top-left corner x2: X coordinate of the bottom-right corner y2: Y coordinate of the bottom-right corner shapeparam: Shape parameters """ CLOSED = True _icon_name = "rectangle.png" def __init__( self, x1: float = 0.0, y1: float = 0.0, x2: float = 0.0, y2: float = 0.0, shapeparam: ShapeParam | None = None, ) -> None: super().__init__(shapeparam=shapeparam) self.set_rect(x1, y1, x2, y2)
[docs] def set_rect(self, x1: float, y1: float, x2: float, y2: float) -> None: """Set the coordinates of the rectangle Args: x1: X coordinate of the top-left corner y1: Y coordinate of the top-left corner x2: X coordinate of the bottom-right corner y2: Y coordinate of the bottom-right corner """ self.set_points([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])
[docs] def get_rect(self) -> tuple[float, float, float, float]: """Return the coordinates of the rectangle Returns: Coordinates of the rectangle """ return tuple(self.points[0]) + tuple(self.points[2])
[docs] def get_center(self) -> tuple[float, float]: """Return center coordinates Returns: Center coordinates """ return compute_center(*self.get_rect())
[docs] def move_point_to( self, handle: int, pos: tuple[float, float], ctrl: bool = False ) -> None: """Move a handle as returned by hit_test to the new position Args: handle: Handle pos: Position ctrl: True if <Ctrl> button is being pressed, False otherwise """ nx, ny = pos x1, y1, x2, y2 = self.get_rect() if handle == 0: self.set_rect(nx, ny, x2, y2) elif handle == 1: self.set_rect(x1, ny, nx, y2) elif handle == 2: self.set_rect(x1, y1, nx, ny) elif handle == 3: self.set_rect(nx, y1, x2, ny) elif handle == -1: delta = (nx, ny) - self.points.mean(axis=0) self.points += delta
def __reduce__(self) -> tuple: """Return the state of the object for pickling""" state = (self.shapeparam, self.points, self.z()) return (self.__class__, (), state) def __setstate__(self, state: tuple) -> None: """Set the state of the object from pickling""" self.shapeparam, self.points, z = state self.setZ(z) self.shapeparam.update_item(self)
assert_interfaces_valid(RectangleShape)
[docs] class ObliqueRectangleShape(PolygonShape): """Oblique rectangle shape Args: x0: X coordinate of the top-left corner y0: Y coordinate of the top-left corner x1: X coordinate of the top-right corner y1: Y coordinate of the top-right corner x2: X coordinate of the bottom-right corner y2: Y coordinate of the bottom-right corner x3: X coordinate of the bottom-left corner y3: Y coordinate of the bottom-left corner shapeparam: Shape parameters """ CLOSED = True ADDITIONNAL_POINTS = 2 # Number of points which are not part of the shape LINK_ADDITIONNAL_POINTS = True # Link additionnal points with dotted lines _icon_name = "oblique_rectangle.png" def __init__( self, x0: float = 0.0, y0: float = 0.0, x1: float = 0.0, y1: float = 0.0, x2: float = 0.0, y2: float = 0.0, x3: float = 0.0, y3: float = 0.0, shapeparam: ShapeParam | None = None, ) -> None: super().__init__(shapeparam=shapeparam) self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
[docs] def set_rect( self, x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, ) -> None: """Set the coordinates of the rectangle Args: x0: X coordinate of the top-left corner y0: Y coordinate of the top-left corner x1: X coordinate of the top-right corner y1: Y coordinate of the top-right corner x2: X coordinate of the bottom-right corner y2: Y coordinate of the bottom-right corner x3: X coordinate of the bottom-left corner y3: Y coordinate of the bottom-left corner :: x: additionnal points (handles used for rotation -- other handles being used for rectangle resizing) (x0, y0)------>(x1, y1) ↑ | | | x x | | | ↓ (x3, y3)<------(x2, y2) """ self.set_points( [ (x0, y0), (x1, y1), (x2, y2), (x3, y3), (0.5 * (x0 + x3), 0.5 * (y0 + y3)), (0.5 * (x1 + x2), 0.5 * (y1 + y2)), ] )
[docs] def get_rect(self) -> tuple[float, float, float, float, float, float, float, float]: """Return the coordinates of the rectangle Returns: Coordinates of the rectangle """ return self.points.ravel()[: -self.ADDITIONNAL_POINTS * 2]
[docs] def get_center(self) -> tuple[float, float]: """Return center coordinates Returns: Center coordinates """ rect = tuple(self.points[0]) + tuple(self.points[2]) return compute_center(*rect)
[docs] def move_point_to( self, handle: int, pos: tuple[float, float], ctrl: bool = False ) -> None: """Move a handle as returned by hit_test to the new position Args: handle: Handle pos: Position ctrl: True if <Ctrl> button is being pressed, False otherwise """ nx, ny = pos x0, y0, x1, y1, x2, y2, x3, y3 = self.get_rect() if handle == 0: if vector_norm(x2, y2, x3, y3) and vector_norm(x2, y2, x1, y1): v0n = np.array((nx - x0, ny - y0)) x3, y3 = vector_projection(v0n, x2, y2, x3, y3) x1, y1 = vector_projection(v0n, x2, y2, x1, y1) x0, y0 = nx, ny if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3): self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == 1: if vector_norm(x3, y3, x0, y0) and vector_norm(x3, y3, x2, y2): v1n = np.array((nx - x1, ny - y1)) x0, y0 = vector_projection(v1n, x3, y3, x0, y0) x2, y2 = vector_projection(v1n, x3, y3, x2, y2) x1, y1 = nx, ny if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3): self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == 2: if vector_norm(x0, y0, x1, y1) and vector_norm(x0, y0, x3, y3): v2n = np.array((nx - x2, ny - y2)) x1, y1 = vector_projection(v2n, x0, y0, x1, y1) x3, y3 = vector_projection(v2n, x0, y0, x3, y3) x2, y2 = nx, ny if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3): self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == 3: if vector_norm(x1, y1, x0, y0) and vector_norm(x1, y1, x2, y2): v3n = np.array((nx - x3, ny - y3)) x0, y0 = vector_projection(v3n, x1, y1, x0, y0) x2, y2 = vector_projection(v3n, x1, y1, x2, y2) x3, y3 = nx, ny if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3): self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == 4: x4, y4 = 0.5 * (x0 + x3), 0.5 * (y0 + y3) x5, y5 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) nx, ny = x0 + nx - x4, y0 + ny - y4 # moving handle #4 to handle #0 v10 = np.array((x0 - x1, y0 - y1)) v12 = np.array((x2 - x1, y2 - y1)) v10n = np.array((nx - x1, ny - y1)) k = np.linalg.norm(v12) / np.linalg.norm(v10) v12n = vector_rotation(-np.pi / 2, *v10n) * k x2, y2 = v12n + np.array([x1, y1]) x3, y3 = v12n + v10n + np.array([x1, y1]) x0, y0 = nx, ny dx = x5 - 0.5 * (x1 + x2) dy = y5 - 0.5 * (y1 + y2) x0, y0 = x0 + dx, y0 + dy x1, y1 = x1 + dx, y1 + dy x2, y2 = x2 + dx, y2 + dy x3, y3 = x3 + dx, y3 + dy self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == 5: x4, y4 = 0.5 * (x0 + x3), 0.5 * (y0 + y3) x5, y5 = 0.5 * (x1 + x2), 0.5 * (y1 + y2) nx, ny = x1 + nx - x5, y1 + ny - y5 # moving handle #5 to handle #1 v01 = np.array((x1 - x0, y1 - y0)) v03 = np.array((x3 - x0, y3 - y0)) v01n = np.array((nx - x0, ny - y0)) k = np.linalg.norm(v03) / np.linalg.norm(v01) v03n = vector_rotation(np.pi / 2, *v01n) * k x3, y3 = v03n + np.array([x0, y0]) x2, y2 = v03n + v01n + np.array([x0, y0]) x1, y1 = nx, ny dx = x4 - 0.5 * (x0 + x3) dy = y4 - 0.5 * (y0 + y3) x0, y0 = x0 + dx, y0 + dy x1, y1 = x1 + dx, y1 + dy x2, y2 = x2 + dx, y2 + dy x3, y3 = x3 + dx, y3 + dy self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) elif handle == -1: delta = (nx, ny) - self.points.mean(axis=0) self.points += delta
def __reduce__(self) -> tuple: """Return the state of the object for pickling""" state = (self.shapeparam, self.points, self.z()) return (self.__class__, (), state) def __setstate__(self, state: tuple) -> None: """Set the state of the object from pickling""" self.shapeparam, self.points, z = state self.setZ(z) self.shapeparam.update_item(self)
assert_interfaces_valid(ObliqueRectangleShape)