def test_adding_pair(self): """Test adding reversible pair """ reversible_pair(SList.add, SList.remove, \ bind1={'before': lambda self, node: self.l[self.l.index(node)+1] }) self.assertTrue(SList.add.im_func in _reverse) self.assertTrue(SList.remove.im_func in _reverse)
def test_adding_pair(self): """Test adding reversible pair """ reversible_pair(SList.add, SList.remove, \ bind1={'before': lambda self, node: self.l[self.l.index(node)+1] }) self.assertTrue(SList.add in _reverse) self.assertTrue(SList.remove in _reverse)
def test_adding_pair(): """Test adding reversible pair. """ reversible_pair( SList.add, SList.remove, bind1={"before": lambda self, node: self.list[self.list.index(node) + 1]}, ) if sys.version_info.major >= 3: # Modern Python assert SList.add in _reverse assert SList.remove in _reverse else: # Legacy Python assert SList.add.__func__ in _reverse assert SList.remove.__func__ in _reverse
class CanvasItemConnector(Connector.default): """ Decorator for the Connector Aspect, does the application-level connection too """ def connect_port(self, sink): self.sink = sink canvas = self.item.canvas handle = self.handle item = self.item cinfo = item.canvas.get_connection(handle) if cinfo: self.disconnect() self.connect_handle(sink, callback=self.disconnect_port) self.post_connect() '''port1 = self.sink.port # to display ports of same type when connecting.... print port1.portinstance.type items = canvas.get_all_items() for i in items: if(i is not self.item): for p in i._ports: if(checkportscanconnect(port1.portinstance,p.portinstance)): print p.portinstance.type''' #print 'ASCEND_CONNECTION_DONE' def disconnect_port(self): self.post_disconnect() #@observed def post_connect(self): #print '~~Connected' handle = self.handle port = self.sink.port line = self.item assert (line.lineinstance is not None) assert (port.portinstance is not None) if handle is line._handles[0]: line.lineinstance.fromport = port.portinstance elif handle is line._handles[-1]: line.lineinstance.toport = port.portinstance else: raise RuntimeError("Invalid handle, neither start nor end") #@observed def post_disconnect(self): #print '~~Disconnected' handle = self.handle port = self.sink.port line = self.item assert (line.lineinstance is not None) if handle is line._handles[0]: line.lineinstance.fromport = None elif handle is line._handles[-1]: line.lineinstance.toport = None reversible_pair(post_connect, post_disconnect, bind1={}, bind2={})
class Canvas: """ Container class for items. """ def __init__(self): self._tree = tree.Tree() self._solver = solver.Solver() self._connections = table.Table(Connection, list(range(4))) self._dirty_items = set() self._dirty_matrix_items = set() self._dirty_index = False self._registered_views = set() solver = property(lambda s: s._solver) @observed def add(self, item, parent=None, index=None): """ Add an item to the canvas. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> len(c._tree.nodes) 1 >>> i._canvas is c True """ assert item not in self._tree.nodes, f"Adding already added node {item}" self._tree.add(item, parent, index) self._dirty_index = True self.update_matrix(item, parent) item._set_canvas(self) self.request_update(item) @observed def _remove(self, item): """ Remove is done in a separate, @observed, method so the undo system can restore removed items in the right order. """ item._set_canvas(None) self._tree.remove(item) self._update_views(removed_items=(item, )) self._dirty_items.discard(item) self._dirty_matrix_items.discard(item) def remove(self, item): """ Remove item from the canvas. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.remove(i) >>> c._tree.nodes [] >>> i._canvas """ for child in reversed(self.get_children(item)): self.remove(child) self.remove_connections_to_item(item) self._remove(item) reversible_pair( add, _remove, bind1={ "parent": lambda self, item: self.get_parent(item), "index": lambda self, item: self._tree.get_siblings(item).index(item), }, ) @observed def reparent(self, item, parent, index=None): """ Set new parent for an item. """ self._tree.reparent(item, parent, index) self._dirty_index = True reversible_method( reparent, reverse=reparent, bind={ "parent": lambda self, item: self.get_parent(item), "index": lambda self, item: self._tree.get_siblings(item).index(item), }, ) def get_all_items(self): """ Get a list of all items. >>> c = Canvas() >>> c.get_all_items() [] >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.get_all_items() # doctest: +ELLIPSIS [<gaphas.item.Item ...>] """ return self._tree.nodes def get_root_items(self): """ Return the root items of the canvas. >>> c = Canvas() >>> c.get_all_items() [] >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> c.get_root_items() # doctest: +ELLIPSIS [<gaphas.item.Item ...>] """ return self._tree.get_children(None) def get_parent(self, item): """ See `tree.Tree.get_parent()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> c.get_parent(i) >>> c.get_parent(ii) # doctest: +ELLIPSIS <gaphas.item.Item ...> """ return self._tree.get_parent(item) def get_ancestors(self, item): """ See `tree.Tree.get_ancestors()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_ancestors(i)) [] >>> list(c.get_ancestors(ii)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>] >>> list(c.get_ancestors(iii)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>, <gaphas.item.Item ...>] """ return self._tree.get_ancestors(item) def get_children(self, item): """ See `tree.Tree.get_children()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_children(iii)) [] >>> list(c.get_children(ii)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>] >>> list(c.get_children(i)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>] """ return self._tree.get_children(item) def get_all_children(self, item): """ See `tree.Tree.get_all_children()`. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> ii = item.Item() >>> c.add(ii, i) >>> iii = item.Item() >>> c.add(iii, ii) >>> list(c.get_all_children(iii)) [] >>> list(c.get_all_children(ii)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>] >>> list(c.get_all_children(i)) # doctest: +ELLIPSIS [<gaphas.item.Item ...>, <gaphas.item.Item ...>] """ return self._tree.get_all_children(item) @observed def connect_item(self, item, handle, connected, port, constraint=None, callback=None): """ Create a connection between two items. The connection is registered and the constraint is added to the constraint solver. The pair (item, handle) should be unique and not yet connected. The callback is invoked when the connection is broken. :Parameters: item Connecting item (i.e. a line). handle Handle of connecting item. connected Connected item (i.e. a box). port Port of connected item. constraint Constraint to keep the connection in place. callback Function to be called on disconnection. ConnectionError is raised in case handle is already registered on a connection. """ if self.get_connection(handle): raise ConnectionError( f"Handle {handle} of item {item} is already connected") self._connections.insert(item, handle, connected, port, constraint, callback) if constraint: self._solver.add_constraint(constraint) def disconnect_item(self, item, handle=None): """ Disconnect the connections of an item. If handle is not None, only the connection for that handle is disconnected. """ # disconnect on canvas level for cinfo in list(self._connections.query(item=item, handle=handle)): self._disconnect_item(*cinfo) @observed def _disconnect_item(self, item, handle, connected, port, constraint, callback): """ Perform the real disconnect. """ # Same arguments as connect_item, makes reverser easy if constraint: self._solver.remove_constraint(constraint) if callback: callback() self._connections.delete(item, handle, connected, port, constraint, callback) reversible_pair(connect_item, _disconnect_item) def remove_connections_to_item(self, item): """ Remove all connections (handles connected to and constraints) for a specific item (to and from the item). This is some brute force cleanup (e.g. if constraints are referenced by items, those references are not cleaned up). """ disconnect_item = self._disconnect_item # remove connections from this item for cinfo in list(self._connections.query(item=item)): disconnect_item(*cinfo) # remove constraints to this item for cinfo in list(self._connections.query(connected=item)): disconnect_item(*cinfo) @observed def reconnect_item(self, item, handle, port=None, constraint=None): """ Update an existing connection. This is used to provide a new constraint to the connection. ``item`` and ``handle`` are the keys to the to-be-updated connection. >>> c = Canvas() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii, i) >>> iii = item.Line() >>> c.add(iii, ii) We need a few constraints, because that's what we're updating: >>> from gaphas.constraint import EqualsConstraint >>> cons1 = EqualsConstraint(i.handles()[0].pos.x, i.handles()[0].pos.x) >>> cons2 = EqualsConstraint(i.handles()[0].pos.y, i.handles()[0].pos.y) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], cons1) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=<gaphas.item.Line object at 0x...) >>> c.get_connection(i.handles()[0]).constraint is cons1 True >>> cons1 in c.solver.constraints True >>> c.reconnect_item(i, i.handles()[0], cons2) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=<gaphas.item.Line object at 0x...) >>> c.get_connection(i.handles()[0]).constraint is cons2 True >>> cons1 in c.solver.constraints False >>> cons2 in c.solver.constraints True An exception is raised if no connection exists: >>> c.reconnect_item(ii, ii.handles()[0], cons2) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: No data available for item ... """ # checks: cinfo = self.get_connection(handle) if not cinfo: raise ValueError( f'No data available for item "{item}" and handle "{handle}"') if cinfo.constraint: self._solver.remove_constraint(cinfo.constraint) self._connections.delete(item=cinfo.item, handle=cinfo.handle) self._connections.insert( item, handle, cinfo.connected, port or cinfo.port, constraint, cinfo.callback, ) if constraint: self._solver.add_constraint(constraint) reversible_method( reconnect_item, reverse=reconnect_item, bind={ "port": lambda self, item, handle: self.get_connection(handle).port, "constraint": lambda self, item, handle: self.get_connection(handle).constraint, }, ) def get_connection(self, handle): """ Get connection information for specified handle. >>> c = Canvas() >>> from gaphas.item import Line >>> line = Line() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0]) >>> c.get_connection(i.handles()[0]) # doctest: +ELLIPSIS Connection(item=<gaphas.item.Line object at 0x...) >>> c.get_connection(i.handles()[1]) # doctest: +ELLIPSIS >>> c.get_connection(ii.handles()[0]) # doctest: +ELLIPSIS """ try: return next(self._connections.query(handle=handle)) except StopIteration as ex: return None def get_connections(self, item=None, handle=None, connected=None, port=None): """ Return an iterator of connection information. The list contains (item, handle). As a result an item may be in the list more than once (depending on the number of handles that are connected). If ``item`` is connected to itself it will also appear in the list. >>> c = Canvas() >>> from gaphas import item >>> i = item.Line() >>> c.add(i) >>> ii = item.Line() >>> c.add(ii) >>> iii = item.Line() >>> c.add (iii) >>> c.connect_item(i, i.handles()[0], ii, ii.ports()[0], None) >>> list(c.get_connections(item=i)) # doctest: +ELLIPSIS [Connection(item=<gaphas.item.Line object at 0x...] >>> list(c.get_connections(connected=i)) [] >>> list(c.get_connections(connected=ii)) # doctest: +ELLIPSIS [Connection(item=<gaphas.item.Line object at 0x...] >>> c.connect_item(ii, ii.handles()[0], iii, iii.ports()[0], None) >>> list(c.get_connections(item=ii)) # doctest: +ELLIPSIS [Connection(item=<gaphas.item.Line object at 0x...] >>> list(c.get_connections(connected=iii)) # doctest: +ELLIPSIS [Connection(item=<gaphas.item.Line object at 0x...] """ return self._connections.query(item=item, handle=handle, connected=connected, port=port) def sort(self, items, reverse=False): """ Sort a list of items in the order in which they are traversed in the canvas (Depth first). >>> c = Canvas() >>> from gaphas import item >>> i1 = item.Line() >>> c.add(i1) >>> i2 = item.Line() >>> c.add(i2) >>> i3 = item.Line() >>> c.add (i3) >>> c.update() # ensure items are indexed >>> i1._canvas_index 0 >>> s = c.sort([i2, i3, i1]) >>> s[0] is i1 and s[1] is i2 and s[2] is i3 True """ return sorted(items, key=attrgetter("_canvas_index"), reverse=reverse) def get_matrix_i2c(self, item, calculate=False): """ Get the Item to Canvas matrix for ``item``. item: The item who's item-to-canvas transformation matrix should be found calculate: True will allow this function to actually calculate it, instead of raising an `AttributeError` when no matrix is present yet. Note that out-of-date matrices are not recalculated. """ if item._matrix_i2c is None or calculate: self.update_matrix(item) return item._matrix_i2c def get_matrix_c2i(self, item, calculate=False): """ Get the Canvas to Item matrix for ``item``. See `get_matrix_i2c()`. """ if item._matrix_c2i is None or calculate: self.update_matrix(item) return item._matrix_c2i def get_matrix_i2i(self, from_item, to_item, calculate=False): i2c = self.get_matrix_i2c(from_item, calculate) c2i = self.get_matrix_c2i(to_item, calculate) try: return i2c.multiply(c2i) except AttributeError: # Fall back to old behaviour return i2c * c2i @observed def request_update(self, item, update=True, matrix=True): """ Set an update request for the item. >>> c = Canvas() >>> from gaphas import item >>> i = item.Item() >>> ii = item.Item() >>> c.add(i) >>> c.add(ii, i) >>> len(c._dirty_items) 0 >>> c.update_now() >>> len(c._dirty_items) 0 """ if update: self._dirty_items.add(item) if matrix: self._dirty_matrix_items.add(item) self.update() reversible_method(request_update, reverse=request_update) def request_matrix_update(self, item): """ Schedule only the matrix to be updated. """ self.request_update(item, update=False, matrix=True) def require_update(self): """ Returns ``True`` or ``False`` depending on if an update is needed. >>> c=Canvas() >>> c.require_update() False >>> from gaphas import item >>> i = item.Item() >>> c.add(i) >>> c.require_update() False Since we're not in a GTK+ mainloop, the update is not scheduled asynchronous. Therefore ``require_update()`` returns ``False``. """ return bool(self._dirty_items) @AsyncIO(single=True) def update(self): """ Update the canvas, if called from within a gtk-mainloop, the update job is scheduled as idle job. """ self.update_now() def _pre_update_items(self, items, context): for item in items: item.pre_update(context) def _post_update_items(self, items, context): for item in items: item.post_update(context) @nonrecursive def update_now(self): """ Perform an update of the items that requested an update. """ sort = self.sort if self._dirty_index: self.update_index() self._dirty_index = False def dirty_items_with_ancestors(): for item in self._dirty_items: yield item yield from self._tree.get_ancestors(item) dirty_items = sort(dirty_items_with_ancestors(), reverse=True) try: context = Context(cairo=instant_cairo_context()) # allow programmers to perform tricks and hacks before item # full update (only called for items that requested a full update) self._pre_update_items(dirty_items, context) # recalculate matrices dirty_matrix_items = self.update_matrices(self._dirty_matrix_items) self._dirty_matrix_items.clear() self.update_constraints(dirty_matrix_items) # no matrix can change during constraint solving assert ( not self._dirty_matrix_items ), f"No matrices may have been marked dirty ({self._dirty_matrix_items})" # item's can be marked dirty due to external constraints solving if len(dirty_items) != len(self._dirty_items): dirty_items = sort(self._dirty_items, reverse=True) # normalize items, which changed after constraint solving; # recalculate matrices of normalized items dirty_matrix_items.update(self._normalize(dirty_items)) # ensure constraints are still true after normalization self._solver.solve() # item's can be marked dirty due to normalization and solving if len(dirty_items) != len(self._dirty_items): dirty_items = sort(self._dirty_items, reverse=True) self._dirty_items.clear() self._post_update_items(dirty_items, context) except Exception as e: logging.error("Error while updating canvas", exc_info=e) assert ( len(self._dirty_items) == 0 and len(self._dirty_matrix_items) == 0 ), f"dirty: {self._dirty_items}; matrix: {self._dirty_matrix_items}" self._update_views(dirty_items, dirty_matrix_items) def update_matrices(self, items): """ Recalculate matrices of the items. Items' children matrices are recalculated, too. Return items, which matrices were recalculated. """ changed = set() for item in items: parent = self._tree.get_parent(item) if parent is not None and parent in items: # item's matrix will be updated thanks to parent's matrix # update continue self.update_matrix(item, parent) changed.add(item) changed_children = self.update_matrices( set(self.get_children(item))) changed.update(changed_children) return changed def update_matrix(self, item, parent=None): """ Update matrices of an item. """ try: orig_matrix_i2c = cairo.Matrix(*item._matrix_i2c) except: orig_matrix_i2c = None item._matrix_i2c = cairo.Matrix(*item.matrix) if parent is not None: try: item._matrix_i2c = item._matrix_i2c.multiply( parent._matrix_i2c) except AttributeError: # Fall back to old behaviour item._matrix_i2c *= parent._matrix_i2c if orig_matrix_i2c is None or orig_matrix_i2c != item._matrix_i2c: # calculate c2i matrix and view matrices item._matrix_c2i = cairo.Matrix(*item._matrix_i2c) item._matrix_c2i.invert() def update_constraints(self, items): """ Update constraints. Also variables may be marked as dirty before the constraint solver kicks in. """ # request solving of external constraints associated with dirty items request_resolve = self._solver.request_resolve for item in items: for p in item._canvas_projections: request_resolve(p[0], projections_only=True) request_resolve(p[1], projections_only=True) # solve all constraints self._solver.solve() def _normalize(self, items): """ Update handle positions of items, so the first handle is always located at (0, 0). Return those items, which matrices changed due to first handle movement. For example having an item >>> from gaphas.item import Element >>> c = Canvas() >>> e = Element() >>> c.add(e) >>> e.min_width = e.min_height = 0 >>> c.update_now() >>> e.handles() [<Handle object on (0, 0)>, <Handle object on (10, 0)>, <Handle object on (10, 10)>, <Handle object on (0, 10)>] and moving its first handle a bit >>> e.handles()[0].pos.x += 1 >>> list(map(float, e.handles()[0].pos)) [1.0, 0.0] After normalization >>> c._normalize([e]) # doctest: +ELLIPSIS {<gaphas.item.Element object at 0x...>} >>> e.handles() [<Handle object on (0, 0)>, <Handle object on (9, 0)>, <Handle object on (9, 10)>, <Handle object on (0, 10)>] """ dirty_matrix_items = set() for item in items: if item.normalize(): dirty_matrix_items.add(item) return self.update_matrices(dirty_matrix_items) def update_index(self): """ Provide each item in the canvas with an index attribute. This makes for fast searching of items. """ self._tree.index_nodes("_canvas_index") def register_view(self, view): """ Register a view on this canvas. This method is called when setting a canvas on a view and should not be called directly from user code. """ self._registered_views.add(view) def unregister_view(self, view): """ Unregister a view on this canvas. This method is called when setting a canvas on a view and should not be called directly from user code. """ self._registered_views.discard(view) def _update_views(self, dirty_items=(), dirty_matrix_items=(), removed_items=()): """ Send an update notification to all registered views. """ for v in self._registered_views: v.request_update(dirty_items, dirty_matrix_items, removed_items) def __getstate__(self): """ Persist canvas. Dirty item sets and views are not saved. """ d = dict(self.__dict__) for n in ( "_dirty_items", "_dirty_matrix_items", "_dirty_index", "_registered_views", ): try: del d[n] except KeyError: pass return d def __setstate__(self, state): """ Load persisted state. Before loading the state, the constructor is called. """ self.__dict__.update(state) self._dirty_items = set(self._tree.nodes) self._dirty_matrix_items = set(self._tree.nodes) self._dirty_index = True self._registered_views = set() # self.update() def project(self, item, *points): """ Project item's points into canvas coordinate system. If there is only one point returned than projected point is returned. If there are more than one points, then tuple of projected points is returned. """ def reg(cp): item._canvas_projections.add(cp) return cp if len(points) == 1: return reg(CanvasProjection(points[0], item)) elif len(points) > 1: return tuple(reg(CanvasProjection(p, item)) for p in points) else: raise AttributeError( "There should be at least one point specified")
class Line(Item): """ A Line item. Properties: - fuzziness (0.0..n): an extra margin that should be taken into account when calculating the distance from the line (using point()). - orthogonal (bool): whether or not the line should be orthogonal (only straight angles) - horizontal: first line segment is horizontal - line_width: width of the line to be drawn This line also supports arrow heads on both the begin and end of the line. These are drawn with the methods draw_head(context) and draw_tail(context). The coordinate system is altered so the methods do not have to know about the angle of the line segment (e.g. drawing a line from (10, 10) via (0, 0) to (10, -10) will draw an arrow point). """ def __init__(self): super(Line, self).__init__() self._handles = [ Handle(connectable=True), Handle((10, 10), connectable=True) ] self._ports = [] self._update_ports() self._line_width = 2 self._fuzziness = 0 self._orthogonal_constraints = [] self._horizontal = False self._head_angle = self._tail_angle = 0 @observed def _set_line_width(self, line_width): self._line_width = line_width line_width = reversible_property(lambda s: s._line_width, _set_line_width) @observed def _set_fuzziness(self, fuzziness): self._fuzziness = fuzziness fuzziness = reversible_property(lambda s: s._fuzziness, _set_fuzziness) def _update_orthogonal_constraints(self, orthogonal): """ Update the constraints required to maintain the orthogonal line. The actual constraints attribute (``_orthogonal_constraints``) is observed, so the undo system will update the contents properly """ if not self.canvas: self._orthogonal_constraints = orthogonal and [None] or [] return for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) del self._orthogonal_constraints[:] if not orthogonal: return h = self._handles # if len(h) < 3: # self.split_segment(0) eq = EqualsConstraint # lambda a, b: a - b add = self.canvas.solver.add_constraint cons = [] rest = self._horizontal and 1 or 0 for pos, (h0, h1) in enumerate(zip(h, h[1:])): p0 = h0.pos p1 = h1.pos if pos % 2 == rest: # odd cons.append(add(eq(a=p0.x, b=p1.x))) else: cons.append(add(eq(a=p0.y, b=p1.y))) self.canvas.solver.request_resolve(p1.x) self.canvas.solver.request_resolve(p1.y) self._set_orthogonal_constraints(cons) self.request_update() @observed def _set_orthogonal_constraints(self, orthogonal_constraints): """ Setter for the constraints maintained. Required for the undo system. """ self._orthogonal_constraints = orthogonal_constraints reversible_property(lambda s: s._orthogonal_constraints, _set_orthogonal_constraints) @observed def _set_orthogonal(self, orthogonal): """ >>> a = Line() >>> a.orthogonal False """ if orthogonal and len(self.handles()) < 3: raise ValueError( "Can't set orthogonal line with less than 3 handles") self._update_orthogonal_constraints(orthogonal) orthogonal = reversible_property(lambda s: bool(s._orthogonal_constraints), _set_orthogonal) @observed def _inner_set_horizontal(self, horizontal): self._horizontal = horizontal reversible_method( _inner_set_horizontal, _inner_set_horizontal, {"horizontal": lambda horizontal: not horizontal}, ) def _set_horizontal(self, horizontal): """ >>> line = Line() >>> line.horizontal False >>> line.horizontal = False >>> line.horizontal False """ self._inner_set_horizontal(horizontal) self._update_orthogonal_constraints(self.orthogonal) horizontal = reversible_property(lambda s: s._horizontal, _set_horizontal) def setup_canvas(self): """ Setup constraints. In this case orthogonal. """ super(Line, self).setup_canvas() self._update_orthogonal_constraints(self.orthogonal) def teardown_canvas(self): """ Remove constraints created in setup_canvas(). """ super(Line, self).teardown_canvas() for c in self._orthogonal_constraints: self.canvas.solver.remove_constraint(c) @observed def _reversible_insert_handle(self, index, handle): self._handles.insert(index, handle) @observed def _reversible_remove_handle(self, handle): self._handles.remove(handle) reversible_pair( _reversible_insert_handle, _reversible_remove_handle, bind1={ "index": lambda self, handle: self._handles.index(handle) }, ) @observed def _reversible_insert_port(self, index, port): self._ports.insert(index, port) @observed def _reversible_remove_port(self, port): self._ports.remove(port) reversible_pair( _reversible_insert_port, _reversible_remove_port, bind1={ "index": lambda self, port: self._ports.index(port) }, ) def _create_handle(self, pos, strength=WEAK): return Handle(pos, strength=strength) def _create_port(self, p1, p2): return LinePort(p1, p2) def _update_ports(self): """ Update line ports. This destroys all previously created ports and should only be used when initializing the line. """ assert len(self._handles) >= 2, "Not enough segments" self._ports = [] handles = self._handles for h1, h2 in zip(handles[:-1], handles[1:]): self._ports.append(self._create_port(h1.pos, h2.pos)) def opposite(self, handle): """ Given the handle of one end of the line, return the other end. """ handles = self._handles if handle is handles[0]: return handles[-1] elif handle is handles[-1]: return handles[0] else: raise KeyError("Handle is not an end handle") def post_update(self, context): """ """ super(Line, self).post_update(context) h0, h1 = self._handles[:2] p0, p1 = h0.pos, h1.pos self._head_angle = atan2(p1.y - p0.y, p1.x - p0.x) h1, h0 = self._handles[-2:] p1, p0 = h1.pos, h0.pos self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) def point(self, pos): """ >>> a = Line() >>> a.handles()[1].pos = 25, 5 >>> a._handles.append(a._create_handle((30, 30))) >>> a.point((-1, 0)) 1.0 >>> '%.3f' % a.point((5, 4)) '2.942' >>> '%.3f' % a.point((29, 29)) '0.784' """ hpos = [h.pos for h in self._handles] distance, _point = min( map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1))) return max(0, distance - self.fuzziness) def draw_head(self, context): """ Default head drawer: move cursor to the first handle. """ context.cairo.move_to(0, 0) def draw_tail(self, context): """ Default tail drawer: draw line to the last handle. """ context.cairo.line_to(0, 0) def draw(self, context): """ Draw the line itself. See Item.draw(context). """ def draw_line_end(pos, angle, draw): cr = context.cairo cr.save() try: cr.translate(*pos) cr.rotate(angle) draw(context) finally: cr.restore() cr = context.cairo cr.set_line_width(self.line_width) draw_line_end(self._handles[0].pos, self._head_angle, self.draw_head) for h in self._handles[1:-1]: cr.line_to(*h.pos) draw_line_end(self._handles[-1].pos, self._tail_angle, self.draw_tail) cr.stroke()
class Solver: """ Solve constraints. A constraint should have accompanying variables. """ def __init__(self): # a dict of constraint -> name/variable mappings self._constraints = set() self._marked_cons = [] self._solving = False constraints = property(lambda s: s._constraints) def request_resolve(self, variable, projections_only=False): """ Mark a variable as "dirty". This means it it solved the next time the constraints are resolved. If projections_only is set to True, only constraints using the variable through a Projection instance (e.i. variable itself is not in `constraint.Constraint.variables()`) are marked. Example: >>> from gaphas.constraint import EquationConstraint >>> a, b, c = Variable(1.0), Variable(2.0), Variable(3.0) >>> s = Solver() >>> c_eq = EquationConstraint(lambda a,b: a+b, a=a, b=b) >>> s.add_constraint(c_eq) EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20)) >>> c_eq._weakest [Variable(1, 20), Variable(2, 20)] >>> s._marked_cons [EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20))] >>> a.value=5.0 >>> c_eq.weakest() Variable(2, 20) >>> b.value=2.0 >>> c_eq.weakest() Variable(2, 20) >>> a.value=5.0 >>> c_eq.weakest() Variable(2, 20) """ # Peel of Projections: while isinstance(variable, Projection): variable = variable.variable() for c in variable._constraints: if not projections_only or c._solver_has_projections: if not self._solving: if c in self._marked_cons: self._marked_cons.remove(c) c.mark_dirty(variable) self._marked_cons.append(c) else: c.mark_dirty(variable) self._marked_cons.append(c) if self._marked_cons.count(c) > 100: raise JuggleError( f"Variable juggling detected, constraint {c} resolved {self._marked_cons.count(c)} times out of {len(self._marked_cons)}" ) @observed def add_constraint(self, constraint): """ Add a constraint. The actual constraint is returned, so the constraint can be removed later on. Example: >>> from gaphas.constraint import EquationConstraint >>> s = Solver() >>> a, b = Variable(), Variable(2.0) >>> s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(2, 20)) >>> len(s._constraints) 1 >>> a.value 0.0 >>> b.value 2.0 >>> len(s._constraints) 1 """ assert constraint, f"No constraint ({constraint})" self._constraints.add(constraint) self._marked_cons.append(constraint) constraint._solver_has_projections = False for v in constraint.variables(): while isinstance(v, Projection): v = v.variable() constraint._solver_has_projections = True v._constraints.add(constraint) v._solver = self return constraint @observed def remove_constraint(self, constraint): """ Remove a constraint from the solver >>> from gaphas.constraint import EquationConstraint >>> s = Solver() >>> a, b = Variable(), Variable(2.0) >>> c = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) >>> c EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(2, 20)) >>> s.remove_constraint(c) >>> s._marked_cons [] >>> s._constraints set() Removing a constraint twice has no effect: >>> s.remove_constraint(c) """ assert constraint, f"No constraint ({constraint})" for v in constraint.variables(): while isinstance(v, Projection): v = v.variable() v._constraints.discard(constraint) self._constraints.discard(constraint) while constraint in self._marked_cons: self._marked_cons.remove(constraint) reversible_pair(add_constraint, remove_constraint) def request_resolve_constraint(self, c): """ Request resolving a constraint. """ self._marked_cons.append(c) def constraints_with_variable(self, *variables): """ Return an iterator of constraints that work with variable. The variable in question should be exposed by the constraints `constraint.Constraint.variables()` method. >>> from gaphas.constraint import EquationConstraint >>> s = Solver() >>> a, b, c = Variable(), Variable(2.0), Variable(4.0) >>> eq_a_b = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b)) >>> eq_a_b EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(2, 20)) >>> eq_a_c = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=c)) >>> eq_a_c EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(4, 20)) And now for some testing: >>> eq_a_b in s.constraints_with_variable(a) True >>> eq_a_c in s.constraints_with_variable(a) True >>> eq_a_b in s.constraints_with_variable(a, b) True >>> eq_a_c in s.constraints_with_variable(a, b) False Using another variable with the same value does not work: >>> d = Variable(2.0) >>> eq_a_b in s.constraints_with_variable(a, d) False This also works for projections: >>> eq_pr_a_b = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=Projection(a), b=Projection(b))) >>> eq_pr_a_b # doctest: +ELLIPSIS EquationConstraint(<lambda>, a=Projection(Variable(0, 20)), b=Projection(Variable(2, 20))) >>> eq_pr_a_b in s.constraints_with_variable(a, b) True >>> eq_pr_a_b in s.constraints_with_variable(a, c) False >>> eq_pr_a_b in s.constraints_with_variable(a, d) False """ # Use a copy of the original set, so constraints may be # deleted in the meantime. variables = set(variables) for c in set(self._constraints): if variables.issubset(set(c.variables())): yield c elif c._solver_has_projections: found = True for v in c.variables(): if v in variables: continue while isinstance(v, Projection): v = v.variable() if v in variables: break else: found = False if not found: break # quit for loop, variable not in constraint else: # All iteration have completed succesfully, # so all variables are in the constraint yield c def solve(self): """ Example: >>> from gaphas.constraint import EquationConstraint >>> a, b, c = Variable(1.0), Variable(2.0), Variable(3.0) >>> s = Solver() >>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=a, b=b)) EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20)) >>> a.value = 5.0 >>> s.solve() >>> len(s._marked_cons) 0 >>> b._value -5.0 >>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=b, b=c)) EquationConstraint(<lambda>, a=Variable(-5, 20), b=Variable(3, 20)) >>> len(s._constraints) 2 >>> len(s._marked_cons) 1 >>> b._value -5.0 >>> s.solve() >>> b._value -3.0 >>> a.value = 10 >>> s.solve() >>> c._value 10.0 """ marked_cons = self._marked_cons try: self._solving = True # Solve each constraint. Using a counter makes it # possible to also solve constraints that are marked as # a result of other variabled being solved. n = 0 while n < len(marked_cons): c = marked_cons[n] if not c.disabled: c.solve() n += 1 self._marked_cons = [] finally: self._solving = False