# -*- 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
if TYPE_CHECKING:
from plotpy.styles.shape import ShapeParam
[docs]
class SegmentShape(PolygonShape):
"""Segment shape
Args:
x1: X coordinate of the first point
y1: Y coordinate of the first point
x2: X coordinate of the second point
y2: Y coordinate of the second point
shapeparam: Shape parameters
"""
CLOSED = False
ADDITIONNAL_POINTS = 1 # Number of points which are not part of the shape
_icon_name = "segment.png"
def __init__(
self,
x1: float = 0.0,
y1: float = 0.0,
x2: float = 0.0,
y2: float = 0.0,
shapeparam: ShapeParam = 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 segment coordinates
Args:
x1: X coordinate of the first point
y1: Y coordinate of the first point
x2: X coordinate of the second point
y2: Y coordinate of the second point
"""
self.set_points([(x1, y1), (x2, y2), (0.5 * (x1 + x2), 0.5 * (y1 + y2))])
[docs]
def get_rect(self) -> tuple[float, float, float, float]:
"""Return the segment coordinates
Returns:
Segment coordinates as a tuple (x1, y1, x2, y2)
"""
return tuple(self.points[0]) + tuple(self.points[1])
[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 ctrl:
# compute linear coefficient y = a * x + b
# When ctrl is pressed, the point is moved on the line
# defined by the segment, and the line is conserved
# (the segment is not rotated)
if x2 == x1:
yA = ny
xA = x1
elif y2 == y1:
yA = y1
xA = nx
else:
a = (y2 - y1) / (x2 - x1)
b = y1 - a * x1
xA = a / (a * a + 1) * (nx / a + ny - b)
yA = a * xA + b
if handle == 0:
self.set_rect(xA, yA, x2, y2)
elif handle == 1:
self.set_rect(x1, y1, xA, yA)
elif handle in (2, -1):
# selection du point milieu, on déplace le segment
# c'est identique au comportmeent sans la touche Ctrl
delta = (nx, ny) - self.points.mean(axis=0)
self.points += delta
else:
# Ctrl is not pressed, the segment may be rotated freely
if handle == 0:
self.set_rect(nx, ny, x2, y2)
elif handle == 1:
self.set_rect(x1, y1, nx, ny)
elif handle in (2, -1):
delta = (nx, ny) - self.points.mean(axis=0)
self.points += delta
def __reduce__(self) -> tuple:
"""Reduce object to picklable state"""
state = (self.shapeparam, self.points, self.z())
return (self.__class__, (), state)
def __setstate__(self, state: tuple) -> None:
"""Set object state from pickled state"""
param, points, z = state
# ----------------------------------------------------------------------
# compatibility with previous version of SegmentShape:
x1, y1, x2, y2, x3, y3 = points.ravel()
v12 = np.array((x2 - x1, y2 - y1))
v13 = np.array((x3 - x1, y3 - y1))
if np.linalg.norm(v12) < np.linalg.norm(v13):
# old pickle format
points = np.flipud(np.roll(points, -1, axis=0))
# ----------------------------------------------------------------------
self.points = points
self.setZ(z)
self.shapeparam = param
self.shapeparam.update_item(self)
assert_interfaces_valid(SegmentShape)