from __future__ import annotations
from typing import Callable, SupportsFloat
from gaphas.matrix import Matrix
from gaphas.solver import NORMAL, BaseConstraint, Variable
from gaphas.types import Pos, SupportsFloatPos, TypedProperty
[docs]
class Position:
"""A point constructed of two `Variable`'s.
>>> vp = Position(3, 5)
>>> vp.x, vp.y
(Variable(3, 20), Variable(5, 20))
>>> vp.pos
(Variable(3, 20), Variable(5, 20))
>>> vp[0], vp[1]
(Variable(3, 20), Variable(5, 20))
"""
def __init__(self, x, y, strength=NORMAL):
self._x = Variable(x, strength)
self._y = Variable(y, strength)
self._handlers: set[Callable[[Position, Pos], None]] = set()
self._setting_pos = 0
def add_handler(self, handler: Callable[[Position, Pos], None]) -> None:
if not self._handlers:
self._x.add_handler(self._propagate_x)
self._y.add_handler(self._propagate_y)
self._handlers.add(handler)
def remove_handler(self, handler: Callable[[Position, Pos], None]) -> None:
self._handlers.discard(handler)
if not self._handlers:
self._x.remove_handler(self._propagate_x)
self._y.remove_handler(self._propagate_y)
def notify(self, oldpos: Pos) -> None:
for handler in self._handlers:
handler(self, oldpos)
def _propagate_x(self, variable, oldval):
if not self._setting_pos:
self.notify((oldval, self._y.value))
def _propagate_y(self, variable, oldval):
if not self._setting_pos:
self.notify((self._x.value, oldval))
@property
def strength(self) -> int:
"""Strength."""
return self._x.strength
def _set_x(self, v: SupportsFloat) -> None:
self._x.value = v
x: TypedProperty[Variable, SupportsFloat]
x = property(lambda s: s._x, _set_x, doc="Position.x")
def _set_y(self, v: SupportsFloat) -> None:
self._y.value = v
y: TypedProperty[Variable, SupportsFloat]
y = property(lambda s: s._y, _set_y, doc="Position.y")
def _set_pos(self, pos: Position | SupportsFloatPos) -> None:
"""Set handle position (Item coordinates)."""
oldpos = (self._x.value, self._y.value)
self._setting_pos += 1
try:
self._x.value, self._y.value = pos
finally:
self._setting_pos -= 1
self.notify(oldpos)
pos: TypedProperty[tuple[Variable, Variable], Position | SupportsFloatPos]
pos = property(lambda s: (s._x, s._y), _set_pos, doc="The position.")
def tuple(self) -> tuple[float, float]:
return (self._x.value, self._y.value)
def __str__(self):
return f"<{self.__class__.__name__} object on ({self._x}, {self._y})>"
__repr__ = __str__
def __getitem__(self, index):
"""Shorthand for returning the x(0) or y(1) component of the point.
>>> h = Position(3, 5)
>>> h[0]
Variable(3, 20)
>>> h[1]
Variable(5, 20)
"""
return (self._x, self._y)[index]
def __iter__(self):
return iter((self._x, self._y))
def __eq__(self, other):
return isinstance(other, Position) and self.x == other.x and self.y == other.y
[docs]
class MatrixProjection(BaseConstraint):
def __init__(self, pos: Position, matrix: Matrix):
proj_pos = Position(0, 0, pos.strength)
super().__init__(proj_pos.x, proj_pos.y, pos.x, pos.y)
self._orig_pos = pos
self._proj_pos = proj_pos
self.matrix = matrix
self.solve_for(self._proj_pos.x)
def add_handler(self, handler):
"""Add a callback handler."""
if not self._handlers:
self.matrix.add_handler(self._on_matrix_changed)
super().add_handler(handler)
def remove_handler(self, handler):
"""Remove a previously assigned handler."""
super().remove_handler(handler)
if not self._handlers:
self.matrix.remove_handler(self._on_matrix_changed)
@property
def pos(self) -> Position:
"""The projected position."""
return self._proj_pos
def _set_x(self, x):
self._proj_pos.x = x
x: TypedProperty[Variable, SupportsFloat]
x = property(
lambda s: s._proj_pos.x, _set_x, doc="The projected position's ``x`` part."
)
def _set_y(self, y):
self._proj_pos.y = y
y: TypedProperty[Variable, SupportsFloat]
y = property(
lambda s: s._proj_pos.y, _set_y, doc="The projected position's ``y`` part."
)
def mark_dirty(self, var):
if var is self._orig_pos.x or var is self._orig_pos.y:
super().mark_dirty(self._orig_pos.x)
super().mark_dirty(self._orig_pos.y)
else:
super().mark_dirty(self._proj_pos.x)
super().mark_dirty(self._proj_pos.y)
def solve_for(self, var):
if var is self._orig_pos.x or var is self._orig_pos.y:
self._orig_pos.x, self._orig_pos.y = self.matrix.inverse().transform_point(
*self._proj_pos
)
else:
self._proj_pos.x, self._proj_pos.y = self.matrix.transform_point(
*self._orig_pos
)
def _on_matrix_changed(self, matrix, _orig):
self.mark_dirty(self._orig_pos.x)
self.notify()
def __getitem__(self, index):
return self._proj_pos[index]