summaryrefslogtreecommitdiff
path: root/cad/src/graphics/drawables/handles.py
blob: ec6a30c8f33f61b3ec1b103c1efdfdd174e18053 (plain)
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
311
312
313
314
315
316
# Copyright 2004-2008 Nanorex, Inc.  See LICENSE file for details.
"""
handles.py - graphical handles used in Extrude Mode.

Deprecated for new code, since they don't use the Selobj_API
used by most handle-like things.

@author: Bruce
@version: $Id$
@copyright: 2004-2008 Nanorex, Inc.  See LICENSE file for details.

TODO:

Needs cleanup.
"""

from Numeric import sqrt

from geometry.VQT import V
from geometry.VQT import vlen
from geometry.VQT import norm
from geometry.VQT import orthodist
from graphics.drawing.CS_draw_primitives import drawsphere
from utilities.constants import ave_colors
from utilities.constants import magenta
from utilities.constants import blue

class handleWithHandleSet:
    """
    used to wrap handles returned from a handleset, so they can use its methods
    """
    def __init__(self, handle, handleset, copy_id = None):
        self.handle = handle
        self.handleset = handleset
        self.copy_id = copy_id
    def click(self): #e now leftDown, but later, "button press" leftUp
        self.handleset.click_handle( self.handle, self.copy_id)
    def move(self, motion):
        self.handleset.move_handle( self.handle, self.copy_id, motion )
    def __repr__(self):
        return "handleWithHandleSet( %r, %r, %r)" % (self.handle, self.handleset, self.copy_id)
    def __str__(self):
        return self.handleset.str_handle( self.handle, self.copy_id)
    def leftDown_status_msg(self):
        return self.handleset.leftDown_status_msg( self.handle, self.copy_id)
    pass

class HandleSet:
    """
    maintain a set of spheres, able to be intersected with a ray, or a 3d point
    """
    color = (0.5,0.5,0.5) # default color (gray50)
    radius_multiplier = 1.0 # this might be patched to some other value by our owner;
     # should affect all internal computations using radii, but not returned radii inside handle tuples ####NIM
    def __init__(self):
        self.origin = V(0,0,0) # changed from this only by certain subclasses, in practice
        self.handles = [] # list of (pos,radius,info) tuples
        # handlpos and maxradius are not used now, but might be used later to optimize this.
        self.handlpos = [] # list of their pos's (compare to singlpos in class Chunk (_recompute_singlpos))
        self.maxradius = 0.01 # not true, but best if it's always positive, I think
    #e to optimize, we might want a "compile" method which caches Array versions of these lists
    def addHandle(self, pos, radius, info):
        """
        add a handle of the given position, radius, and info,
        and return its index, unique in this Set
        """
        self.handles.append((pos,radius,info))
        self.handlpos.append(pos)
        if radius > self.maxradius:
            self.maxradius = radius
        return len(self.handles) # index of new handle #e needed?
    def compile(self): #e unused??
        pass #e cache Array versions of some of our lists
    def move(self, offset):
        self.origin = self.origin + offset
        ## warning: this would be wrong (due to destructive mod of a vector): self.origin += motion
    def draw(self, glpane, offset = V(0,0,0), color = None, info = {}): # this code is copied/modified into a subclass, sorry
        """
        draw our spheres (in practice we'll need to extend this for different sets...)
        """
##        self.radius_multiplier = 1.0 # this might be changed by certain subclass's process_optional_info method
##        self.process_optional_info(info) # might reset instvars that affect following code... (kluge?)
        color = color or self.color
        ##detailLevel = 0 # just an icosahedron
        detailLevel = 1 # easier to click on this way
        ##radius = 0.33 # the one we store might be too large? no, i guess it's ok.
        #e (i might prefer an octahedron, or a miniature-convex-hull-of-extrude-unit)
        offset = offset + self.origin
        radius_multiplier = self.radius_multiplier
        color = tuple(color) + (1.0,) ## experiment 050218: alpha factor #bruce 051230 moved outside of loop to fix bug 1250
        for (pos,radius,info) in self.handles:
            drawsphere(color, pos + offset, radius * radius_multiplier, detailLevel)
##    def process_optional_info(self, info):
##        "some subclasses should override this to let info affect draw method"
##        pass
    def findHandles_containing(self, point):
        """
        return a list of all the handles (in arbitrary order)
        which (as balls) contain the given 3d point
        """
        res = []
        for (pos,radius,info) in self.handles: #e revise this code if we cluster them, esp with bigger radius
            if vlen(point - pos) <= radius * self.radius_multiplier:
                res.append((pos,radius,info))
        return res
##    def findHandles_near(self, point, radius = None):
##        """return a list (in arbitrary order) of pairs (dist, handle) for all the handles
##           which are near the given point (within the given radius (default very large###e???),
##           *or* within their own sphere-radius). #### WRONG CODE
##        """
##        assert 0
    def findHandles_exact(self, p1, p2, cutoff = 0.0, backs_ok = 1, offset = V(0,0,0)):
        """
        @return: a list of (dist, handle) pairs, in arbitrary order, which
        includes, for each handle (spherical surface) hit by the ray from p1
        thru p2, its front-surface intersection with the ray, unless that has
        dist < cutoff and backs_ok, in which case include its back-surface
        intersection (unless *that* has dist < cutoff).
        """
        #e For now, just be simple, don't worry about speed.
        # Someday we can preprocess self.handlpos using Numeric functions,
        # like in nearSinglets and/or findSinglets
        # (I have untested prototype code for this in extrude-outs.py).
        hh = self.handles
        res = []
        v = norm(p2-p1)
        # is this modifying the vector in-place, causing a bug??
        ## offset += self.origin # treat our handles' pos as relative to this
        # I don't know, but one of the three instances of += was doing this!!!
        # probably i was resetting the atom or mol pos....
        offset = offset + self.origin # treat our handles' pos as relative to this
        radius_multiplier = self.radius_multiplier
        for (pos,radius,info) in hh:
            ## bug in this? pos += offset
            pos = pos + offset
            radius *= radius_multiplier
            dist, wid = orthodist(p1, v, pos)
            if radius >= wid: # the ray hits the sphere
                delta = sqrt(radius*radius - wid*wid)
                front = dist - delta # depth from p1 of front surface of sphere, where it's hit
                if front >= cutoff:
                    res.append((front,(pos,radius,info)))
                elif backs_ok:
                    back = dist + delta
                    if back >= cutoff:
                        res.append((back,(pos,radius,info)))
        return res
    def frontDistHandle(self, p1, p2, cutoff = 0.0, backs_ok = 1, offset = V(0,0,0), copy_id = None):
        """
        @return: None, or the frontmost (dist, handle) pair, as computed by
        findHandles_exact; but turn the handle into a pyobj for convenience of
        caller.
        """
        # check: i don't know if retval needs self.radius_multiplier...
        # review: will we need to let caller know whether it was the front or
        # back surface we hit? or even the exact position on the sphere? if
        # so, add more data to the returned pair.
        dhdh = self.findHandles_exact(p1, p2, cutoff, backs_ok, offset = offset)
        if not dhdh: return None
        dhdh.sort()
        (dist, handle0) = dhdh[0]
        handle = self.wrap_handle(handle0, copy_id)
        return (dist, handle)
    def wrap_handle(self, handle, copy_id):
        (pos,radius,info) = handle # check format
        return handleWithHandleSet( handle, self, copy_id = copy_id)
    def click_handle( self, handle, copy_id):
        "click one of our own handles; subclasses should override this"
        pass
    def move_handle( self, handle, copy_id, motion):
        "move one of our own handles (e.g. when it's dragged); subclasses should override this"
        pass
    def str_handle( self, handle, copy_id):
        "subclasses are encouraged to override this so it looks good in messages to the user"
        (pos,radius,info) = handle
        return "<handle pos=%r, radius=%3f, info=%r, in copy %r of %r>" % (pos,radius,info,copy_id,self)
    def leftDown_status_msg( self, handle, copy_id):
        "subclasses should override this"
        return ""
    def handle_setpos( self, ind, newpos):
        "patch our arrays to change pos of just one handle"
        (pos,radius,info) = self.handles[ind]
        pos = newpos
        self.handles[ind] = (pos,radius,info)
        self.handlpos[ind] = pos #e might fail if we ever make self.compile() do something
        pass
    pass

class draggableHandle_HandleSet(HandleSet):
    "a handleset, with behavior to let you drag the entire thing, and something else too" # used for "purple center"...
    def __init__(self, color = magenta, motion_callback = None, statusmsg = "draggable handle"):
        self.color = color
        self.motion_callback = motion_callback
        self.statusmsg = statusmsg
        HandleSet.__init__(self)
    def move_handle( self, handle, copy_id, motion):
        self.move(motion)
        if self.motion_callback:
            self.motion_callback(motion)
        return
    def leftDown_status_msg( self, handle, copy_id):
        return self.statusmsg
    pass

class repunitHandleSet(HandleSet): #e this really belongs in extrudeMode.py, not in this file
    "a handleset for an extrudeable unit, which lets copy_id specify which repunit is meant"
    def __init__(self, *args, **kws):
        self.target = kws.pop('target') # must be specified; the extrudeMode object we're helping to control
        HandleSet.__init__(self, *args, **kws)
    def move_handle( self, handle, copy_id, motion):
        self.target.drag_repunit(copy_id, motion)
    def hset_name(self, copy_id): #e move this name code to the hset itself
        if copy_id:
            name = "repeat unit #%d" % copy_id
        else:
            name = "base unit"
        return name
    def str_handle( self, handle, copy_id):
        name = self.hset_name(copy_id)
        (pos,radius,info) = handle
        return "atom at pos=%r in %s" % (pos,name)
    def leftDown_status_msg( self, handle, copy_id):
        name = self.hset_name(copy_id)
        if copy_id:
            return "%s (can be dragged to change the offset)" % name
        else:
            return name # + " (can be dragged to move the entire model -- not implemented)"
        pass
    pass


class niceoffsetsHandleSet(HandleSet): #e this really belongs in extrudeMode.py, not in this file
    "a handleset for holding the set of nice offsets"
    #e in future, we can show things on mouseover, perhaps like:
    # mouseover of nice_offset handle will light up its two singlets;
    # mouseover of a singlet will light up the applicable other singlets
    # and nice_offset handles; etc.
    special_pos = V(0,0,0)
    special_color = blue
    def __init__(self, *args, **kws):
        self.target = kws.pop('target') # must be specified; the extrudeMode object we're helping to control
        HandleSet.__init__(self, *args, **kws)
    def leftDown_status_msg( self, handle, copy_id):
        (pos,radius,info) = handle
        i1,i2 = info
        # this text is meant for the present situation, where we jump on mousedown;
        # better behavior would be buttonlike (jump on mouseup, iff still over handle),
        # and the text should be adjusted when we implement that #e
        return "overlapped bondpoints base#%d and rep#%d" % (i1,i2)
    def click_handle( self, handle, copy_id):
        self.target.click_nice_offset_handle(handle)
##    def process_optional_info(self, info):
##        bond_tolerance = info.get('bond_tolerance',1.0)
##        self.radius_multiplier = bond_tolerance # affects draw() method
    def draw(self, glpane, offset = V(0,0,0), color = None, info = {}): # modified copy of superclass draw method
        "draw our spheres (in practice we'll need to extend this for different sets...)"
##        self.radius_multiplier = 1.0 # this might be changed by certain subclass's process_optional_info method
##        self.process_optional_info(info) # might reset instvars that affect following code... (kluge?)
        color = color or self.color
        ##detailLevel = 0 # just an icosahedron
        detailLevel = 1 # easier to click on this way
        ##radius = 0.33 # the one we store might be too large? no, i guess it's ok.
        #e (i might prefer an octahedron, or a miniature-convex-hull-of-extrude-unit)
        offset = offset + self.origin
        radius_multiplier = self.radius_multiplier
        special_pos = self.special_pos # patched in ###nim?
        special_pos = special_pos + offset #k?? or just self.origin??
        special_color = self.special_color # default is used
##        count = 0
        for (pos,radius,info) in self.handles:
            radius *= radius_multiplier
            pos = pos + offset
            dist = vlen(special_pos - pos)
            if dist <= radius:
                color2 = ave_colors( 1.0 - dist/radius, special_color, color )
##                count += 1
            else:
                color2 = color
            ## experiment 050218: add alpha factor to color
            color2 = tuple(color2) + (0.25,)
            drawsphere(color2, pos, radius, detailLevel)
##        self.color2_count = count # kluge, probably not used since should equal nbonds
        return
    pass


# we'll use the atoms of the mols
# and a sep set or two
# of special ones
# and singlet-pair spheres (maybe variabe size)

# ok to only use spheres for now
# if the api could permit changing that

# we don't want to force every stored handle to be an object!

# ..

# each handleset can compute, for one of its handles:
# what to do on mouseover
#  change their color
#  give them a status message
#  (change cursor?)
# what to do if you click, drag, mouseup (click - like button) (true button down/off/up behavior would be good)
# what to do for a long drag




# just these kinds for now:
# rep unit atoms
# base unit atoms
# magenta
# white

# end