From a98325963623606d51be5e44a394d70e42d98c1e Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 22 May 2020 22:20:33 +0200 Subject: [PATCH] Move projections into their own module --- docs/solver.rst | 2 +- gaphas/canvas.py | 113 +--------------------------- gaphas/projections.py | 167 ++++++++++++++++++++++++++++++++++++++++++ gaphas/solver.py | 59 +-------------- 4 files changed, 171 insertions(+), 170 deletions(-) create mode 100644 gaphas/projections.py diff --git a/docs/solver.rst b/docs/solver.rst index 0b1af63..a536c23 100644 --- a/docs/solver.rst +++ b/docs/solver.rst @@ -27,7 +27,7 @@ Projections There's one special thing about Variables: since each item has it's own coordinate system ((0, 0) point, handy when the item is rendered), it's pretty hard to make sure (for example) a line can connect to a box and ''stays'' connected, even when the box is dragged around. How can such a constraint be maintained? This is where Projections come into play. A Projection can be used to project a variable on another space (read: coordinate system). -The default projection (to canvas coordinates) is located in `gaphas.canvas` and is known as `CanvasProjection`. +The default projection (to canvas coordinates) is located in `gaphas.projections` and is known as `CanvasProjection`. When a constraint contains projections, it is most likely that this constraint connects two items together. At least the constraint is not entirely bound to the item's coordinate space. This knowledge is used when an item is moved. A move operation typically only requires a change in coordinates, relative to the item's parent item (this is why having a (0,0) point per item is so handy). This means that constraints local to the item not not need to be resolved. Constraints with links outside the item's space should be solved though. Projections play an important role in determining which constraints should be resolved. diff --git a/gaphas/canvas.py b/gaphas/canvas.py index 9ab4efd..4617851 100644 --- a/gaphas/canvas.py +++ b/gaphas/canvas.py @@ -35,6 +35,7 @@ import cairo from gaphas import solver, table, tree from gaphas.decorators import AsyncIO, nonrecursive from gaphas.state import observed, reversible_method, reversible_pair +from gaphas.projections import CanvasProjection # # Information about two connected items @@ -896,118 +897,6 @@ class Canvas: raise AttributeError("There should be at least one point specified") -class VariableProjection(solver.Projection): - """ - Project a single `solver.Variable` to another space/coordinate system. - - The value has been set in the "other" coordinate system. A - callback is executed when the value changes. - - It's a simple Variable-like class, following the Projection protocol: - - >>> def notify_me(val): - ... print('new value', val) - >>> p = VariableProjection('var placeholder', 3.0, callback=notify_me) - >>> p.value - 3.0 - >>> p.value = 6.5 - new value 6.5 - """ - - def __init__(self, var, value, callback): - self._var = var - self._value = value - self._callback = callback - - def _set_value(self, value): - self._value = value - self._callback(value) - - value = property(lambda s: s._value, _set_value) - - def variable(self): - return self._var - - -class CanvasProjection: - """ - Project a point as Canvas coordinates. Although this is a - projection, it behaves like a tuple with two Variables - (Projections). - - >>> canvas = Canvas() - >>> from gaphas.item import Element - >>> a = Element() - >>> canvas.add(a) - >>> a.matrix.translate(30, 2) - >>> canvas.request_matrix_update(a) - >>> canvas.update_now() - >>> canvas.get_matrix_i2c(a) - cairo.Matrix(1, 0, 0, 1, 30, 2) - >>> p = CanvasProjection(a.handles()[2].pos, a) - >>> a.handles()[2].pos - - >>> p[0].value - 40.0 - >>> p[1].value - 12.0 - >>> p[0].value = 63 - >>> p._point - - - When the variables are retrieved, new values are calculated. - """ - - def __init__(self, point, item): - self._point = point - self._item = item - - def _on_change_x(self, value): - item = self._item - self._px = value - self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i( - item - ).transform_point(value, self._py) - item.canvas.request_update(item, matrix=False) - - def _on_change_y(self, value): - item = self._item - self._py = value - self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i( - item - ).transform_point(self._px, value) - item.canvas.request_update(item, matrix=False) - - def _get_value(self): - """ - Return two delegating variables. Each variable should contain - a value attribute with the real value. - """ - item = self._item - x, y = self._point.x, self._point.y - self._px, self._py = item.canvas.get_matrix_i2c(item).transform_point(x, y) - return self._px, self._py - - pos = property( - lambda self: list( - map( - VariableProjection, - self._point, - self._get_value(), - (self._on_change_x, self._on_change_y), - ) - ) - ) - - def __getitem__(self, key): - # Note: we can not use bound methods as callbacks, since that will - # cause pickle to fail. - return self.pos[key] - - def __iter__(self): - return iter(self.pos) - - # Additional tests in @observed methods __test__ = { "Canvas.add": Canvas.add, diff --git a/gaphas/projections.py b/gaphas/projections.py new file mode 100644 index 0000000..3ddbb3d --- /dev/null +++ b/gaphas/projections.py @@ -0,0 +1,167 @@ +class Projection: + """ + Projections are used to convert values from one space to another, + e.g. from Canvas to Item space or visa versa. + + In order to be a Projection the ``value`` and ``strength`` + properties should be implemented and a method named ``variable()`` + should be present. + + Projections should inherit from this class. + + Projections may be nested. + + This default implementation projects a variable to it's own: + + >>> v = Variable(4.0) + >>> v + Variable(4, 20) + >>> p = Projection(v) + >>> p.value + 4.0 + >>> p.value = -1 + >>> p.value + -1.0 + >>> v.value + -1.0 + >>> p.strength + 20 + >>> p.variable() + Variable(-1, 20) + """ + + def __init__(self, var): + self._var = var + + def _set_value(self, value): + self._var.value = value + + value = property(lambda s: s._var.value, _set_value) + + strength = property(lambda s: s._var.strength) + + def variable(self): + """ + Return the variable owned by the projection. + """ + return self._var + + def __float__(self): + return float(self.variable()._value) + + def __str__(self): + return f"{self.__class__.__name__}({self.variable()})" + + __repr__ = __str__ + + +class VariableProjection(Projection): + """ + Project a single `solver.Variable` to another space/coordinate system. + + The value has been set in the "other" coordinate system. A + callback is executed when the value changes. + + It's a simple Variable-like class, following the Projection protocol: + + >>> def notify_me(val): + ... print('new value', val) + >>> p = VariableProjection('var placeholder', 3.0, callback=notify_me) + >>> p.value + 3.0 + >>> p.value = 6.5 + new value 6.5 + """ + + def __init__(self, var, value, callback): + self._var = var + self._value = value + self._callback = callback + + def _set_value(self, value): + self._value = value + self._callback(value) + + value = property(lambda s: s._value, _set_value) + + def variable(self): + return self._var + + +class CanvasProjection: + """ + Project a point as Canvas coordinates. Although this is a + projection, it behaves like a tuple with two Variables + (Projections). + + >>> canvas = Canvas() + >>> from gaphas.item import Element + >>> a = Element() + >>> canvas.add(a) + >>> a.matrix.translate(30, 2) + >>> canvas.request_matrix_update(a) + >>> canvas.update_now() + >>> canvas.get_matrix_i2c(a) + cairo.Matrix(1, 0, 0, 1, 30, 2) + >>> p = CanvasProjection(a.handles()[2].pos, a) + >>> a.handles()[2].pos + + >>> p[0].value + 40.0 + >>> p[1].value + 12.0 + >>> p[0].value = 63 + >>> p._point + + + When the variables are retrieved, new values are calculated. + """ + + def __init__(self, point, item): + self._point = point + self._item = item + + def _on_change_x(self, value): + item = self._item + self._px = value + self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i( + item + ).transform_point(value, self._py) + item.canvas.request_update(item, matrix=False) + + def _on_change_y(self, value): + item = self._item + self._py = value + self._point.x.value, self._point.y.value = item.canvas.get_matrix_c2i( + item + ).transform_point(self._px, value) + item.canvas.request_update(item, matrix=False) + + def _get_value(self): + """ + Return two delegating variables. Each variable should contain + a value attribute with the real value. + """ + item = self._item + x, y = self._point.x, self._point.y + self._px, self._py = item.canvas.get_matrix_i2c(item).transform_point(x, y) + return self._px, self._py + + pos = property( + lambda self: list( + map( + VariableProjection, + self._point, + self._get_value(), + (self._on_change_x, self._on_change_y), + ) + ) + ) + + def __getitem__(self, key): + # Note: we can not use bound methods as callbacks, since that will + # cause pickle to fail. + return self.pos[key] + + def __iter__(self): + return iter(self.pos) diff --git a/gaphas/solver.py b/gaphas/solver.py index 6df7842..2e4e8b9 100644 --- a/gaphas/solver.py +++ b/gaphas/solver.py @@ -34,6 +34,8 @@ every constraint is being asked to solve itself variables to make the constraint valid again. """ from gaphas.state import observed, reversible_pair, reversible_property +from gaphas.projections import Projection + # epsilon for float comparison # is simple abs(x - y) > EPSILON enough for canvas needs? @@ -309,63 +311,6 @@ class Variable: return self._value.__rtruediv__(other) -class Projection: - """ - Projections are used to convert values from one space to another, - e.g. from Canvas to Item space or visa versa. - - In order to be a Projection the ``value`` and ``strength`` - properties should be implemented and a method named ``variable()`` - should be present. - - Projections should inherit from this class. - - Projections may be nested. - - This default implementation projects a variable to it's own: - - >>> v = Variable(4.0) - >>> v - Variable(4, 20) - >>> p = Projection(v) - >>> p.value - 4.0 - >>> p.value = -1 - >>> p.value - -1.0 - >>> v.value - -1.0 - >>> p.strength - 20 - >>> p.variable() - Variable(-1, 20) - """ - - def __init__(self, var): - self._var = var - - def _set_value(self, value): - self._var.value = value - - value = property(lambda s: s._var.value, _set_value) - - strength = property(lambda s: s._var.strength) - - def variable(self): - """ - Return the variable owned by the projection. - """ - return self._var - - def __float__(self): - return float(self.variable()._value) - - def __str__(self): - return f"{self.__class__.__name__}({self.variable()})" - - __repr__ = __str__ - - class Solver: """ Solve constraints. A constraint should have accompanying