PK!!uGgaphas/__init__.py""" Gaphas ====== Gaphor's Canvas. This module contains the application independant parts of Gaphor's Canvas. Notes ===== In py-cairo 1.8.0 (or 1.8.1, or 1.8.2) the multiplication order has been reverted. This causes bugs in Gaphas. Also a new method ``multiply()`` has been introduced. This method is used in Gaphas instead of the multiplier (``*``). In both the ``Canvas`` and ``View`` class a workaround is provided in case an older version of py-cairo is used. Copyright notice ================ Copyright 2018 Arjan Molenaar & Dan Yeaw Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from __future__ import absolute_import __version__ = "$Revision$" # $HeadURL$ from .canvas import Canvas from .connector import Handle from .item import Item, Line, Element from .view import View, GtkView # vi:sw=4:et:ai PK!#""gaphas/aspect.py"""Defines aspects for Items. Aspects form intermediate items between tools and items. Note: This module uses Phillip J. Eby's simplegeneric module. This module transforms the generic class (used as fall-back) to a generic function. In order to inherit from this class you should inherit from Class.default. The simplegeneric module is dispatching only based on the first argument. For Gaphas that's enough. """ from __future__ import absolute_import from builtins import object from gi.repository import Gdk from simplegeneric import generic from gaphas.item import Item, Element class ItemFinder(object): """ Find an item on the canvas. """ def __init__(self, view): self.view = view def get_item_at_point(self, pos): item, handle = self.view.get_handle_at_point(pos) return item or self.view.get_item_at_point(pos) Finder = generic(ItemFinder) class ItemSelection(object): """ A role for items. When dealing with selection. Behaviour can be overridden by applying the @aspect decorator to a subclass. """ def __init__(self, item, view): self.item = item self.view = view def select(self): """ Set selection on the view. """ self.view.focused_item = self.item def unselect(self): self.view.focused_item = None self.view.unselect_item(self.item) Selection = generic(ItemSelection) class ItemInMotion(object): """ Aspect for dealing with motion on an item. In this case the item is moved. """ def __init__(self, item, view): self.item = item self.view = view self.last_x, self.last_y = None, None def start_move(self, pos): self.last_x, self.last_y = pos def move(self, pos): """ Move the item. x and y are in view coordinates. """ item = self.item view = self.view v2i = view.get_matrix_v2i(item) x, y = pos dx, dy = x - self.last_x, y - self.last_y dx, dy = v2i.transform_distance(dx, dy) self.last_x, self.last_y = x, y item.matrix.translate(dx, dy) item.canvas.request_matrix_update(item) def stop_move(self): pass InMotion = generic(ItemInMotion) class ItemHandleFinder(object): """ Deals with the task of finding handles. """ def __init__(self, item, view): self.item = item self.view = view def get_handle_at_point(self, pos): return self.view.get_handle_at_point(pos) HandleFinder = generic(ItemHandleFinder) class ItemHandleSelection(object): """ Deal with selection of the handle. """ def __init__(self, item, handle, view): self.item = item self.handle = handle self.view = view def select(self): pass def unselect(self): pass HandleSelection = generic(ItemHandleSelection) @HandleSelection.when_type(Element) class ElementHandleSelection(ItemHandleSelection): CURSORS = ("nw-resize", "ne-resize", "se-resize", "sw-resize") def select(self): index = self.item.handles().index(self.handle) if index < 4: display = self.view.get_display() cursor = Gdk.Cursor.new_from_name(display, self.CURSORS[index]) self.view.get_window().set_cursor(cursor) def unselect(self): from .view import DEFAULT_CURSOR cursor = Gdk.Cursor(DEFAULT_CURSOR) self.view.get_window().set_cursor(cursor) class ItemHandleInMotion(object): """ Move a handle (role is applied to the handle) """ GLUE_DISTANCE = 10 def __init__(self, item, handle, view): self.item = item self.handle = handle self.view = view self.last_x, self.last_y = None, None def start_move(self, pos): self.last_x, self.last_y = pos canvas = self.item.canvas cinfo = canvas.get_connection(self.handle) if cinfo: canvas.solver.remove_constraint(cinfo.constraint) def move(self, pos): item = self.item handle = self.handle view = self.view v2i = view.get_matrix_v2i(item) x, y = v2i.transform_point(*pos) self.handle.pos = (x, y) sink = self.glue(pos) # do not request matrix update as matrix recalculation will be # performed due to item normalization if required item.request_update(matrix=False) return sink def stop_move(self): pass def glue(self, pos, distance=GLUE_DISTANCE): """ Glue to an item near a specific point. Returns a ConnectionSink or None. """ item = self.item handle = self.handle view = self.view if not handle.connectable: return None connectable, port, glue_pos = view.get_port_at_point( pos, distance=distance, exclude=(item,) ) # check if item and found item can be connected on closest port if port is not None: assert connectable is not None connector = Connector(self.item, self.handle) sink = ConnectionSink(connectable, port) if connector.allow(sink): # transform coordinates from view space to the item # space and update position of item's handle v2i = view.get_matrix_v2i(item).transform_point handle.pos = v2i(*glue_pos) return sink return None HandleInMotion = generic(ItemHandleInMotion) class ItemConnector(object): """Connect or disconnect an item's handle to another item or port. """ GLUE_DISTANCE = 10 # Glue distance in view points def __init__(self, item, handle): self.item = item self.handle = handle def allow(self, sink): return True def glue(self, sink): """ Glue the Connector handle on the sink's port. """ handle = self.handle item = self.item matrix = item.canvas.get_matrix_i2i(item, sink.item) pos = matrix.transform_point(*handle.pos) gluepos, dist = sink.port.glue(pos) matrix.invert() handle.pos = matrix.transform_point(*gluepos) def connect(self, sink): """ Connect the handle to a sink (item, port). Note that connect() also takes care of disconnecting in case a handle is reattached to another element. """ cinfo = self.item.canvas.get_connection(self.handle) # Already connected? disconnect first. if cinfo: self.disconnect() if not self.allow(sink): return self.glue(sink) self.connect_handle(sink) def connect_handle(self, sink, callback=None): """ Create constraint between handle of a line and port of connectable item. :Parameters: sink Connectable item and port. callback Function to be called on disconnection. """ canvas = self.item.canvas handle = self.handle item = self.item constraint = sink.port.constraint(canvas, item, handle, sink.item) canvas.connect_item( item, handle, sink.item, sink.port, constraint, callback=callback ) def disconnect(self): """ Disconnect the handle from the attached element. """ self.item.canvas.disconnect_item(self.item, self.handle) Connector = generic(ItemConnector) class ItemConnectionSink(object): """Makes an item a sink. A sink is another item that an item's handle is connected to like a connectable item or port. """ def __init__(self, item, port): self.item = item self.port = port def find_port(self, pos): """ Glue to the closest item on the canvas. If the item can connect, it returns a port. """ port = None max_dist = 10e6 for p in self.item.ports(): pg, d = p.glue(pos) if d >= max_dist: continue port = p max_dist = d return port ConnectionSink = generic(ItemConnectionSink) ## ## Painter aspects ## class ItemPaintFocused(object): """ Paints on top of all items, just for the focused item and only when it's hovered (see gaphas.painter.FocusedItemPainter) """ def __init__(self, item, view): self.item = item self.view = view def paint(self, context): pass PaintFocused = generic(ItemPaintFocused) # vim:sw=4:et:ai PK!M||gaphas/canvas.py""" A Canvas owns a set of Items and acts as a container for both the items and a constraint solver. Connections =========== Getting Connection Information ============================== To get connected item to a handle:: c = canvas.get_connection(handle) if c is not None: print c.connected print c.port print c.constraint To get all connected items (i.e. items on both sides of a line):: classes = (i.connected for i in canvas.get_connections(item=line)) To get connecting items (i.e. all lines connected to a class):: lines = (c.item for c in canvas.get_connections(connected=item)) """ from __future__ import absolute_import import logging from builtins import map from builtins import next from builtins import object from builtins import range from collections import namedtuple from cairo import Matrix from gaphas import solver from gaphas import table from gaphas import tree from gaphas.decorators import nonrecursive, AsyncIO from .state import observed, reversible_method, reversible_pair # # Information about two connected items # # - item: connecting item # - handle: handle of connecting item (points connected item) # - connected: connected item # - port: port of connected item # - constraint: optional connection constraint # - callback: optional disconnection callback # Connection = namedtuple("Connection", "item handle connected port constraint callback") class ConnectionError(Exception): """ Exception raised when there is an error when connecting an items with each other. """ class Context(object): """ Context used for updating and drawing items in a drawing canvas. >>> c=Context(one=1,two='two') >>> c.one 1 >>> c.two 'two' >>> try: c.one = 2 ... except: 'got exc' 'got exc' """ def __init__(self, **kwargs): self.__dict__.update(**kwargs) def __setattr__(self, key, value): raise AttributeError("context is not writable") class Canvas(object): """ 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, "Adding already added node %s" % 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 [] """ 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 [] """ 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 """ 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 [] >>> list(c.get_ancestors(iii)) # doctest: +ELLIPSIS [, ] """ 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 [] >>> list(c.get_children(i)) # doctest: +ELLIPSIS [] """ 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 [] >>> list(c.get_all_children(i)) # doctest: +ELLIPSIS [, ] """ 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( "Handle %r of item %r is already connected" % (handle, item) ) 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, 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=>> 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=>> 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( 'No data available for item "%s" and handle "%s"' % (item, 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, cinfo.port, constraint, cinfo.callback ) if constraint: self._solver.add_constraint(constraint) reversible_method( reconnect_item, reverse=reconnect_item, bind={ "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=>> 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=>> list(c.get_connections(connected=i)) [] >>> list(c.get_connections(connected=ii)) # doctest: +ELLIPSIS [Connection(item=>> c.connect_item(ii, ii.handles()[0], iii, iii.ports()[0], None) >>> list(c.get_connections(item=ii)) # doctest: +ELLIPSIS [Connection(item=>> list(c.get_connections(connected=iii)) # doctest: +ELLIPSIS [Connection(item=>> 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 self._tree.sort(items, index_key="_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, cr): context_map = dict() c = Context(cairo=cr) for item in items: item.pre_update(c) def _post_update_items(self, items, cr): c = Context(cairo=cr) for item in items: item.post_update(c) def _extend_dirty_items(self, dirty_items): # item's can be marked dirty due to external constraints solving if self._dirty_items: dirty_items.extend(self._dirty_items) self._dirty_items.clear() dirty_items = self.sort(set(dirty_items), reverse=True) @nonrecursive def update_now(self): """ Peform an update of the items that requested an update. """ if self._dirty_index: self.update_index() self._dirty_index = False sort = self.sort extend_dirty_items = self._extend_dirty_items # perform update requests for parents of dirty items dirty_items = self._dirty_items for item in set(dirty_items): dirty_items.update(self._tree.get_ancestors(item)) # order the dirty items, so they are updated bottom to top dirty_items = sort(self._dirty_items, reverse=True) self._dirty_items.clear() try: cr = self._obtain_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, cr) # 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, ( "No matrices may have been marked dirty (%s)" % (self._dirty_matrix_items,) ) # item's can be marked dirty due to external constraints solving extend_dirty_items(dirty_items) assert not self._dirty_items, "No items may have been marked dirty (%s)" % ( self._dirty_items, ) # normalize items, which changed after constraint solving; # store those items, whose matrices changed normalized_items = self._normalize(dirty_items) # recalculate matrices of normalized items dirty_matrix_items.update(self.update_matrices(normalized_items)) # ensure constraints are still true after normalization self._solver.solve() # item's can be marked dirty due to normalization and solving extend_dirty_items(dirty_items) assert not self._dirty_items, "No items may have been marked dirty (%s)" % ( self._dirty_items, ) self._post_update_items(dirty_items, cr) 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 ), "dirty: %s; matrix: %s" % (self._dirty_items, 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 = Matrix(*item._matrix_i2c) except: orig_matrix_i2c = None item._matrix_i2c = 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 = 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() [, , , ] 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 {} >>> e.handles() [, , , ] """ dirty_matrix_items = set() for item in items: if item.normalize(): dirty_matrix_items.add(item) return 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 _obtain_cairo_context(self): """ Try to obtain a Cairo context. This is a not-so-clean way to solve issues like calculating the bounding box for a piece of text (for that you'll need a CairoContext). The Cairo context is created by a View registered as view on this canvas. By lack of registered views, a PNG image surface is created that is used to create a context. >>> c = Canvas() >>> c.update_now() """ for view in self._registered_views: try: return view.window.cairo_create() except AttributeError: pass else: import cairo surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) return cairo.Context(surface) 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 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(object): """ 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, "Canvas.remove": Canvas.remove, "Canvas.request_update": Canvas.request_update, } # vim:sw=4:et:ai PK!r;gaphas/connector.py""" Basic connectors such as Ports and Handles. """ from builtins import object from gaphas.constraint import LineConstraint, PositionConstraint from gaphas.geometry import distance_line_point, distance_point_point from gaphas.solver import solvable, NORMAL from gaphas.state import observed, reversible_property def deprecated(e): return e class Position(object): """ 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)) """ x = solvable(varname="_v_x") y = solvable(varname="_v_y") def __init__(self, pos, strength=NORMAL): self.x, self.y = pos self.x.strength = strength self.y.strength = strength @observed def _set_pos(self, pos): """ Set handle position (Item coordinates). """ self.x, self.y = pos pos = property(lambda s: (s.x, s.y), _set_pos) def set_x(self, vx): """ Set the variable for x. NOTE: This changes the variable object itself, not only the value! """ self._v_x = vx def set_y(self, vy): """ Set the variable for y. NOTE: This changes the variable object itself, not only the value! """ self._v_y = vy def __str__(self): return "<%s object on (%g, %g)>" % ( self.__class__.__name__, float(self.x), float(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] class Handle(object): """ Handles are used to support modifications of Items. If the handle is connected to an item, the ``connected_to`` property should refer to the item. A ``disconnect`` handler should be provided that handles all disconnect behaviour (e.g. clean up constraints and ``connected_to``). Note for those of you that use the Pickle module to persist a canvas: The property ``disconnect`` should contain a callable object (with __call__() method), so the pickle handler can also pickle that. Pickle is not capable of pickling ``instancemethod`` or ``function`` objects. """ def __init__(self, pos=(0, 0), strength=NORMAL, connectable=False, movable=True): self._pos = Position(pos, strength) self._connectable = connectable self._movable = movable self._visible = True def _set_pos(self, pos): """ Shortcut for ``handle.pos.pos = pos`` >>> h = Handle((10, 10)) >>> h.pos >>> h.pos = (20, 15) >>> h.pos """ self._pos.pos = pos pos = property(lambda s: s._pos, _set_pos) def _set_x(self, x): """ Shortcut for ``handle.pos.x = x`` """ self._pos.x = x def _get_x(self): return self._pos.x x = property(deprecated(_get_x), deprecated(_set_x)) def _set_y(self, y): """ Shortcut for ``handle.pos.y = y`` """ self._pos.y = y def _get_y(self): return self._pos.y y = property(deprecated(_get_y), deprecated(_set_y)) @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) @observed def _set_movable(self, movable): self._movable = movable movable = reversible_property(lambda s: s._movable, _set_movable) @observed def _set_visible(self, visible): self._visible = visible visible = reversible_property(lambda s: s._visible, _set_visible) def __str__(self): return "<%s object on (%g, %g)>" % ( self.__class__.__name__, float(self._pos.x), float(self._pos.y), ) __repr__ = __str__ class Port(object): """Port connectable part of an item. The Item's handle connects to a port. """ def __init__(self): super(Port, self).__init__() self._connectable = True @observed def _set_connectable(self, connectable): self._connectable = connectable connectable = reversible_property(lambda s: s._connectable, _set_connectable) def glue(self, pos): """ Get glue point on the port and distance to the port. """ raise NotImplemented("Glue method not implemented") def constraint(self, canvas, item, handle, glue_item): """ Create connection constraint between item's handle and glue item. """ raise NotImplemented("Constraint method not implemented") class LinePort(Port): """ Port defined as a line between two handles. """ def __init__(self, start, end): super(LinePort, self).__init__() self.start = start self.end = end def glue(self, pos): """ Get glue point on the port and distance to the port. >>> p1, p2 = (0.0, 0.0), (100.0, 100.0) >>> port = LinePort(p1, p2) >>> port.glue((50, 50)) ((50.0, 50.0), 0.0) >>> port.glue((0, 10)) ((5.0, 5.0), 7.0710678118654755) """ d, pl = distance_line_point(self.start, self.end, pos) return pl, d def constraint(self, canvas, item, handle, glue_item): """ Create connection line constraint between item's handle and the port. """ line = canvas.project(glue_item, self.start, self.end) point = canvas.project(item, handle.pos) return LineConstraint(line, point) class PointPort(Port): """ Port defined as a point. """ def __init__(self, point): super(PointPort, self).__init__() self.point = point def glue(self, pos): """ Get glue point on the port and distance to the port. >>> h = Handle((10, 10)) >>> port = PointPort(h.pos) >>> port.glue((10, 0)) (, 10.0) """ d = distance_point_point(self.point, pos) return self.point, d def constraint(self, canvas, item, handle, glue_item): """ Return connection position constraint between item's handle and the port. """ origin = canvas.project(glue_item, self.point) point = canvas.project(item, handle.pos) c = PositionConstraint(origin, point) return c # PositionConstraint(origin, point) # vim: sw=4:et:ai PK!@9EEgaphas/constraint.py""" This module contains several flavors of constraint classes. Each has a method `Constraint.solve_for(name)` and a method `Constraint.mark_dirty(v)`. These methods are used by the constraint solver (`solver.Solver`) to set the variables. Variables should be of type `solver.Variable`. See classes' documentation below for constraints description and for examples of their usage. EqualsConstraint Make 'a' and 'b' equal. LessThanConstraint Ensure one variable stays smaller than another. CenterConstraint Ensures a Variable is kept between two other variables. EquationConstraint Solve a linear equation. BalanceConstraint Keeps three variables in line, maintaining a specific ratio. LineConstraint Solves the equation where a line is connected to a line or side at a specific point. New constraint class should derive from Constraint class abstract class and implement `Constraint.solve_for(Variable)` method to update a variable with appropriate value. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function from builtins import object import math from gaphas.solver import Projection # is simple abs(x - y) > EPSILON enough for canvas needs? EPSILON = 1e-6 def _update(variable, value): if abs(variable.value - value) > EPSILON: variable.value = value class Constraint(object): """ Constraint base class. - _variables - list of all variables - _weakest - list of weakest variables """ disabled = False def __init__(self, *variables): """ Create new constraint, register all variables, and find weakest variables. Any value can be added. It is assumed to be a variable if it has a 'strength' attribute. """ self._variables = [] for v in variables: if hasattr(v, "strength"): self._variables.append(v) self.create_weakest_list() # Used by the Solver for efficiency self._solver_has_projections = False def create_weakest_list(self): """ Create list of weakest variables. """ # strength = min([v.strength for v in self._variables]) strength = min(v.strength for v in self._variables) self._weakest = [v for v in self._variables if v.strength == strength] def variables(self): """ Return an iterator which iterates over the variables that are held by this constraint. """ return self._variables def weakest(self): """ Return the weakest variable. The weakest variable should be always as first element of Constraint._weakest list. """ return self._weakest[0] def mark_dirty(self, v): """ Mark variable v dirty and if possible move it to the end of Constraint._weakest list to maintain weakest variable invariants (see gaphas.solver module documentation). """ weakest = self.weakest() # Fast lane: if v is weakest: self._weakest.remove(v) self._weakest.append(v) return # Handle projected variables well: global Projection p = weakest while isinstance(weakest, Projection): weakest = weakest.variable() if v is weakest: self._weakest.remove(p) self._weakest.append(p) return def solve(self): """ Solve the constraint. This is done by determining the weakest variable and calling solve_for() for that variable. The weakest variable is always in the set of variables with the weakest strength. The least recently changed variable is considered the weakest. """ wvar = self.weakest() self.solve_for(wvar) def solve_for(self, var): """ Solve the constraint for a given variable. The variable itself is updated. """ raise NotImplemented class EqualsConstraint(Constraint): """ Constraint, which ensures that two arguments ``a`` and ``b`` are equal: a + delta = b for example >>> from gaphas.solver import Variable >>> a, b = Variable(1.0), Variable(2.0) >>> eq = EqualsConstraint(a, b) >>> eq.solve_for(a) >>> a Variable(2, 20) >>> a.value = 10.8 >>> eq.solve_for(b) >>> b Variable(10.8, 20) """ def __init__(self, a=None, b=None, delta=0.0): super(EqualsConstraint, self).__init__(a, b, delta) self.a = a self.b = b self.delta = delta def solve_for(self, var): assert var in (self.a, self.b, self.delta) _update( *( (var is self.a) and (self.a, self.b.value - self.delta) or (var is self.b) and (self.b, self.a.value + self.delta) or (self.delta, self.b.value - self.a.value) ) ) class CenterConstraint(Constraint): """ Simple Constraint, takes three arguments: 'a', 'b' and center. When solved, the constraint ensures 'center' is located in the middle of 'a' and 'b'. >>> from gaphas.solver import Variable >>> a, b, center = Variable(1.0), Variable(3.0), Variable() >>> eq = CenterConstraint(a, b, center) >>> eq.solve_for(a) >>> a Variable(1, 20) >>> center Variable(2, 20) >>> a.value = 10 >>> eq.solve_for(b) >>> b Variable(3, 20) >>> center Variable(6.5, 20) """ def __init__(self, a=None, b=None, center=None): super(CenterConstraint, self).__init__(a, b, center) self.a = a self.b = b self.center = center def solve_for(self, var): assert var in (self.a, self.b, self.center) v = (self.a.value + self.b.value) / 2.0 _update(self.center, v) class LessThanConstraint(Constraint): """ Ensure ``smaller`` is less than ``bigger``. The variable that is passed as to-be-solved is left alone (cause it is the variable that has not been moved lately). Instead the other variable is solved. >>> from gaphas.solver import Variable >>> a, b = Variable(3.0), Variable(2.0) >>> lt = LessThanConstraint(smaller=a, bigger=b) >>> lt.solve_for(a) >>> a, b (Variable(3, 20), Variable(3, 20)) >>> b.value = 0.8 >>> lt.solve_for(b) >>> a, b (Variable(0.8, 20), Variable(0.8, 20)) Also minimal delta between two values can be set >>> a, b = Variable(10.0), Variable(8.0) >>> lt = LessThanConstraint(smaller=a, bigger=b, delta=5) >>> lt.solve_for(a) >>> a, b (Variable(10, 20), Variable(15, 20)) """ def __init__(self, smaller=None, bigger=None, delta=0.0): super(LessThanConstraint, self).__init__(smaller, bigger, delta) self.smaller = smaller self.bigger = bigger self.delta = delta def solve_for(self, var): if self.smaller.value > self.bigger.value - self.delta: if var is self.smaller: self.bigger.value = self.smaller.value + self.delta elif var is self.bigger: self.smaller.value = self.bigger.value - self.delta elif var is self.delta: self.delta.value = self.bigger.value - self.smaller.value # Constants for the EquationConstraint ITERLIMIT = 1000 # iteration limit class EquationConstraint(Constraint): """ Equation solver using attributes and introspection. Takes a function, named arg value (opt.) and returns a Constraint object Calling EquationConstraint.solve_for will solve the equation for variable ``arg``, so that the outcome is 0. >>> from gaphas.solver import Variable >>> a, b, c = Variable(), Variable(4), Variable(5) >>> cons = EquationConstraint(lambda a, b, c: a + b - c, a=a, b=b, c=c) >>> cons.solve_for(a) >>> a Variable(1, 20) >>> a.value = 3.4 >>> cons.solve_for(b) >>> b Variable(1.6, 20) From: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303396 """ def __init__(self, f, **args): super(EquationConstraint, self).__init__(*list(args.values())) self._f = f self._args = {} # see important note on order of operations in __setattr__ below. for arg in f.__code__.co_varnames[0 : f.__code__.co_argcount]: self._args[arg] = None self._set(**args) def __repr__(self): argstring = ", ".join( ["%s=%s" % (arg, str(value)) for (arg, value) in list(self._args.items())] ) if argstring: return "EquationConstraint(%s, %s)" % (self._f.__code__.co_name, argstring) else: return "EquationConstraint(%s)" % self._f.__code__.co_name def __getattr__(self, name): """ Used to extract function argument values. """ self._args[name] return self.solve_for(name) def __setattr__(self, name, value): """ Sets function argument values. """ # Note - once self._args is created, no new attributes can # be added to self.__dict__. This is a good thing as it throws # an exception if you try to assign to an arg which is inappropriate # for the function in the solver. if "_args" in self.__dict__: if name in self._args: self._args[name] = value elif name in self.__dict__: self.__dict__[name] = value else: raise KeyError(name) else: object.__setattr__(self, name, value) def _set(self, **args): """ Sets values of function arguments. """ for arg in args: self._args[arg] # raise exception if arg not in _args setattr(self, arg, args[arg]) def solve_for(self, var): """ Solve this constraint for the variable named 'arg' in the constraint. """ args = {} for nm, v in list(self._args.items()): args[nm] = v.value if v is var: arg = nm v = self._solve_for(arg, args) if var.value != v: var.value = v def _solve_for(self, arg, args): """ Newton's method solver """ # args = self._args close_runs = 10 # after getting close, do more passes if args[arg]: x0 = args[arg] else: x0 = 1 if x0 == 0: x1 = 1 else: x1 = x0 * 1.1 def f(x): """function to solve""" args[arg] = x return self._f(**args) fx0 = f(x0) n = 0 while True: # Newton's method loop here fx1 = f(x1) if fx1 == 0 or x1 == x0: # managed to nail it exactly break if abs(fx1 - fx0) < EPSILON: # very close close_flag = True if close_runs == 0: # been close several times break else: close_runs -= 1 # try some more else: close_flag = False if n > ITERLIMIT: print("Failed to converge; exceeded iteration limit") break slope = (fx1 - fx0) / (x1 - x0) if slope == 0: if close_flag: # we're close but have zero slope, finish break else: print("Zero slope and not close enough to solution") break x2 = x0 - fx0 / slope # New 'x1' fx0 = fx1 x0 = x1 x1 = x2 n += 1 return x1 class BalanceConstraint(Constraint): """ Ensure that a variable ``v`` is between values specified by ``band`` and in distance proportional from ``band[0]``. Consider >>> from gaphas.solver import Variable, WEAK >>> a, b, c = Variable(2.0), Variable(3.0), Variable(2.3, WEAK) >>> bc = BalanceConstraint(band=(a,b), v=c) >>> c.value = 2.4 >>> c Variable(2.4, 10) >>> bc.solve_for(c) >>> a, b, c (Variable(2, 20), Variable(3, 20), Variable(2.3, 10)) Band does not have to be ``band[0] < band[1]`` >>> a, b, c = Variable(3.0), Variable(2.0), Variable(2.45, WEAK) >>> bc = BalanceConstraint(band=(a,b), v=c) >>> c.value = 2.50 >>> c Variable(2.5, 10) >>> bc.solve_for(c) >>> a, b, c (Variable(3, 20), Variable(2, 20), Variable(2.45, 10)) """ def __init__(self, band=None, v=None, balance=None): super(BalanceConstraint, self).__init__(band[0], band[1], v) self.band = band self.balance = balance self.v = v if self.balance is None: self.update_balance() def update_balance(self): b1, b2 = self.band w = b2 - b1 if w != 0: self.balance = (self.v - b1) / w else: self.balance = 0 def solve_for(self, var): b1, b2 = self.band w = b2.value - b1.value value = b1.value + w * self.balance _update(var, value) class LineConstraint(Constraint): """ Ensure a point is kept on a line. Attributes: - _line: line defined by tuple ((x1, y1), (x2, y2)) - _point: point defined by tuple (x, y) """ def __init__(self, line, point): super(LineConstraint, self).__init__( line[0][0], line[0][1], line[1][0], line[1][1], point[0], point[1] ) self._line = line self._point = point self.update_ratio() def update_ratio(self): """ >>> from gaphas.solver import Variable >>> line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) >>> point = (Variable(15), Variable(4)) >>> lc = LineConstraint(line=line, point=point) >>> lc.update_ratio() >>> lc.ratio_x, lc.ratio_y (0.5, 0.2) >>> line[1][0].value = 40 >>> line[1][1].value = 30 >>> lc.solve_for(point[0]) >>> lc.ratio_x, lc.ratio_y (0.5, 0.2) >>> point (Variable(20, 20), Variable(6, 20)) """ sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point try: self.ratio_x = float(px.value - sx.value) / float(ex.value - sx.value) except ZeroDivisionError: self.ratio_x = 0.0 try: self.ratio_y = float(py.value - sy.value) / float(ey.value - sy.value) except ZeroDivisionError: self.ratio_y = 0.0 def solve_for(self, var=None): self._solve() def _solve(self): """ Solve the equation for the connected_handle. >>> from gaphas.solver import Variable >>> line = (Variable(0), Variable(0)), (Variable(30), Variable(20)) >>> point = (Variable(15), Variable(4)) >>> lc = LineConstraint(line=line, point=point) >>> lc.update_ratio() >>> lc.solve_for(point[0]) >>> point (Variable(15, 20), Variable(4, 20)) >>> line[1][0].value = 40 >>> line[1][1].value = 30 >>> lc.solve_for(point[0]) >>> point (Variable(20, 20), Variable(6, 20)) """ sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point x = sx.value + (ex.value - sx.value) * self.ratio_x y = sy.value + (ey.value - sy.value) * self.ratio_y _update(px, x) _update(py, y) class PositionConstraint(Constraint): """ Ensure that point is always in origin position. Attributes: - _origin: origin position - _point: point to be in origin position """ def __init__(self, origin, point): super(PositionConstraint, self).__init__( origin[0], origin[1], point[0], point[1] ) self._origin = origin self._point = point def solve_for(self, var=None): """ Ensure that point's coordinates are the same as coordinates of the origin position. """ x, y = self._origin[0].value, self._origin[1].value _update(self._point[0], x) _update(self._point[1], y) class LineAlignConstraint(Constraint): """ Ensure a point is kept on a line in position specified by align and padding information. Align is specified as a number between 0 and 1, for example 0 keep point at one end of the line 1 keep point at other end of the line 0.5 keep point in the middle of the line Align can be adjusted with `delta` parameter, which specifies the padding of the point. :Attributes: _line Line defined by tuple ((x1, y1), (x2, y2)). _point Point defined by tuple (x, y). _align Align of point. _delta Padding of the align. """ def __init__(self, line, point, align=0.5, delta=0.0): super(LineAlignConstraint, self).__init__( line[0][0], line[0][1], line[1][0], line[1][1], point[0], point[1] ) self._line = line self._point = point self._align = align self._delta = delta def solve_for(self, var=None): sx, sy = self._line[0] ex, ey = self._line[1] px, py = self._point a = math.atan2(ey.value - sy.value, ex.value - sx.value) x = sx.value + (ex.value - sx.value) * self._align + self._delta * math.cos(a) y = sy.value + (ey.value - sy.value) * self._align + self._delta * math.sin(a) _update(px, x) _update(py, y) # vim:sw=4:et:ai PK!fgaphas/decorators.py""" Custom decorators. """ from __future__ import print_function import threading from builtins import object import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib DEBUG_ASYNC = False class AsyncIO(object): """ Instead of calling the function, schedule an idle handler at a given priority. This requires the async'ed method to be called from within the GTK main loop. Otherwise the method is executed directly. Note: the current implementation of async single mode only works for methods, not functions. Calling the async function from outside the gtk main loop will yield immediate execution: async just works on functions (as long as ``single=False``): >>> a = AsyncIO()(lambda: 'Hi') >>> a() 'Hi' Simple method: >>> class A(object): ... @AsyncIO(single=False, priority=GLib.PRIORITY_HIGH) ... def a(self): ... print('idle-a', GLib.main_depth()) Methods can also set single mode to True (the method is only scheduled once). >>> class B(object): ... @AsyncIO(single=True) ... def b(self): ... print('idle-b', GLib.main_depth()) Also a timeout property can be provided: >>> class C(object): ... @AsyncIO(timeout=50) ... def c1(self): ... print('idle-c1', GLib.main_depth()) ... @AsyncIO(single=True, timeout=60) ... def c2(self): ... print('idle-c2', GLib.main_depth()) This is a helper function used to test classes A and B from within the GTK+ main loop: >>> def delayed(): ... print("before") ... a = A() ... b = B() ... c = C() ... c.c1() ... c.c1() ... c.c2() ... c.c2() ... a.a() ... b.b() ... a.a() ... b.b() ... a.a() ... b.b() ... print("after") ... GLib.timeout_add(100, Gtk.main_quit) >>> GLib.timeout_add(1, delayed) > 0 # timeout id may vary True >>> from gi.repository import Gtk >>> Gtk.main() before after idle-a 1 idle-a 1 idle-a 1 idle-b 1 idle-c1 1 idle-c1 1 idle-c2 1 As you can see, although ``b.b()`` has been called three times, it's only executed once. """ def __init__(self, single=False, timeout=0, priority=GLib.PRIORITY_DEFAULT): self.single = single self.timeout = timeout self.priority = priority def source(self, func): timeout = self.timeout if timeout > 0: s = GLib.Timeout(timeout) else: s = GLib.Idle() s.set_callback(func) s.priority = self.priority return s def __call__(self, func): async_id = "_async_id_%s" % func.__name__ source = self.source def wrapper(*args, **kwargs): global getattr, setattr, delattr # execute directly if we're not in the main loop. if GLib.main_depth() == 0: return func(*args, **kwargs) elif not self.single: def async_wrapper(*aargs): if DEBUG_ASYNC: print("async:", func, args, kwargs) func(*args, **kwargs) source(async_wrapper).attach() else: # Idle handlers should be registered per instance holder = args[0] try: if getattr(holder, async_id): return except AttributeError as e: def async_wrapper(*aargs): if DEBUG_ASYNC: print("async:", func, args, kwargs) try: func(*args, **kwargs) finally: delattr(holder, async_id) return False setattr(holder, async_id, source(async_wrapper).attach()) return wrapper def nonrecursive(func): """ Enforce a function or method is not executed recursively: >>> class A(object): ... @nonrecursive ... def a(self, x=1): ... print(x) ... self.a(x+1) >>> A().a() 1 >>> A().a() 1 """ m = threading.Lock() def wrapper(*args, **kwargs): """ Decorate function with a mutex that prohibits recursive execution. """ if m.acquire(False): try: return func(*args, **kwargs) finally: m.release() return wrapper class recursive(object): """ This decorator limits the recursion for a specific function >>> class A(object): ... def __init__(self): self.r = 0 ... @recursive(10) ... def a(self, x=0): ... self.r += 1 ... self.a() >>> a = A() >>> a.a() >>> a.r 10 """ def __init__(self, limit=10000): self.limit = limit def __call__(self, func): def wrapper(*args, **kwargs): try: func._recursion_level += 1 except AttributeError: # _recursion_level not present func._recursion_level = 0 if func._recursion_level < self.limit: try: return func(*args, **kwargs) finally: func._recursion_level -= 1 return wrapper # vim:sw=4:et:ai PK!ŗgaphas/examples.py""" Simple example items. These items are used in various tests. """ from __future__ import absolute_import from __future__ import division from gaphas.connector import Handle, PointPort, LinePort, Position from gaphas.item import Element, Item, NW, NE, SW, SE from gaphas.solver import WEAK from .util import text_align, text_multiline, path_ellipse class Box(Element): """ A Box has 4 handles (for a start): NW +---+ NE SW +---+ SE """ def __init__(self, width=10, height=10): super(Box, self).__init__(width, height) def draw(self, context): c = context.cairo nw = self._handles[NW].pos c.rectangle(nw.x, nw.y, self.width, self.height) if context.hovered: c.set_source_rgba(0.8, 0.8, 1, 0.8) else: c.set_source_rgba(1, 1, 1, 0.8) c.fill_preserve() c.set_source_rgb(0, 0, 0.8) c.stroke() class PortoBox(Box): """ Box item with few flavours of port(o)s. Default box ports are disabled. Three, non-default connectable ports are created (represented by ``x`` on the picture). - point port on the east edge, fovable with a handle - static point port in the middle of the south edge - line port from north-west to south east corner NW +--------+ NE |xx | | xx |x | xx | | xx| SW +--------+ SE x """ def __init__(self, width=10, height=10): super(PortoBox, self).__init__(width, height) # disable default ports for p in self._ports: p.connectable = False nw = self._handles[NW] sw = self._handles[SW] ne = self._handles[NE] se = self._handles[SE] # handle for movable port self._hm = Handle(strength=WEAK) self._hm.pos = width, height / 2.0 self._handles.append(self._hm) # movable port self._ports.append(PointPort(self._hm.pos)) # keep movable port at right edge self.constraint(vertical=(self._hm.pos, ne.pos), delta=10) self.constraint(above=(ne.pos, self._hm.pos)) self.constraint(above=(self._hm.pos, se.pos)) # static point port self._sport = PointPort(Position((width / 2.0, height))) l = sw.pos, se.pos self.constraint(line=(self._sport.point, l)) self._ports.append(self._sport) # line port self._lport = LinePort(nw.pos, se.pos) self._ports.append(self._lport) def draw(self, context): super(PortoBox, self).draw(context) c = context.cairo if context.hovered: c.set_source_rgba(0.0, 0.8, 0, 0.8) else: c.set_source_rgba(0.9, 0.0, 0.0, 0.8) # draw movable port x, y = self._hm.pos c.rectangle(x - 20, y - 5, 20, 10) c.rectangle(x - 1, y - 1, 2, 2) # draw static port x, y = self._sport.point c.rectangle(x - 2, y - 2, 4, 4) c.fill_preserve() # draw line port x1, y1 = self._lport.start x2, y2 = self._lport.end c.move_to(x1, y1) c.line_to(x2, y2) c.set_source_rgb(0, 0, 0.8) c.stroke() class Text(Item): """ Simple item showing some text on the canvas. """ def __init__(self, text=None, plain=False, multiline=False, align_x=1, align_y=-1): super(Text, self).__init__() self.text = text is None and "Hello" or text self.plain = plain self.multiline = multiline self.align_x = align_x self.align_y = align_y def draw(self, context): cr = context.cairo if self.multiline: text_multiline(cr, 0, 0, self.text) elif self.plain: cr.show_text(self.text) else: text_align(cr, 0, 0, self.text, self.align_x, self.align_y) def point(self, pos): return 0 class FatLine(Item): """ Simple, vertical line with two handles. todo: rectangle port instead of line port would be nicer """ def __init__(self): super(FatLine, self).__init__() self._handles.extend((Handle(), Handle())) h1, h2 = self._handles self._ports.append(LinePort(h1.pos, h2.pos)) self.constraint(vertical=(h1.pos, h2.pos)) self.constraint(above=(h1.pos, h2.pos), delta=20) def _set_height(self, height): h1, h2 = self._handles h2.pos.y = height def _get_height(self): h1, h2 = self._handles return h2.pos.y height = property(_get_height, _set_height) def draw(self, context): cr = context.cairo cr.set_line_width(10) h1, h2 = self.handles() cr.move_to(0, 0) cr.line_to(0, self.height) cr.stroke() class Circle(Item): def __init__(self): super(Circle, self).__init__() self._handles.extend((Handle(), Handle())) def _set_radius(self, r): h1, h2 = self._handles h2.pos.x = r h2.pos.y = r def _get_radius(self): h1, h2 = self._handles p1, p2 = h1.pos, h2.pos return ((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2) ** 0.5 radius = property(_get_radius, _set_radius) def setup_canvas(self): super(Circle, self).setup_canvas() h1, h2 = self._handles h1.movable = False def point(self, pos): h1, _ = self._handles p1 = h1.pos x, y = pos dist = ((x - p1.x) ** 2 + (y - p1.y) ** 2) ** 0.5 return dist - self.radius def draw(self, context): cr = context.cairo path_ellipse(cr, 0, 0, 2 * self.radius, 2 * self.radius) cr.stroke() # vim: sw=4:et:ai PK!uE?gaphas/freehand.py""" Cairo context using Steve Hanov's freehand drawing code. # Crazyline. By Steve Hanov, 2008 Released to the public domain. # The idea is to draw a curve, setting two control points at # random close to each side of the line. The longer the line, the # sloppier it's drawn. See: http://stevehanov.ca/blog/index.php?id=33 and http://stevehanov.ca/blog/index.php?id=93 """ from __future__ import absolute_import from builtins import object from math import sqrt from random import Random from .painter import Context class FreeHandCairoContext(object): KAPPA = 0.5522847498 def __init__(self, cr, sloppiness=0.5): """ Create context with given sloppiness. Range [0..2.0] gives acceptable results. * Draftsman: 0.0 * Artist: 0.25 * Cartoonist: 0.5 * Child: 1.0 * Drunk: 2.0 """ self.cr = cr self.sloppiness = sloppiness # In range 0.0 .. 2.0 def __getattr__(self, key): return getattr(self.cr, key) def line_to(self, x, y): cr = self.cr sloppiness = self.sloppiness from_x, from_y = cr.get_current_point() # calculate the length of the line. length = sqrt((x - from_x) * (x - from_x) + (y - from_y) * (y - from_y)) # This offset determines how sloppy the line is drawn. It depends on # the length, but maxes out at 20. offset = length / 10 * sloppiness if offset > 20: offset = 20 dev_x, dev_y = cr.user_to_device(x, y) rand = Random((from_x, from_y, dev_x, dev_y, length, offset)).random # Overshoot the destination a little, as one might if drawing with a pen. to_x = x + sloppiness * rand() * offset / 4 to_y = y + sloppiness * rand() * offset / 4 # t1 and t2 are coordinates of a line shifted under or to the right of # our original. t1_x = from_x + offset t1_y = from_y + offset t2_x = to_x + offset t2_y = to_y + offset # create a control point at random along our shifted line. r = rand() control1_x = t1_x + r * (t2_x - t1_x) control1_y = t1_y + r * (t2_y - t1_y) # now make t1 and t2 the coordinates of our line shifted above # and to the left of the original. t1_x = from_x - offset t2_x = to_x - offset t1_y = from_y - offset t2_y = to_y - offset # create a second control point at random along the shifted line. r = rand() control2_x = t1_x + r * (t2_x - t1_x) control2_y = t1_y + r * (t2_y - t1_y) # draw the line! cr.curve_to(control1_x, control1_y, control2_x, control2_y, to_x, to_y) def rel_line_to(self, dx, dy): cr = self.cr from_x, from_y = cr.get_current_point() self.line_to(from_x + dx, from_y + dy) def curve_to(self, x1, y1, x2, y2, x3, y3): cr = self.cr from_x, from_y = cr.get_current_point() dev_x, dev_y = cr.user_to_device(x3, y3) rand = Random((from_x, from_y, dev_x, dev_y, x1, y1, x2, y2, x3, y3)).random r = rand() c1_x = from_x + r * (x1 - from_x) c1_y = from_y + r * (y1 - from_y) r = rand() c2_x = x3 + r * (x2 - x3) c2_y = y3 + r * (y2 - y3) cr.curve_to(c1_x, c1_y, c2_x, c2_y, x3, y3) def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3): cr = self.cr from_x, from_y = cr.get_current_point() self.curve_to( from_x + dx1, from_y + dy1, from_x + dx2, from_y + dy2, from_x + dx3, from_y + dy3, ) def corner_to(self, cx, cy, x, y): cr = self.cr from_x, from_y = cr.get_current_point() # calculate radius of the circle. radius1 = Math.sqrt( (cx - from_x) * (cx - from_x) + (cy - from_y) * (cy - from_y) ) radius2 = Math.sqrt((cx - x) * (cx - x) + (cy - y) * (cy - y)) dev_x, dev_y = cr.user_to_device(x, y) rand = Random((cx, cy, dev_x, dev_y, radius1, radius2)).random # place first control point c1_x = from_x + self.KAPPA * (cx - from_x) + rand() * sloppiness * radius1 / 2 c1_y = from_y + self.KAPPA * (cy - from_y) + rand() * sloppiness * radius1 / 2 # place second control point c2_x = x + self.KAPPA * (cx - x) + rand() * sloppiness * radius2 / 1.5 c2_y = y + self.KAPPA * (cy - y) + rand() * sloppiness * radius2 / 1.5 cr.curve_to(c1_x, c1_y, c2_x, c2_y, x3, y3) def rectangle(self, x, y, width, height): x1 = x + width y1 = y + height self.move_to(x, y) self.line_to(x1, y) self.line_to(x1, y1) self.line_to(x, y1) if self.sloppiness > 0.1: self.line_to(x, y) else: self.close_path() class FreeHandPainter(object): def __init__(self, subpainter, sloppiness=1.0, view=None): self.subpainter = subpainter self.view = view self.sloppiness = sloppiness def set_view(self, view): self.view = view self.subpainter.set_view(view) def paint(self, context): subcontext = Context( cairo=FreeHandCairoContext(context.cairo, self.sloppiness), items=context.items, area=context.area, ) self.subpainter.paint(subcontext) # vi:sw=4:et:ai PK!c'6IIgaphas/geometry.py""" Geometry functions. Rectangle is a utility class for working with rectangles (unions and intersections). A point is represented as a tuple `(x, y)`. """ from __future__ import division from builtins import object from math import sqrt class Rectangle(object): """ Python Rectangle implementation. Rectangles can be added (union), substituted (intersection) and points and rectangles can be tested to be in the rectangle. >>> r1= Rectangle(1,1,5,5) >>> r2 = Rectangle(3,3,6,7) Test if two rectangles intersect: >>> if r1 - r2: 'yes' 'yes' >>> r1, r2 = Rectangle(1,2,3,4), Rectangle(1,2,3,4) >>> r1 == r2 True >>> r = Rectangle(-5, 3, 10, 8) >>> r.width = 2 >>> r Rectangle(-5, 3, 2, 8) >>> r = Rectangle(-5, 3, 10, 8) >>> r.height = 2 >>> r Rectangle(-5, 3, 10, 2) """ def __init__(self, x=0, y=0, width=None, height=None, x1=0, y1=0): if width is None: self.x = min(x, x1) self.width = abs(x1 - x) else: self.x = x self.width = width if height is None: self.y = min(y, y1) self.height = abs(y1 - y) else: self.y = y self.height = height def _set_x1(self, x1): """ """ width = x1 - self.x if width < 0: width = 0 self.width = width x1 = property(lambda s: s.x + s.width, _set_x1) def _set_y1(self, y1): """ """ height = y1 - self.y if height < 0: height = 0 self.height = height y1 = property(lambda s: s.y + s.height, _set_y1) def expand(self, delta): """ >>> r = Rectangle(-5, 3, 10, 8) >>> r.expand(5) >>> r Rectangle(-10, -2, 20, 18) """ self.x -= delta self.y -= delta self.width += delta * 2 self.height += delta * 2 def __repr__(self): """ >>> Rectangle(5,7,20,25) Rectangle(5, 7, 20, 25) """ if self: return "%s(%g, %g, %g, %g)" % ( self.__class__.__name__, self.x, self.y, self.width, self.height, ) return "%s()" % self.__class__.__name__ def __iter__(self): """ >>> tuple(Rectangle(1,2,3,4)) (1, 2, 3, 4) """ return iter((self.x, self.y, self.width, self.height)) def __getitem__(self, index): """ >>> Rectangle(1,2,3,4)[1] 2 """ return (self.x, self.y, self.width, self.height)[index] def __bool__(self): """ >>> r=Rectangle(1,2,3,4) >>> if r: 'yes' 'yes' >>> r = Rectangle(1,1,0,0) >>> if r: 'no' """ return self.width > 0 and self.height > 0 def __eq__(self, other): return ( (isinstance(self, type(other))) and self.x == other.x and self.y == other.y and self.width == other.width and self.height == self.height ) def __add__(self, obj): """ Create a new Rectangle is the union of the current rectangle with another Rectangle, tuple `(x,y)` or tuple `(x, y, width, height)`. >>> r=Rectangle(5, 7, 20, 25) >>> r + (0, 0) Traceback (most recent call last): ... TypeError: Can only add Rectangle or tuple (x, y, width, height), not (0, 0). >>> r + (20, 30, 40, 50) Rectangle(5, 7, 55, 73) """ return Rectangle(self.x, self.y, self.width, self.height).__iadd__(obj) def __iadd__(self, obj): """ >>> r = Rectangle() >>> r += Rectangle(5, 7, 20, 25) >>> r += (0, 0, 30, 10) >>> r Rectangle(0, 0, 30, 32) >>> r += 'aap' Traceback (most recent call last): ... TypeError: Can only add Rectangle or tuple (x, y, width, height), not 'aap'. """ try: x, y, width, height = obj except ValueError: raise TypeError( "Can only add Rectangle or tuple (x, y, width, height), not %s." % repr(obj) ) x1, y1 = x + width, y + height if self: ox1, oy1 = self.x + self.width, self.y + self.height self.x = min(self.x, x) self.y = min(self.y, y) self.x1 = max(ox1, x1) self.y1 = max(oy1, y1) else: self.x, self.y, self.width, self.height = x, y, width, height return self def __sub__(self, obj): """ Create a new Rectangle is the union of the current rectangle with another Rectangle or tuple (x, y, width, height). >>> r = Rectangle(5, 7, 20, 25) >>> r - (20, 30, 40, 50) Rectangle(20, 30, 5, 2) >>> r - (30, 40, 40, 50) Rectangle() """ return Rectangle(self.x, self.y, self.width, self.height).__isub__(obj) def __isub__(self, obj): """ >>> r = Rectangle() >>> r -= Rectangle(5, 7, 20, 25) >>> r -= (0, 0, 30, 10) >>> r Rectangle(5, 7, 20, 3) >>> r -= 'aap' Traceback (most recent call last): ... TypeError: Can only substract Rectangle or tuple (x, y, width, height), not 'aap'. """ try: x, y, width, height = obj except ValueError: raise TypeError( "Can only substract Rectangle or tuple (x, y, width, height), not %s." % repr(obj) ) x1, y1 = x + width, y + height if self: ox1, oy1 = self.x + self.width, self.y + self.height self.x = max(self.x, x) self.y = max(self.y, y) self.x1 = min(ox1, x1) self.y1 = min(oy1, y1) else: self.x, self.y, self.width, self.height = x, y, width, height return self def __contains__(self, obj): """ Check if a point `(x, y)` in inside rectangle `(x, y, width, height)` or if a rectangle instance is inside with the rectangle. >>> r=Rectangle(10, 5, 12, 12) >>> (0, 0) in r False >>> (10, 6) in r True >>> (12, 12) in r True >>> (100, 4) in r False >>> (11, 6, 5, 5) in r True >>> (11, 6, 15, 15) in r False >>> Rectangle(11, 6, 5, 5) in r True >>> Rectangle(11, 6, 15, 15) in r False >>> 'aap' in r Traceback (most recent call last): ... TypeError: Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not 'aap'. """ try: x, y, width, height = obj x1, y1 = x + width, y + width except ValueError: # point try: x, y = obj x1, y1 = obj except ValueError: raise TypeError( "Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not %s." % repr(obj) ) return x >= self.x and x1 <= self.x1 and y >= self.y and y1 <= self.y1 def distance_point_point(point1, point2=(0.0, 0.0)): """ Return the distance from point ``point1`` to ``point2``. >>> '%.3f' % distance_point_point((0,0), (1,1)) '1.414' """ dx = point1[0] - point2[0] dy = point1[1] - point2[1] return sqrt(dx * dx + dy * dy) def distance_point_point_fast(point1, point2=(0.0, 0.0)): """ Return the distance from point ``point1`` to ``point2``. This version is faster than ``distance_point_point()``, but less precise. >>> distance_point_point_fast((0,0), (1,1)) 2 """ dx = point1[0] - point2[0] dy = point1[1] - point2[1] return abs(dx) + abs(dy) def distance_rectangle_point(rect, point): """ Return the distance (fast) from a rectangle ``(x, y, width,height)`` to a ``point``. >>> distance_rectangle_point(Rectangle(0, 0, 10, 10), (11, -1)) 2 >>> distance_rectangle_point((0, 0, 10, 10), (11, -1)) 2 >>> distance_rectangle_point((0, 0, 10, 10), (-1, 11)) 2 """ dx = dy = 0 px, py = point rx, ry, rw, rh = tuple(rect) if px < rx: dx = rx - px elif px > rx + rw: dx = px - (rx + rw) if py < ry: dy = ry - py elif py > ry + rh: dy = py - (ry + rh) return abs(dx) + abs(dy) def point_on_rectangle(rect, point, border=False): """ Return the point on which ``point`` can be projecten on the rectangle. ``border = True`` will make sure the point is bound to the border of the reactangle. Otherwise, if the point is in the rectangle, it's okay. >>> point_on_rectangle(Rectangle(0, 0, 10, 10), (11, -1)) (10, 0) >>> point_on_rectangle((0, 0, 10, 10), (5, 12)) (5, 10) >>> point_on_rectangle(Rectangle(0, 0, 10, 10), (12, 5)) (10, 5) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (3, 4)) (3, 4) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (0, 3)) (1, 3) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (4, 3)) (4, 3) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (4, 9), border=True) (4, 11) >>> point_on_rectangle((1, 1, 10, 10), (4, 6), border=True) (1, 6) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (5, 3), border=True) (5, 1) >>> point_on_rectangle(Rectangle(1, 1, 10, 10), (8, 4), border=True) (11, 4) >>> point_on_rectangle((1, 1, 10, 100), (5, 8), border=True) (1, 8) >>> point_on_rectangle((1, 1, 10, 100), (5, 98), border=True) (5, 101) """ px, py = point rx, ry, rw, rh = tuple(rect) x_inside = y_inside = False if px < rx: px = rx elif px > rx + rw: px = rx + rw elif border: x_inside = True if py < ry: py = ry elif py > ry + rh: py = ry + rh elif border: y_inside = True if x_inside and y_inside: # Find point on side closest to the point if min(abs(rx - px), abs(rx + rw - px)) > min(abs(ry - py), abs(ry + rh - py)): if py < ry + rh / 2.0: py = ry else: py = ry + rh else: if px < rx + rw / 2.0: px = rx else: px = rx + rw return px, py def distance_line_point(line_start, line_end, point): """ Calculate the distance of a ``point`` from a line. The line is marked by begin and end point ``line_start`` and ``line_end``. A tuple is returned containing the distance and point on the line. >>> distance_line_point((0., 0.), (2., 4.), point=(3., 4.)) (1.0, (2.0, 4.0)) >>> distance_line_point((0., 0.), (2., 4.), point=(-1., 0.)) (1.0, (0.0, 0.0)) >>> distance_line_point((0., 0.), (2., 4.), point=(1., 2.)) (0.0, (1.0, 2.0)) >>> d, p = distance_line_point((0., 0.), (2., 4.), point=(2., 2.)) >>> '%.3f' % d '0.894' >>> '(%.3f, %.3f)' % p '(1.200, 2.400)' """ # The original end point: true_line_end = line_end # "Move" the line, so it "starts" on (0, 0) line_end = line_end[0] - line_start[0], line_end[1] - line_start[1] point = point[0] - line_start[0], point[1] - line_start[1] line_len_sqr = line_end[0] * line_end[0] + line_end[1] * line_end[1] # Both points are very near each other. if line_len_sqr < 0.0001: return distance_point_point(point), line_start projlen = (line_end[0] * point[0] + line_end[1] * point[1]) / line_len_sqr if projlen < 0.0: # Closest point is the start of the line. return distance_point_point(point), line_start elif projlen > 1.0: # Point has a projection after the line_end. return distance_point_point(point, line_end), true_line_end else: # Projection is on the line. multiply the line_end with the projlen # factor to obtain the point on the line. proj = line_end[0] * projlen, line_end[1] * projlen return ( distance_point_point((proj[0] - point[0], proj[1] - point[1])), (line_start[0] + proj[0], line_start[1] + proj[1]), ) def intersect_line_line(line1_start, line1_end, line2_start, line2_end): """ Find the point where the lines (segments) defined by ``(line1_start, line1_end)`` and ``(line2_start, line2_end)`` intersect. If no intersection occurs, ``None`` is returned. >>> intersect_line_line((3, 0), (8, 10), (0, 0), (10, 10)) (6, 6) >>> intersect_line_line((0, 0), (10, 10), (3, 0), (8, 10)) (6, 6) >>> intersect_line_line((0, 0), (10, 10), (8, 10), (3, 0)) (6, 6) >>> intersect_line_line((8, 10), (3, 0), (0, 0), (10, 10)) (6, 6) >>> intersect_line_line((0, 0), (0, 10), (3, 0), (8, 10)) >>> intersect_line_line((0, 0), (0, 10), (3, 0), (3, 10)) Ticket #168: >>> intersect_line_line((478.0, 117.0), (478.0, 166.0), (527.5, 141.5), (336.5, 139.5)) (478.5, 141.48167539267016) >>> intersect_line_line((527.5, 141.5), (336.5, 139.5), (478.0, 117.0), (478.0, 166.0)) (478.5, 141.48167539267016) This is a Python translation of the ``lines_intersect``, C Code from Graphics Gems II, Academic Press, Inc. The original routine was written by Mukesh Prasad. EULA: The Graphics Gems code is copyright-protected. In other words, you cannot claim the text of the code as your own and resell it. Using the code is permitted in any program, product, or library, non-commercial or commercial. Giving credit is not required, though is a nice gesture. The code comes as-is, and if there are any flaws or problems with any Gems code, nobody involved with Gems - authors, editors, publishers, or webmasters - are to be held responsible. Basically, don't be a jerk, and remember that anything free comes with no guarantee. """ # # This function computes whether two line segments, # respectively joining the input points (x1,y1) -- (x2,y2) # and the input points (x3,y3) -- (x4,y4) intersect. # If the lines intersect, the output variables x, y are # set to coordinates of the point of intersection. # # All values are in integers. The returned value is rounded # to the nearest integer point. # # If non-integral grid points are relevant, the function # can easily be transformed by substituting floating point # calculations instead of integer calculations. # # Entry # x1, y1, x2, y2 Coordinates of endpoints of one segment. # x3, y3, x4, y4 Coordinates of endpoints of other segment. # # Exit # x, y Coordinates of intersection point. # # The value returned by the function is one of: # # DONT_INTERSECT 0 # DO_INTERSECT 1 # COLLINEAR 2 # # Error conditions: # # Depending upon the possible ranges, and particularly on 16-bit # computers, care should be taken to protect from overflow. # # In the following code, 'long' values have been used for this # purpose, instead of 'int'. # x1, y1 = line1_start x2, y2 = line1_end x3, y3 = line2_start x4, y4 = line2_end # long a1, a2, b1, b2, c1, c2; /* Coefficients of line eqns. */ # long r1, r2, r3, r4; /* 'Sign' values */ # long denom, offset, num; /* Intermediate values */ # Compute a1, b1, c1, where line joining points 1 and 2 # is "a1 x + b1 y + c1 = 0". a1 = y2 - y1 b1 = x1 - x2 c1 = x2 * y1 - x1 * y2 # Compute r3 and r4. r3 = a1 * x3 + b1 * y3 + c1 r4 = a1 * x4 + b1 * y4 + c1 # Check signs of r3 and r4. If both point 3 and point 4 lie on # same side of line 1, the line segments do not intersect. if r3 and r4 and (r3 * r4) >= 0: return None # ( DONT_INTERSECT ) # Compute a2, b2, c2 a2 = y4 - y3 b2 = x3 - x4 c2 = x4 * y3 - x3 * y4 # Compute r1 and r2 r1 = a2 * x1 + b2 * y1 + c2 r2 = a2 * x2 + b2 * y2 + c2 # Check signs of r1 and r2. If both point 1 and point 2 lie # on same side of second line segment, the line segments do # not intersect. if r1 and r2 and (r1 * r2) >= 0: # SAME_SIGNS( r1, r2 )) return None # ( DONT_INTERSECT ) # Line segments intersect: compute intersection point. # The denom / 2 is to get rounding instead of truncating. It # is added or subtracted to the numerator, depending upon the # sign of the numerator. denom = a1 * b2 - a2 * b1 x_num = b1 * c2 - b2 * c1 y_num = a2 * c1 - a1 * c2 if not denom: return None # ( COLLINEAR ) elif isinstance(denom, float): # denom is float, use normal division offset = abs(denom) / 2 x = ((x_num < 0) and (x_num - offset) or (x_num + offset)) / denom y = ((y_num < 0) and (y_num - offset) or (y_num + offset)) / denom else: # denom is int, use integer division offset = abs(denom) // 2 x = ((x_num < 0) and (x_num - offset) or (x_num + offset)) // denom y = ((y_num < 0) and (y_num - offset) or (y_num + offset)) // denom return x, y def rectangle_contains(inner, outer): """ Returns True if ``inner`` rect is contained in ``outer`` rect. """ ix, iy, iw, ih = inner ox, oy, ow, oh = outer return ox <= ix and oy <= iy and ox + ow >= ix + iw and oy + oh >= iy + ih def rectangle_intersects(recta, rectb): """ Return True if ``recta`` and ``rectb`` intersect. >>> rectangle_intersects((5,5,20, 20), (10, 10, 1, 1)) True >>> rectangle_intersects((40, 30, 10, 1), (1, 1, 1, 1)) False """ ax, ay, aw, ah = recta bx, by, bw, bh = rectb return ax <= bx + bw and ax + aw >= bx and ay <= by + bh and ay + ah >= by def rectangle_clip(recta, rectb): """ Return the clipped rectangle of ``recta`` and ``rectb``. If they do not intersect, ``None`` is returned. >>> rectangle_clip((0, 0, 20, 20), (10, 10, 20, 20)) (10, 10, 10, 10) """ ax, ay, aw, ah = recta bx, by, bw, bh = rectb x = max(ax, bx) y = max(ay, by) w = min(ax + aw, bx + bw) - x h = min(ay + ah, by + bh) - y if w < 0 or h < 0: return None return (x, y, w, h) # vim:sw=4:et:ai PK!*PX,z"z"gaphas/guide.py""" Module implements guides when moving items and handles around. """ from __future__ import division from builtins import map from builtins import object from functools import reduce from simplegeneric import generic from gaphas.aspect import InMotion, HandleInMotion, PaintFocused from gaphas.aspect import ItemInMotion, ItemHandleInMotion, ItemPaintFocused from gaphas.item import Item, Element, Line class ItemGuide(object): """ Get edges on an item, on which we can align the items. """ def __init__(self, item): self.item = item def horizontal(self): """ Return horizontal edges (on y axis) """ return () def vertical(self): """ Return vertical edges (on x axis) """ return () Guide = generic(ItemGuide) @Guide.when_type(Element) class ElementGuide(ItemGuide): """Guide to align Element items. """ def horizontal(self): y = self.item.height return (0, y / 2, y) def vertical(self): x = self.item.width return (0, x / 2, x) @Guide.when_type(Line) class LineGuide(ItemGuide): """Guide for orthogonal lines. """ def horizontal(self): line = self.item if line.orthogonal: if line.horizontal: for i, h in enumerate(line.handles()): if i % 2 == 1: yield h.pos.y else: for i, h in enumerate(line.handles()): if i % 2 == 0 and i > 0: yield h.pos.y def vertical(self): line = self.item if line.orthogonal: if line.horizontal: for i, h in enumerate(line.handles()): if i % 2 == 0 and i > 0: yield h.pos.x else: for i, h in enumerate(line.handles()): if i % 2 == 1: yield h.pos.x class Guides(object): def __init__(self, v, h): self.v = v self.h = h def vertical(self): return self.v def horizontal(self): return self.h class GuideMixin(object): """ Helper methods for guides. """ MARGIN = 2 def find_vertical_guides(self, item_vedges, pdx, height, excluded_items): view = self.view item = self.item i2v = self.view.get_matrix_i2v margin = self.MARGIN items = [] for x in item_vedges: items.append( view.get_items_in_rectangle((x - margin, 0, margin * 2, height)) ) try: guides = list( map(Guide, reduce(set.union, list(map(set, items))) - excluded_items) ) except TypeError: guides = [] vedges = set() for g in guides: for x in g.vertical(): vedges.add(i2v(g.item).transform_point(x, 0)[0]) dx, edges_x = self.find_closest(item_vedges, vedges) return dx, edges_x def find_horizontal_guides(self, item_hedges, pdy, width, excluded_items): view = self.view item = self.item i2v = self.view.get_matrix_i2v margin = self.MARGIN items = [] for y in item_hedges: items.append( view.get_items_in_rectangle((0, y - margin, width, margin * 2)) ) try: guides = list( map(Guide, reduce(set.union, list(map(set, items))) - excluded_items) ) except TypeError: guides = [] # Translate edges to canvas or view coordinates hedges = set() for g in guides: for y in g.horizontal(): hedges.add(i2v(g.item).transform_point(0, y)[1]) dy, edges_y = self.find_closest(item_hedges, hedges) return dy, edges_y def get_excluded_items(self): """ Get a set of items excluded from guide calculation. """ item = self.item view = self.view excluded_items = set(view.canvas.get_all_children(item)) excluded_items.add(item) excluded_items.update(view.selected_items) return excluded_items def get_view_dimensions(self): try: allocation = self.view.get_allocation() except AttributeError as e: return 0, 0 return allocation.width, allocation.height def queue_draw_guides(self): view = self.view try: guides = view.guides except AttributeError: return w, h = self.get_view_dimensions() for x in guides.vertical(): view.queue_draw_area(x - 1, 0, x + 2, h) for y in guides.horizontal(): view.queue_draw_area(0, y - 1, w, y + 2) def find_closest(self, item_edges, edges): delta = 0 min_d = 1000 closest = [] for e in edges: for ie in item_edges: d = abs(e - ie) if d < min_d: min_d = d delta = e - ie closest = [e] elif d == min_d: closest.append(e) if min_d <= self.MARGIN: return delta, closest else: return 0, () @InMotion.when_type(Item) class GuidedItemInMotion(GuideMixin, ItemInMotion): """ Move the item, lock position on any element that's located at the same location. """ def move(self, pos): item = self.item view = self.view transform = view.get_matrix_i2v(item).transform_point w, h = self.get_view_dimensions() px, py = pos pdx, pdy = px - self.last_x, py - self.last_y excluded_items = self.get_excluded_items() item_guide = Guide(item) item_vedges = [transform(x, 0)[0] + pdx for x in item_guide.vertical()] dx, edges_x = self.find_vertical_guides(item_vedges, pdx, h, excluded_items) item_hedges = [transform(0, y)[1] + pdy for y in item_guide.horizontal()] dy, edges_y = self.find_horizontal_guides(item_hedges, pdy, w, excluded_items) newpos = px + dx, py + dy # Call super class, with new position sink = super(GuidedItemInMotion, self).move(newpos) self.queue_draw_guides() view.guides = Guides(edges_x, edges_y) self.queue_draw_guides() return sink def stop_move(self): self.queue_draw_guides() try: del self.view.guides except AttributeError: # No problem if guides do not exist. pass @HandleInMotion.when_type(Item) class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion): """Move a handle and lock the position of other elements. Locks the position of another element that's located at the same position. """ def move(self, pos): sink = super(GuidedItemHandleInMotion, self).move(pos) if not sink: item = self.item handle = self.handle view = self.view x, y = pos v2i = view.get_matrix_v2i(item) excluded_items = self.get_excluded_items() w, h = self.get_view_dimensions() dx, edges_x = self.find_vertical_guides((x,), 0, h, excluded_items) dy, edges_y = self.find_horizontal_guides((y,), 0, w, excluded_items) newpos = x + dx, y + dy x, y = v2i.transform_point(*newpos) self.handle.pos = (x, y) # super(GuidedItemHandleInMotion, self).move(newpos) self.queue_draw_guides() view.guides = Guides(edges_x, edges_y) self.queue_draw_guides() item.request_update() def stop_move(self): self.queue_draw_guides() try: del self.view.guides except AttributeError: # No problem if guides do not exist. pass @PaintFocused.when_type(Item) class GuidePainter(ItemPaintFocused): def paint(self, context): try: guides = self.view.guides except AttributeError: return cr = context.cairo view = self.view allocation = view.get_allocation() w, h = allocation.width, allocation.height cr.save() try: cr.set_line_width(1) cr.set_source_rgba(0.0, 0.0, 1.0, 0.6) for g in guides.vertical(): cr.move_to(g, 0) cr.line_to(g, h) cr.stroke() for g in guides.horizontal(): cr.move_to(0, g) cr.line_to(w, g) cr.stroke() finally: cr.restore() # vim:sw=4:et:ai PK!OSWWgaphas/item.py""" Basic items. """ from __future__ import absolute_import from builtins import map from builtins import object from builtins import range from builtins import zip from functools import reduce from math import atan2 from weakref import WeakKeyDictionary try: # python 3.0 (better be prepared) from weakref import WeakSet except ImportError: from .weakset import WeakSet from .matrix import Matrix from .geometry import distance_line_point, distance_rectangle_point from .connector import Handle, LinePort from .solver import solvable, WEAK, VERY_STRONG, REQUIRED from .constraint import ( EqualsConstraint, LessThanConstraint, LineConstraint, LineAlignConstraint, ) from .state import observed, reversible_method, reversible_pair, reversible_property class Item(object): """ Base class (or interface) for items on a canvas.Canvas. Attributes: - matrix: item's transformation matrix - canvas: canvas, which owns an item - constraints: list of item constraints, automatically registered when the item is added to a canvas; may be extended in subclasses Private: - _canvas: canvas, which owns an item - _handles: list of handles owned by an item - _ports: list of ports, connectable areas of an item - _matrix_i2c: item to canvas coordinates matrix - _matrix_c2i: canvas to item coordinates matrix - _matrix_i2v: item to view coordinates matrices - _matrix_v2i: view to item coordinates matrices - _sort_key: used to sort items - _canvas_projections: used to sort items """ def __init__(self): self._canvas = None self._matrix = Matrix() self._handles = [] self._constraints = [] self._ports = [] # used by gaphas.canvas.Canvas to hold conversion matrices self._matrix_i2c = None self._matrix_c2i = None # used by gaphas.view.GtkView to hold item 2 view matrices (view=key) self._matrix_i2v = WeakKeyDictionary() self._matrix_v2i = WeakKeyDictionary() self._canvas_projections = WeakSet() @observed def _set_canvas(self, canvas): """ Set the canvas. Should only be called from Canvas.add and Canvas.remove(). """ assert not canvas or not self._canvas or self._canvas is canvas if self._canvas: self.teardown_canvas() self._canvas = canvas if canvas: self.setup_canvas() canvas = reversible_property( lambda s: s._canvas, _set_canvas, doc="Canvas owning this item" ) constraints = property(lambda s: s._constraints, doc="Item constraints") def setup_canvas(self): """ Called when the canvas is set for the item. This method can be used to create constraints. """ add = self.canvas.solver.add_constraint for c in self._constraints: add(c) def teardown_canvas(self): """ Called when the canvas is unset for the item. This method can be used to dispose constraints. """ self.canvas.disconnect_item(self) remove = self.canvas.solver.remove_constraint for c in self._constraints: remove(c) @observed def _set_matrix(self, matrix): """ Set the conversion matrix (parent -> item) """ if not isinstance(matrix, Matrix): matrix = Matrix(*matrix) self._matrix = matrix matrix = reversible_property(lambda s: s._matrix, _set_matrix) def request_update(self, update=True, matrix=True): if self._canvas: self._canvas.request_update(self, update=update, matrix=matrix) def pre_update(self, context): """ Perform any changes before item update here, for example: - change matrix - move handles Gaphas does not guarantee that any canvas invariant is valid at this point (i.e. constraints are not solved, first handle is not in position (0, 0), etc). """ pass def post_update(self, context): """ Method called after item update. If some variables should be used during drawing or in another update, then they should be calculated in post method. Changing matrix or moving handles programmatically is really not advised to be performed here. All canvas invariants are true. """ pass def normalize(self): """ Update handle positions of the item, so the first handle is always located at (0, 0). Note that, since this method basically does some housekeeping during the update phase, there's no need to keep track of the changes. Alternative implementation can also be created, e.g. set (0, 0) in the center of a circle or change it depending on the location of a rotation point. Returns ``True`` if some updates have been done, ``False`` otherwise. See ``canvas._normalize()`` for tests. """ updated = False handles = self._handles if handles: x, y = list(map(float, handles[0].pos)) if x: self.matrix.translate(x, 0) updated = True for h in handles: h.pos.x -= x if y: self.matrix.translate(0, y) updated = True for h in handles: h.pos.y -= y return updated def draw(self, context): """ Render the item to a canvas view. Context contains the following attributes: - cairo: the Cairo Context use this one to draw - view: the view that is to be rendered to - selected, focused, hovered, dropzone: view state of items (True/False) - draw_all: a request to draw everything, for bounding box calculations """ pass def handles(self): """ Return a list of handles owned by the item. """ return self._handles def ports(self): """ Return list of ports. """ return self._ports def point(self, pos): """ Get the distance from a point (``x``, ``y``) to the item. ``x`` and ``y`` are in item coordinates. """ pass def constraint( self, horizontal=None, vertical=None, left_of=None, above=None, line=None, delta=0.0, align=None, ): """ Utility (factory) method to create item's internal constraint between two positions or between a position and a line. Position is a tuple of coordinates, i.e. ``(2, 4)``. Line is a tuple of positions, i.e. ``((2, 3), (4, 2))``. This method shall not be used to create constraints between two different items. Created constraint is returned. :Parameters: horizontal=(p1, p2) Keep positions ``p1`` and ``p2`` aligned horizontally. vertical=(p1, p2) Keep positions ``p1`` and ``p2`` aligned vertically. left_of=(p1, p2) Keep position ``p1`` on the left side of position ``p2``. above=(p1, p2) Keep position ``p1`` above position ``p2``. line=(p, l) Keep position ``p`` on line ``l``. """ cc = None # created constraint if horizontal: p1, p2 = horizontal cc = EqualsConstraint(p1[1], p2[1], delta) elif vertical: p1, p2 = vertical cc = EqualsConstraint(p1[0], p2[0], delta) elif left_of: p1, p2 = left_of cc = LessThanConstraint(p1[0], p2[0], delta) elif above: p1, p2 = above cc = LessThanConstraint(p1[1], p2[1], delta) elif line: pos, l = line if align is None: cc = LineConstraint(line=l, point=pos) else: cc = LineAlignConstraint(line=l, point=pos, align=align, delta=delta) else: raise ValueError("Constraint incorrectly specified") assert cc is not None self._constraints.append(cc) return cc def __getstate__(self): """ Persist all, but calculated values (``_matrix_?2?``). """ d = dict(self.__dict__) for n in ("_matrix_i2c", "_matrix_c2i", "_matrix_i2v", "_matrix_v2i"): try: del d[n] except KeyError: pass d["_canvas_projections"] = tuple(self._canvas_projections) return d def __setstate__(self, state): """ Set state. No ``__init__()`` is called. """ for n in ("_matrix_i2c", "_matrix_c2i"): setattr(self, n, None) for n in ("_matrix_i2v", "_matrix_v2i"): setattr(self, n, WeakKeyDictionary()) self.__dict__.update(state) self._canvas_projections = WeakSet(state["_canvas_projections"]) [NW, NE, SE, SW] = list(range(4)) class Element(Item): """ An Element has 4 handles (for a start):: NW +---+ NE | | SW +---+ SE """ min_width = solvable(strength=REQUIRED, varname="_min_width") min_height = solvable(strength=REQUIRED, varname="_min_height") def __init__(self, width=10, height=10): super(Element, self).__init__() self._handles = [h(strength=VERY_STRONG) for h in [Handle] * 4] handles = self._handles h_nw = handles[NW] h_ne = handles[NE] h_sw = handles[SW] h_se = handles[SE] # Share variables h_sw.pos.set_x(h_nw.pos.x) h_se.pos.set_x(h_ne.pos.x) h_ne.pos.set_y(h_nw.pos.y) h_se.pos.set_y(h_sw.pos.y) # edge of element define default element ports self._ports = [ LinePort(h_nw.pos, h_ne.pos), LinePort(h_ne.pos, h_se.pos), LinePort(h_se.pos, h_sw.pos), LinePort(h_sw.pos, h_nw.pos), ] # initialize min_x variables self.min_width, self.min_height = 10, 10 # create minimal size constraints self.constraint(left_of=(h_nw.pos, h_se.pos), delta=self._min_width) self.constraint(above=(h_nw.pos, h_se.pos), delta=self._min_height) self.width = width self.height = height # TODO: constraints that calculate width and height based on handle pos # self.constraints.append(EqualsConstraint(p1[1], p2[1], delta)) def setup_canvas(self): super(Element, self).setup_canvas() # Trigger solver to honour width/height by SE handle pos self._handles[SE].pos.x.dirty() self._handles[SE].pos.y.dirty() def _set_width(self, width): """ >>> b=Element() >>> b.width = 20 >>> b.width 20.0 >>> b._handles[NW].pos.x Variable(0, 40) >>> b._handles[SE].pos.x Variable(20, 40) """ h = self._handles h[SE].pos.x = h[NW].pos.x + width def _get_width(self): """ Width of the box, calculated as the distance from the left and right handle. """ h = self._handles return float(h[SE].pos.x) - float(h[NW].pos.x) width = property(_get_width, _set_width) def _set_height(self, height): """ >>> b=Element() >>> b.height = 20 >>> b.height 20.0 >>> b.height = 2 >>> b.height 2.0 >>> b._handles[NW].pos.y Variable(0, 40) >>> b._handles[SE].pos.y Variable(2, 40) """ h = self._handles h[SE].pos.y = h[NW].pos.y + height def _get_height(self): """ Height. """ h = self._handles return float(h[SE].pos.y) - float(h[NW].pos.y) height = property(_get_height, _set_height) def normalize(self): """ Normalize only NW and SE handles """ updated = False handles = self._handles handles = (handles[NW], handles[SE]) x, y = list(map(float, handles[0].pos)) if x: self.matrix.translate(x, 0) updated = True for h in handles: h.pos.x -= x if y: self.matrix.translate(0, y) updated = True for h in handles: h.pos.y -= y return updated def point(self, pos): """ Distance from the point (x, y) to the item. >>> e = Element() >>> e.point((20, 10)) 10.0 """ h = self._handles pnw, pse = h[NW].pos, h[SE].pos return distance_rectangle_point( list(map(float, (pnw.x, pnw.y, pse.x, pse.y))), pos ) 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 closest_segment(self, pos): """ Obtain a tuple (distance, point_on_line, segment). Distance is the distance from point to the closest line segment Point_on_line is the reflection of the point on the line. Segment is the line segment closest to (x, y) >>> a = Line() >>> a.closest_segment((4, 5)) (0.7071067811865476, (4.5, 4.5), 0) """ h = self._handles hpos = list(map(getattr, h, ["pos"] * len(h))) # create a list of (distance, point_on_line) tuples: distances = list( map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1)) ) distances, pols = list(zip(*distances)) return reduce(min, list(zip(distances, pols, list(range(len(distances)))))) 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' """ distance, point, segment = self.closest_segment(pos) 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() ### debug code to draw line ports ### cr.set_line_width(1) ### cr.set_source_rgb(1.0, 0.0, 0.0) ### for p in self.ports(): ### cr.move_to(*p.start) ### cr.line_to(*p.end) ### cr.stroke() __test__ = {"Line._set_orthogonal": Line._set_orthogonal} # vim: sw=4:et:ai PK! 6  gaphas/matrix.py""" Some Gaphor specific updates to the canvas. This is done by setting the correct properties on gaphas' modules. Matrix ------ Small utility class wrapping cairo.Matrix. The `Matrix` class adds state preservation capabilities. """ from __future__ import absolute_import from __future__ import division from builtins import object __version__ = "$Revision$" # $HeadURL$ import cairo from .state import observed, reversible_method class Matrix(object): """ Matrix wrapper. This version sends @observed messages on state changes >>> cairo.Matrix() cairo.Matrix(1, 0, 0, 1, 0, 0) >>> Matrix() Matrix(1, 0, 0, 1, 0, 0) """ def __init__(self, xx=1.0, yx=0.0, xy=0.0, yy=1.0, x0=0.0, y0=0.0): self._matrix = cairo.Matrix(xx, yx, xy, yy, x0, y0) @staticmethod def init_rotate(radians): return cairo.Matrix.init_rotate(radians) @observed def invert(self): return self._matrix.invert() @observed def rotate(self, radians): return self._matrix.rotate(radians) @observed def scale(self, sx, sy): return self._matrix.scale(sx, sy) @observed def translate(self, tx, ty): self._matrix.translate(tx, ty) @observed def multiply(self, m): return self._matrix.multiply(m) reversible_method(invert, invert) reversible_method(rotate, rotate, {"radians": lambda radians: -radians}) reversible_method(scale, scale, {"sx": lambda sx: 1 / sx, "sy": lambda sy: 1 / sy}) reversible_method( translate, translate, {"tx": lambda tx: -tx, "ty": lambda ty: -ty} ) def transform_distance(self, dx, dy): self._matrix.transform_distance(dx, dy) def transform_point(self, x, y): self._matrix.transform_point(x, y) def __eq__(self, other): return self._matrix.__eq__(other) def __ne__(self, other): return self._matrix.__ne__(other) def __le__(self, other): return self._matrix.__le__(other) def __lt__(self, other): return self._matrix.__lt__(other) def __ge__(self, other): return self._matrix.__ge__(other) def __gt__(self, other): return self._matrix.__gt__(other) def __getitem__(self, val): return self._matrix.__getitem__(val) @observed def __mul__(self, other): return self._matrix.__mul__(other) @observed def __rmul__(self, other): return self._matrix.__rmul__(other) def __repr__(self): return "Matrix(%g, %g, %g, %g, %g, %g)" % tuple(self._matrix) # vim:sw=4:et PK!E**gaphas/painter.py""" The painter module provides different painters for parts of the canvas. Painters can be swapped in and out. Each painter takes care of a layer in the canvas (such as grid, items and handles). """ from __future__ import division from builtins import object __version__ = "$Revision$" # $HeadURL$ from cairo import Matrix, ANTIALIAS_NONE, LINE_JOIN_ROUND from gaphas.canvas import Context from gaphas.geometry import Rectangle from gaphas.item import Line from gaphas.aspect import PaintFocused DEBUG_DRAW_BOUNDING_BOX = False # The tolerance for Cairo. Bigger values increase speed and reduce accuracy # (default: 0.1) TOLERANCE = 0.8 class Painter(object): """ Painter interface. """ def __init__(self, view=None): self.view = view def set_view(self, view): self.view = view def paint(self, context): """ Do the paint action (called from the View). """ pass class PainterChain(Painter): """ Chain up a set of painters. like ToolChain. """ def __init__(self, view=None): super(PainterChain, self).__init__(view) self._painters = [] def set_view(self, view): self.view = view for painter in self._painters: painter.set_view(self.view) def append(self, painter): """ Add a painter to the list of painters. """ self._painters.append(painter) painter.set_view(self.view) return self def prepend(self, painter): """ Add a painter to the beginning of the list of painters. """ self._painters.insert(0, painter) def paint(self, context): """ See Painter.paint(). """ for painter in self._painters: painter.paint(context) class DrawContext(Context): """ Special context for draw()'ing the item. The draw-context contains stuff like the cairo context and properties like selected and focused. """ deprecated = False def __init__(self, **kwargs): super(DrawContext, self).__init__(**kwargs) class ItemPainter(Painter): draw_all = False def _draw_item(self, item, cairo, area=None): view = self.view cairo.save() try: cairo.set_matrix(view.matrix) cairo.transform(view.canvas.get_matrix_i2c(item)) item.draw( DrawContext( painter=self, cairo=cairo, _area=area, _item=item, selected=(item in view.selected_items), focused=(item is view.focused_item), hovered=(item is view.hovered_item), dropzone=(item is view.dropzone_item), draw_all=self.draw_all, ) ) finally: cairo.restore() def _draw_items(self, items, cairo, area=None): """ Draw the items. """ for item in items: self._draw_item(item, cairo, area=area) if DEBUG_DRAW_BOUNDING_BOX: self._draw_bounds(item, cairo) def _draw_bounds(self, item, cairo): view = self.view try: b = view.get_item_bounding_box(item) except KeyError: pass # No bounding box right now.. else: cairo.save() cairo.identity_matrix() cairo.set_source_rgb(0.8, 0, 0) cairo.set_line_width(1.0) cairo.rectangle(*b) cairo.stroke() cairo.restore() def paint(self, context): cairo = context.cairo cairo.set_tolerance(TOLERANCE) cairo.set_line_join(LINE_JOIN_ROUND) self._draw_items(context.items, cairo, context.area) class CairoBoundingBoxContext(object): """ Delegate all calls to the wrapped CairoBoundingBoxContext, intercept ``stroke()``, ``fill()`` and a few others so the bounding box of the item involved can be calculated. """ def __init__(self, cairo): self._cairo = cairo self._bounds = None # a Rectangle object def __getattr__(self, key): return getattr(self._cairo, key) def get_bounds(self): """ Return the bounding box. """ return self._bounds or Rectangle() def _update_bounds(self, bounds): if bounds: if not self._bounds: self._bounds = bounds else: self._bounds += bounds def _extents(self, extents_func, line_width=False): """ Calculate the bounding box for a given drawing operation. if ``line_width`` is True, the current line-width is taken into account. """ cr = self._cairo cr.save() cr.identity_matrix() x0, y0, x1, y1 = extents_func() b = Rectangle(x0, y0, x1=x1, y1=y1) cr.restore() if b and line_width: # Do this after the restore(), so we can get the proper width. lw = cr.get_line_width() / 2 d = cr.user_to_device_distance(lw, lw) b.expand(d[0] + d[1]) self._update_bounds(b) return b def fill(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.fill_extents) cr.fill() def fill_preserve(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.fill_extents) def stroke(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.stroke_extents, line_width=True) cr.stroke() def stroke_preserve(self, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: b = self._extents(cr.stroke_extents, line_width=True) def show_text(self, utf8, b=None): """ Interceptor for Cairo drawing method. """ cr = self._cairo if not b: x, y = cr.get_current_point() e = cr.text_extents(utf8) x0, y0 = cr.user_to_device(x + e[0], y + e[1]) x1, y1 = cr.user_to_device(x + e[0] + e[2], y + e[1] + e[3]) b = Rectangle(x0, y0, x1=x1, y1=y1) self._update_bounds(b) cr.show_text(utf8) class BoundingBoxPainter(ItemPainter): """ This specific case of an ItemPainter is used to calculate the bounding boxes (in canvas coordinates) for the items. """ draw_all = True def _draw_item(self, item, cairo, area=None): cairo = CairoBoundingBoxContext(cairo) super(BoundingBoxPainter, self)._draw_item(item, cairo) bounds = cairo.get_bounds() # Update bounding box with handles. view = self.view i2v = view.get_matrix_i2v(item).transform_point for h in item.handles(): cx, cy = i2v(*h.pos) bounds += (cx - 5, cy - 5, 9, 9) bounds.expand(1) view.set_item_bounding_box(item, bounds) def _draw_items(self, items, cairo, area=None): """ Draw the items. """ for item in items: self._draw_item(item, cairo) def paint(self, context): self._draw_items(context.items, context.cairo) class HandlePainter(Painter): """ Draw handles of items that are marked as selected in the view. """ def _draw_handles(self, item, cairo, opacity=None, inner=False): """ Draw handles for an item. The handles are drawn in non-antialiased mode for clarity. """ view = self.view cairo.save() i2v = view.get_matrix_i2v(item) if not opacity: opacity = (item is view.focused_item) and 0.7 or 0.4 cairo.set_line_width(1) get_connection = view.canvas.get_connection for h in item.handles(): if not h.visible: continue # connected and not being moved, see HandleTool.on_button_press if get_connection(h): r, g, b = 1, 0, 0 # connected but being moved, see HandleTool.on_button_press elif get_connection(h): r, g, b = 1, 0.6, 0 elif h.movable: r, g, b = 0, 1, 0 else: r, g, b = 0, 0, 1 cairo.identity_matrix() cairo.set_antialias(ANTIALIAS_NONE) cairo.translate(*i2v.transform_point(*h.pos)) cairo.rectangle(-4, -4, 8, 8) if inner: cairo.rectangle(-3, -3, 6, 6) cairo.set_source_rgba(r, g, b, opacity) cairo.fill_preserve() if h.connectable: cairo.move_to(-2, -2) cairo.line_to(2, 3) cairo.move_to(2, -2) cairo.line_to(-2, 3) cairo.set_source_rgba(r / 4.0, g / 4.0, b / 4.0, opacity * 1.3) cairo.stroke() cairo.restore() def paint(self, context): view = self.view canvas = view.canvas cairo = context.cairo # Order matters here: for item in canvas.sort(view.selected_items): self._draw_handles(item, cairo) # Draw nice opaque handles when hovering an item: item = view.hovered_item if item and item not in view.selected_items: self._draw_handles(item, cairo, opacity=0.25) item = view.dropzone_item if item and item not in view.selected_items: self._draw_handles(item, cairo, opacity=0.25, inner=True) class ToolPainter(Painter): """ ToolPainter allows the Tool defined on a view to do some special drawing. """ def paint(self, context): view = self.view cairo = context.cairo if view.tool: cairo.save() cairo.identity_matrix() view.tool.draw(context) cairo.restore() class FocusedItemPainter(Painter): """ This painter allows for drawing on top of all the other layers for the focused item. """ def paint(self, context): view = self.view item = view.hovered_item if item and item is view.focused_item: PaintFocused(item, view).paint(context) def DefaultPainter(view=None): """ Default painter, containing item, handle and tool painters. """ return ( PainterChain(view) .append(ItemPainter()) .append(HandlePainter()) .append(FocusedItemPainter()) .append(ToolPainter()) ) # vim: sw=4:et:ai PK!aloogaphas/picklers.py""" Some extra picklers needed to gracefully dump and load a canvas. """ from future import standard_library standard_library.install_aliases() import copyreg # Allow instancemethod to be pickled: import types def construct_instancemethod(funcname, self, clazz): func = getattr(clazz, funcname) return types.MethodType(func, self, clazz) def reduce_instancemethod(im): return ( construct_instancemethod, (im.__func__.__name__, im.__self__, im.__self__.__class__), ) copyreg.pickle(types.MethodType, reduce_instancemethod, construct_instancemethod) # Allow cairo.Matrix to be pickled: import cairo def construct_cairo_matrix(*args): return cairo.Matrix(*args) def reduce_cairo_matrix(m): return construct_cairo_matrix, tuple(m) copyreg.pickle(cairo.Matrix, reduce_cairo_matrix, construct_cairo_matrix) # vim:sw=4:et:ai PK!16#0#0gaphas/quadtree.py""" Quadtree ======== A quadtree is a tree data structure in which each internal node has up to four children. Quadtrees are most often used to partition a two dimensional space by recursively subdividing it into four quadrants or regions. The regions may be square or rectangular, or may have arbitrary shapes. This data structure was named a quadtree by Raphael Finkel and J.L. Bentley in 1974. A similar partitioning is also known as a Q-tree. All forms of Quadtrees share some common features: * They decompose space into adaptable cells. * Each cell (or bucket) has a maximum capacity. When maximum capacity is reached, the bucket splits. * The tree directory follows the spatial decomposition of the Quadtree. (From Wikipedia, the free encyclopedia) """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import operator from builtins import map from builtins import object from builtins import zip from .geometry import rectangle_contains, rectangle_intersects, rectangle_clip class Quadtree(object): """ The Quad-tree. Rectangles use the same scheme throughout Gaphas: (x, y, width, height). >>> qtree = Quadtree((0, 0, 100, 100)) >>> for i in range(20): ... qtree.add('%d' % i, ((i * 4) % 90, (i * 10) % 90, 10, 10)) >>> len(qtree) 20 >>> qtree.dump() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE <....QuadtreeBucket object at 0x...> (0, 0, 100, 100) 11 (44, 20, 10, 10) 12 (48, 30, 10, 10) <....QuadtreeBucket object at 0x...> (0, 0, 50.0, 50.0) 0 (0, 0, 10, 10) 1 (4, 10, 10, 10) 2 (8, 20, 10, 10) 3 (12, 30, 10, 10) 4 (16, 40, 10, 10) 9 (36, 0, 10, 10) 10 (40, 10, 10, 10) <....QuadtreeBucket object at 0x...> (50.0, 0, 50.0, 50.0) 13 (52, 40, 10, 10) 18 (72, 0, 10, 10) 19 (76, 10, 10, 10) <....QuadtreeBucket object at 0x...> (0, 50.0, 50.0, 50.0) 5 (20, 50, 10, 10) 6 (24, 60, 10, 10) 7 (28, 70, 10, 10) 8 (32, 80, 10, 10) <....QuadtreeBucket object at 0x...> (50.0, 50.0, 50.0, 50.0) 14 (56, 50, 10, 10) 15 (60, 60, 10, 10) 16 (64, 70, 10, 10) 17 (68, 80, 10, 10) Find all items in the tree: >>> sorted(qtree.find_inside((0, 0, 100, 100))) ['0', '1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '3', '4', '5', '6', '7', '8', '9'] Or just the items in a section of the tree: >>> sorted(qtree.find_inside((40, 40, 40, 40))) ['13', '14', '15', '16'] >>> sorted([qtree.get_bounds(item) for item in qtree.find_inside((40, 40, 40, 40))]) [(52, 40, 10, 10), (56, 50, 10, 10), (60, 60, 10, 10), (64, 70, 10, 10)] >>> sorted(qtree.find_intersect((40, 40, 20, 20))) ['12', '13', '14', '15'] >>> sorted([qtree.get_bounds(item) for item in qtree.find_intersect((40, 40, 20, 20))]) [(48, 30, 10, 10), (52, 40, 10, 10), (56, 50, 10, 10), (60, 60, 10, 10)] >>> qtree.rebuild() """ def __init__(self, bounds=(0, 0, 0, 0), capacity=10): """ Create a new Quadtree instance. Bounds is the boundries of the quadtree. this is fixed and do not change depending on the contents. Capacity defines the number of elements in one tree bucket (default: 10) """ self._capacity = capacity self._bucket = QuadtreeBucket(bounds, capacity) # Easy lookup item->(bounds, data, clipped bounds) mapping self._ids = dict() bounds = property(lambda s: s._bucket.bounds) def resize(self, bounds): """ Resize the tree. The tree structure is rebuild. """ self._bucket = QuadtreeBucket(bounds, self._capacity) self.rebuild() def get_soft_bounds(self): """ Calculate the size of all items in the tree. This size may be beyond the limits of the tree itself. Returns a tuple (x, y, width, height). >>> qtree = Quadtree() >>> qtree.add('1', (10, 20, 30, 40)) >>> qtree.add('2', (20, 30, 40, 10)) >>> qtree.bounds (0, 0, 0, 0) >>> qtree.soft_bounds (10, 20, 50, 40) Quadtree's bounding box is not adjusted: >>> qtree.bounds (0, 0, 0, 0) """ x_y_w_h = list( zip( *list( map( operator.getitem, iter(list(self._ids.values())), [0] * len(self._ids), ) ) ) ) if not x_y_w_h: return 0, 0, 0, 0 x0 = min(x_y_w_h[0]) y0 = min(x_y_w_h[1]) add = operator.add x1 = max(list(map(add, x_y_w_h[0], x_y_w_h[2]))) y1 = max(list(map(add, x_y_w_h[1], x_y_w_h[3]))) return (x0, y0, x1 - x0, y1 - y0) soft_bounds = property(get_soft_bounds) def add(self, item, bounds, data=None): """ Add an item to the tree. If an item already exists, its bounds are updated and the item is moved to the right bucket. Data can be used to add some extra info to the item """ # Clip item bounds to fit in top-level bucket # Keep original bounds in _ids, for reference clipped_bounds = rectangle_clip(bounds, self._bucket.bounds) if item in self._ids: old_clip = self._ids[item][2] if old_clip: bucket = self._bucket.find_bucket(old_clip) assert item in bucket.items # Fast lane, if item moved just a little it may still reside # in the same bucket. We do not need to search from top-level. if ( bucket and clipped_bounds and rectangle_contains(clipped_bounds, bucket.bounds) ): bucket.update(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) return elif bucket: bucket.remove(item) if clipped_bounds: self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) def remove(self, item): """ Remove an item from the tree. """ bounds, data, clipped_bounds = self._ids[item] del self._ids[item] if clipped_bounds: self._bucket.find_bucket(clipped_bounds).remove(item) def clear(self): """ Remove all items from the tree. """ self._bucket.clear() self._ids.clear() def rebuild(self): """ Rebuild the tree structure. """ # Clean bucket and items: self._bucket.clear() for item, (bounds, data, _) in list(dict(self._ids).items()): clipped_bounds = rectangle_clip(bounds, self._bucket.bounds) if clipped_bounds: self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds) self._ids[item] = (bounds, data, clipped_bounds) def get_bounds(self, item): """ Return the bounding box for the given item. """ return self._ids[item][0] def get_data(self, item): """ Return the data for the given item, None if no data was provided. """ return self._ids[item][1] def get_clipped_bounds(self, item): """ Return the bounding box for the given item. The bounding box is clipped on the boundries of the tree (provided on construction or with resize()). """ return self._ids[item][2] def find_inside(self, rect): """ Find all items in the given rectangle (x, y, with, height). Returns a set. """ return set(self._bucket.find(rect, method=rectangle_contains)) def find_intersect(self, rect): """ Find all items that intersect with the given rectangle (x, y, width, height). Returns a set. """ return set(self._bucket.find(rect, method=rectangle_intersects)) def __len__(self): """ Return number of items in tree. """ return len(self._ids) def __contains__(self, item): """ Check if an item is in tree. """ return item in self._ids def dump(self): """ Print structure to stdout. """ self._bucket.dump() class QuadtreeBucket(object): """ A node in a Quadtree structure. """ def __init__(self, bounds, capacity): """ Set bounding box for the node as (x, y, width, height). """ self.bounds = bounds self.capacity = capacity self.items = {} self._buckets = [] def add(self, item, bounds): """ Add an item to the quadtree. The bucket is split when nessecary. Items are otherwise added to this bucket, not some sub-bucket. """ assert rectangle_contains(bounds, self.bounds) # create new subnodes if threshold is reached if not self._buckets and len(self.items) >= self.capacity: x, y, w, h = self.bounds rw, rh = w / 2.0, h / 2.0 cx, cy = x + rw, y + rh self._buckets = [ QuadtreeBucket((x, y, rw, rh), self.capacity), QuadtreeBucket((cx, y, rw, rh), self.capacity), QuadtreeBucket((x, cy, rw, rh), self.capacity), QuadtreeBucket((cx, cy, rw, rh), self.capacity), ] # Add items to subnodes items = list(self.items.items()) self.items.clear() for i, b in items: self.find_bucket(b).add(i, b) self.find_bucket(bounds).add(item, bounds) else: self.items[item] = bounds def remove(self, item): """ Remove an item from the quadtree bucket. The item should be contained by *this* bucket (not a sub-bucket). """ del self.items[item] def update(self, item, new_bounds): """ Update the position of an item within the current bucket. The item should live in the current bucket, but may be placed in a sub-bucket. """ assert item in self.items self.remove(item) self.find_bucket(new_bounds).add(item, new_bounds) def find_bucket(self, bounds): """ Find the bucket that holds a bounding box. This method should be used to find a bucket that fits, before add() or remove() is called. """ if self._buckets: sx, sy, sw, sh = self.bounds cx, cy = sx + sw / 2.0, sy + sh / 2.0 x, y, w, h = bounds index = 0 if x >= cx: index += 1 elif x + w > cx: return self if y >= cy: index += 2 elif y + h > cy: return self return self._buckets[index].find_bucket(bounds) return self def find(self, rect, method): """ Find all items in the given rectangle (x, y, with, height). Method can be either the contains or intersects function. Returns an iterator. """ if rectangle_intersects(rect, self.bounds): for item, bounds in list(self.items.items()): if method(bounds, rect): yield item for bucket in self._buckets: for item in bucket.find(rect, method=method): yield item def clear(self): """ Clear the bucket, including sub-buckets. """ del self._buckets[:] self.items.clear() def dump(self, indent=""): print(indent, self, self.bounds) indent += " " for item, bounds in sorted(self.items.items(), key=lambda items: items[1]): print(indent, item, bounds) for bucket in self._buckets: bucket.dump(indent) # vim:sw=4:et:ai PK!b) !!gaphas/segment.py""" Allow for easily adding segments to lines. """ from __future__ import division from builtins import object from builtins import zip from cairo import Matrix, ANTIALIAS_NONE from simplegeneric import generic from gaphas.aspect import ConnectionSink from gaphas.aspect import HandleFinder, HandleSelection, PaintFocused from gaphas.aspect import ItemHandleFinder, ItemHandleSelection, ItemPaintFocused from gaphas.geometry import distance_point_point_fast, distance_line_point from gaphas.item import Line @generic class Segment(object): def __init__(self, item, view): raise TypeError @Segment.when_type(Line) class LineSegment(object): def __init__(self, item, view): self.item = item self.view = view def split(self, pos): item = self.item handles = item.handles() x, y = self.view.get_matrix_v2i(item).transform_point(*pos) for h1, h2 in zip(handles, handles[1:]): xp = (h1.pos.x + h2.pos.x) / 2 yp = (h1.pos.y + h2.pos.y) / 2 if distance_point_point_fast((x, y), (xp, yp)) <= 4: segment = handles.index(h1) handles, ports = self.split_segment(segment) return handles and handles[0] def split_segment(self, segment, count=2): """ Split one item segment into ``count`` equal pieces. Two lists are returned - list of created handles - list of created ports :Parameters: segment Segment number to split (starting from zero). count Amount of new segments to be created (minimum 2). """ item = self.item if segment < 0 or segment >= len(item.ports()): raise ValueError("Incorrect segment") if count < 2: raise ValueError("Incorrect count of segments") def do_split(segment, count): handles = item.handles() p0 = handles[segment].pos p1 = handles[segment + 1].pos dx, dy = p1.x - p0.x, p1.y - p0.y new_h = item._create_handle((p0.x + dx / count, p0.y + dy / count)) item._reversible_insert_handle(segment + 1, new_h) p0 = item._create_port(p0, new_h.pos) p1 = item._create_port(new_h.pos, p1) item._reversible_remove_port(item.ports()[segment]) item._reversible_insert_port(segment, p0) item._reversible_insert_port(segment + 1, p1) if count > 2: do_split(segment + 1, count - 1) do_split(segment, count) # force orthogonal constraints to be recreated item._update_orthogonal_constraints(item.orthogonal) self._recreate_constraints() handles = item.handles()[segment + 1 : segment + count] ports = item.ports()[segment : segment + count - 1] return handles, ports def merge_segment(self, segment, count=2): """ Merge two (or more) item segments. Tuple of two lists is returned, list of deleted handles and list of deleted ports. :Parameters: segment Segment number to start merging from (starting from zero). count Amount of segments to be merged (minimum 2). """ item = self.item if len(item.ports()) < 2: raise ValueError("Cannot merge item with one segment") if segment < 0 or segment >= len(item.ports()): raise ValueError("Incorrect segment") if count < 2 or segment + count > len(item.ports()): raise ValueError("Incorrect count of segments") # remove handle and ports which share position with handle deleted_handles = item.handles()[segment + 1 : segment + count] deleted_ports = item.ports()[segment : segment + count] for h in deleted_handles: item._reversible_remove_handle(h) for p in deleted_ports: item._reversible_remove_port(p) # create new port, which replaces old ports destroyed due to # deleted handle p1 = item.handles()[segment].pos p2 = item.handles()[segment + 1].pos port = item._create_port(p1, p2) item._reversible_insert_port(segment, port) # force orthogonal constraints to be recreated item._update_orthogonal_constraints(item.orthogonal) self._recreate_constraints() return deleted_handles, deleted_ports def _recreate_constraints(self): """ Create connection constraints between connecting lines and an item. :Parameters: connected Connected item. """ connected = self.item def find_port(line, handle, item): # port = None # max_dist = sys.maxint canvas = item.canvas ix, iy = canvas.get_matrix_i2i(line, item).transform_point(*handle.pos) # find the port using item's coordinates sink = ConnectionSink(item, None) return sink.find_port((ix, iy)) if not connected.canvas: # No canvas, no constraints return canvas = connected.canvas for cinfo in list(canvas.get_connections(connected=connected)): item, handle = cinfo.item, cinfo.handle port = find_port(item, handle, connected) constraint = port.constraint(canvas, item, handle, connected) cinfo = canvas.get_connection(handle) canvas.reconnect_item(item, handle, constraint=constraint) @HandleFinder.when_type(Line) class SegmentHandleFinder(ItemHandleFinder): """Find a handle on a line. Creates a new handle if the mouse is located between two handles. The position aligns with the points drawn by the SegmentPainter. """ def get_handle_at_point(self, pos): view = self.view item = view.hovered_item handle = None if self.item is view.focused_item: try: segment = Segment(self.item, self.view) except TypeError: pass else: handle = segment.split(pos) if not handle: item, handle = super(SegmentHandleFinder, self).get_handle_at_point(pos) return item, handle @HandleSelection.when_type(Line) class SegmentHandleSelection(ItemHandleSelection): """ In addition to the default behaviour, merge segments if the handle is released. """ def unselect(self): item = self.item handle = self.handle handles = item.handles() # don't merge using first or last handle if handles[0] is handle or handles[-1] is handle: return True handle_index = handles.index(handle) segment = handle_index - 1 # cannot merge starting from last segment if segment == len(item.ports()) - 1: segment = -1 assert segment >= 0 and segment < len(item.ports()) - 1 before = handles[handle_index - 1] after = handles[handle_index + 1] d, p = distance_line_point(before.pos, after.pos, handle.pos) if d < 2: assert len(self.view.canvas.solver._marked_cons) == 0 Segment(item, self.view).merge_segment(segment) if handle: item.request_update() @PaintFocused.when_type(Line) class LineSegmentPainter(ItemPaintFocused): """ This painter draws pseudo-handles on gaphas.item.Line objects. Each line can be split by dragging those points, which will result in a new handle. ConnectHandleTool take care of performing the user interaction required for this feature. """ def paint(self, context): view = self.view item = view.hovered_item if item and item is view.focused_item: cr = context.cairo h = item.handles() for h1, h2 in zip(h[:-1], h[1:]): p1, p2 = h1.pos, h2.pos cx = (p1.x + p2.x) / 2 cy = (p1.y + p2.y) / 2 cr.save() cr.identity_matrix() m = Matrix(*view.get_matrix_i2v(item)) cr.set_antialias(ANTIALIAS_NONE) cr.translate(*m.transform_point(cx, cy)) cr.rectangle(-3, -3, 6, 6) cr.set_source_rgba(0, 0.5, 0, 0.4) cr.fill_preserve() cr.set_source_rgba(0.25, 0.25, 0.25, 0.6) cr.set_line_width(1) cr.stroke() cr.restore() # vim:sw=4:et:ai PK!}KKgaphas/solver.py""" Constraint solver allows to define constraint between two or more different variables and keep this constraint always true when one or more of the constrained variables change. For example, one may want to keep two variables always equal. Variables change and at some point of time we want to make all constraints valid again. This process is called solving constraints. Gaphas' solver allows to define constraints between Variable instances. Constraint classes are defined in `gaphas.constraint` module. How It Works ------------ Every constraint contains list of variables and has to be registered in solver object. Variables change (`Variable.dirty()`, `Solver.request_resolve()` methods) and their constraints are marked by solver as dirty. To solve constraints, solver loops through dirty constraints and asks constraint for a variable (called weakest variable), which - has the lowest strength - or if there are many variables with the same, lowest strength value return first unchanged variable with lowest strength - or if there is no unchanged, then return the first changed with the lowest strength (weakest variable invariants defined above) Having weakest variable (`constraint.Constraint.weakest()` method) every constraint is being asked to solve itself (`constraint.Constraint.solve_for()` method) changing appropriate variables to make the constraint valid again. """ from __future__ import absolute_import from __future__ import division from builtins import object from .state import observed, reversible_pair, reversible_property # epsilon for float comparison # is simple abs(x - y) > EPSILON enough for canvas needs? EPSILON = 1e-6 # Variable Strengths: VERY_WEAK = 0 WEAK = 10 NORMAL = 20 STRONG = 30 VERY_STRONG = 40 REQUIRED = 100 class Variable(object): """Representation of a variable in the constraint solver. Each Variable has a @value and a @strength. In a constraint the weakest variables are changed. You can even do some calculating with it. The Variable always represents a float variable. """ def __init__(self, value=0.0, strength=NORMAL): self._value = float(value) self._strength = strength # These variables are set by the Solver: self._solver = None self._constraints = set() def __hash__(self): return object.__hash__(self) @observed def _set_strength(self, strength): self._strength = strength for c in self._constraints: c.create_weakest_list() strength = reversible_property(lambda s: s._strength, _set_strength) def dirty(self): """ Mark the variable dirty in both the constraint solver and attached constraints. Variables are marked dirty also during constraints solving to solve all dependent constraints, i.e. two equals constraints between 3 variables. """ solver = self._solver if not solver: return solver.request_resolve(self) @observed def set_value(self, value): oldval = self._value if abs(oldval - value) > EPSILON: self._value = float(value) self.dirty() value = reversible_property(lambda s: s._value, set_value) def __str__(self): return "Variable(%g, %d)" % (self._value, self._strength) __repr__ = __str__ def __float__(self): return float(self._value) def __eq__(self, other): """ >>> Variable(5) == 5 True >>> Variable(5) == 4 False >>> Variable(5) != 5 False """ return abs(self._value - other) < EPSILON def __ne__(self, other): """ >>> Variable(5) != 4 True >>> Variable(5) != 5 False """ return abs(self._value - other) > EPSILON def __gt__(self, other): """ >>> Variable(5) > 4 True >>> Variable(5) > 5 False """ return self._value.__gt__(float(other)) def __lt__(self, other): """ >>> Variable(5) < 4 False >>> Variable(5) < 6 True """ return self._value.__lt__(float(other)) def __ge__(self, other): """ >>> Variable(5) >= 5 True """ return self._value.__ge__(float(other)) def __le__(self, other): """ >>> Variable(5) <= 5 True """ return self._value.__le__(float(other)) def __add__(self, other): """ >>> Variable(5) + 4 9.0 """ return self._value.__add__(float(other)) def __sub__(self, other): """ >>> Variable(5) - 4 1.0 >>> Variable(5) - Variable(4) 1.0 """ return self._value.__sub__(float(other)) def __mul__(self, other): """ >>> Variable(5) * 4 20.0 >>> Variable(5) * Variable(4) 20.0 """ return self._value.__mul__(float(other)) def __floordiv__(self, other): """ >>> Variable(21) // 4 5.0 >>> Variable(21) // Variable(4) 5.0 """ return self._value.__floordiv__(float(other)) def __mod__(self, other): """ >>> Variable(5) % 4 1.0 >>> Variable(5) % Variable(4) 1.0 """ return self._value.__mod__(float(other)) def __divmod__(self, other): """ >>> divmod(Variable(21), 4) (5.0, 1.0) >>> divmod(Variable(21), Variable(4)) (5.0, 1.0) """ return self._value.__divmod__(float(other)) def __pow__(self, other): """ >>> pow(Variable(5), 4) 625.0 >>> pow(Variable(5), Variable(4)) 625.0 """ return self._value.__pow__(float(other)) def __div__(self, other): """ >>> Variable(5) / 4. 1.25 >>> Variable(5) / Variable(4) 1.25 """ return self._value.__div__(float(other)) def __truediv__(self, other): """ >>> Variable(5.) / 4 1.25 >>> 10 / Variable(5.) 2.0 """ return self._value.__truediv__(float(other)) # .. And the other way around: def __radd__(self, other): """ >>> 4 + Variable(5) 9.0 >>> Variable(4) + Variable(5) 9.0 """ return self._value.__radd__(float(other)) def __rsub__(self, other): """ >>> 6 - Variable(5) 1.0 """ return self._value.__rsub__(other) def __rmul__(self, other): """ >>> 4 * Variable(5) 20.0 """ return self._value.__rmul__(other) def __rfloordiv__(self, other): """ >>> 21 // Variable(4) 5.0 """ return self._value.__rfloordiv__(other) def __rmod__(self, other): """ >>> 5 % Variable(4) 1.0 """ return self._value.__rmod__(other) def __rdivmod__(self, other): """ >>> divmod(21, Variable(4)) (5.0, 1.0) """ return self._value.__rdivmod__(other) def __rpow__(self, other): """ >>> pow(4, Variable(5)) 1024.0 """ return self._value.__rpow__(other) def __rdiv__(self, other): """ >>> 5 / Variable(4.) 1.25 """ return self._value.__rdiv__(other) def __rtruediv__(self, other): """ >>> 5. / Variable(4) 1.25 """ return self._value.__rtruediv__(other) class Projection(object): """ 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 "%s(%s)" % (self.__class__.__name__, self.variable()) __repr__ = __str__ class Solver(object): """ 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(, a=Variable(1, 20), b=Variable(2, 20)) >>> c_eq._weakest [Variable(1, 20), Variable(2, 20)] >>> s._marked_cons [EquationConstraint(, 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( "Variable juggling detected, constraint %s resolved %d times out of %d" % (c, self._marked_cons.count(c), 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(, 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, "No constraint (%s)" % (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(, 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, "No constraint (%s)" % (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(, 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(, 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(, 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(, 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(, 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 class solvable(object): """ Easy-to-use drop Variable descriptor. >>> class A(object): ... x = solvable(varname='_v_x') ... y = solvable(STRONG) ... def __init__(self): ... self.x = 12 >>> a = A() >>> a.x Variable(12, 20) >>> a._v_x Variable(12, 20) >>> a.x = 3 >>> a.x Variable(3, 20) >>> a.y Variable(0, 30) """ def __init__(self, strength=NORMAL, varname=None): self._strength = strength self._varname = varname or "_variable_%x" % id(self) def __get__(self, obj, class_=None): if not obj: return self try: return getattr(obj, self._varname) except AttributeError: setattr(obj, self._varname, Variable(strength=self._strength)) return getattr(obj, self._varname) def __set__(self, obj, value): try: getattr(obj, self._varname).value = float(value) except AttributeError: v = Variable(strength=self._strength) setattr(obj, self._varname, v) v.value = value def setvar(self, obj, v): setattr(obj, self._varname, v) class JuggleError(AssertionError): """ Variable juggling exception. Raised when constraint's variables are marking each other dirty forever. """ __test__ = { "Solver.add_constraint": Solver.add_constraint, "Solver.remove_constraint": Solver.remove_constraint, } # vim:sw=4:et:ai PK!r!!gaphas/state.py""" This module is the central point where Gaphas' classes report their state changes. Invocations of method and state changing properties are emited to all functions (or bound methods) registered in the 'observers' set. Use `observers.add()` and `observers.remove()` to add/remove handlers. This module also contains a second layer: a state inverser. Instead of emiting the invoked method, it emits a signal (callable, \\*\\*kwargs) that can be applied to revert the state of the object to the point before the method invokation. For this to work the revert_handler has to be added to the observers set:: gaphas.state.observers.add(gaphas.state.revert_handler) """ import sys from builtins import zip from threading import Lock from types import MethodType from decorator import decorator if sys.version_info.major >= 3: # Modern Python from inspect import getfullargspec as getargspec else: # Legacy Python from inspect import getargspec # This string is added to each docstring in order to denote is's observed # OBSERVED_DOCSTRING = \ # '\n\n This method is @observed. See gaphas.state for extra info.\n' # Tell @observed to dispatch invokation messages by default # May be changed (but be sure to do that right at the start of your # application,otherwise you have no idea what's enabled and what's not!) DISPATCH_BY_DEFAULT = True # Add/remove methods from this subscribers list. # Subscribers should have signature method(event) where event is a # Event has the form: (func, keywords) # Since most events originate from methods, it's save to call # saveapply(func, keywords) for those functions subscribers = set() # Subscribe to low-level change events: observers = set() # Perform locking (should be per thread?). mutex = Lock() def observed(func): """ Simple observer, dispatches events to functions registered in the observers list. On the function an ``__observer__`` property is set, which references to the observer decorator. This is nessesary, since the event handlers expect the outer most function to be returned (that's what they see). Also note that the events are dispatched *before* the function is invoked. This is an important feature, esp. for the reverter code. """ def wrapper(func, *args, **kwargs): o = func.__observer__ acquired = mutex.acquire(False) try: if acquired: dispatch((o, args, kwargs), queue=observers) return func(*args, **kwargs) finally: if acquired: mutex.release() dec = decorator(wrapper)(func) func.__observer__ = dec return dec def dispatch(event, queue): """ Dispatch an event to a queue of event handlers. Event handlers should have signature: handler(event). >>> def handler(event): ... print("event handled", event) >>> observers.add(handler) >>> @observed ... def callme(): ... pass >>> callme() # doctest: +ELLIPSIS event handled (, (), {}) >>> class Callme(object): ... @observed ... def callme(self): ... pass >>> Callme().callme() # doctest: +ELLIPSIS event handled (>> observers.remove(handler) >>> callme() """ for s in queue: s(event) _reverse = dict() def reversible_function(func, reverse, bind={}): """ Straight forward reversible method, if func is invoked, reverse is dispatched with bind as arguments. """ global _reverse func = getfunction(func) _reverse[func] = (reverse, getargspec(reverse), bind) reversible_method = reversible_function def reversible_pair(func1, func2, bind1={}, bind2={}): """ Treat a pair of functions (func1 and func2) as each others inverse operation. bind1 provides arguments that can overrule the default values (or add additional values). bind2 does the same for func2. See `revert_handler()` for doctesting. """ global _reverse # We need the function, since that's what's in the events func1 = getfunction(func1) func2 = getfunction(func2) _reverse[func1] = (func2, getargspec(func2), bind2) _reverse[func2] = (func1, getargspec(func1), bind1) def reversible_property(fget=None, fset=None, fdel=None, doc=None, bind={}): """ Replacement for the property descriptor. In addition to creating a property instance, the property is registered as reversible and reverse events can be send out when changes occur. Caveat: we can't handle both fset and fdel in the proper way. Therefore fdel should somehow invoke fset. (personally, I hardly use fdel) See revert_handler() for doctesting. """ # given fset, read the value argument name (second arg) and create a # bind {value: lambda self: fget(self)} # TODO! handle fdel if fset: spec = getargspec(fset) argnames = spec[0] assert len(argnames) == 2 argself, argvalue = argnames func = getfunction(fset) b = { argvalue: eval( "lambda %(self)s: fget(%(self)s)" % {"self": argself}, {"fget": fget} ) } b.update(bind) _reverse[func] = (func, spec, b) return property(fget=fget, fset=fset, fdel=fdel, doc=doc) def revert_handler(event): """ Event handler, generates undoable statements and puts them on the subscribers queue. First thing to do is to actually enable the revert_handler: >>> observers.add(revert_handler) First let's define our simple list: >>> class SList(object): ... def __init__(self): ... self.l = list() ... def add(self, node, before=None): ... if before: self.l.insert(self.l.index(before), node) ... else: self.l.append(node) ... add = observed(add) ... @observed ... def remove(self, node): ... self.l.remove(self.l.index(node)) >>> sl = SList() >>> sl.add(10) >>> sl.l [10] >>> sl.add(11) >>> sl.l [10, 11] >>> sl.add(12, before=11) >>> sl.l [10, 12, 11] It works, so let's add some reversible stuff: >>> reversible_pair(SList.add, SList.remove, \ bind1={'before': lambda self, node: self.l[self.l.index(node)+1] }) >>> def handler(event): ... print('handle', event) >>> subscribers.add(handler) >>> sl.add(20) # doctest: +ELLIPSIS handle (>> class PropTest(object): ... def __init__(self): self._a = 0 ... @observed ... def _set_a(self, value): self._a = value ... a = reversible_property(lambda s: s._a, _set_a) >>> pt = PropTest() >>> pt.a 0 >>> pt.a = 10 # doctest: +ELLIPSIS handle (, {'self': , 'value': 0}) >>> subscribers.remove(handler) """ global _reverse func, args, kwargs = event spec = getargspec(func) reverse, revspec, bind = _reverse.get(func, (None, None, {})) if not reverse: return kw = dict(kwargs) kw.update(dict(list(zip(spec[0], args)))) for arg, binding in list(bind.items()): kw[arg] = saveapply(binding, kw) argnames = list(revspec[0]) if spec[1]: argnames.append(revspec[1]) if spec[2]: argnames.append(revspec[2]) kwargs = {} for arg in argnames: kwargs[arg] = kw.get(arg) dispatch((reverse, kwargs), queue=subscribers) def saveapply(func, kw): """ Do apply a set of keywords to a method or function. The function names should be known at meta-level, since arguments are applied as func(\\*\\*kwargs). """ spec = getargspec(func) argnames = list(spec[0]) if spec[1]: argnames.append(spec[1]) if spec[2]: argnames.append(spec[2]) kwargs = {} for arg in argnames: kwargs[arg] = kw.get(arg) return func(**kwargs) def getfunction(func): """ Return the function associated with a class method. """ if isinstance(func, MethodType): if sys.version_info.major >= 3: # Modern Python return func else: # Legacy Python return func.__func__ return func # vim:sw=4:et:ai PK!("ccgaphas/table.py""" Table is a storage class that can be used to store information, like one would in a database table, with indexes on the desired "columns." """ from builtins import str from builtins import zip from builtins import object from functools import reduce class Table(object): """ A Table structure with indexing. Optimized for lookups. """ def __init__(self, columns, indexes): """ Create a new Store instance with columns and indexes: >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (2,)) """ fields = columns._fields self._type = columns self._indexes = tuple(fields[i] for i in indexes) # create data structure, which acts as cache index = {} for n in fields: index[n] = dict() self._index = index columns = property(lambda s: s._type) def insert(self, *values): """ Add a set of values to the store. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (1, 2,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) The number of values should match the number of columns defined at construction time. >>> s.insert('x', 'z') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Number of arguments doesn't match the number of columns (2 != 3) """ if len(values) != len(self._type._fields): raise ValueError( "Number of arguments doesn't match the number of columns (%d != %d)" % (len(values), len(self._type._fields)) ) # Add value to index entries index = self._index data = self._type._make(values) for n in self._indexes: v = getattr(data, n) if v in index[n]: index[n][v].add(data) else: index[n][v] = set([data]) def delete(self, *_row, **kv): """ Remove value from the table. Either a complete set may be given or just one entry in "column=value" style. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (0, 1,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) >>> s.insert('a', 'v', 'd') >>> list(sorted(s.query(foo='a'))) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> s.delete('a', 'b', 'c') >>> list(s.query(foo='a')) [C(foo='a', bar='v', baz='d')] Query style: >>> s.insert('a', 'b', 'c') >>> list(sorted(s.query(foo='a'))) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> s.delete(foo='a') >>> list(s.query(foo='a')) [] >>> list(s.query(foo=1)) [C(foo=1, bar=2, baz=3)] Delete a non existent value: >>> s.delete(foo='notPresent') Cannot provide both a row and a query value: >>> s.delete(('x', 'z'), foo=1) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Should either provide a row or a query statement, not both """ fields = self._type._fields if _row and kv: raise ValueError( "Should either provide a row or a query statement, not both" ) if _row: assert len(_row) == len(fields) kv = dict(list(zip(self._indexes, _row))) rows = list(self.query(**kv)) index = self._index for row in rows: for i, n in enumerate(self._indexes): v = row[i] if v in index[n]: index[n][v].remove(row) if len(index[n][v]) == 0: del index[n][v] def query(self, **kv): """ Get rows (tuples) for each key defined. An iterator is returned. >>> from collections import namedtuple >>> C = namedtuple('C', "foo bar baz") >>> s = Table(C, (0, 1,)) >>> s.insert('a', 'b', 'c') >>> s.insert(1, 2, 3) >>> s.insert('a', 'v', 'd') >>> list(sorted(s.query(foo='a'))) [C(foo='a', bar='b', baz='c'), C(foo='a', bar='v', baz='d')] >>> list(s.query(foo='a', bar='v')) [C(foo='a', bar='v', baz='d')] >>> list(s.query(foo='a', bar='q')) [] >>> list(s.query(bar=2)) [C(foo=1, bar=2, baz=3)] >>> list(s.query(foo=42)) [] >>> list(s.query(invalid_column_name=42)) # doctest: +ELLIPSIS Traceback (most recent call last): ... KeyError: "Invalid column 'invalid_column_name'" >>> list(s.query(baz=42)) # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: Column 'baz' is not indexed """ index = self._index bad = set(kv.keys()) - set(self._type._fields) if len(bad) == 1: raise KeyError("Invalid column '%s'" % bad.pop()) elif len(bad) > 1: raise KeyError("Invalid columns '%s'" % str(tuple(bad))) bad = set(kv.keys()) - set(self._indexes) if len(bad) == 1: raise AttributeError("Column '%s' is not indexed" % bad.pop()) elif len(bad) > 1: raise AttributeError("Columns %s are not indexed" % str(tuple(bad))) r = iter([]) items = tuple((n, v) for n, v in list(kv.items()) if v is not None) if all(v in index[n] for n, v in items): rows = (index[n][v] for n, v in items) try: r = iter(reduce(set.intersection, rows)) except TypeError as ex: pass return r # vi:sw=4:et:ai PK!1a1agaphas/tool.py""" Tools provide interactive behavior to a `View` by handling specific events sent by view. Some of implemented tools are `HoverTool` make the item under the mouse cursor the "hovered item" `ItemTool` handle selection and movement of items `HandleTool` handle selection and movement of handles `RubberbandTool` for rubber band selection of multiple items `PanTool` for easily moving the canvas around `PlacementTool` for placing items on the canvas The tools are chained with `ToolChain` class (it is a tool as well), which allows to combine functionality provided by different tools. Tools can handle events in different ways - event can be ignored - tool can handle the event (obviously) """ from __future__ import division from __future__ import print_function from builtins import object from gi.repository import Gtk, Gdk from gaphas.aspect import ( Finder, Selection, InMotion, HandleFinder, HandleSelection, HandleInMotion, Connector, ) from gaphas.canvas import Context DEBUG_TOOL_CHAIN = False Event = Context class Tool(object): """ Base class for a tool. This class A word on click events: Mouse (pointer) button click. A button press is normally followed by a button release. Double and triple clicks should work together with the button methods. A single click is emited as: on_button_press on_button_release In case of a double click: on_button_press (x 2) on_double_click on_button_release In case of a tripple click: on_button_press (x 3) on_triple_click on_button_release """ # Custom events: GRAB = -100 UNGRAB = -101 # Map GDK events to tool methods EVENT_HANDLERS = { Gdk.EventType.BUTTON_PRESS: "on_button_press", Gdk.EventType.BUTTON_RELEASE: "on_button_release", Gdk.EventType._2BUTTON_PRESS: "on_double_click", Gdk.EventType._3BUTTON_PRESS: "on_triple_click", Gdk.EventType.MOTION_NOTIFY: "on_motion_notify", Gdk.EventType.KEY_PRESS: "on_key_press", Gdk.EventType.KEY_RELEASE: "on_key_release", Gdk.EventType.SCROLL: "on_scroll", # Custom events: GRAB: "on_grab", UNGRAB: "on_ungrab", } # Those events force the tool to release the grabbed tool. FORCE_UNGRAB_EVENTS = (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType._3BUTTON_PRESS) def __init__(self, view=None): self.view = view def set_view(self, view): self.view = view def _dispatch(self, event): """ Deal with the event. The event is dispatched to a specific handler for the event type. """ handler = self.EVENT_HANDLERS.get(event.type) if handler: try: h = getattr(self, handler) except AttributeError: pass # No handler else: return bool(h(event)) return False def handle(self, event): return self._dispatch(event) def draw(self, context): """ Some tools (such as Rubberband selection) may need to draw something on the canvas. This can be done through the draw() method. This is called after all items are drawn. The context contains the following fields: - context: the render context (contains context.view and context.cairo) - cairo: the Cairo drawing context """ pass class ToolChain(Tool): """ A ToolChain can be used to chain tools together, for example HoverTool, HandleTool, SelectionTool. The grabbed item is bypassed in case a double or triple click event is received. Should make sure this doesn't end up in dangling states. """ def __init__(self, view=None): super(ToolChain, self).__init__(view) self._tools = [] self._grabbed_tool = None def set_view(self, view): self.view = view for tool in self._tools: tool.set_view(self.view) def append(self, tool): """ Append a tool to the chain. Self is returned. """ self._tools.append(tool) tool.view = self.view return self def grab(self, tool): if not self._grabbed_tool: if DEBUG_TOOL_CHAIN: print("Grab tool", tool) # Send grab event event = Event(type=Tool.GRAB) tool.handle(event) self._grabbed_tool = tool def ungrab(self, tool): if self._grabbed_tool is tool: if DEBUG_TOOL_CHAIN: print("UNgrab tool", self._grabbed_tool) # Send ungrab event event = Event(type=Tool.UNGRAB) tool.handle(event) self._grabbed_tool = None def validate_grabbed_tool(self, event): """ Check if it's valid to have a grabbed tool on an event. If not the grabbed tool will be released. """ if event.type in self.FORCE_UNGRAB_EVENTS: self.ungrab(self._grabbed_tool) def handle(self, event): """ Handle the event by calling each tool until the event is handled or grabbed. If a tool is returning True on a button press event, the motion and button release events are also passed to this """ handler = self.EVENT_HANDLERS.get(event.type) self.validate_grabbed_tool(event) if self._grabbed_tool and handler: try: return self._grabbed_tool.handle(event) finally: if event.type == Gdk.EventType.BUTTON_RELEASE: self.ungrab(self._grabbed_tool) else: for tool in self._tools: if DEBUG_TOOL_CHAIN: print("tool", tool) rt = tool.handle(event) if rt: if event.type == Gdk.EventType.BUTTON_PRESS: self.view.grab_focus() self.grab(tool) return rt def draw(self, context): if self._grabbed_tool: self._grabbed_tool.draw(context) class HoverTool(Tool): """ Make the item under the mouse cursor the "hovered item". """ def on_motion_notify(self, event): view = self.view pos = event.get_coords()[1:] view.hovered_item = Finder(view).get_item_at_point(pos) class ItemTool(Tool): """ ItemTool does selection and dragging of items. On a button click, the currently "hovered item" is selected. If CTRL or SHIFT are pressed, already selected items remain selected. The last selected item gets the focus (e.g. receives key press events). The roles used are Selection (select, unselect) and InMotion (move). """ def __init__(self, view=None, buttons=(1,)): super(ItemTool, self).__init__(view) self._buttons = buttons self._movable_items = set() def get_item(self): return self.view.hovered_item def movable_items(self): """ Filter the items that should eventually be moved. Returns InMotion aspects for the items. """ view = self.view get_ancestors = view.canvas.get_ancestors selected_items = set(view.selected_items) for item in selected_items: # Do not move subitems of selected items if not set(get_ancestors(item)).intersection(selected_items): yield InMotion(item, view) def on_button_press(self, event): ### TODO: make keys configurable view = self.view item = self.get_item() if event.get_button()[1] not in self._buttons: return False # Deselect all items unless CTRL or SHIFT is pressed # or the item is already selected. if not ( event.get_state()[1] & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) or item in view.selected_items ): del view.selected_items if item: if ( view.hovered_item in view.selected_items and event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK ): selection = Selection(item, view) selection.unselect() else: selection = Selection(item, view) selection.select() self._movable_items.clear() return True def on_button_release(self, event): if event.get_button()[1] not in self._buttons: return False for inmotion in self._movable_items: inmotion.stop_move() self._movable_items.clear() return True def on_motion_notify(self, event): """ Normally do nothing. If a button is pressed move the items around. """ if event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK: if not self._movable_items: self._movable_items = set(self.movable_items()) for inmotion in self._movable_items: inmotion.start_move(event.get_coords()[1:]) for inmotion in self._movable_items: inmotion.move(event.get_coords()[1:]) return True class HandleTool(Tool): """ Tool for moving handles around. By default this tool does not provide connecting handles to another item (see `ConnectHandleTool`). """ def __init__(self, view=None): super(HandleTool, self).__init__(view) self.grabbed_handle = None self.grabbed_item = None self.motion_handle = None def grab_handle(self, item, handle): """ Grab a specific handle. This can be used from the PlacementTool to set the state of the handle tool. """ assert item is None and handle is None or handle in item.handles() self.grabbed_item = item self.grabbed_handle = handle selection = HandleSelection(item, handle, self.view) selection.select() def ungrab_handle(self): """ Reset grabbed_handle and grabbed_item. """ item = self.grabbed_item handle = self.grabbed_handle self.grabbed_handle = None self.grabbed_item = None if handle: selection = HandleSelection(item, handle, self.view) selection.unselect() def on_button_press(self, event): """ Handle button press events. If the (mouse) button is pressed on top of a Handle (item.Handle), that handle is grabbed and can be dragged around. """ view = self.view item, handle = HandleFinder(view.hovered_item, view).get_handle_at_point( event.get_coords()[1:] ) if handle: # Deselect all items unless CTRL or SHIFT is pressed # or the item is already selected. ### TODO: duplicate from ItemTool if not ( event.get_state()[1] & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) or view.hovered_item in view.selected_items ): del view.selected_items ###/ view.hovered_item = item view.focused_item = item self.motion_handle = None self.grab_handle(item, handle) return True def on_button_release(self, event): """ Release a grabbed handle. """ # queue extra redraw to make sure the item is drawn properly grabbed_handle, grabbed_item = self.grabbed_handle, self.grabbed_item if self.motion_handle: self.motion_handle.stop_move() self.motion_handle = None self.ungrab_handle() if grabbed_handle: grabbed_item.request_update() return True def on_motion_notify(self, event): """ Handle motion events. If a handle is grabbed: drag it around, else, if the pointer is over a handle, make the owning item the hovered-item. """ view = self.view if ( self.grabbed_handle and event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK ): canvas = view.canvas item = self.grabbed_item handle = self.grabbed_handle pos = event.get_coords()[1:] if not self.motion_handle: self.motion_handle = HandleInMotion(item, handle, self.view) self.motion_handle.start_move(pos) self.motion_handle.move(pos) return True class RubberbandTool(Tool): def __init__(self, view=None): super(RubberbandTool, self).__init__(view) self.x0, self.y0, self.x1, self.y1 = 0, 0, 0, 0 def on_button_press(self, event): self.x0, self.y0 = event.get_coords()[1:] self.x1, self.y1 = event.get_coords()[1:] return True def on_button_release(self, event): self.queue_draw(self.view) x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 self.view.select_in_rectangle( (min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) ) return True def on_motion_notify(self, event): if event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK: view = self.view self.queue_draw(view) self.x1, self.y1 = event.get_coords()[1:] self.queue_draw(view) return True def queue_draw(self, view): x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 view.queue_draw_area(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) def draw(self, context): cr = context.cairo x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1 cr.set_line_width(1.0) cr.set_source_rgba(0.5, 0.5, 0.7, 0.6) cr.rectangle(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) cr.fill() PAN_MASK = ( Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK | Gdk.ModifierType.CONTROL_MASK ) PAN_VALUE = 0 class PanTool(Tool): """ Captures drag events with the middle mouse button and uses them to translate the canvas within the view. Trumps the ZoomTool, so should be placed later in the ToolChain. """ def __init__(self, view=None): super(PanTool, self).__init__(view) self.x0, self.y0 = 0, 0 self.speed = 10 def on_button_press(self, event): if not event.get_state()[1] & PAN_MASK == PAN_VALUE: return False if event.get_button()[1] == 2: self.x0, self.y0 = event.get_coords()[1:] return True def on_button_release(self, event): self.x0, self.y0 = event.get_coords()[1:] return True def on_motion_notify(self, event): if event.get_state()[1] & Gdk.ModifierType.BUTTON2_MASK: view = self.view self.x1, self.y1 = event.get_coords()[1:] dx = self.x1 - self.x0 dy = self.y1 - self.y0 view._matrix.translate(dx / view._matrix[0], dy / view._matrix[3]) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) self.x0 = self.x1 self.y0 = self.y1 return True def on_scroll(self, event): # Ensure no modifiers if not event.get_state()[1] & PAN_MASK == PAN_VALUE: return False view = self.view direction = event.get_scroll_direction()[1] if direction == Gdk.ScrollDirection.LEFT: view._matrix.translate(self.speed / view._matrix[0], 0) elif direction == Gdk.ScrollDirection.RIGHT: view._matrix.translate(-self.speed / view._matrix[0], 0) elif direction == Gdk.ScrollDirection.UP: view._matrix.translate(0, self.speed / view._matrix[3]) elif direction == Gdk.ScrollDirection.DOWN: view._matrix.translate(0, -self.speed / view._matrix[3]) view.request_update((), view._canvas.get_all_items()) return True ZOOM_MASK = ( Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK ) ZOOM_VALUE = Gdk.ModifierType.CONTROL_MASK class ZoomTool(Tool): """Tool for zooming. Uses two different user inputs to zoom: - Ctrl + middle-mouse dragging in the up-down direction. - Ctrl + mouse-wheel """ def __init__(self, view=None): super(ZoomTool, self).__init__(view) self.x0, self.y0 = 0, 0 self.lastdiff = 0 def on_button_press(self, event): if ( event.get_button()[1] == 2 and event.get_state()[1] & ZOOM_MASK == ZOOM_VALUE ): pos = event.get_coords()[1:] self.x0 = pos[0] self.y0 = pos[1] self.lastdiff = 0 return True def on_button_release(self, event): self.lastdiff = 0 return True def on_motion_notify(self, event): if ( event.get_state()[1] & ZOOM_MASK == ZOOM_VALUE and event.get_state()[1] & Gdk.ModifierType.BUTTON2_MASK ): view = self.view pos = event.get_coords()[1:] dy = pos[1] - self.y0 sx = view._matrix[0] sy = view._matrix[3] ox = (view._matrix[4] - self.x0) / sx oy = (view._matrix[5] - self.y0) / sy if abs(dy - self.lastdiff) > 20: if dy - self.lastdiff < 0: factor = 1.0 / 0.9 else: factor = 0.9 m = view.matrix m.translate(-ox, -oy) m.scale(factor, factor) m.translate(+ox, +oy) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) self.lastdiff = dy return True def on_scroll(self, event): if event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK: view = self.view sx = view._matrix[0] sy = view._matrix[3] pos = event.get_coords()[1:] ox = (view._matrix[4] - pos[0]) / sx oy = (view._matrix[5] - pos[1]) / sy factor = 0.9 if event.get_scroll_direction()[1] == Gdk.ScrollDirection.UP: factor = 1.0 / factor view._matrix.translate(-ox, -oy) view._matrix.scale(factor, factor) view._matrix.translate(+ox, +oy) # Make sure everything's updated view.request_update((), view._canvas.get_all_items()) return True class PlacementTool(Tool): def __init__(self, view, factory, handle_tool, handle_index): super(PlacementTool, self).__init__(view) self._factory = factory self.handle_tool = handle_tool handle_tool.set_view(view) self._handle_index = handle_index self._new_item = None self.grabbed_handle = None # handle_tool = property(lambda s: s._handle_tool, doc="Handle tool") handle_index = property( lambda s: s._handle_index, doc="Index of handle to be used by handle_tool" ) new_item = property(lambda s: s._new_item, doc="The newly created item") def on_button_press(self, event): view = self.view canvas = view.canvas pos = event.get_coords()[1:] new_item = self._create_item(pos) # Enforce matrix update, as a good matrix is required for the handle # positioning: canvas.get_matrix_i2c(new_item, calculate=True) self._new_item = new_item view.focused_item = new_item h = new_item.handles()[self._handle_index] if h.movable: self.handle_tool.grab_handle(new_item, h) self.grabbed_handle = h return True def _create_item(self, pos, **kw): view = self.view item = self._factory(**kw) x, y = view.get_matrix_v2i(item).transform_point(*pos) item.matrix.translate(x, y) return item def on_button_release(self, event): if self.grabbed_handle: self.handle_tool.on_button_release(event) self.grabbed_handle = None self._new_item = None return True def on_motion_notify(self, event): if self.grabbed_handle: return self.handle_tool.on_motion_notify(event) else: # act as if the event is handled if we have a new item return bool(self._new_item) class TextEditTool(Tool): """ Demo of a text edit tool (just displays a text edit box at the cursor position. """ def __init__(self, view=None): super(TextEditTool, self).__init__(view) def on_double_click(self, event): """ Create a popup window with some editable text. """ window = Gtk.Window() window.set_property("decorated", False) window.set_resize_mode(Gtk.ResizeMode.IMMEDIATE) # window.set_modal(True) window.set_parent_window(self.view.get_window()) buffer = Gtk.TextBuffer() text_view = Gtk.TextView() text_view.set_buffer(buffer) text_view.show() window.add(text_view) rect = Gdk.Rectangle() pos = event.get_coords()[1:] rect.x, rect.y, rect.width, rect.height = int(pos[0]), int(pos[1]), 50, 50 window.size_allocate(rect) # window.move(int(event.x), int(event.y)) cursor_pos = self.view.get_toplevel().get_screen().get_display().get_pointer() window.move(cursor_pos[1], cursor_pos[2]) window.connect("focus-out-event", self._on_focus_out_event, buffer) text_view.connect("key-press-event", self._on_key_press_event, buffer) # text_view.set_size_request(50, 50) window.show() # text_view.grab_focus() # window.set_uposition(event.x, event.y) # window.focus return True def _on_key_press_event(self, widget, event, buffer): # if event.keyval == Gdk.KEY_Return: # widget.get_toplevel().destroy() if event.get_keyval()[1] == Gdk.KEY_Escape: widget.get_toplevel().destroy() def _on_focus_out_event(self, widget, event, buffer): widget.destroy() class ConnectHandleTool(HandleTool): """ Tool for connecting two items. There are two items involved. Handle of connecting item (usually a line) is being dragged by an user towards another item (item in short). Port of an item is found by the tool and connection is established by creating a constraint between line's handle and item's port. """ def glue(self, item, handle, vpos): """ Perform a small glue action to ensure the handle is at a proper location for connecting. """ if self.motion_handle: return self.motion_handle.glue(vpos) else: return HandleInMotion(item, handle, self.view).glue(vpos) def connect(self, item, handle, vpos): """ Connect a handle of a item to connectable item. Connectable item is found by `ConnectHandleTool.glue` method. :Parameters: item Connecting item. handle Handle of connecting item. vpos Position to connect to (or near at least) """ connector = Connector(item, handle) # find connectable item and its port sink = self.glue(item, handle, vpos) # no new connectable item, then diconnect and exit if sink: connector.connect(sink) else: cinfo = item.canvas.get_connection(handle) if cinfo: connector.disconnect() def on_button_release(self, event): view = self.view item = self.grabbed_item handle = self.grabbed_handle try: if handle and handle.connectable: pos = event.get_coords()[1:] self.connect(item, handle, pos) finally: return super(ConnectHandleTool, self).on_button_release(event) def DefaultTool(view=None): """ The default tool chain build from HoverTool, ItemTool and HandleTool. """ return ( ToolChain(view) .append(HoverTool()) .append(ConnectHandleTool()) .append(PanTool()) .append(ZoomTool()) .append(ItemTool()) .append(TextEditTool()) .append(RubberbandTool()) ) # vim: sw=4:et:ai PK!paw(w(gaphas/tree.py""" Simple class containing the tree structure for the canvas items. """ from builtins import map from builtins import object from builtins import range from operator import attrgetter class Tree(object): """ A Tree structure. Nodes are stores in a depth-first order. ``None`` is the root node. @invariant: len(self._children) == len(self._nodes) + 1 """ def __init__(self): # List of nodes in the tree, sorted in the order they ought to be # rendered self._nodes = [] # Per entry a list of children is maintained. self._children = {None: []} # For easy and fast lookups, also maintain a child -> parent mapping self._parents = {} nodes = property(lambda s: list(s._nodes)) def get_parent(self, node): """ Return the parent item of ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.get_parent('n2') 'n1' """ return self._parents.get(node) def get_children(self, node): """ Return all child objects of ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_children('n1') ['n2', 'n3'] >>> tree.get_children('n2') [] """ return self._children[node] def get_siblings(self, node): """ Get all siblings of ``node``, including ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_siblings('n2') ['n2', 'n3'] """ parent = self.get_parent(node) return self._children[parent] def get_next_sibling(self, node): """ Return the node on the same level after ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_next_sibling('n2') 'n3' >>> tree.get_next_sibling('n3') # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: list index out of range """ parent = self.get_parent(node) siblings = self._children[parent] return siblings[siblings.index(node) + 1] def get_previous_sibling(self, node): """ Return the node on the same level before ``node``. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.get_previous_sibling('n3') 'n2' >>> tree.get_previous_sibling('n2') # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: list index out of range """ parent = self.get_parent(node) siblings = self._children[parent] index = siblings.index(node) - 1 if index < 0: raise IndexError("list index out of range") return siblings[index] def get_all_children(self, node): """ Iterate all children (and children of children and so forth) >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n2') >>> tree.get_children('n1') ['n2'] >>> tree.get_all_children('n1') # doctest: +ELLIPSIS >>> list(tree.get_all_children('n1')) ['n2', 'n3'] """ children = self.get_children(node) for c in children: yield c for cc in self.get_all_children(c): yield cc def get_ancestors(self, node): """ Iterate all parents and parents of parents, etc. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n2') >>> tree.get_parent('n3') 'n2' >>> tree.get_ancestors('n3') # doctest: +ELLIPSIS >>> list(tree.get_ancestors('n3')) ['n2', 'n1'] >>> list(tree.get_ancestors('n1')) [] """ parent = self.get_parent(node) while parent: yield parent parent = self.get_parent(parent) def index_nodes(self, index_key): """ Provide each item in the tree with an index attribute. This makes for fast sorting of items. >>> class A(object): ... def __init__(self, n): ... self.n = n ... def __repr__(self): ... return self.n >>> t = Tree() >>> a = A('a') >>> t.add(a) >>> t.add(A('b')) >>> t.add(A('c'), parent=a) >>> t.nodes [a, c, b] >>> t.index_nodes('my_key') >>> t.nodes[0].my_key, t.nodes[1].my_key, t.nodes[2].my_key (0, 1, 2) For sorting, see ``sort()``. """ nodes = self.nodes lnodes = len(nodes) list(map(setattr, nodes, [index_key] * lnodes, list(range(lnodes)))) def sort(self, nodes, index_key, reverse=False): """ Sort a set (or list) of nodes. >>> class A(object): ... def __init__(self, n): ... self.n = n ... def __repr__(self): ... return self.n >>> t = Tree() >>> a = A('a') >>> t.add(a) >>> t.add(A('b')) >>> t.add(A('c'), parent=a) >>> t.nodes # the series from Tree.index_nodes [a, c, b] >>> t.index_nodes('my_key') >>> selection = (t.nodes[2], t.nodes[1]) >>> t.sort(selection, index_key='my_key') [c, b] """ if index_key: return sorted(nodes, key=attrgetter(index_key), reverse=reverse) else: raise NotImplemented("index_key should be provided.") def _add_to_nodes(self, node, parent, index=None): """ Helper method to place nodes on the right location in the nodes list Called only from add() and reparent() """ nodes = self._nodes siblings = self._children[parent] try: atnode = siblings[index] except (TypeError, IndexError): index = len(siblings) # self._add_to_nodes(node, parent) if parent: try: next_uncle = self.get_next_sibling(parent) except IndexError: # parent has no younger brothers.. # place it before the next uncle of grant_parent: return self._add_to_nodes(node, self.get_parent(parent)) else: nodes.insert(nodes.index(next_uncle), node) else: # append to root node: nodes.append(node) else: nodes.insert(nodes.index(atnode), node) def _add(self, node, parent=None, index=None): """ Helper method for both add() and reparent(). """ assert node not in self._nodes siblings = self._children[parent] self._add_to_nodes(node, parent, index) # Fix parent-child and child-parent relationship try: siblings.insert(index, node) except TypeError: siblings.append(node) # Create new entry for it's own children: if parent: self._parents[node] = parent def add(self, node, parent=None, index=None): """ Add node to the tree. parent is the parent node, which may be None if the item should be added to the root item. For usage, see the unit tests. """ self._add(node, parent, index) self._children[node] = [] def _remove(self, node): # Remove from parent item self.get_siblings(node).remove(node) # Remove data entries: del self._children[node] self._nodes.remove(node) try: del self._parents[node] except KeyError: pass def remove(self, node): """ Remove ``node`` from the tree. For usage, see the unit tests. """ # First remove children: for c in reversed(list(self._children[node])): self.remove(c) self._remove(node) def _reparent_nodes(self, node, parent): """ Helper for reparent(). The _children and _parent trees can be left intact as far as children of the reparented node are concerned. Only the position in the _nodes list changes. """ self._nodes.remove(node) self._add_to_nodes(node, parent) for c in self._children[node]: self._reparent_nodes(c, node) def reparent(self, node, parent, index=None): """ Set new parent for a ``node``. ``Parent`` can be ``None``, indicating it's added to the top. >>> tree = Tree() >>> tree.add('n1') >>> tree.add('n2', parent='n1') >>> tree.add('n3', parent='n1') >>> tree.nodes ['n1', 'n2', 'n3'] >>> tree.reparent('n2', 'n3') >>> tree.get_parent('n2') 'n3' >>> tree.get_children('n3') ['n2'] >>> tree.nodes ['n1', 'n3', 'n2'] If a node contains children, those are also moved: >>> tree.add('n4') >>> tree.nodes ['n1', 'n3', 'n2', 'n4'] >>> tree.reparent('n1', 'n4') >>> tree.get_parent('n1') 'n4' >>> list(tree.get_all_children('n4')) ['n1', 'n3', 'n2'] >>> tree.nodes ['n4', 'n1', 'n3', 'n2'] """ if parent is self.get_parent(node): return # Remove all node references: old_parent = self.get_parent(node) self._children[old_parent].remove(node) self._nodes.remove(node) if old_parent: del self._parents[node] self._add(node, parent, index) # reorganize children in nodes list for c in self._children[node]: self._reparent_nodes(c, node) # vi: sw=4:et:ai PK!Da] gaphas/util.py"""Helper functions and classes for Cairo (drawing engine used by the canvas). """ from __future__ import division from math import pi import cairo def text_extents(cr, text, font=None, multiline=False, padding=1): """ Simple way to determine the size of a piece of text. """ if not text: return 0, 0 if font: cr.save() text_set_font(cr, font) if multiline: width, height = 0, 0 for line in text.split("\n"): x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(line) width = max(width, w) height += h + padding else: x_bear, y_bear, width, height, x_adv, y_adv = cr.text_extents(text) # width, height = width + x_bearing, height + y_bearing if font: cr.restore() return width, height def text_center(cr, x, y, text): text_align(cr, x, y, text, align_x=0, align_y=0) def text_align(cr, x, y, text, align_x=0, align_y=0, padding_x=0, padding_y=0): """ Draw text relative to (x, y). x, y - coordinates text - text to print (utf8) align_x - -1 (top), 0 (middle), 1 (bottom) align_y - -1 (left), 0 (center), 1 (right) padding_x - padding (extra offset), always > 0 padding_y - padding (extra offset), always > 0 """ if not text: return x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) if align_x == 0: x = 0.5 - (w / 2 + x_bear) + x elif align_x < 0: x = -(w + x_bear) + x - padding_x else: x = x + padding_x if align_y == 0: y = 0.5 - (h / 2 + y_bear) + y elif align_y < 0: y = -(h + y_bear) + y - padding_y else: y = -y_bear + y + padding_y cr.move_to(x, y) cr.show_text(text) def text_multiline(cr, x, y, text, padding=1): """ Draw a string of text with embedded newlines. cr - cairo context x - leftmost x y - topmost y text - text to draw padding - additional padding between lines. """ if not text: return # cr.move_to(x, y) for line in text.split("\n"): x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) y += h cr.move_to(x, y) cr.show_text(line) def text_underline(cr, x, y, text, offset=1.5): """ Draw text with underline. """ x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text) cr.move_to(x, y - y_bear) cr.show_text(text) cr.move_to(x, y - y_bear + offset) cr.set_line_width(1.0) cr.rel_line_to(x_adv, 0) cr.stroke() def text_set_font(cr, font): """ Set the font from a string. E.g. 'sans 10' or 'sans italic bold 12' only restriction is that the font name should be the first option and the font size as last argument """ font = font.split() cr.select_font_face( font[0], "italic" in font and cairo.FONT_SLANT_ITALIC or cairo.FONT_SLANT_NORMAL, "bold" in font and cairo.FONT_WEIGHT_BOLD or cairo.FONT_WEIGHT_NORMAL, ) cr.set_font_size(float(font[-1])) def path_ellipse(cr, x, y, width, height, angle=0): """ Draw an ellipse. x - center x y - center y width - width of ellipse (in x direction when angle=0) height - height of ellipse (in y direction when angle=0) angle - angle in radians to rotate, clockwise """ cr.save() cr.translate(x, y) cr.rotate(angle) cr.scale(width / 2.0, height / 2.0) cr.move_to(1.0, 0.0) cr.arc(0.0, 0.0, 1.0, 0.0, 2.0 * pi) cr.restore() # vim:sw=4:et PK!oogaphas/view.py"""This module contains everything to display a Canvas on a screen. """ from __future__ import absolute_import from __future__ import division from builtins import map from builtins import object from cairo import Matrix from gi.repository import Gtk, GObject, Gdk from .canvas import Context from .decorators import AsyncIO from .decorators import nonrecursive from .geometry import Rectangle, distance_point_point_fast from .painter import DefaultPainter, BoundingBoxPainter from .quadtree import Quadtree from .tool import DefaultTool # Handy debug flag for drawing bounding boxes around the items. DEBUG_DRAW_BOUNDING_BOX = False DEBUG_DRAW_QUADTREE = False # The default cursor (use in case of a cursor reset) DEFAULT_CURSOR = Gdk.CursorType.LEFT_PTR class View(object): """ View class for gaphas.Canvas objects. """ def __init__(self, canvas=None): self._matrix = Matrix() self._painter = DefaultPainter(self) self._bounding_box_painter = BoundingBoxPainter(self) # Handling selections. # TODO: Move this to a context? self._selected_items = set() self._focused_item = None self._hovered_item = None self._dropzone_item = None self._qtree = Quadtree() self._bounds = Rectangle(0, 0, 0, 0) self._canvas = None if canvas: self._set_canvas(canvas) matrix = property(lambda s: s._matrix, doc="Canvas to view transformation matrix") def _set_canvas(self, canvas): """ Use view.canvas = my_canvas to set the canvas to be rendered in the view. """ if self._canvas: self._qtree.clear() self._selected_items.clear() self._focused_item = None self._hovered_item = None self._dropzone_item = None self._canvas = canvas canvas = property(lambda s: s._canvas, _set_canvas) def emit(self, *args, **kwargs): """ Placeholder method for signal emission functionality. """ pass def queue_draw_item(self, *items): """ Placeholder for item redraw queueing. """ pass def select_item(self, item): """ Select an item. This adds @item to the set of selected items. """ self.queue_draw_item(item) if item not in self._selected_items: self._selected_items.add(item) self.emit("selection-changed", self._selected_items) def unselect_item(self, item): """ Unselect an item. """ self.queue_draw_item(item) if item in self._selected_items: self._selected_items.discard(item) self.emit("selection-changed", self._selected_items) def select_all(self): for item in self.canvas.get_all_items(): self.select_item(item) def unselect_all(self): """ Clearing the selected_item also clears the focused_item. """ self.queue_draw_item(*self._selected_items) self._selected_items.clear() self.focused_item = None self.emit("selection-changed", self._selected_items) selected_items = property( lambda s: s._selected_items, select_item, unselect_all, "Items selected by the view", ) def _set_focused_item(self, item): """ Set the focused item, this item is also added to the selected_items set. """ if not item is self._focused_item: self.queue_draw_item(self._focused_item, item) if item: self.select_item(item) if item is not self._focused_item: self._focused_item = item self.emit("focus-changed", item) def _del_focused_item(self): """ Items that loose focus remain selected. """ self._set_focused_item(None) focused_item = property( lambda s: s._focused_item, _set_focused_item, _del_focused_item, "The item with focus (receives key events a.o.)", ) def _set_hovered_item(self, item): """ Set the hovered item. """ if item is not self._hovered_item: self.queue_draw_item(self._hovered_item, item) self._hovered_item = item self.emit("hover-changed", item) def _del_hovered_item(self): """ Unset the hovered item. """ self._set_hovered_item(None) hovered_item = property( lambda s: s._hovered_item, _set_hovered_item, _del_hovered_item, "The item directly under the mouse pointer", ) def _set_dropzone_item(self, item): """ Set dropzone item. """ if item is not self._dropzone_item: self.queue_draw_item(self._dropzone_item, item) self._dropzone_item = item self.emit("dropzone-changed", item) def _del_dropzone_item(self): """ Unset dropzone item. """ self._set_dropzone_item(None) dropzone_item = property( lambda s: s._dropzone_item, _set_dropzone_item, _del_dropzone_item, "The item which can group other items", ) def _set_painter(self, painter): """ Set the painter to use. Painters should implement painter.Painter. """ self._painter = painter painter.set_view(self) self.emit("painter-changed") painter = property(lambda s: s._painter, _set_painter) def _set_bounding_box_painter(self, painter): """ Set the painter to use for bounding box calculations. """ self._bounding_box_painter = painter painter.set_view(self) self.emit("painter-changed") bounding_box_painter = property( lambda s: s._bounding_box_painter, _set_bounding_box_painter ) def get_item_at_point(self, pos, selected=True): """ Return the topmost item located at ``pos`` (x, y). Parameters: - selected: if False returns first non-selected item """ items = self._qtree.find_intersect((pos[0], pos[1], 1, 1)) for item in self._canvas.sort(items, reverse=True): if not selected and item in self.selected_items: continue # skip selected items v2i = self.get_matrix_v2i(item) ix, iy = v2i.transform_point(*pos) item_distance = item.point((ix, iy)) if item_distance is None: print("Item distance is None for {}".format(item)) continue if item_distance < 0.5: return item return None def get_handle_at_point(self, pos, distance=6): """ Look for a handle at ``pos`` and return the tuple (item, handle). """ def find(item): """ Find item's handle at pos """ v2i = self.get_matrix_v2i(item) d = distance_point_point_fast(v2i.transform_distance(0, distance)) x, y = v2i.transform_point(*pos) for h in item.handles(): if not h.movable: continue hx, hy = h.pos if -d < (hx - x) < d and -d < (hy - y) < d: return h # The focused item is the preferred item for handle grabbing if self.focused_item: h = find(self.focused_item) if h: return self.focused_item, h # then try hovered item if self.hovered_item: h = find(self.hovered_item) if h: return self.hovered_item, h # Last try all items, checking the bounding box first x, y = pos items = self.get_items_in_rectangle( (x - distance, y - distance, distance * 2, distance * 2), reverse=True ) found_item, found_h = None, None for item in items: h = find(item) if h: return item, h return None, None def get_port_at_point(self, vpos, distance=10, exclude=None): """ Find item with port closest to specified position. List of items to be ignored can be specified with `exclude` parameter. Tuple is returned - found item - closest, connectable port - closest point on found port (in view coordinates) :Parameters: vpos Position specified in view coordinates. distance Max distance from point to a port (default 10) exclude Set of items to ignore. """ v2i = self.get_matrix_v2i vx, vy = vpos max_dist = distance port = None glue_pos = None item = None rect = (vx - distance, vy - distance, distance * 2, distance * 2) items = self.get_items_in_rectangle(rect, reverse=True) for i in items: if i in exclude: continue for p in i.ports(): if not p.connectable: continue ix, iy = v2i(i).transform_point(vx, vy) pg, d = p.glue((ix, iy)) if d >= max_dist: continue max_dist = d item = i port = p # transform coordinates from connectable item space to view # space i2v = self.get_matrix_i2v(i).transform_point glue_pos = i2v(*pg) return item, port, glue_pos def get_items_in_rectangle(self, rect, intersect=True, reverse=False): """ Return the items in the rectangle 'rect'. Items are automatically sorted in canvas' processing order. """ if intersect: items = self._qtree.find_intersect(rect) else: items = self._qtree.find_inside(rect) return self._canvas.sort(items, reverse=reverse) def select_in_rectangle(self, rect): """ Select all items who have their bounding box within the rectangle @rect. """ items = self._qtree.find_inside(rect) list(map(self.select_item, items)) def zoom(self, factor): """ Zoom in/out by factor @factor. """ # TODO: should the scale factor be clipped? self._matrix.scale(factor, factor) # Make sure everything's updated # map(self.update_matrix, self._canvas.get_all_items()) self.request_update((), self._canvas.get_all_items()) def set_item_bounding_box(self, item, bounds): """ Update the bounding box of the item. ``bounds`` is in view coordinates. Coordinates are calculated back to item coordinates, so matrix-only updates can occur. """ v2i = self.get_matrix_v2i(item).transform_point ix0, iy0 = v2i(bounds.x, bounds.y) ix1, iy1 = v2i(bounds.x1, bounds.y1) self._qtree.add(item=item, bounds=bounds, data=(ix0, iy0, ix1, iy1)) def get_item_bounding_box(self, item): """ Get the bounding box for the item, in view coordinates. """ return self._qtree.get_bounds(item) bounding_box = property(lambda s: s._bounds) def update_bounding_box(self, cr, items=None): """ Update the bounding boxes of the canvas items for this view, in canvas coordinates. """ painter = self._bounding_box_painter if items is None: items = self.canvas.get_all_items() # The painter calls set_item_bounding_box() for each rendered item. painter.paint(Context(cairo=cr, items=items, area=None)) # Update the view's bounding box with the rest of the items self._bounds = Rectangle(*self._qtree.soft_bounds) def paint(self, cr): self._painter.paint( Context(cairo=cr, items=self.canvas.get_all_items(), area=None) ) def get_matrix_i2v(self, item): """ Get Item to View matrix for ``item``. """ if self not in item._matrix_i2v: self.update_matrix(item) return item._matrix_i2v[self] def get_matrix_v2i(self, item): """ Get View to Item matrix for ``item``. """ if self not in item._matrix_v2i: self.update_matrix(item) return item._matrix_v2i[self] def update_matrix(self, item): """ Update item matrices related to view. """ matrix_i2c = self.canvas.get_matrix_i2c(item) try: i2v = matrix_i2c.multiply(self._matrix) except AttributeError: # Fall back to old behaviour i2v = matrix_i2c * self._matrix item._matrix_i2v[self] = i2v v2i = Matrix(*i2v) v2i.invert() item._matrix_v2i[self] = v2i def _clear_matrices(self): """ Clear registered data in Item's _matrix{i2c|v2i} attributes. """ for item in self.canvas.get_all_items(): try: del item._matrix_i2v[self] del item._matrix_v2i[self] except KeyError: pass class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View): # NOTE: Inherit from GTK+ class first, otherwise BusErrors may occur! """ GTK+ widget for rendering a canvas.Canvas to a screen. The view uses Tools from `tool.py` to handle events and Painters from `painter.py` to draw. Both are configurable. The widget already contains adjustment objects (`hadjustment`, `vadjustment`) to be used for scrollbars. This view registers itself on the canvas, so it will receive update events. """ # Just defined a name to make GTK register this class. __gtype_name__ = "GaphasView" # Signals: emited after the change takes effect. __gsignals__ = { "dropzone-changed": ( GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,), ), "hover-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), "focus-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), "selection-changed": ( GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,), ), "tool-changed": (GObject.SignalFlags.RUN_LAST, None, ()), "painter-changed": (GObject.SignalFlags.RUN_LAST, None, ()), } __gproperties__ = { "hscroll-policy": ( Gtk.ScrollablePolicy, "hscroll-policy", "hscroll-policy", Gtk.ScrollablePolicy.MINIMUM, GObject.ParamFlags.READWRITE, ), "hadjustment": ( Gtk.Adjustment, "hadjustment", "hadjustment", GObject.ParamFlags.READWRITE, ), "vscroll-policy": ( Gtk.ScrollablePolicy, "vscroll-policy", "vscroll-policy", Gtk.ScrollablePolicy.MINIMUM, GObject.ParamFlags.READWRITE, ), "vadjustment": ( Gtk.Adjustment, "vadjustment", "vadjustment", GObject.ParamFlags.READWRITE, ), } def __init__(self, canvas=None): Gtk.DrawingArea.__init__(self) self._dirty_items = set() self._dirty_matrix_items = set() View.__init__(self, canvas) self.connect("size-allocate", self.on_size_allocate) self.connect("draw", self.on_draw) self.set_can_focus(True) self.add_events( Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK ) self._hadjustment = None self._vadjustment = None self._hadjustment_handler_id = None self._vadjustment_handler_id = None self._hscroll_policy = None self._vscroll_policy = None self._set_tool(DefaultTool()) def do_get_property(self, prop): if prop.name == "hadjustemnet": return self._hadjustment elif prop.name == "vadjustment": return self._vadjustment elif prop.name == "hscroll-policy": return self._hscroll_policy elif prop.name == "vscroll-policy": return self._vscroll_policy else: raise AttributeError("Unknown property %s" % prop.name) def do_set_property(self, prop, value): if prop.name == "hadjustment": if value is not None: self._hadjustment = value self._hadjustment_handler_id = self._hadjustment.connect( "value-changed", self.on_adjustment_changed ) self.update_adjustments() elif prop.name == "vadjustment": if value is not None: self._vadjustment = value self._vadjustment_handler_id = self._vadjustment.connect( "value-changed", self.on_adjustment_changed ) self.update_adjustments() elif prop.name == "hscroll-policy": self._hscroll_policy = value elif prop.name == "vscroll-policy": self._vscroll_policy = value else: raise AttributeError("Unknown property %s" % prop.name) def emit(self, *args, **kwargs): """ Delegate signal emissions to the DrawingArea (=GTK+) """ Gtk.DrawingArea.emit(self, *args, **kwargs) def _set_canvas(self, canvas): """ Use view.canvas = my_canvas to set the canvas to be rendered in the view. This extends the behaviour of View.canvas. The view is also registered. """ if self._canvas: self._clear_matrices() self._canvas.unregister_view(self) super(GtkView, self)._set_canvas(canvas) if self._canvas: self._canvas.register_view(self) self.request_update(self._canvas.get_all_items()) self.queue_draw_refresh() canvas = property(lambda s: s._canvas, _set_canvas) def _set_tool(self, tool): """ Set the tool to use. Tools should implement tool.Tool. """ self._tool = tool tool.set_view(self) self.emit("tool-changed") tool = property(lambda s: s._tool, _set_tool) hadjustment = property(lambda s: s._hadjustment) vadjustment = property(lambda s: s._vadjustment) def zoom(self, factor): """ Zoom in/out by factor ``factor``. """ super(GtkView, self).zoom(factor) self.queue_draw_refresh() @AsyncIO(single=True) def update_adjustments(self, allocation=None): if not allocation: allocation = self.get_allocation() aw, ah = allocation.width, allocation.height hadjustment = self._hadjustment vadjustment = self._vadjustment # canvas limits (in view coordinates) c = Rectangle(*self._qtree.soft_bounds) # view limits v = Rectangle(0, 0, aw, ah) # union of these limits gives scrollbar limits if v in c: u = c else: u = c + v if hadjustment is None: self._hadjustment = Gtk.Adjustment.new( value=v.x, lower=u.x, upper=u.x1, step_increment=aw // 10, page_increment=aw, page_size=aw, ) else: self._hadjustment.set_value(v.x) self._hadjustment.set_lower(u.x) self._hadjustment.set_upper(u.x1) self._hadjustment.set_step_increment(aw // 10) self._hadjustment.set_page_increment(aw) self._hadjustment.set_page_size(aw) if vadjustment is None: self._vadjustment = Gtk.Adjustment.new( value=v.y, lower=u.y, upper=u.y1, step_increment=ah // 10, page_increment=ah, page_size=ah, ) else: self._vadjustment.set_value(v.y) self._vadjustment.set_lower(u.y) self._vadjustment.set_upper(u.y1) self._vadjustment.set_step_increment(ah // 10) self._vadjustment.set_page_increment(ah) self._vadjustment.set_page_size(ah) def queue_draw_item(self, *items): """ Like ``DrawingArea.queue_draw_area``, but use the bounds of the item as update areas. Of course with a pythonic flavor: update any number of items at once. TODO: Should we also create a (sorted) list of items that need redrawal? """ get_bounds = self._qtree.get_bounds items = [_f for _f in items if _f] try: # create a copy, otherwise we'll change the original rectangle bounds = Rectangle(*get_bounds(items[0])) for item in items[1:]: bounds += get_bounds(item) self.queue_draw_area(*bounds) except IndexError: pass except KeyError: pass # No bounds calculated yet? bummer. def queue_draw_area(self, x, y, w, h): """ Wrap draw_area to convert all values to ints. """ try: super(GtkView, self).queue_draw_area(int(x), int(y), int(w + 1), int(h + 1)) except OverflowError: # Okay, now the zoom factor is very large or something a = self.get_allocation() super(GtkView, self).queue_draw_area(0, 0, a.width, a.height) def queue_draw_refresh(self): """ Redraw the entire view. """ a = self.get_allocation() super(GtkView, self).queue_draw_area(0, 0, a.width, a.height) def request_update(self, items, matrix_only_items=(), removed_items=()): """ Request update for items. Items will get a full update treatment, while ``matrix_only_items`` will only have their bounding box recalculated. """ if items: self._dirty_items.update(items) if matrix_only_items: self._dirty_matrix_items.update(matrix_only_items) # Remove removed items: if removed_items: self._dirty_items.difference_update(removed_items) self.queue_draw_item(*removed_items) for item in removed_items: self._qtree.remove(item) self.selected_items.discard(item) if self.focused_item in removed_items: self.focused_item = None if self.hovered_item in removed_items: self.hovered_item = None if self.dropzone_item in removed_items: self.dropzone_item = None self.update() @AsyncIO(single=True) def update(self): """ Update view status according to the items updated by the canvas. """ if not self.get_window(): return dirty_items = self._dirty_items dirty_matrix_items = self._dirty_matrix_items try: self.queue_draw_item(*dirty_items) # Mark old bb section for update self.queue_draw_item(*dirty_matrix_items) for i in dirty_matrix_items: if i not in self._qtree: dirty_items.add(i) self.update_matrix(i) continue self.update_matrix(i) if i not in dirty_items: # Only matrix has changed, so calculate new bb based # on quadtree data (= bb in item coordinates). bounds = self._qtree.get_data(i) i2v = self.get_matrix_i2v(i).transform_point x0, y0 = i2v(bounds[0], bounds[1]) x1, y1 = i2v(bounds[2], bounds[3]) vbounds = Rectangle(x0, y0, x1=x1, y1=y1) self._qtree.add(i, vbounds, bounds) self.queue_draw_item(*dirty_matrix_items) # Request bb recalculation for all 'really' dirty items self.update_bounding_box(set(dirty_items)) finally: self._dirty_items.clear() self._dirty_matrix_items.clear() @AsyncIO(single=False) def update_bounding_box(self, items): """ Update bounding box is not necessary. """ cr = self.get_window().cairo_create() cr.save() cr.rectangle(0, 0, 0, 0) cr.clip() try: super(GtkView, self).update_bounding_box(cr, items) finally: cr.restore() self.queue_draw_item(*items) self.update_adjustments() @nonrecursive def do_size_allocate(self, allocation): """ Allocate the widget size ``(x, y, width, height)``. """ Gtk.DrawingArea.do_size_allocate(self, allocation) self.set_allocation(allocation) self.update_adjustments(allocation) self._qtree.resize((0, 0, allocation.width, allocation.height)) def on_size_allocate(self, widget, allocation): pass def do_realize(self): Gtk.DrawingArea.do_realize(self) # Ensure updates are propagated self._canvas.register_view(self) if self._canvas: self.request_update(self._canvas.get_all_items()) def do_unrealize(self): if self.canvas: # Although Item._matrix_{i2v|v2i} keys are automatically removed # (weak refs), better do it explicitly to be sure. self._clear_matrices() self._qtree.clear() self._dirty_items.clear() self._dirty_matrix_items.clear() self._canvas.unregister_view(self) Gtk.DrawingArea.do_unrealize(self) def on_draw(self, widget, ctx): """ Render canvas to the screen. """ if not self._canvas: return cr = self.get_window().cairo_create() allocation = self.get_allocation() x = allocation.x y = allocation.y w = allocation.width h = allocation.height # Draw no more than necessary. cr.rectangle(x, y, w, h) cr.clip() area = Rectangle(x, y, width=w, height=h) self._painter.paint( Context(cairo=cr, items=self.get_items_in_rectangle(area), area=area) ) if DEBUG_DRAW_BOUNDING_BOX: cr.save() cr.identity_matrix() cr.set_source_rgb(0, 0.8, 0) cr.set_line_width(1.0) b = self._bounds cr.rectangle(b[0], b[1], b[2], b[3]) cr.stroke() cr.restore() # Draw Quadtree structure if DEBUG_DRAW_QUADTREE: def draw_qtree_bucket(bucket): cr.rectangle(*bucket.bounds) cr.stroke() for b in bucket._buckets: draw_qtree_bucket(b) cr.set_source_rgb(0, 0, 0.8) cr.set_line_width(1.0) draw_qtree_bucket(self._qtree._bucket) return False def do_event(self, event): """ Handle GDK events. Events are delegated to a `tool.Tool`. """ if self._tool: return self._tool.handle(event) and True or False return False def on_adjustment_changed(self, adj): """ Change the transformation matrix of the view to reflect the value of the x/y adjustment (scrollbar). """ value = adj.get_value() if value == 0.0: return # Can not use self._matrix.translate(-value , 0) here, since # the translate method effectively does a m * self._matrix, which # will result in the translation being multiplied by the orig. matrix m = Matrix() if adj is self._hadjustment: m.translate(-value, 0) elif adj is self._vadjustment: m.translate(0, -value) self._matrix *= m # Force recalculation of the bounding boxes: self.request_update((), self._canvas.get_all_items()) self.queue_draw_refresh() # vim: sw=4:et:ai PK!]gaphas/weakset.py""" Backport of the Python 3.0 weakref.WeakSet() class. Note that, since it's shamelessly copied from the Python 3.0 distribution, this file is licensed under the Python Software Foundation License, version 2. """ from builtins import object from _weakref import ref __all__ = ["WeakSet"] class WeakSet(object): def __init__(self, data=None): self.data = set() def _remove(item, selfref=ref(self)): self = selfref() if self is not None: self.data.discard(item) self._remove = _remove if data is not None: self.update(data) def __iter__(self): for itemref in self.data: item = itemref() if item is not None: yield item def __len__(self): return sum(x() is not None for x in self.data) def __contains__(self, item): """ >>> class C(object): pass >>> a = C() >>> b = C() >>> ws = WeakSet((a, b)) >>> a in ws True >>> a = C() >>> a in ws False """ return ref(item) in self.data def __reduce__(self): return (self.__class__, (list(self),), getattr(self, "__dict__", None)) def add(self, item): self.data.add(ref(item, self._remove)) def clear(self): """ >>> class C(object): pass >>> s = C(), C() >>> ws = WeakSet(s) >>> list(ws) # doctest: +ELLIPSIS [, ] >>> ws.clear() >>> list(ws) [] """ self.data.clear() def copy(self): return self.__class__(self) def pop(self): """ >>> class C(object): pass >>> a, b = C(), C() >>> ws = WeakSet((a, b)) >>> len(ws) 2 >>> ws.pop() # doctest: +ELLIPSIS >>> len(ws) 1 """ while True: try: itemref = self.data.pop() except KeyError: raise KeyError("pop from empty WeakSet") item = itemref() if item is not None: return item def remove(self, item): self.data.remove(ref(item)) def discard(self, item): self.data.discard(ref(item)) def update(self, other): if isinstance(other, self.__class__): self.data.update(other.data) else: for element in other: self.add(element) def __ior__(self, other): self.update(other) return self # Helper functions for simple delegating methods. def _apply(self, other, method): if not isinstance(other, self.__class__): other = self.__class__(other) newdata = method(other.data) newset = self.__class__() newset.data = newdata return newset def difference(self, other): return self._apply(other, self.data.difference) __sub__ = difference def difference_update(self, other): if self is other: self.data.clear() else: self.data.difference_update(ref(item) for item in other) def __isub__(self, other): if self is other: self.data.clear() else: self.data.difference_update(ref(item) for item in other) return self def intersection(self, other): return self._apply(other, self.data.intersection) __and__ = intersection def intersection_update(self, other): self.data.intersection_update(ref(item) for item in other) def __iand__(self, other): self.data.intersection_update(ref(item) for item in other) return self def issubset(self, other): return self.data.issubset(ref(item) for item in other) __lt__ = issubset def __le__(self, other): return self.data <= set(ref(item) for item in other) def issuperset(self, other): return self.data.issuperset(ref(item) for item in other) __gt__ = issuperset def __ge__(self, other): return self.data >= set(ref(item) for item in other) def __eq__(self, other): """ >>> class C(object): pass >>> a, b = C(), C() >>> ws1 = WeakSet((a, b)) >>> ws2 = WeakSet((a, b)) >>> ws1 == ws2 True >>> ws1 == WeakSet((a, )) False """ return self.data == set(ref(item) for item in other) def symmetric_difference(self, other): return self._apply(other, self.data.symmetric_difference) __xor__ = symmetric_difference def symmetric_difference_update(self, other): if self is other: self.data.clear() else: self.data.symmetric_difference_update(ref(item) for item in other) def __ixor__(self, other): if self is other: self.data.clear() else: self.data.symmetric_difference_update(ref(item) for item in other) return self def union(self, other): return self._apply(other, self.data.union) __or__ = union def isdisjoint(self, other): return len(self.intersection(other)) == 0 # vim:sw=4:et:ai PK!Hi&.'gaphas-1.0.0.dist-info/entry_points.txtN+I/N.,()*O-)PzPi<..PK!''gaphas-1.0.0.dist-info/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS PK!HWYgaphas-1.0.0.dist-info/WHEEL A н#Z;/"b&F]xzwC;dhfCSTֻ0*Ri.4œh6-]{H, JPK!H4f*΄gaphas-1.0.0.dist-info/METADATA}rƶ;π6 ]biof M~ [7$%q<.o eu_t'qӕI|_Ex4.g ܛh' N<VQzqO(6:QIOx>OJoƜuiyqRtRMtsx<Ӄ=/zqUTO2~_;hYQA8WWE8@4՟-Xܤ6 hr}W :?Þ~x}f1$3}V'u\7FGjVEҌk6ey~VEN_wwaecAv;]Z IR56a:m]uX*m|v2ޣ)~{gwݣӢL!'}ܲQW682mZ}߾x3Ik~T !ioiEEMCGk[NN?_Ӱ\c]l{kG4T纂G8l 85 =~ t\.z¤ą/g9~Ey Muvo /=[~2 5"3 3=/~ ]6~(^DtŵB&nT &񘉧Qn H!*BPgIJEBQ@8~tlluYT5ih*&8Goh-wLkyƓ"EE(V߾zׇo^6$Ms~\ZO|wpr3 GC2P]`Gf`g`ej"*Õ߻GEq ?`OmJe=4&OGKm?o { KOLK'JԃW1@Er cQL&l1yݍI&-w@Y"/v& 9\L zl*ZFcm /=Evֲo8?(͸:$)Xu9K&R7uU[9>YU0&Nid:=EVL@H Y#:<͊+ .ND:ta5I`k*b2,cdJg6 @Ʉ8 chW|E=v_K 0u %2uH<$ASFF5gFC׻@Vg`hLk)`y`s2V#Z45tZŒi\ԴrMNV+b%OXA+#OS〔i \#F;4 <.{m ej}p3M o M;"l9g.[/ꋭȶz*ZYf\&1>6Id[~iIr_KALtKPQ:ӽ}ڞ{I')I][s%6""cFo`1A2̤N_ D lpe޴ߣoק/;s@Q#d3 qu}uZ5jV PX*5cy)]uC?#Mh;<޺ "0v3, dn0~0}.PM䊏 +[OL덏~ZL{c܊|=HOC4@HscIȀv?-ၵyKb[Zlb9!Sg}x OzP A=ހ"nd[c0L"oj*ݷQ`(%b[w{jS#-83߳x`\ԐY}EE05$<-(@7#$Tc-i6EQE[x&cTJ| "Dv4˲ut9T -lj&H ,'-w$zSu1(A"Bq@L0le॑]fD8({೑Z7%m V8| 7 2xK=lð l σ[oОnDVBkx%a{Q;*,3NgBd˟,d Znԃq1v>ykd?-@_z ԅ jRG>y*t1^p9c40biVA;Ҍ1y? [g= 4} i^6x %\E nAZ![E|$ &񞚀]͘F *0I?l2c!a3qgA:ˆ쳯aj-m{D 8#CTk H>c7ZMmy<El3n,17BeF Xjfe2OetWz =ث'agp5Vb(vr(؋ڃP#nR$OvD+ +ɠ$*[KhV~+{jŸEp[ig :DEOgF5I\ @>V(bM<U) J lcTvh hm m~-8)igi.r1ظ+c2 >h9""_Q.8&̅<{BI  Й/Rb  ۋ` # 9 _tdrmҒqa\dxCA(v` _ h IX ʖf4:ԚU~@8tue j(Ns Õ gX`NPRm"؉ V ֪l!zqU¸$Q? fttx-,w] ]\mS1D>t-nSg !TiV 38]%a>,f U1ixK8`, EH^녢u\U'edfhM xWź%ֈudKe n5qW%&c[T¾'Yqe9δA2 w?\ Ftz_h"_2X*9L0!`ҥ@ ܡDa68 -d"ZXQjЈ 9]ƼZtpm 8IтDlMSbOSNw!hB Y/ 0a՝FQwgTE\8zWhk# `ˀ[z,:V1+JnC`= 3s6ƾ+„uq=;.:PL-ÒoJF:JqNG0ڬ.`J[N8{;6Qť(>o򌉥.y:҉Ky yNcHa+1fHcZSB{u@ġs"hXx)#Kzk=^@)c5i}һ B"(G)O5 ]5,.GҕE'bQHi\9żp5t #JVQBd²jw8Qz]tW$i1nuT2mihQ؆x8шKX2 ӳ/e߹ fH&ypX4UٸviP~椫@FV*X+ie4O I$[kpIoP > + 8(2 >زnK8zZ"GH%Ψ$N(."m쀶'O!QSaW.#3 Bs:NH]kx:g3jaʞ'imUXIbrJpשqtU`2(,.|_ 1´U (YS87ԑ8Л WRW٪J`nISi,j5$rEsl) P& V, b@PN|MU}WG`M}?0*r4 n"]U(W!?<~͚ŅB'2jX ijieY8*5S*q-u\EI,wwmVpn|<b ]B$fVƛWYQ_+XǺG/6l ݌~Vs D,ű6"<)\VRu|tسaOcb18m5" <*mIpI^\ʆ] Od0؊$c B4`-XxH؅*dSALPHC*KlO4'S FxiF_i( /|D#%u;x[jl+,EHG/-yt^%FmS}Oq_n3"P˳Jt1:p(~WhƜ#1LнZ;s#s焴sR7#,%RmpIk^lu\O1(«yjR>N!ʟ/:9u, Hk[‚ʍQ6VԶ L,+N_w u>ԆیoR'grަlԜ 0-EI+NF{uVT=w{Hlp&G9Vtҩt՜r wDD!*.Jawe53F]bGqϊQZIՉ^9~uS\{;o;@<̭.\S|]ilt[ 񖩥P 0R{"bª!,z8gE_]b/~Y$r* lU!cBq7\&*Ea7C]W%n eQTCgչ rP`.;#S%ڵ=Mw'$h{u(ap[5ǵM4$c\6WevPuW12 nZnx>JbG*&8pd굳}lgL;y,IWn-!w3àKp/Oa}HTq&;ZWL0+xfʼ N[_>dZӡF#fuE(Q,((}.tYw K[YGZjgoڣ~-C߷MK8،+"01o*G=J<<40\F|81O I}m t3ׇ*:<|".%}W??K/aB a[GF F,N@V+ÂAzXЭd}݃.>XT6}%%`߱:pwrc[2KkE)%|}.n츠ލR w)IK B #IuD1$։Ƅ=n΍;ze}^m=KZ PĒ.yNgSm IYt6vQO>)<7O!7K)>6ӨR:Ϡ Cvx9g|& )u#yJ-DyihK-nSnb"ɖz-mr $LϾCwCT|Z\vT j:F%\n>I9xGF˳a7Ί kg;q ?eZRسq1j]lnX0¼6zQj[+1x{:\tC::?>Q̥) ]ֳ_0@1;nh+4Npݭ'{[}nZr+>0'`º9:3E@;gܝZ1B #:͝ o[G7=-ޣN.CB6_3/vݵ.BO>ܖʽGVDA\p|H@&9LG Su s#P^tLָȚyn-uTrѸTY*e_R<º)\ >y(=8`vre&ƽ8P~ 﫭IQQ)_PNþ )({x򁤇d6eu: E/!SMBSP);^=GDR=jYl5@:Zv'Zu+(''oH1DRjfD~`V`9PL1LmK GG k+;ڮzGV-+vU3 z+X \5-fP%cpd{yS qR15{}uzg h0P/_߼>=>zˣӃSI W"vI_Xt\?2}ΧT7eX(˘l?yWÇ[,[O[_~1 ?6?yfNj)3Q-D7o#߇o>yц|]/i/]]]E9ݍ%[D;Vw#~?|G;-IBzZL2] u?ԆYϡ׭}G{{,8c#jծn |Z, E>|-sn#4x޹ͱxF߮")nky_/XPj,ATq}=1ÙsQRQbq[ٯ$V1Ss1蝒g/?a ĊQC?a/Sewi'M=|Wj=9Ƀyz)}D?b mx:=R^U(K:Wp!^bP9`@P yke <uޱ;oEZӌZ 46%d7>OL8^?_Ʉ˞{*" IАojNkwvodR,M_|r:DeD d6յ:!ff `\cEfCǃG_DQ!qQ.*=?>RaOu]kPA ]Ի2ϴME;?/>yn'|W}CP.N qQ! rS/6W鍵b:wQ7jG \PK!Hvx3pgaphas-1.0.0.dist-info/RECORDuɲH}= V3$^0 nIH&7q$lsE]0.XCfnĥ'rqJh7!)] 37^0ٿ$F+/,gd31;G0L)UKˆ1XbX_A| ܜ,j4tgȏ۴!WEp+wMMªڅ0_>UC"3:v)B1ùգEb7_[N ׊Όl{M%%9L^,`vK⦃}V`l ļXBvyd0յ4U.gTnEBW[њ&c*G(Ow}tNf6qV;]W%UwVRE]Z~5>YVPiey".֍7:aI@IV%( Jq}IKRZ1M]LBzKSUߔ@j09wZ+y1>aL/T#rP\'(Husm0[Bbl^mh:UjOi{]:4a`|҄`T3r{zpEy^$x|U؟ԃ\1~6fG?NHqj+S90wa[>vvzԼt_9sYGh!WEEPR-课ChA\Rh+„?҆[ѣMQ_`vK6}&Is]L:gMc)vQG[a!G 2&IB3}'EJLf2jnhE"ÿeObs/,_*N}d] N]1x:VdemCn)&|dAvyw/>`j~)%\z9] 1'l18ȢHPK!!uGgaphas/__init__.pyPK!#""Fgaphas/aspect.pyPK!M||{'gaphas/canvas.pyPK!r;Pgaphas/connector.pyPK!@9EE~gaphas/constraint.pyPK!f|gaphas/decorators.pyPK!ŗjgaphas/examples.pyPK!uE?12gaphas/freehand.pyPK!c'6IIGgaphas/geometry.pyPK!*PX,z"z"gaphas/guide.pyPK!OSWWijgaphas/item.pyPK! 6  x gaphas/matrix.pyPK!E**gaphas/painter.pyPK!alooq@gaphas/picklers.pyPK!16#0#0Dgaphas/quadtree.pyPK!b) !!ctgaphas/segment.pyPK!}KKQgaphas/solver.pyPK!r!!gaphas/state.pyPK!("ccGgaphas/table.pyPK!1a1agaphas/tool.pyPK!paw(w(4|gaphas/tree.pyPK!Da] פgaphas/util.pyPK!oogaphas/view.pyPK!]#gaphas/weakset.pyPK!Hi&.'8gaphas-1.0.0.dist-info/entry_points.txtPK!''y8gaphas-1.0.0.dist-info/LICENSEPK!HWYt`gaphas-1.0.0.dist-info/WHEELPK!H4f*΄agaphas-1.0.0.dist-info/METADATAPK!Hvx3p-gaphas-1.0.0.dist-info/RECORDPK`