-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathzooming.py
310 lines (264 loc) · 11.5 KB
/
zooming.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
from tkinter import *
from math import log, exp
import operator
__doc__="""
todo:
scale constraints don't work yet
certain scale constraints should affect position, maybe vice versa
"""
class Pair:
"""ordered pair of numbers. element-wise add/mult/etc. access the numbers by index or .x .y"""
def __init__(self,a,b):
self.a=a
self.b=b
def __add__(self,other): return Pair(self[0]+other[0],self[1]+other[1])
def __sub__(self,other): return Pair(self[0]-other[0],self[1]-other[1])
def __mul__(self,other): return Pair(self[0]*other[0],self[1]*other[1])
def __truediv__(self,other): return Pair(self[0]/other[0],self[1]/other[1])
def __iadd__(self,other):
self[0]+=other[0]
self[1]+=other[1]
return self
def __isub__(self,other):
self[0]-=other[0]
self[1]-=other[1]
return self
def __imul__(self,other):
self[0]*=other[0]
self[1]*=other[1]
return self
def __itruediv__(self,other):
self[0]/=other[0]
self[1]/=other[1]
return self
def __neg__(self):
r= Pair(-self[0],-self[1])
return r
def __getattr__(self,attr):
if attr in ('x','a'): return self[0]
if attr in ('y','b'): return self[1]
# raise NotImplementedError(attr)
# had to comment this out in order to work with Python 2.4.3
def __coerce__(self,other):
return None
def __str__(self):
return "(%s,%s)"%(self.a,self.b)
def __repr__(self):
return "Pair%s" % str(self)
def __iter__(self):
return iter((self[0],self[1]))
def __getitem__(self,i):
if i==0: return self.a
if i==1: return self.b
raise IndexError
def __setitem__(self,i,val):
if i==0: self.a=val
if i==1: self.b=val
if i<0 or i>1:
raise IndexError
class Constraint:
def __init__(self,varname,op,value):
"""
for example:
leftedge>0 (world coords)
scl.x==scl.y
"""
if varname not in 'left top right bottom sclx scly'.split():
raise ValueError("varname '%s' is unknown" % varname)
if op not in ('<','>','<=','>=','=='):
raise ValueError("op '%s' is unknown" % op)
try:
value=float(value) # make sure possible floats are actually float type
except ValueError:
pass
if not isinstance(value,float) and value not in ('sclx','scly'):
raise ValueError("value '%s' is unknown" % value)
self.var=varname
self.op=op
self.value=value
def __str__(self):
return "<Constraint %s %s %s>" % (self.var,self.op,self.value)
def calc(self,v,edge,transformed):
"""
calc receives requested value 'v' and returns a new
value. returning v itself means that the
constraint had no effect-- the requested value will not
violate the constraint.
this function might decide to adjust v to
meet the constraint though. the available data is a
transformed edge coodrinate and the actual edge
coordinate. the coordinate system is unspecified, but all
values will be in the same coord system.
"""
if ((self.op==">" and transformed>edge) or
(self.op=="<" and transformed<edge) or
(self.op=="==" and transformed!=edge)):
#print "%s correcting by %s, ev=%s, tv=%s" % (self,transformed-edge,edge,transformed)
v=v-(transformed-edge)
return v
class Zooming(Canvas):
"""canvas that supports zoom and pan. in this object, the 'world'
coordinates are the unchanging ones. you convert to/from 'canvas'
coordinates, which are the ones suitable for all canvas
operations.
maybe sometime i'll wrap all the canvas ops so you can work
exclusively in world coords."""
def __init__(self,*k,**kw):
Canvas.__init__(self,*k,**kw)
# our transformation is stored in off/scl
# world points get offset by off, then
# scaled by scl. so the offsets stay in world units.
self.off=Pair(0,0)
self.scl=Pair(1,1)
self.constraints=[] # list of Constraint objs
def addconstraint(self,*args):
self.constraints.append(Constraint(*args))
# some identity transformations will give the new constraint a
# chance to push the transformation around:
if self.winfo_ismapped(): # only if the winfo sizes are correct
self.move(0,0)
self.scale(0,0,1,1)
def delconstraint(self,varname,op):
keep=[]
for c in self.constraints:
if not (c.var==varname and c.op==op):
keep.append(c)
self.constraints=keep
def move(self,dx,dy):
"""canvas move command, but always moves everything and
updates the transformation. dx,dy are world coordinates."""
for c in self.constraints:
if c.var=="left":
dx = self.c2w_dx(c.calc(v=self.w2c_dx(dx),
edge=0,
transformed=self.world2canvas(c.value+dx,0)[0]))
if c.var=="right":
dx = self.c2w_dx(c.calc(v=self.w2c_dx(dx),
edge=self.winfo_width(),
transformed=self.world2canvas(c.value+dx,0)[0]))
if c.var=="top":
dy = self.c2w_dy(c.calc(v=self.w2c_dy(dy),
edge=0,
transformed=self.world2canvas(0,c.value+dy)[1]))
if c.var=="bottom":
dy = self.c2w_dy(c.calc(v=self.w2c_dy(dy),
edge=self.winfo_height(),
transformed=self.world2canvas(0,c.value+dy)[1]))
cdx,cdy=self.world2canvas_vector(dx,dy)
Canvas.move(self,"all",cdx,cdy)
self.off+=(dx,dy)
def move_can(self,dx,dy):
"""like move, but you give canvas units (pixels) instead of world units"""
return self.move(*(Pair(dx,dy)/self.scl))
def scale(self,xOrigin,yOrigin,xScale,yScale):
"""canvas scale command, but also updates the transformation. all objects are scaled.
origin is in canvas coordinates (use world2canvas to convert from world coords if necessary)."""
if xScale==0 or yScale==0:
print("Zooming.scale received scale factor of 0 - ignoring")
return
for c in self.constraints:
if c.var=="sclx":
xScale = -c.calc(-xScale, edge=xScale, transformed=c.value/self.scl.x)
if c.var=="scly":
yScale = -c.calc(-yScale, edge=yScale, transformed=c.value/self.scl.y)
factor = Pair(xScale,yScale)
# to acheive the desired effect of the xOrigin,yOrigin canvas
# point holding stationary during the scale, we remember the
# world coords of that 'scale center' before the scaling
worldorigin_prescale=Pair(*self.canvas2world(xOrigin,yOrigin))
# ..apply the scaling
self.scl*=factor
# ..and see where the scale center moved (in world coords)
worldorigin_postscale=Pair(*self.canvas2world(xOrigin,yOrigin))
# now, we simply correct the offset so the scale center does not move
self.off+=(worldorigin_postscale-worldorigin_prescale)
Canvas.scale(self,"all",xOrigin,yOrigin,xScale,yScale)
# a null move will let the offset constraints take effect
self.move(0,0)
def setscale(self,xOrigin,yOrigin,xScale,yScale):
"""sets scale factor absolutely, rather than multiplying to the existing scale factor"""
self.scale(xOrigin,yOrigin,xScale/self.scl.x,yScale/self.scl.y)
def world2canvas(self,*worldcoord):
"""takes a world coordinate as a tuple and returns the canvas coordinate"""
r=(Pair(*worldcoord)+self.off)*self.scl
return tuple(r)
def canvas2world(self,*canvascoord):
"""takes a canvas coordinate as a tuple and returns the world coordinate"""
r=(Pair(*canvascoord)/self.scl)-self.off
return tuple(r)
def canvas2world_vector(self,*v):
"""converts a canvas vector to world vector"""
return Pair(*v)/self.scl
def world2canvas_vector(self,*v):
"""converts a world vector to canvas vector"""
return Pair(*v)*self.scl
# convenience (?) - *for vectors only*
def c2w_dx(self,dx): return self.canvas2world_vector(dx,0)[0]
def w2c_dx(self,dx): return self.world2canvas_vector(dx,0)[0]
def c2w_dy(self,dy): return self.canvas2world_vector(0,dy)[1]
def w2c_dy(self,dy): return self.world2canvas_vector(0,dy)[1]
def panbindings(self):
"""Ctrl-LMB used to pan the canvas like me10"""
def press(self,ev):
self.lastmouse=Pair(ev.x,ev.y)
def motion(self,ev):
self.move_can(ev.x-self.lastmouse.x,ev.y-self.lastmouse.y)
self.lastmouse=Pair(ev.x,ev.y)
def release(self,ev):
pass
self.bind("<Control-ButtonPress-1>",lambda ev: press(self,ev))
self.bind("<Control-B1-Motion>",lambda ev: motion(self,ev))
# Use the ctrl-B1-rel event in the app to trigger cline regen
#self.bind("<Control-B1-ButtonRelease>",lambda ev: release(self,ev))
def zoombindings(self):
"""Ctrl-RMB to zoom the canvas like me10"""
def press(self,ev):
self.firstmouse=Pair(ev.x,ev.y)
self.prevmouse=Pair(ev.x,ev.y)
def motion(self,ev):
self.scale(self.firstmouse.x,self.firstmouse.y,
1+.02*(ev.y-self.prevmouse.y),
1+.02*(ev.y-self.prevmouse.y))
self.prevmouse=Pair(ev.x,ev.y)
def release(self,ev):
pass
self.bind("<Control-ButtonPress-3>",lambda ev: press(self,ev))
self.bind("<Control-B3-Motion>",lambda ev: motion(self,ev))
# Use the ctrl-B3-rel event in the app to trigger cline regen
#self.bind("<Control-B3-ButtonRelease>",lambda ev: release(self,ev))
class outline_marker:
pass
if __name__=='__main__':
from math import sin,cos
root=Tk()
z=Zooming(root,width=400,height=400)
z.pack(fill=BOTH,expand=1)
z.panbindings()
z.zoombindings()
apply(z.create_rectangle,(z.world2canvas(5,5)+z.world2canvas(395,395)),{'tags':'demo'})
z.scale(60,60,.5,.5)
apply(z.create_rectangle,(z.world2canvas(5,5)+z.world2canvas(15,15)),{'tags':'demo'})
z.scale(60,60,2,2)
apply(z.create_rectangle,(z.world2canvas(15,15)+z.world2canvas(25,25)),{'tags':'demo'})
z.itemconfigure("demo",width=2)
# this draws a grid, with a crazy scalings
# between each new set of lines
unscale=Pair(1,1)
for x in [i*15 for i in range(1,27)]:
z.scale(x*10,x*10,2,2)
apply(z.create_line,(z.world2canvas(x,0)+z.world2canvas(x,400)),{'tags':'grid'})
apply(z.create_line,(z.world2canvas(0,x)+z.world2canvas(400,x)),{'tags':'grid'})
z.scale(x*10,x*10,.5,.5)
sca=Pair(sin(x+.1),cos(x+.1))
z.scale(50,50,sca[0],sca[1])
unscale/=sca
z.scale(50,50,unscale[0],unscale[1])
z.itemconfigure("grid",width=1)
apply(z.create_rectangle,(z.world2canvas(153,153)+z.world2canvas(395,395)))
# z.addconstraint("right","<",400)
# z.addconstraint("left",">",0)
# z.addconstraint("left","==",50)
# z.addconstraint("top",">",0)
# z.addconstraint("bottom","<",500)
z.addconstraint("sclx","==",1)
root.mainloop()