1 |
import pygtk |
2 |
pygtk.require('2.0') |
3 |
|
4 |
import math |
5 |
import gtk |
6 |
import cairo |
7 |
|
8 |
from gaphas.item import Item |
9 |
from gaphas.state import observed, reversible_property, disable_dispatching |
10 |
from gaphas.tool import HandleTool |
11 |
|
12 |
from gaphas import GtkView, View |
13 |
from gaphas.item import Line, SW, NE, NW, SE, Element, Handle |
14 |
from gaphas.tool import Tool, HoverTool, PlacementTool, HandleTool, ToolChain |
15 |
from gaphas.tool import ItemTool, RubberbandTool |
16 |
from gaphas.geometry import point_on_rectangle, distance_rectangle_point |
17 |
from gaphas.constraint import LineConstraint, LessThanConstraint, EqualsConstraint, Constraint, _update, BalanceConstraint |
18 |
from gaphas.canvas import CanvasProjection |
19 |
from gaphas.solver import Variable, solvable, WEAK, NORMAL, STRONG, VERY_STRONG |
20 |
|
21 |
from gaphas.painter import ItemPainter |
22 |
from gaphas import state |
23 |
from gaphas.util import text_extents |
24 |
|
25 |
from gaphas import painter |
26 |
|
27 |
from blockcanvas import * |
28 |
from panzoom import * |
29 |
#painter.DEBUG_DRAW_BOUNDING_BOX = True |
30 |
|
31 |
#------------------------------------------------------------------------------ |
32 |
|
33 |
class Port(object): |
34 |
""" |
35 |
Ports are special places onto which Handles can be connected, specifically |
36 |
when users are drawing 'connector' lines between modelling 'blocks'. |
37 |
|
38 |
A subclass of Item called 'Block' will be given the ability to store |
39 |
an array of these Ports, in such a way that gluing methods will be able to |
40 |
select appropriate ports for a given connector line. The Ports also give |
41 |
data about where these available connected are located graphically, in the |
42 |
Block's local coordinate system. |
43 |
|
44 |
It is not intended that the number or location of ports would be editable |
45 |
once a port is instantiated. Only the connection of a port to a handle |
46 |
is editable by the user. |
47 |
|
48 |
It is intended that subclasses to the Port class would be created to provide |
49 |
finer control over whether or not a certain handle can be permitted to |
50 |
connect to any given port. |
51 |
|
52 |
Attributes: |
53 |
- block: Block to which Port belongs |
54 |
- connected_to: Handle to which port is connected, if any |
55 |
- x: port x-location in item's local coordinate system |
56 |
- y: port y-location in item's local coordinate system |
57 |
|
58 |
Private: |
59 |
- _x: port x-location in item's local coordinate system |
60 |
- _y: port y-location in item's local coordinate system |
61 |
- _connected_to: Handle to which port is connected, if any |
62 |
""" |
63 |
|
64 |
_x = solvable() |
65 |
_y = solvable() |
66 |
|
67 |
def __init__(self, block, strength = STRONG): |
68 |
self.block = block |
69 |
self._x.strength = strength |
70 |
self._y.strength = strength |
71 |
self._connected_to = None |
72 |
|
73 |
@observed |
74 |
def _set_x(self, x): |
75 |
self._x = x |
76 |
|
77 |
x = reversible_property(lambda s: s._x, _set_x, bind={'x': lambda self: float(self.x) }) |
78 |
disable_dispatching(_set_x) |
79 |
|
80 |
@observed |
81 |
def _set_y(self, y): |
82 |
self._y = y |
83 |
|
84 |
y = reversible_property(lambda s: s._y, _set_y, bind={'y': lambda self: float(self.y) }) |
85 |
disable_dispatching(_set_y) |
86 |
|
87 |
@observed |
88 |
def _set_connected_to(self, connected_to): |
89 |
self._connected_to = connected_to |
90 |
|
91 |
connected_to = reversible_property(lambda s: s._connected_to, |
92 |
_set_connected_to) |
93 |
|
94 |
def draw(self, context): |
95 |
""" |
96 |
Render the item to a canvas view. |
97 |
Context contains the following attributes: |
98 |
|
99 |
- cairo: the Cairo Context use this one to draw |
100 |
- view: the view that is to be rendered to |
101 |
- selected, focused, hovered, dropzone: view state of items (True/False) |
102 |
- draw_all: a request to draw everything, for bounding box calculation |
103 |
""" |
104 |
pass |
105 |
|
106 |
def point(self, x, y): |
107 |
""" |
108 |
Get the distance from a point (``x``, ``y``) to the item. |
109 |
``x`` and ``y`` are in item coordinates. |
110 |
|
111 |
Defined here because a port is just a 'point' at this stage. |
112 |
""" |
113 |
return math.sqrt((x-self.x)**2 + (y-self.y)**2) |
114 |
|
115 |
@observed |
116 |
def _set_pos(self, pos): |
117 |
""" |
118 |
Set handle position (Item coordinates). |
119 |
""" |
120 |
self.x, self.y = pos |
121 |
|
122 |
pos = property(lambda s: (s.x, s.y), _set_pos) |
123 |
|
124 |
class PointConstraint(Constraint): |
125 |
""" |
126 |
Ensure that point B is always kept on top of point A |
127 |
|
128 |
Attributes: |
129 |
_A: first point, defined by (x,y) |
130 |
_B: second point, defined by (x,y) |
131 |
""" |
132 |
|
133 |
def __init__(self, A, B): |
134 |
print "A =",A |
135 |
print "A[0] =",A[0].variable() |
136 |
print "A[0].strength =",A[0].variable().strength |
137 |
|
138 |
print "B =",B |
139 |
print "B[0] =",B[0].variable() |
140 |
print "B[0].strength =",B[0].variable().strength |
141 |
|
142 |
# assert isinstance(p1[0],Variable) |
143 |
# assert isinstance(p1[1],Variable) |
144 |
# assert isinstance(p2[0],Variable) |
145 |
# assert isinstance(p2[1],Variable) |
146 |
|
147 |
super(PointConstraint, self).__init__(A[0],A[1],B[0],B[1]) |
148 |
self._A = A |
149 |
self._B = B |
150 |
|
151 |
def solve_for(self, var=None): |
152 |
print "Solving PointConstraint",self,"for var",var |
153 |
|
154 |
_update(self._B[0], self._A[0].value) |
155 |
_update(self._B[1], self._A[1].value) |
156 |
|
157 |
class Block(Element): |
158 |
""" |
159 |
This is an ASCEND 'block' in the canvas-based modeller. The block will have |
160 |
sets of input and output ports to which connector lines can be 'glued'. |
161 |
The block will also have a corresponding ASCEND MODEL type, and a name |
162 |
which will be used in ASCEND to refer to this block. Each of the ports will |
163 |
be special visual elements, but note that these are not 'handles', because |
164 |
they can not be used to resize/modify the element. |
165 |
""" |
166 |
|
167 |
def __init__(self, label="unnamed", width=64, height=64): |
168 |
|
169 |
self.ports = [] |
170 |
self.label = label |
171 |
super(Block, self).__init__(width, height) |
172 |
|
173 |
def draw(self, context): |
174 |
#print 'Box.draw', self |
175 |
c = context.cairo |
176 |
nw = self._handles[NW] |
177 |
c.rectangle(nw.x, nw.y, self.width, self.height) |
178 |
if context.hovered: |
179 |
c.set_source_rgba(.8,.8,1, .8) |
180 |
else: |
181 |
c.set_source_rgba(1,1,1, .8) |
182 |
c.fill_preserve() |
183 |
c.set_source_rgb(0,0,0.8) |
184 |
c.stroke() |
185 |
|
186 |
phalfsize = 3 |
187 |
for p in self.ports: |
188 |
c.rectangle(p.x - phalfsize, p.y - phalfsize, 2*phalfsize, 2*phalfsize) |
189 |
if p.connected_to is None: |
190 |
c.set_source_rgba(0.8,0.8,1, 0.8) |
191 |
else: |
192 |
c.set_source_rgba(1,0,0,1) |
193 |
c.fill_preserve() |
194 |
c.set_source_rgb(0.8,0.8,0) |
195 |
c.stroke() |
196 |
|
197 |
def glue(self,item, handle, ix, iy): |
198 |
gluerange = 10 |
199 |
mindist = -1; |
200 |
minport = None |
201 |
for p in self.ports: |
202 |
dist = math.sqrt((ix-p.x)**2 + (iy-p.y)**2) |
203 |
if dist < gluerange: |
204 |
if not minport or dist<mindist: |
205 |
mindist = dist |
206 |
minport = p |
207 |
return mindist, minport |
208 |
|
209 |
def pre_update(self,context): |
210 |
print "PRE-UPDATE BLOCK" |
211 |
|
212 |
class DefaultBlock(Block): |
213 |
""" |
214 |
This is a 'default block' with a certain number of input and output ports |
215 |
shown depending on the values sent to __init__. It is drawn as a simple |
216 |
box with the input ports on the left and the output ports on the right. |
217 |
""" |
218 |
|
219 |
def __init__(self, label="unnamed", width=64, height=64, inputs=2, outputs=3): |
220 |
|
221 |
super(DefaultBlock, self).__init__(label, width, height) |
222 |
|
223 |
eq = EqualsConstraint |
224 |
bal = BalanceConstraint |
225 |
handles = self._handles |
226 |
h_nw = handles[NW] |
227 |
h_ne = handles[NE] |
228 |
h_sw = handles[SW] |
229 |
h_se = handles[SE] |
230 |
|
231 |
for i in range(inputs): |
232 |
p = Port(self) |
233 |
self.ports.append(p) |
234 |
self._constraints.append(eq(p.x, h_nw.x)) |
235 |
self._constraints.append(bal(band=(h_nw.y, h_sw.y),v=p.y, balance=(0.5 + i)/inputs)) |
236 |
|
237 |
for i in range(outputs): |
238 |
p = Port(self) |
239 |
self.ports.append(p) |
240 |
self._constraints.append(eq(p.x, h_ne.x)) |
241 |
self._constraints.append(bal(band=(h_ne.y,h_se.y),v=p.y, balance=(0.5 + i)/outputs)) |
242 |
|
243 |
def draw(self, context): |
244 |
# draw the box itself |
245 |
c = context.cairo |
246 |
nw = self._handles[NW] |
247 |
c.rectangle(nw.x, nw.y, self.width, self.height) |
248 |
if context.hovered: |
249 |
c.set_source_rgba(.8,.8,1, .8) |
250 |
else: |
251 |
c.set_source_rgba(1,1,1, .8) |
252 |
c.fill_preserve() |
253 |
c.set_source_rgb(0,0,0.8) |
254 |
c.stroke() |
255 |
|
256 |
# now the draw the ports using the base class |
257 |
super(DefaultBlock, self).draw(context) |
258 |
|
259 |
class PortConnectingHandleTool(HandleTool): |
260 |
""" |
261 |
This is a HandleTool which supports the connection of lines to the Ports |
262 |
of Blocks, for the purpose of building up process flow diagrams, control |
263 |
diagrams, etc, for the proposed canvas-based modeller of ASCEND. |
264 |
""" |
265 |
|
266 |
def glue(self, view, item, handle, wx, wy): |
267 |
""" |
268 |
This allows the tool to glue a handle to any connectable Port of a Block. |
269 |
The distance from the item to the handle is determined in canvas |
270 |
coordinates, using a 10 pixel glue distance. |
271 |
|
272 |
Returns the closest Port that is within the glue distance. |
273 |
""" |
274 |
if not handle.connectable: |
275 |
return |
276 |
|
277 |
# Make glue distance depend on the zoom ratio (should be about 10 pixels) |
278 |
inverse = cairo.Matrix(*view.matrix) |
279 |
inverse.invert() |
280 |
#glue_distance, dummy = inverse.transform_distance(10, 0) |
281 |
glue_distance = 10 |
282 |
glue_port = None |
283 |
glue_point = None |
284 |
#print "Gluing..." |
285 |
for i in view.canvas.get_all_items(): |
286 |
if not hasattr(i,'ports'): |
287 |
continue |
288 |
if not i is item: |
289 |
#print "Trying glue to",i |
290 |
v2i = view.get_matrix_v2i(i).transform_point |
291 |
ix, iy = v2i(wx, wy) |
292 |
distance, port = i.glue(item, handle, ix, iy) |
293 |
# Transform distance to world coordinates |
294 |
#distance, dumy = matrix_i2w(i).transform_distance(distance, 0) |
295 |
if not port is None and distance <= glue_distance: |
296 |
glue_distance = distance |
297 |
i2v = view.get_matrix_i2v(i).transform_point |
298 |
glue_point = i2v(port.x, port.y) |
299 |
glue_port = port |
300 |
else: |
301 |
print "i is item" |
302 |
if glue_point: |
303 |
v2i = view.get_matrix_v2i(item).transform_point |
304 |
handle.x, handle.y = v2i(*glue_point) |
305 |
#print "Found glue point ",handle.x,handle.y |
306 |
return glue_port |
307 |
|
308 |
def connect(self, view, item, handle, wx, wy): |
309 |
""" |
310 |
Connect a handle to a port. 'item' is the line to which the handle |
311 |
belongs; wx and wy are the location of the cursor, so we run the 'glue' |
312 |
routine to find the desired gluing point, then make the connection to |
313 |
the object which 'glue' returns, which will be a Port object (in the |
314 |
context of this tool). |
315 |
|
316 |
In this "method" the following assumptios are made: |
317 |
1. Only ``Port``s of ``Block``s will accept connections from handles. |
318 |
2. The only items with connectable handles are ``Line``s |
319 |
|
320 |
""" |
321 |
|
322 |
# create a special local handle_disconnect function |
323 |
def handle_disconnect(): |
324 |
try: |
325 |
view.canvas.solver.remove_constraint(handle.connection_data) |
326 |
except KeyError: |
327 |
print 'constraint was already removed for', item, handle |
328 |
pass # constraint was alreasy removed |
329 |
else: |
330 |
print 'constraint removed for', item, handle |
331 |
handle.connected_port.connected_to = None |
332 |
handle.connection_data = None |
333 |
handle.connection_port = None |
334 |
handle.connected_to = None |
335 |
|
336 |
# Remove disconnect handler: |
337 |
handle.disconnect = lambda: 0 |
338 |
|
339 |
#print 'Handle.connect', view, item, handle, wx, wy |
340 |
glue_port = self.glue(view, item, handle, wx, wy) |
341 |
|
342 |
if glue_port and hasattr(handle,'connected_port') and handle.connected_port is glue_port: |
343 |
try: |
344 |
view.canvas.solver.remove_constraint(handle.connection_data) |
345 |
except KeyError: |
346 |
pass |
347 |
else: |
348 |
# ie no glue_port found, or the handle connected to something else |
349 |
if handle.connected_to: |
350 |
handle.disconnect() |
351 |
|
352 |
if glue_port: |
353 |
if isinstance(glue_port, Port): |
354 |
print "Gluing to port",glue_port |
355 |
|
356 |
print "handle.pos =",handle.pos |
357 |
print "glue_port =",glue_port |
358 |
print "glue_port.pos = ",glue_port.pos |
359 |
print "glue_port.block =",glue_port.block |
360 |
|
361 |
handle.connection_data = PointConstraint( |
362 |
B=CanvasProjection(handle.pos,item) |
363 |
,A=CanvasProjection(glue_port.pos, glue_port.block) |
364 |
) |
365 |
view.canvas.solver.add_constraint(handle.connection_data) |
366 |
#glue_port.block._constraints.append(handle.connection_data) |
367 |
|
368 |
handle.connected_to = glue_port.block |
369 |
handle.connected_port = glue_port |
370 |
handle.disconnect = handle_disconnect |
371 |
glue_port.connected_to = handle |
372 |
|
373 |
def disconnect(self, view, item, handle): |
374 |
if handle.connected_to: |
375 |
print 'Handle.disconnect', view, item, handle |
376 |
view.canvas.solver.remove_constraint(handle.connection_data) |
377 |
|
378 |
|
379 |
|
380 |
# vim: sw=4:et:ai |