# Cythonize the encode machinery? import colorsys from .rate_control import rate import platform from math import sqrt, tan, pi import time try: clock = time.perf_counter # time.clock is deprecated in Python 3.3, gone in 3.8 except: clock = time.clock import sys from . import __version__, __gs_version__ from ._notebook_helpers import _isnotebook from ._vector_import_helper import (vector, mag, norm, dot, adjust_up, adjust_axis, object_rotate) # List of names that will be imported from this file with import * __all__ = ['Camera', 'GlowWidget', 'version', 'GSversion', 'Mouse', 'arrow', 'attach_arrow', 'attach_trail', 'baseObj', 'box', 'bumpmaps', 'button', 'canvas', 'checkbox', 'clock', 'color', 'combin', 'compound', 'cone', 'controls', 'curve', 'curveMethods', 'cylinder', 'distant_light', 'ellipsoid', 'event_return', 'extrusion', 'faces', 'frame', 'gcurve', 'gdots', 'ghbars', 'gobj', 'graph', 'gvbars', 'helix', 'label', 'local_light', 'menu', 'meta_canvas', 'points', 'pyramid', 'quad', 'radio', 'ring', 'simple_sphere', 'sleep', 'slider', 'sphere', 'standardAttributes', 'text', 'textures', 'triangle', 'vertex', 'wtext', 'winput', 'keysdown'] __p = platform.python_version() _ispython3 = (__p[0] == '3') if _ispython3: from inspect import signature # Python 3; needed to allow zero arguments in a bound function else: from inspect import getargspec # Python 2; needed to allow zero arguments in a bound function # __version__ is the version number of the Jupyter VPython installer, generated in building the installer. version = [__version__, 'jupyter'] GSversion = [__gs_version__, 'glowscript'] keysdownlist = [] # list of keys currently pressed # To print immediately, do this: # print(.....) # sys.stdout.flush() ##def eprint(*args, **kwargs): # this may output when ordinary print won't ## print(*args, file=sys.stderr, **kwargs) ## ##def jprint(s): # prints to terminal, for debugging purposes ## os.write(1, json.dumps(s, separators=(',', ':')).encode('utf_8')+ b'\n') # scalar attribute: { <'a' or 'b'>: string } # string is str(idx)+attrs[]+str(attributevalue) # for example: {'a':'23K1.72'} thickness of object 23 is 1.72 # vector attrribute: # string is str(idx)+ attrs[]+str(val.x)+','+str(val.y)+','+str(val.z) # for example: {'a':'37f23.54678,32.12345,-65.00123'} axis of object 37 # text attribute: # string is str(value) # boolean values are integers (0,1) # method: {'m': str(idx)+methods[]+} # attrs are X in {'a': '23X....'} __attrs = {'pos':'a', 'up':'b', 'color':'c', 'trail_color':'d', # don't use single and double quotes; available: comma, but maybe that would cause trouble 'ambient':'e', 'axis':'f', 'size':'g', 'origin':'h', 'direction':'j', 'linecolor':'k', 'bumpaxis':'l', 'dot_color':'m', 'foreground':'n', 'background':'o', 'ray':'p', 'center':'E', 'forward':'#', 'resizable':'+', # scalar attributes 'graph':'q', 'canvas':'r', 'trail_radius':'s', 'visible':'t', 'opacity':'u','shininess':'v', 'emissive':'w', 'make_trail':'x', 'trail_type':'y', 'interval':'z', 'pps':'A', 'retain':'B', 'red':'C', 'green':'D', 'blue':'F','length':'G', 'width':'H', 'height':'I', 'radius':'J', 'thickness':'K', 'shaftwidth':'L', 'headwidth':'M', 'headlength':'N', 'pickable':'O', 'coils':'P', 'xoffset':'Q', 'yoffset':'R', 'border':'S', 'line':'T', 'box':'U', 'space':'V', 'linewidth':'W', 'xmin':'X', 'xmax':'Y', 'ymin':'Z', 'ymax':'`', 'ctrl':'~', 'shift':'!', 'alt':'@', # text attributes: 'text':'$', 'align':'%', 'caption':'^', 'fast':'-', 'title':'&', 'xtitle':'*', 'ytitle':'(', # Miscellany: 'lights':')', 'objects':'_', 'bind':'=', 'pixel_pos':'[', 'texpos':']', 'v0':'{', 'v1':'}', 'v2':';', 'v3':':', 'vs':'<', 'type':'>', 'font':'?', 'texture':'/'} # attrsb are X in {'b': '23X....'}; ran out of easily typable one-character codes __attrsb = {'userzoom':'a', 'userspin':'b', 'range':'c', 'autoscale':'d', 'fov':'e', 'normal':'f', 'data':'g', 'checked':'h', 'disabled':'i', 'selected':'j', 'vertical':'k', 'min':'l', 'max':'m', 'step':'n', 'value':'o', 'left':'p', 'right':'q', 'top':'r', 'bottom':'s', '_cloneid':'t', 'logx':'u', 'logy':'v', 'dot':'w', 'dot_radius':'x', 'markers':'y', 'legend':'z', 'label':'A', 'delta':'B', 'marker_color':'C', 'size_units':'D', 'userpan':'E', 'scroll':'F'} # methods are X in {'m': '23X....'} # pos is normally updated as an attribute, but for interval-based trails, it is updated (multiply) as a method __methods = {'select':'a', 'pos':'b', 'start':'c', 'stop':'d', 'clear':'f', # unused eghijklmnopvxyzCDFAB 'plot':'q', 'add_to_trail':'s', 'follow':'t', '_attach_arrow':'u', 'clear_trail':'w', 'bind':'G', 'unbind':'H', 'waitfor':'I', 'pause':'J', 'pick':'K', 'GSprint':'L', 'delete':'M', 'capture':'N'} __vecattrs = ['pos', 'up', 'color', 'trail_color', 'axis', 'size', 'origin', '_attach_arrow', 'direction', 'linecolor', 'bumpaxis', 'dot_color', 'ambient', 'add_to_trail', 'foreground', 'background', 'ray', 'ambient', 'center', 'forward', 'normal', 'marker_color'] __textattrs = ['text', 'align', 'caption', 'title', 'xtitle', 'ytitle', 'selected', 'label', 'capture', 'append_to_caption', 'append_to_title', 'bind', 'unbind', 'pause', 'GSprint'] def _encode_attr2(sendval, val, ismethods): s = '' if sendval in __vecattrs: # it would be good to do some kind of compression of doubles s += "{:.16G},{:.16G},{:.16G}".format(val[0], val[1], val[2]) elif sendval in __textattrs: # '\n' doesn't survive JSON transmission, so we replace '\n' with '
' (and convert back in glowcomm) if not isinstance(val, str): val = print_to_string(val) val = val.replace('\n', '
') s += val elif sendval == 'rotate': for p in val: s += "{:.16G},".format(p) s = s[:-1] elif sendval == 'plot' or sendval == 'data': for p in val: s += "{:.16G},{:.16G},".format(p[0], p[1]) s = s[:-1] elif sendval in ['v0', 'v1', 'v2', 'v3']: # val is the vertex object referenced by triangle or quad s += str(val.idx) elif sendval == 'vs': if len(val) == 3: s += "{d},{d},{d}".format(val[0].idx, val[1].idx, val[2].idx) else: s += "{d},{d},{d},{d}".format(val[0].idx, val[1].idx, val[2].idx, val[3].idx) elif ismethods: #if sendval in ['follow', 'waitfor', 'delete']: val = str(val) # scene.camera.follow(object idx) s += str(val) else: s += "{:.16G}".format(val) return s def _encode_attr(D, ismethods): # ismethods is True if a list of method operations # If ismethods is True, D is [ [idx, method, data], [idx, method, data], etc. ] # If ismethods is False: D is {idx:{'pos':vec, etc.}, idx:{'pos':vec, etc.}, etc.} # For 'attrs', convert {'opacity': 0.5, 'idx': 3} to 'a(idx)X(value)' or 'b(idx)X(value)' # where (idx) is a number, X is the attribute code in attrs, (value) is a number or x,y,z or a string # For a method, we have {idx, method, value} to be converted to 'm(idx)X(value)' out = [] if ismethods: # methods; a list for d in D: s = 'm'+str(d[0])+__methods[d[1]] s += _encode_attr2(d[1], d[2], True) out.append(s) else: # attributes, a dictionary for idx,d in D.items(): for sendval,val in d.items(): if sendval in __attrs: s = 'a'+str(idx)+__attrs[sendval] else: s = 'b'+str(idx)+__attrsb[sendval] s += _encode_attr2(sendval, val, False) out.append(s) return out if sys.version > '3': long = int def list_to_vec(L): return vector(L[0], L[1], L[2]) class baseObj(object): glow = None objCnt = 0 sent = True # set to True by a render in the non-notebook case _view_constructed = False _canvas_constructing = False # 'cmds': list of constructors, # 'attrs': {idx:{'pos':vec, etc.}, idx:{'pos':vec, etc.}, etc.}, # 'methods': [ [idx, method, date], [idx, method, date], etc. ] updates = {'cmds':[], 'methods':[], 'attrs':{}} object_registry = {} ## idx -> instance attach_arrows = [] attach_trails = [] ## needed only for functions attrs = set() # each element is (idx, attr name) @classmethod def initialize(cls): cls.updates = {'cmds':[], 'methods':[], 'attrs':{}} cls.attrs = set() @classmethod def empty(cls): b = cls.updates return b['cmds'] == [] and b['methods'] == [] and len(cls.attrs) == 0 @classmethod def handle_attach(cls): # called when about to send data to the browser ## update every attach_arrow if relevant vector has changed for aa in cls.attach_arrows: obj = baseObj.object_registry[aa._obj] vval = getattr(obj, aa.attr) # could be 'velocity', for example if not isinstance(vval, vector): raise AttributeError("attach_arrow value must be a vector.") if (isinstance(aa._last_val, vector) and aa._last_val.equals(vval)): continue aa.addmethod('_attach_arrow', vval.value) aa._last_val = vector(vval) # keep copy of last vector ## update every attach_trail that depends on a function for aa in cls.attach_trails: if aa._obj == '_func': fval = aa._func() if not isinstance(fval, vector): raise AttributeError('attach_trail value must be a vector') aa.addmethod('add_to_trail', fval.value) else: fval = cls.object_registry[aa._obj].pos if ( isinstance(aa._last_val, vector) and aa._last_val.equals(fval) ): continue aa._last_val = fval def __init__(self, **kwargs): if not (baseObj._view_constructed or baseObj._canvas_constructing): if _isnotebook: from .with_notebook import _ else: from .no_notebook import _ baseObj._view_constructed = True self.idx = baseObj.objCnt ## an integer self.object_registry[self.idx] = self if kwargs is not None: for key, value in kwargs.items(): object.__setattr__(self, key, value) baseObj.incrObjCnt() def delete(self): # Currently delete decrements the object count but doesn't # delete any objects in the browser....so, let's not decrement # the count for now. # baseObj.decrObjCnt() # The body of this and __del__ are the same other than the # decrement here, so let's just call __del__. self.__del__() def appendcmd(self,cmd): # The following code makes sure that constructors are sent to the front end first. cmd['idx'] = self.idx while not baseObj.sent: # baseObj.sent is always True in the notebook case time.sleep(0.001) baseObj.updates['cmds'].append(cmd) # this is an "atomic" (uninterruptable) operation def addmethod(self, method, data): while not baseObj.sent: # baseObj.sent is always True in the notebook case time.sleep(0.001) baseObj.updates['methods'].append((self.idx, method, data)) # this is an "atomic" (uninterruptable) operation def addattr(self, attr): while not baseObj.sent: # baseObj.sent is always True in the notebook case time.sleep(0.001) baseObj.attrs.add((self.idx, attr)) # this is an "atomic" (uninterruptable) operation @classmethod def package(cls, objdata): # package up the data to send to the browser ret = {} m = None if objdata['cmds'] != []: ret['cmds'] = objdata['cmds'] if objdata['methods'] != []: m = _encode_attr(objdata['methods'], True) if objdata['attrs'] != {}: ret['attrs'] = _encode_attr(objdata['attrs'], False) if m is not None: ret['attrs'] += m elif m is not None: ret['attrs'] = m return ret @classmethod def trigger(cls): # isnotebook; called by a canvas update event from browser, coming from GlowWidget.handle_msg if cls.empty(): objdata = 'trigger' # handshake with browser else: cls.handle_attach() for a in baseObj.attrs: idx, attr = a val = getattr(baseObj.object_registry[idx], attr) if type(val) is vector: val = [val.x, val.y, val.z] if idx in baseObj.updates['attrs']: baseObj.updates['attrs'][idx][attr] = val else: baseObj.updates['attrs'][idx] = {attr:val} objdata = cls.package(baseObj.updates) sender(objdata) cls.initialize() baseObj.sent = True @classmethod def incrObjCnt(cls): cls.objCnt += 1 @classmethod def decrObjCnt(cls): cls.objCnt -= 1 def __del__(self): cmd = {"cmd": "delete", "idx": self.idx} if (baseObj.glow is not None and sender is not None): sender([cmd]) else: self.appendcmd(cmd) # Jupyter does not immediately transmit data to the browser from a thread, # which made for an awkward thread in early versions of Jupyter VPython, and # even caused display mistakes due to losing portions of data sent to the browser # from within a thread. # Now there is no threading in Jupyter VPython. Data is sent to the # browser from the trigger() function, which is called by a # canvas_update event sent to Python from the browser (glowcomm.js), currently # every 33 milliseconds. When trigger() is called, it immediately signals # the browser to set a timeout of 33 ms to send another signal to Python. # Note that a typical VPython program starts out by creating objects (constructors) and # specifying their attributes. The 33 ms signal from the browser is adequate to ensure # prompt data transmissions to the browser. # The situation with non-notebook use is similar, but the http server is threaded, # in order to serve glowcomm.html, jpg texture files, and font files, and the # websocket is also threaded. # In both the notebook and non-notebook cases output is buffered in baseObj.updates # and sent as a block to the browser at render times. class GlowWidget(object): def __init__(self, wsport=None, wsuri=None): global sender baseObj.glow = self if _isnotebook: from ipykernel.comm import Comm if (wsport): self.comm = Comm(target_name='glow', data={'wsport':wsport, 'wsuri':wsuri}) else: self.comm = Comm(target_name='glow') self.comm.on_close(self.handle_close) self.comm.on_msg(self.handle_msg) sender = self.comm.send self.show = True else: sender = None ## baseObj.object_registry = {} ## idx -> instance def handle_msg(self, msg): events = msg['content']['data'] # this is a list of time-ordered events for evt in events: if 'widget' in evt: obj = baseObj.object_registry[evt['idx']] if evt['widget'] == 'button': pass elif evt['widget'] == 'slider': obj._value = evt['value'] elif evt['widget'] == 'menu': obj._selected = obj._choices[evt['value']] elif evt['widget'] == 'checkbox': obj._checked = evt['value'] elif evt['widget'] == 'radio': obj._checked = evt['value'] elif evt['widget'] == 'winput': obj._text = evt['text'] obj._number = evt['value'] # inspect the bound function and see what it's expecting if _ispython3: # Python 3 a = signature(obj._bind) if str(a) != '()': obj._bind( obj ) else: obj._bind() else: # Python 2 a = getargspec(obj._bind) if len(a.args) > 0: obj._bind( obj ) else: obj._bind() else: ## a canvas event if 'trigger' not in evt: cvs = baseObj.object_registry[evt['canvas']] cvs.handle_event(evt) if _isnotebook: baseObj.trigger() def handle_close(self, data): print ("comm closed") def _wait(cvs): # wait for an event cvs._waitfor = None if _isnotebook: baseObj.trigger() # in notebook environment must send methods immediately while cvs._waitfor is None: rate(30) return cvs._waitfor class color(object): black = vector(0,0,0) white = vector(1,1,1) red = vector(1,0,0) green = vector(0,1,0) blue = vector(0,0,1) yellow = vector(1,1,0) cyan = vector(0,1,1) magenta = vector(1,0,1) orange = vector(1,0.6,0) purple = vector(0.4,0.2,0.6) @classmethod def gray(cls,luminance): return vector(luminance,luminance,luminance) @classmethod def rgb_to_hsv(cls,v): T = [v.x, v.y, v.z] c = colorsys.rgb_to_hsv(*T) return list_to_vec(c) @classmethod def hsv_to_rgb(cls,v): T = [v.x, v.y, v.z] c = colorsys.hsv_to_rgb(*T) return list_to_vec(c) @classmethod def rgb_to_grayscale(cls,v): luminance = 0.21*v.x + 0.71*v.y + 0.07*v.z return vector(luminance, luminance, luminance) class textures(object): flower = ":flower_texture.jpg" granite=":granite_texture.jpg" gravel=":gravel_texture.jpg" earth=":earth_texture.jpg" metal=":metal_texture.jpg" rock=":rock_texture.jpg" rough=":rough_texture.jpg" rug=":rug_texture.jpg" stones=":stones_texture.jpg" stucco=":stucco_texture.jpg" wood=":wood_texture.jpg" wood_old=":wood_old_texture.jpg" class bumpmaps(object): gravel=":gravel_bumpmap.jpg" rock=":rock_bumpmap.jpg" stones=":stones_bumpmap.jpg" stucco=":stucco_bumpmap.jpg" wood_old=":wood_old_bumpmap.jpg" class standardAttributes(baseObj): # vector-no-interactions, vector-interactions, scalar-no-interactions, scalar-interactions attrLists = {'box':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'texture', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height']], 'sphere':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'texture', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height', 'radius']], 'simple_sphere':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'texture', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height', 'radius']], 'arrow':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up'], ['visible', 'opacity', 'shininess', 'emissive', 'texture', 'frame', 'material', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'texture', 'shaftwidth', 'headwidth', 'headlength', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height']], 'ring':[['pos', 'color', 'trail_color', 'size'], ['axis', 'up'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'texture', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height', 'thickness']], 'helix':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'coils', 'thickness', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height']], 'curve':[['origin', 'color'], ['axis', 'size', 'up'], ['visible', 'shininess', 'emissive', 'radius', 'retain', 'pickable'], ['red', 'green', 'blue','length', 'width', 'height']], 'points':[['color'], [], ['visible', 'shininess', 'emissive', 'radius', 'retain', 'pickable', 'size_units'], ['red', 'green', 'blue']], 'label':[['pos', 'color', 'background', 'linecolor'], [], ['visible', 'xoffset', 'yoffset', 'font', 'height', 'opacity', 'border', 'line', 'box', 'space', 'align', 'linewidth', 'pixel_pos'], ['text']], 'local_light':[['pos', 'color'], [], ['visible'], []], 'distant_light':[['direction', 'color'], [], ['visible'], []], 'compound':[['pos', 'color', 'trail_color'], ['axis', 'size', 'up', 'origin'], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'texture', 'retain', 'trail_color', 'trail_radius', 'obj_idxs', 'pickable'], ['red', 'green', 'blue', 'length', 'width', 'height']], 'vertex':[['pos', 'color', 'normal', 'bumpaxis', 'texpos'], [], ['visible', 'opacity','shininess', 'emissive'], ['red', 'green', 'blue']], 'triangle': [ [], [], ['texture', 'bumpmap', 'visible', 'pickable'], ['v0', 'v1', 'v2'] ], 'quad': [ [], [], ['texture', 'bumpmap', 'visible', 'pickable'], ['v0', 'v1', 'v2', 'v3'] ], 'attach_arrow': [ [ 'color', 'attrval'], [], ['shaftwidth', 'scale', 'obj', 'attr'], [] ], 'attach_trail': [ ['color'], [], ['radius', 'pps', 'retain', 'type', '_obj'], [] ], 'wtext': [[], [], ['location', 'text'], []], 'extrusion':[ ['pos', 'color', 'start_face_color', 'end_face_color'], [ 'axis', 'size', 'up' ], ['path', 'shape', 'visible', 'opacity','shininess', 'emissive', 'show_start_face', 'show_end_face', 'make_trail', 'trail_type', 'interval', 'show_start_face', 'show_end_face', 'retain', 'trail_color', 'trail_radius', 'texture', 'pickable' ], ['red', 'green', 'blue','length', 'width', 'height'] ], 'text': [ ['pos', 'up', 'color', 'start_face_color', 'end_face_color', 'axis'], [], ['visible', 'opacity','shininess', 'emissive', 'make_trail', 'trail_type', 'interval', 'retain', 'trail_color', 'trail_radius', 'obj_idxs', 'pickable', 'align', 'text', 'font', 'billboard', 'show_start_face', 'show_end_face', 'descender', 'vertical_spacing', 'depth', 'length', 'width', 'height'], ['red', 'green', 'blue'] ] } attrLists['pyramid'] = attrLists['box'] attrLists['cylinder'] = attrLists['sphere'] attrLists['cone'] = attrLists['sphere'] attrLists['ellipsoid'] = attrLists['sphere'] def setup(self, args): super(standardAttributes, self).__init__() self._constructing = True ## calls to setters are from constructor objName = self._objName = args['_objName'] ## identifies object type if objName[:8] == 'compound': objName = 'compound' del args['_objName'] # default values self._pos = vector(0,0,0) self._axis = vector(1,0,0) self._up = vector(0,1,0) self._color = vector(1,1,1) defaultSize = args['_default_size'] if defaultSize is not None: # is not points or vertex or triangle or quad self._size = defaultSize ## because VP differs from JS del args['_default_size'] self._texture = None self._opacity = 1.0 self._shininess = 0.6 self._emissive = False self._make_trail = False self._trail_type = 'curve' self._trail_color = self._color self._interval = 0 # means no interval set self._retain = -1 self._trail_radius = None # set by default after size set self._canvas = None if 'canvas' in args: ## converted to idx below self._canvas = args['canvas'] self._visible = True self._frame = None self._size_units = 'pixels' self._texture = None self._pickable = True self._save_oldaxis = None # used in linking axis and up self._save_oldup = None # used in linking axis and up _special_clone = None if '_cloneid' in args: # text, extrusion, or compound is being cloned _special_clone = args['_cloneid'] del args['_cloneid'] argsToSend = [] ## send to GlowScript only attributes specified in constructor ## override defaults for vector attributes without side effects attrs = standardAttributes.attrLists[objName][0] for a in attrs: if a in args: argsToSend.append(a) val = args[a] if isinstance(val, vector): setattr(self, '_'+a, vector(val)) ## '_' bypasses setters; copy of val else: raise AttributeError(a+' must be a vector') del args[a] vectorInteractions = [ ('size','axis'), ('axis','size'), ('axis','up'), ('up','axis')] # override defaults for vector attributes with side effects # For consistency with GlowScript, axis is listed before up in the attrLists, # so that setting axis may affect up, but then setting up can affect axis afterwards. attrs = standardAttributes.attrLists[objName][1] for a in attrs: if a in args: val = args[a] if isinstance(val, vector): setattr(self, a, vector(val)) ## use setter to take care of side effects; copy of val if a not in argsToSend: argsToSend.append(a) for vi in vectorInteractions: if vi[0] == a: if vi[1] not in argsToSend: argsToSend.append(vi[1]) elif objName == 'points' and a == 'size': ## in this case size is a scalar argsToSend.append(a) else: raise AttributeError(a+' must be a vector') del args[a] if defaultSize is not None: ## If not 3D text, always send size because Python size differs from JS size if 'size' not in argsToSend and objName != 'text': argsToSend.append('size') self._trail_radius = 0.1 * self._size.y ## default depends on size elif objName == 'points': self._trail_radius = self._radius # points object # override defaults for scalar attributes without side effects attrs = standardAttributes.attrLists[objName][2] for a in attrs: if a in args: argsToSend.append(a) setattr(self, '_'+a, args[a]) ## by-pass setters del args[a] scalarInteractions={'red':'color', 'green':'color', 'blue':'color', 'radius':'size', 'thickness':'size', 'length':'size', 'height':'size', 'width':'size', 'v0':'v0', 'v1':'v1', 'v2':'v2', 'v3':'v3', 'text':'text'} # override defaults for scalar attributes with side effects attrs = standardAttributes.attrLists[objName][3] for a in attrs: if a in args: setattr(self, a, args[a]) ## use setter to take care of side effects if scalarInteractions[a] not in argsToSend: argsToSend.append(scalarInteractions[a]) # e.g. if a is radius, send size del args[a] # set values of user-defined attributes for key, value in args.items(): # Assign all other properties setattr(self, key, value) cmd = {"cmd": objName, "idx": self.idx} # now put all args to send into cmd nosize = ['compound', 'extrusion', 'text'] # size will be computed by the constuctor for a in argsToSend: aval = getattr(self,a) if isinstance(aval, vector): aval = aval.value elif isinstance(aval, vertex): aval = aval.idx if objName in nosize and a == 'size': continue # do not send superfluous size #cmd["attrs"].append({"attr":a, "value": aval}) cmd[a] = aval # set canvas if self.canvas is None: ## not specified in constructor self.canvas = canvas.get_selected() #cmd["attrs"].append({"attr": 'canvas', "value": self.canvas.idx}) cmd['canvas'] = self.canvas.idx # Lights are listed in canvas.lights, not canvas.objects if not (objName == 'distant_light' or objName == 'local_light'): self.canvas.objz(self,'add') self._constructing = False ## from now on any setter call will not be from constructor # _special_clone is True if text, extrusion, or compound objects if _special_clone is not None: cmd["_cloneid"] = _special_clone self.appendcmd(cmd) # if ('frame' in args and args['frame'] is not None): # frame.objects.append(self) # frame.update_obj_list() # attribute vectors have these methods which call self.addattr() # The vector class calls a change function when there's a change in x, y, or z. noSize = ['points', 'label', 'vertex', 'triangle', 'quad', 'attach_arrow', 'attach_trail'] self._color.on_change = self._on_color_change if objName not in noSize: self._axis.on_change = self._on_axis_change self._size.on_change = self._on_size_change self._up.on_change = self._on_up_change noPos = ['curve', 'points', 'triangle', 'quad', 'attach_arrow'] if objName not in noPos: self._pos.on_change = self._on_pos_change elif objName == 'curve': self._origin.on_change = self._on_origin_change if objName == 'vertex': self._bumpaxis.on_change = self._on_bumpaxis_change self._normal.on_change = self._on_normal_change # Ensure that if axis or up is <0,0,0> in constructor, we'll recover eventually: if self._axis.mag2 == 0: self._save_oldaxis = vector(1,0,0) if self._up.mag2 == 0: self._save_oldup = vector(0,1,0) @property def pos(self): return self._pos @pos.setter def pos(self,value): self._pos.value = value if not self._constructing: if self._make_trail and self._interval > 0: self.addmethod('pos', value.value) else: self.addattr('pos') @property def up(self): return self._up @up.setter def up(self,value): self._save_oldup = adjust_axis(self._up, value, self._axis, self._save_oldup) # this sets self._axis and self._up if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') @property def axis(self): return self._axis @axis.setter def axis(self,value): self._save_oldaxis = adjust_up(self._axis, value, self._up, self._save_oldaxis) # this sets self._axis and self._up if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') m = value.mag if abs(self._size._x - m) > 0.0001*self._size._x: # need not update size if very small change self._size._x = m if not self._constructing: self.addattr('size') @property def size(self): return self._size @size.setter def size(self,value): self._size.value = value if not self._constructing: self.addattr('size') a = self._axis.norm() * value.x if mag(self._axis) == 0: a = vector(value.x,0,0) v = self._axis if not v.equals(a): self._axis.value = a if not self._constructing: self.addattr('axis') @property def length(self): return self._size.x @length.setter def length(self,value): self._axis = self._axis.norm() * value self._size._x = value if not self._constructing: self.addattr('axis') self.addattr('size') @property def height(self): return self._size.y @height.setter def height(self,value): self._size._y = value if not self._constructing: self.addattr('size') @property def width(self): return self._size.z @width.setter def width(self,value): self._size._z = value if not self._constructing: self.addattr('size') @property def color(self): return self._color @color.setter def color(self,value): self._color.value = value if not self._constructing: self.addattr('color') @property def red(self): return self._color.x @red.setter def red(self,value): self._color = (value,self.green,self.blue) if not self._constructing: self.addattr('color') @property def green(self): return self._color.y @green.setter def green(self,value): self._color = (self.red,value,self.blue) if not self._constructing: self.addattr('color') @property def blue(self): return self._color.z @blue.setter def blue(self,value): self._color = (self.red,self.green,value) if not self._constructing: self.addattr('color') @property def visible(self): return self._visible @visible.setter def visible(self,value): self._visible = value if not self._constructing: self.addattr('visible') @property def pickable(self): return self._pickable @pickable.setter def pickable(self,value): self._pickable = value if not self._constructing: self.addattr('pickable') @property def canvas(self): return self._canvas @canvas.setter def canvas(self,value): self._canvas = value if not self._constructing: raise AttributeError('canvas cannot be modified') @property def opacity(self): return self._opacity @opacity.setter def opacity(self,value): self._opacity = value if not self._constructing: self.addattr('opacity') @property def emissive(self): return self._emissive @emissive.setter def emissive(self,value): self._emissive = value if not self._constructing: self.addattr('emissive') @property def texture(self): return self._texture @texture.setter def texture(self,value): self._texture = value if not self._constructing: self.appendcmd({"texture":value}) @property def shininess(self): return self._shininess @shininess.setter def shininess(self,value): self._shininess = value if not self._constructing: self.addattr('shininess') @property def make_trail(self): return self._make_trail @make_trail.setter def make_trail(self,value): self._make_trail = value if not self._constructing: self.addattr('make_trail') @property def trail_type(self): return self._trail_type @trail_type.setter def trail_type(self,value): if (value not in ['curve', 'points']): raise Exception("ArgumentError: trail_type must be 'curve' or 'points'") self._trail_type = value if not self._constructing: self.addattr('trail_type') @property def trail_color(self): return self._trail_color @trail_color.setter def trail_color(self, value): if isinstance(value, vector): self._trail_color.value = value else: raise TypeError('trail_color must be a vector') if not self._constructing: self.addattr('trail_color') @property def interval(self): return self._interval @interval.setter def interval(self,value): self._interval = value if not self._constructing: self.addattr('interval') @property def retain(self): return self._retain @retain.setter def retain(self,value): self._retain = value if not self._constructing: self.addattr('retain') @property def trail_radius(self): return self._trail_radius @trail_radius.setter def trail_radius(self,value): self._trail_radius = value if not self._constructing: self.addattr('trail_radius') @property def pps(self): return self._pps @pps.setter def pps(self, value): self._pps = value if not self._constructing: self.addattr('pps') @property def frame(self): return self._frame @frame.setter def frame(self,value): self._frame = value def rotate(self, angle=None, axis=None, origin=None): saveorigin = origin if angle == 0: return if angle is None: raise TypeError('You must specify an angle through which to rotate') if axis is None: rotaxis = self.axis else: rotaxis = axis if isinstance(self, curve): if origin is None: origin = self.origin pos = self.origin else: if origin is None: origin = self.pos pos = self.pos # Update local values of axis and up; setting self._axis and self._up to avoid axis/up connections object_rotate(self._axis, self._up, angle, rotaxis) self.addattr('axis') self.addattr('up') if saveorigin is not None and not origin.equals(self._pos): # This code is done only if origin is not the same as the original pos newpos = origin+(pos-origin).rotate(angle, rotaxis) if isinstance(self, curve): self._origin.value = newpos self.addattr('origin') else: self._pos.value = newpos self.addattr('pos') def _on_size_change(self): # the vector class calls this when there's a change in x, y, or z self._axis.value = self._axis.norm() * self._size.x # update axis length when box.size.x is changed self.addattr('size') def _on_pos_change(self): # the vector class calls this when there's a change in x, y, or z self.addattr('pos') def _on_axis_change(self): # the vector class calls this when there's a change in x, y, or z if self._objName != 'text': self._size.x = self._axis.mag self.addattr('axis') def _on_up_change(self): # the vector class calls this when there's a change in x, y, or z self.addattr('up') def _on_color_change(self): # the vector class calls this when there's a change in x, y, or z self.addattr('color') def clear_trail(self): self.addmethod('clear_trail', 'None') def _ipython_display_(self): # don't print something when making an (anonymous) object pass def clone(self, **args): objName = self._objName if objName == 'triangle' or objName == 'quad': raise TypeError('Cannot clone a '+objName+' object.') elif objName == 'text': # the text object is a wrapper around an extrusion, which is a compound oldargs = {'pos':self._pos, 'canvas':self.canvas, 'color':self._color, 'opacity':self._opacity, 'axis':self._axis, 'up':self._up, '_text':self._text, '_length':self._length, '_height':self._height, '_depth':self._depth, '_font':self._font, '_align':self._align, '_billboard':self._billboard, '_show_start_face':self._show_start_face, '_show_end_face':self._show_end_face, '_start_face_color':self._start_face_color, '_end_face_color':self._end_face_color, '_descender':self._descender, '_shininess':self._shininess, '_emissive':self._emissive, '_pickable':self._pickable} elif objName == 'curve': oldargs = {'origin':self._origin, 'pos':self.pos, 'color':self._color, r'adius':self._radius, 'size':self._size, 'axis':self._axis, 'up':self._up, 'shininess':self._shininess, 'emissive':self._emissive, 'visible':True, 'pickable':self._pickable} elif objName == 'helix': oldargs = {'pos':self.pos, 'color':self._color, 'thickness':self._thickness, 'coils':self._coils, 'size':self._size, 'axis':self._axis, 'up':self._up, 'shininess':self._shininess, 'emissive':self._emissive, 'visible':True, 'pickable':self._pickable} else: oldargs = {'pos':self._pos, 'color':self._color, 'opacity':self._opacity, 'size':self._size, 'axis':self._axis, 'up':self._up, 'texture':self.texture, 'shininess':self._shininess, 'emissive':self._emissive, 'visible':True, 'pickable':self._pickable} if objName == 'arrow': oldargs['shaftwidth'] = self._shaftwidth oldargs['headwidth'] = self._headwidth oldargs['headlength'] = self._headlength if objName == 'text' or objName == 'extrusion' or objName[:8] == 'compound': oldargs['_cloneid'] = self.idx # wait for objects being cloned to exist while not baseObj.empty(): rate(60) if objName[:8] == 'compound' or objName == 'extrusion': ret = compound([], **oldargs) else: ret = type(self)(**oldargs) for k, v in args.items(): # overrides and user attrs setattr(ret, k, v) return ret def __del__(self): super(standardAttributes, self).__del__() class box(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) args['_objName'] = "box" super(box, self).setup(args) class sphere(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(2,2,2) args['_objName'] = "sphere" super(sphere, self).setup(args) @property def radius(self): return self._size.y/2 @radius.setter def radius(self,value): d = 2*value self.size = vector(d,d,d) # size will call addattr @property def size(self): return self._size @size.setter def size(self,value): if not isinstance(value, vector): raise TypeError('size must be a vector') self._size = value if not self._constructing: self.addattr('size') # changing a sphere size should not affect axis @property def axis(self): return self._axis @axis.setter def axis(self,value): # changing a sphere axis should not affect size self._save_oldaxis = adjust_up(self._axis, value, self._up, self._save_oldaxis) # this sets self._axis and self._up self._axis.value = value if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') class simple_sphere(sphere): def __init__(self, **args): args['_default_size'] = vector(2,2,2) args['_objName'] = "simple_sphere" super(simple_sphere, self).setup(args) class cylinder(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,2,2) args['_objName'] = "cylinder" super(cylinder, self).setup(args) @property def radius(self): return self._size.y/2 @radius.setter def radius(self,value): d = 2*value self.size = vector(self._size.x,d,d) # size will call addattr class cone(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,2,2) args['_objName'] = "cone" super(cone, self).setup(args) @property def radius(self): return self._size.y/2 @radius.setter def radius(self,value): d = 2*value self.size = vector(self._size.x,d,d) # size will call addattr class pyramid(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) args['_objName'] = "pyramid" super(pyramid, self).setup(args) class ellipsoid(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) args['_objName'] = "ellipsoid" super(ellipsoid, self).setup(args) @property def radius(self): return self._size.y/2 @radius.setter def radius(self,value): d = 2*value self.size = vector(d,d,d) # size will call addattraddattr class ring(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(0.2,2.2,2.2) args['_objName'] = "ring" super(ring, self).setup(args) @property def thickness(self): return self._size.x/2 @thickness.setter def thickness(self,value): R1 = self.radius self._size.x = 2*value self._size.y = self._size.z = 2*(R1+value) if not self._constructing: self.addattr('size') @property def radius(self): return (self._size.y-self._size.x)/2 @radius.setter def radius(self,value): R2 = self.thickness self._size.y = self._size.z = 2*(value+R2) if not self._constructing: self.addattr('size') @property ## override methods of parent class def axis(self): return self._axis @axis.setter def axis(self,value): if not isinstance(value, vector): raise TypeError('axis must be a vector') self._save_oldaxis = adjust_up(self._axis, value, self._up, self._save_oldaxis) # this sets self._axis and self._up self._axis = value if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') @property ## override methods of parent class def size(self): return self._size @size.setter def size(self,value): if not isinstance(value, vector): raise TypeError('axis must be a vector') self._size = value if not self._constructing: self.addattr('size') class arrow(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,0.2,0.2) args['_objName'] = "arrow" self._shaftwidth = 0 self._headwidth = 0 self._headlength = 0 super(arrow, self).setup(args) @property def size(self): return self._size @size.setter def size(self,value): # no need to send to browser both arrow.size and arrow.axis self._size.value = value if not self._constructing: self.addattr('size') a = self._axis.norm() * value.x if mag(self._axis) == 0: a = vector(value.x,0,0) v = self._axis if not v.equals(a): self._axis.value = a @property def axis(self): return self._axis @axis.setter def axis(self,value): # no need to send to browser both arrow.size and arrow.axis oldaxis = norm(self._axis) self._axis.value = value m = value.mag if abs(self._size._x - m) > 0.0001*self._size._x: # need not update size if very small change self._size._x = m self._save_oldaxis = adjust_up(norm(oldaxis), self._axis, self._up, self._save_oldaxis) if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') @property def shaftwidth(self): return self._shaftwidth @shaftwidth.setter def shaftwidth(self,value): self._shaftwidth = value if not self._constructing: self.addattr('shaftwidth') @property def headwidth(self): return self._headwidth @headwidth.setter def headwidth(self,value): self._headwidth =value if not self._constructing: self.addattr('headwidth') @property def headlength(self): return self._headlength @headlength.setter def headlength(self,value): self._headlength =value if not self._constructing: self.addattr('headlength') class attach_arrow(standardAttributes): def __init__(self, obj, attr, **args): attrs = ['pos', 'size', 'axis', 'up', 'color'] args['_default_size'] = None self.obj = args['obj'] = obj.idx self.attr = args['attr'] = attr # could be for example "velocity" self.attrval = args['attrval'] = getattr(baseObj.object_registry[self.obj], attr) args['_objName'] = "attach_arrow" self._last_val = None self._scale = 1 self._shaftwidth = 0 super(attach_arrow, self).setup(args) # Only if the attribute is a user attribute do we need to add to attach_arrows: if attr not in attrs: baseObj.attach_arrows.append(self) @property def scale(self): return self._scale @scale.setter def scale(self, value): self._scale = value if not self._constructing: self.addattr("scale") @property def shaftwidth(self): return self._shaftwidth @shaftwidth.setter def shaftwidth(self, value): self._shaftwidth = value if not self._constructing: self.addattr("shaftwidth") def stop(self): self.addmethod('stop', 'None') def start(self): self.addmethod('start', 'None') class attach_trail(standardAttributes): def __init__(self, obj, **args): args['_default_size'] = None args['_objName'] = "attach_trail" self._radius = 0 if callable(obj): # true if obj is a function baseObj.attach_trails.append(self) self._obj = "_func" self._func = obj else: self._radius = obj.size.y * 0.1 self._color = obj.color self._obj = obj.idx args['_obj'] = self._obj self._last_val = None self._type = "curve" self._retain = -1 self._pps = 0 super(attach_trail, self).setup(args) @property def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = value if not self._constructing: self.addattr('radius') @property def retain(self): return self._retain @retain.setter def retain(self, value): self._retain = value if not self._constructing: self.addattr("retain") @property def pps(self): return self._pps @pps.setter def pps(self, value): self._pps = value if not self._constructing: self.addattr("pps") @property def type(self): return self._type @type.setter def type(self, value): self._type = value if not self._constructing: self.addattr("type") def stop(self): self.addmethod('stop', 'None') def start(self): self.addmethod('start', 'None') def clear(self): self.addmethod('clear', 'None') class helix(standardAttributes): def __init__(self,**args): args['_objName'] = 'helix' args['_default_size'] = vector(1,2,2) self._coils = 5 self._thickness = 1/20 ## radius/20 super(helix, self).setup(args) @property def thickness(self): return self._thickness @thickness.setter def thickness(self,value): self._thickness = value if not self._constructing: self.addattr('thickness') @property def coils(self): return self._coils @coils.setter def coils(self,value): self._coils =value if not self._constructing: self.addattr('coils') @property def radius(self): return self._size.y/2 @radius.setter def radius(self,value): d = 2*value self.size = vector(self._size.x,d,d) # size will call addattr if appropriate class compound(standardAttributes): compound_idx = 0 # same numbering scheme as in GlowScript def __init__(self, objList, **args): self._obj_idxs = None idxlist = [] ineligible = [label, curve, helix, points] ## type objects _special_clone = None if '_cloneid' in args: _special_clone = args['_cloneid'] else: cvs = objList[0].canvas for obj in objList: if obj.canvas is not cvs: raise AttributeError('all objects used in compound must belong to the same canvas') if type(obj) in ineligible: raise TypeError('A ' + obj._objName + ' object cannot be used in a compound') idxlist.append(obj.idx) args['obj_idxs'] = idxlist args['_default_size'] = vector(1,1,1) # to keep standardAttributes happy savesize = None if 'size' in args: savesize = args['size'] del args['size'] baseObj.sent = False while not baseObj.sent: # wait for compounding objects to exist if _isnotebook: rate(1000) else: time.sleep(0.001) self.compound_idx += 1 args['_objName'] = 'compound'+str(self.compound_idx) super(compound, self).setup(args) for obj in objList: # GlowScript will make the objects invisible, so need not set obj.visible obj._visible = False ## ideally these should be deleted if _special_clone is None: self.canvas._compound = self # used by event handler to update pos and size _wait(self.canvas) if savesize is not None: self._size = savesize @property def obj_idxs(self): return self._obj_idxs # no setter; must be set in constructor; this is done in standardAttributes @property def axis(self): return self._axis @axis.setter def axis(self,value): # compound axis and size don't interact self._save_oldaxis = adjust_up(self._axis, value, self._up, self._save_oldaxis) # this sets self._axis and self._up if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') @property def size(self): return self._size @size.setter def size(self,value): # compound axis and size don't interact self._size.value = value if not self._constructing: self.addattr('size') @property def origin(self): return self._origin @origin.setter def origin(self,value): # compound origin cannot be reset if not self._constructing: raise AttributeError('The compound "origin" attribute is read-only; change "pos" instead.') self._origin = value def world_to_compound(self, v): v = v-self._pos x_axis = self._axis.hat y_axis = self._up.hat z_axis = x_axis.cross(y_axis) ox = self._size0.x/self._size.x # _size0 is the original size oy = self._size0.y/self._size.y oz = self._size0.z/self._size.z return self._origin + vector(v.dot(x_axis)*ox, v.dot(y_axis)*oy, v.dot(z_axis)*oz) def compound_to_world(self, v): v = v-self._origin x_axis = self._axis.hat y_axis = self._up.hat z_axis = x_axis.cross(y_axis) ox = self._size.x/self._size0.x # _size0 is the original size oy = self._size.y/self._size0.y oz = self._size.z/self._size0.z return self._pos + v.x*ox*x_axis + v.y*oy*y_axis + v.z*oz*z_axis class vertex(standardAttributes): def __init__(self, **args): if 'canvas' in args: cv = args['canvas'] else: cv = canvas.get_selected() if cv.vertexCount > canvas.maxVertices-1: raise ValueError('too many vertex objects in use for this canvas') args['_default_size'] = None args['_objName'] = "vertex" self._triangleCount = 0 self._normal = vector(0,0,1) self._bumpaxis = vector(1,0,0) self._texpos = vector(0,0,0) super(vertex, self).setup(args) @property def triangleCount(self): return self._triangleCount @triangleCount.setter def triangleCount(self, val): raise AttributeError('use decrementTriangleCount or incrementTriangleCount') def decrementTriangleCount(self): if self._triangleCount <= 0: raise ValueError('triangleCount is already 0') self._triangleCount -= 1 def incrementTriangleCount(self): self._triangleCount += 1 @property def normal(self): return self._normal @normal.setter def normal(self, value): if not isinstance(value, vector): raise AttributeError('normal must be a vector') self._normal = vector(value) if not self._constructing: self.addattr('normal') @property def bumpaxis(self): return self._bumpaxis @bumpaxis.setter def bumpaxis(self, value): if not isinstance(value, vector): raise AttributeError('bumpaxis must be a vector') self._bumpaxis = vector(value) if not self._constructing: self.addattr('bumpaxis') @property def texpos(self): return self._texpos @texpos.setter def texpos(self, value): if not isinstance(value, vector): raise AttributeError('texpos must be a 3D vector with a zero z component') if value.z != 0: raise AttributeError('the z component of texpos must currently be zero') self._texpos = value if not self._constructing: self.addattr('texpos') def _on_normal_change(self): self.addattr('normal') def _on_bumpaxis_change(self): self.addattr('bumpaxis') class triangle(standardAttributes): def __init__(self, **args): args['_default_size'] = None args['_objName'] = "triangle" self._v0 = None self._v1 = None self._v2 = None if 'vs' in args: vlist = ['v0', 'v1', 'v2'] for i,val in enumerate(args['vs']): args[vlist[i]] = val del args['vs'] super(triangle, self).setup(args) def __del__(self): self._v0.decrementTriangleCount() self._v1.decrementTriangleCount() self._v2.decrementTriangleCount() super(triangle, self).__del__() @property def v0(self): return self._v0 @v0.setter def v0(self, value): if not self._constructing: self._v0.decrementTriangleCount() ## current v0 now used less self.addattr('v0') self._v0 = value self._v0.incrementTriangleCount() ## new v0 now used more @property def v1(self): return self._v1 @v1.setter def v1(self, value): if not self._constructing: self._v1.decrementTriangleCount() ## current v1 now used less self.addattr('v1') self._v1 = value self._v1.incrementTriangleCount() ## new v1 now used more @property def v2(self): return self._v2 @v2.setter def v2(self, value): if not self._constructing: self._v2.decrementTriangleCount() ## current v2 now used less self.addattr('v2') self._v2 = value self._v2.incrementTriangleCount() ## new v2 now used more @property def vs(self): return [self._v0, self._v1, self._v2] @vs.setter def vs(self, value): if not isinstance(value, list) or len(value) != 3: raise AttributeError('A triangle must be a list of 3 vertex objects.') for i in range(3): # if not isinstance(value[i], vertex): # raise AttributeError('triangle.vs must contain vertex objects.') val = value[i] if i == 0: self.v0 = val elif i == 1: self.v1 = val elif i == 2: self.v2 = val class quad(triangle): def __init__(self, **args): args['_default_size'] = None args['_objName'] = "quad" self._v0 = None self._v1 = None self._v2 = None self._v3 = None if 'vs' in args: vlist = ['v0', 'v1', 'v2', 'v3'] for i,val in enumerate(args['vs']): args[vlist[i]] = val del args['vs'] super(quad, self).setup(args) def __del__(self): self._v0.decrementTriangleCount() self._v1.decrementTriangleCount() self._v2.decrementTriangleCount() self._v3.decrementTriangleCount() super(triangle, self).__del__() @property def v3(self): return self._v3 @v3.setter def v3(self, value): if not self._constructing: self._v3.decrementTriangleCount() ## current v3 now used less self.addattr('v3') self._v3 = value self._v3.incrementTriangleCount() ## new v3 now used more @property def vs(self): return [self._v0, self._v1, self._v2, self._v3] @vs.setter def vs(self, value): if not isinstance(value, list) or len(value) != 4: raise AttributeError('A quad must be a list of 4 vertex objects.') for i in range(4): # if not isinstance(value[i], vertex): # raise AttributeError('quad.vs must contain vertex objects.') val = value[i] if i == 0: self.v0 = val elif i == 1: self.v1 = val elif i == 2: self.v2 = val elif i == 3: self.v3 = val class curveMethods(standardAttributes): def process_args(self, *args1, **args): c = None r = None vis = None if 'color' in args: c = args['color'] if 'radius' in args: r = args['radius'] if 'visible' in args: vis = args['visible'] if len(args1) > 0: if len(args1) == 1: tpos = self.parse_pos(args1[0]) else: ## avoid nested tuples tlist = list(args1) tpos = self.parse_pos(tlist) elif 'pos' in args: pos = args['pos'] tpos = self.parse_pos(pos) ## resolve pos arguments into a list if len(tpos) == 0: raise AttributeError("To add a point to a curve or points object, specify pos information.") pts = [] cps = [] for pt in tpos: col = c rad = r vi = vis cp = {'pos':pt['pos'].value} if 'color' in pt: col = pt['color'] if 'radius' in pt: rad = pt['radius'] if 'visible' in pt: vi = pt['visible'] if col is not None: pt['color'] = col cp['color'] = col.value if rad is not None: pt['radius'] = rad cp['radius'] = rad if vi is not None: pt['visible'] = vi cp['visible'] = vi pts.append(pt) cps.append(cp) return [pts, cps] def parse_pos(self, *vars): # return a list of dictionaries of the form {pos:vec, color:vec ....} # In constructor can have pos=[vec, vec, .....]; no dictionaries ret = [] if isinstance(vars, tuple) and len(vars) > 1 : vars = vars[0] if isinstance(vars, tuple) and isinstance(vars[0], list): vars = vars[0] for v in vars: if isinstance(v, vector) or isinstance(v, list) or isinstance(v, tuple): if not isinstance(v, vector): # legal in GlowScript: pos=[(x,y,z), (x,y,z)] and pos=[[x,y,z], [x,y,z]] v = list_to_vec(v) if not self._constructing: ret.append({'pos':vector(v)}) # make a copy of the vector; it could be (and often is, e.g. in a trail) object.pos else: ret.append(vector(v)) elif isinstance(v, dict) and not self._constructing: ret.append(v) else: if not self._constructing: raise AttributeError("Point information must be a vector or a dictionary") else: raise AttributeError("Point pos must be a vector") return ret def append(self, *args1, **args): pts, cps = self.process_args(*args1, **args) self._pts.extend(pts) self.appendcmd({"val":cps[:],"method":"append","idx":self.idx}) def _on_origin_change(self): self.addattr('origin') @property def npoints(self): return len(self._pts) @npoints.setter def npoints(self,value): raise ValueError('npoints cannot be set') @property def radius(self): return self._radius @radius.setter def radius(self,value): self._radius = value if not self._constructing: self.addattr('radius') def pop(self, *args): if len(self._pts) == 0: return None if len(args) == 0: index = -1 else: index = args[0] val = self._pts.pop(index) self.appendcmd({"val":index,"method":"pop","idx":self.idx}) return val def point(self,N): if N >= len(self._pts) or (N < 0 and -N >= len(self.pts)): raise ValueError('N = {} is outside the bounds 0-{} of the curve points'.format(N, len(self._pos))) info = self._pts[N] if 'color' not in info: info['color'] = self.color if 'radius' not in info: info['radius'] = self.radius if 'visible' not in info: info['visible'] = self.visible if 'retain' not in info: info['retain'] = self.retain return info def clear(self): self._pts = [] self.appendcmd({"val":"None","method":"clear","idx":self.idx}) def shift(self): if len(self._pts) == 0: return None val = self._pts[0] self._pts = self._pts[1:] self.appendcmd({"val":"None","method":"shift","idx":self.idx}) return val def unshift(self, *args1, **args): pts, cps = self.process_args(*args1, **args) self._pts = pts+self._pts self.appendcmd({"val":cps[:], "method":"unshift","idx":self.idx}) def slice(self, start, end): return self._pts[start:end] def splice(self, start, howmany, *args1): # args1 could be p1, p2, p3 or [p1, p2, p3] if howmany < 0: raise ValueError('You cannot delete a negative number of points'.format(howmany)) if start >= len(self._pts) or (start < 0 and -start >= len(self._pts)): raise ValueError('The starting location, {}, is outside the bounds of the list of points'.format(start)) if start >= 0: if start+howmany > len(self._pts): raise ValueError('The starting position plus deletions is beyond the list of points'.format(howmany)) else: if start+howmany >= 0: raise ValueError('The starting position plus deletions is beyond the list of points'.format(howmany)) pts, cps = self.process_args(*args1) self.pts = self._pts[:start]+pts+self._pts[start+howmany:] self.appendcmd({"val":[start, howmany, cps[:]], "method":"splice","idx":self.idx}) def modify(self, N, *arg1, **args): attrs = ['pos', 'color', 'radius', 'visible', 'retain'] if N >= len(self._pts) or (N < 0 and -N >= len(self._pts)): raise ValueError('N = {} is outside the bounds 0-{} of the curve points'.format(N, len(self._pts))) p = self._pts[N] cp = {} if len(arg1) == 1 and isinstance(arg1[0], vector): p['pos'] = arg1[0] cp['pos'] = arg1[0].value else: pos = p['pos'] for a in args: if a == 'x': pos.x = args[a] elif a == 'y': pos.y = args[a] elif a == 'z': pos.z = args[a] elif a in attrs: p[a] = args[a] if a == 'pos': pos = args[a] elif a == 'color': cp[a] = args[a].value else: cp[a] = args[a] cp['pos'] = pos.value self.appendcmd({"val":[N, [cp]], "method":"modify","idx":self.idx}) @property def origin(self): return self._origin @origin.setter def origin(self,value): self._origin.value = value if not self._constructing: self.addattr('origin') @property def pos(self): raise AttributeError('object does not have a "pos" attribute') @pos.setter def pos(self,val): raise AttributeError('use object methods to change its shape') # def __del__(self): # pass class curve(curveMethods): def __init__(self,*args1, **args): args['_objName'] = "curve" args['_default_size'] = vector(1,1,1) self._origin = vector(0,0,0) self._radius = 0 self._pts = [] ## cumulative list of dicts of the form {pos:vec, color=vec, radius=r, visible=T/F} python side tpos = None if 'pos' in args: tpos = args['pos'] del args['pos'] super(curveMethods, self).setup(args) if tpos is not None: if len(args1) > 0: raise AttributeError('Malformed constructor') self.append(tpos) if len(args1) > 0: self.append(*args1, color=self.color, radius=self.radius, visible=self.visible) def _on_origin_change(self): self.addattr('origin') class points(curveMethods): def __init__(self,*args1, **args): args['_objName'] = "points" args['_default_size'] = None ##vector(1,1,1) ##None self._radius = 0 self._pts = [] ## cumulative list of dicts of the form {pos:vec, color=vec, radius=r, visible=T/F} python side tpos = None if 'pos' in args: tpos = args['pos'] del args['pos'] super(curveMethods, self).setup(args) if tpos is not None: if len(args1) > 0: raise AttributeError('Malformed constructor') self.append(tpos) if len(args1) > 0: self._pts.append(*args1) @property def origin(self): raise AttributeError('The points object does not have an origin') @origin.setter def origin(self,value): raise AttributeError('The points object does not have an origin') @property def size_units(self): return self._size_units @size_units.setter def size_units(self,value): self._size_units = value def rotate(self, **args): raise AttributeError('The points object has no rotate method.') class gobj(baseObj): def setup(self, args): super(gobj, self).__init__() ## default values of shared attributes self._color = vector(0,0,0) self._dot_color = vector(0,0,0) self._marker_color = vector(0,0,0) self._dot = False self._delta = 1 self._width = 2 self._radius = 3 self._label = '' self._legend = False self._interval = -1 self._graph = None objName = args['_objName'] del args['_objName'] self._constructing = True ## calls are from constructor argsToSend = [] ## send to GlowScript only attributes specified in constructor ## default values will be used for others ## process data here if 'data' in args: datatemp = args['data'][:] ## make a copy self.plot(datatemp) ## call plot to resolve pos arguments into self._plot del args['data'] elif 'pos' in args: ## process pos here, an old synonym for data postemp = args['pos'][:] ## make a copy self.plot(postemp) ## call plot to resolve pos arguments into self._plot del args['pos'] ## override default vector attributes vectorAttributes = ['color', 'dot_color', 'marker_color'] for a in vectorAttributes: if a in args: argsToSend.append(a) val = args[a] if isinstance(val, vector): setattr(self, '_'+a, val) else: raise AttributeError(a+' must be a vector') del args[a] ## override default scalar attributes for a,val in args.items(): argsToSend.append(a) if a == 'graph': val = val.idx # elif a == 'fast' and _isnotebook and not val: # raise AttributeError('"fast = False" is currently not available in a Jupyter notebook.') setattr(self, '_'+a, val) cmd = {"cmd": objName, "idx": self.idx} for a in argsToSend: aval = getattr(self,a) if isinstance(aval, vector): aval = aval.value cmd[a] = aval self._constructing = False self.appendcmd(cmd) @property def radius(self): return self._radius @radius.setter def radius(self,val): self._radius = val self.addattr('radius') @property def size(self): return 2*self._radius @size.setter def size(self,val): self._radius = val/2 self.addattr('radius') @property def width(self): return self._width @width.setter def width(self,val): self._width = val self.addattr('width') @property def color(self): return self._color @color.setter def color(self,val): if not isinstance(val, vector): raise TypeError('color must be a vector') self._color = val self.addattr('color') @property def fast(self): return self._fast @fast.setter def fast(self,val): # if _isnotebook and not val: # raise AttributeError('"fast = False" is currently not available in a Jupyter notebook.') self._fast = val self.addattr('fast') @property def graph(self): return self._graph @graph.setter def graph(self,val): if not self._constructing: raise AttributeError('graph cannot be modified') else: self._graph = val.idx @property def interval(self): return self._interval @interval.setter def interval(self,val): self._interval = val self.addattr('interval') def __del__(self): cmd = {"cmd": "delete", "idx": self.idx} self.appendcmd(cmd) super(gcurve, self).__del__() def resolveargs(self, *vars): ret = [] if isinstance(vars[0], list) or isinstance(vars[0],tuple): if not isinstance(vars[0][0], list) and not isinstance(vars[0][0],tuple): # plot([x,y], [x,y]) for v in vars: if isinstance(v,tuple): v = list(v) if isinstance(v, list) and len(v) == 2: ret.append(v) else: raise AttributeError("Plot data must be 2-dimensional, like [x,y].") else: # plot([ [x,y], [x,y] ] for v in vars[0]: if isinstance(v,tuple): v = list(v) if isinstance(v, list) and len(v) == 2: ret.append(v) else: raise AttributeError("Plot data must be 2-dimensional, like [x,y].") return ret def preresolve1(self, *args): if isinstance(args[0], list) or isinstance(args[0], tuple): if len(args) == 1 and len(args[0]) == 1: return self.resolveargs(args[0][0]) a = [] for arg in args[0]: a.append(arg) return self.resolveargs(a) else: return self.resolveargs(args) # plot(x,y) def preresolve2(self, args): if 'color' in args: raise AttributeError("Cannot currently change color in a plot statement.") if 'pos' in args: return self.resolveargs(args['pos']) else: raise AttributeError("Must be plot(x,y) or plot(pos=[x,y]) or plot([x,y]) or plot([x,y], ...) or plot([ [x,y], ... ])") def plot(self, *args1, **args2): if len(args1) > 0: p = self.preresolve1(args1) else: p = self.preresolve2(args2) self.addmethod('plot', p) def delete(self): self.addmethod('delete', 'None') @property def label(self): return self._label @label.setter def label(self,val): self._label = val self.addattr('label') @property def legend(self): return self._legend @legend.setter def legend(self,val): self._legend = val self.addattr('legend') @property def data(self): return self._data @data.setter def data(self,val): self._data = val self.addattr('data') def _ipython_display_(self): # don't print something when making an (anonymous) graph object pass class gcurve(gobj): def __init__(self, **args): args['_objName'] = "gcurve" super(gcurve, self).setup(args) @property def markers(self): return self._markers @markers.setter def markers(self,val): self._markers = val self.addattr('markers') @property def marker_color(self): return self._marker_color @marker_color.setter def marker_color(self,val): if not isinstance(val, vector): raise TypeError('marker_color must be a vector') self._marker_color = vector(val) self.addattr('marker_color') @property def dot(self): return self._dot @dot.setter def dot(self,val): self._dot = val self.addattr('dot') @property def dot_color(self): return self._dot_color @dot_color.setter def dot_color(self,val): if not isinstance(val, vector): raise TypeError('dot_color must be a vector') self._dot_color = vector(val) self.addattr('dot_color') @property def dot_radius(self): return self._dot_radius @dot_radius.setter def dot_radius(self,val): self._dot_radius = val self.addattr('dot_radius') class gdots(gobj): def __init__(self, **args): args['_objName'] = "gdots" super(gdots, self).setup(args) class gvbars(gobj): def __init__(self, **args): args['_objName'] = "gvbars" self._delta = 1 super(gvbars, self).setup(args) @property def delta(self): return self._delta @delta.setter def delta(self,val): self._delta = val self.addattr('delta') class ghbars(gvbars): def __init__(self, **args): args['_objName'] = "ghbars" self._delta = 1 super(ghbars, self).setup(args) class graph(baseObj): def __init__(self, **args): objName = 'graph' super(graph,self).__init__() ## default values self._width = 640 self._height = 400 self._align = 'none' self._fast = True self._foreground = vector(0,0,0) self._background = vector(1,1,1) self._title = "" self._xtitle = "" self._ytitle = "" self._scroll = False argsToSend = [] ## override default vector attributes vectorAttributes = ['foreground', 'background'] for a in vectorAttributes: if a in args: argsToSend.append(a) val = args[a] if isinstance(val, vector): setattr(self, '_'+a, val) else: raise AttributeError(a+' must be a vector') del args[a] ## override default scalar attributes scalarAttributes = ['width', 'height', 'title', 'xtitle', 'ytitle','align', 'xmin', 'xmax', 'ymin', 'ymax', 'logx', 'logy', 'fast', 'scroll'] for a in scalarAttributes: if a in args: argsToSend.append(a) setattr(self, '_'+a, args[a]) del args[a] # user defined attributes for a in args: setattr(self, '_'+a, args[a]) cmd = {"cmd": objName, "idx": self.idx} if self._scroll: if not ('xmin' in argsToSend and 'xmax' in argsToSend): raise AttributeError("For a scrolling graph, both xmin and xmax must be specified.") if self._xmax <= self._xmin: raise AttributeError("For a scrolling graph, xmax must be greater than xmin.") ## send only args specified in constructor for a in argsToSend: aval = getattr(self,a) if isinstance(aval, vector): aval = aval.value cmd[a] = aval self.appendcmd(cmd) @property def fast(self): return self._fast @fast.setter def fast(self,val): self._fast = val self.addattr('fast') @property def scroll(self): return self._scroll @scroll.setter def scroll(self,val): self._scroll = val self.addattr('scroll') @property def width(self): return self._width @width.setter def width(self,val): self._width = val self.addattr('width') @property def height(self): return self._height @height.setter def height(self,val): self._height = val self.addattr('height') @property def align(self): return self._align @align.setter def align(self,val): if not (val == 'left' or val == 'right' or val == 'none'): raise NameError("align must be 'left', 'right', or 'none' (the default).") self._align = val self.addattr('align') @property def title(self): return self._title @title.setter def title(self,val): self._title = val #self.appendcmd({attr='title', 'idx'=self.idx}) self.addattr('title') @property def xtitle(self): return self._xtitle @xtitle.setter def xtitle(self,val): self._xtitle = val self.addattr('xtitle') @property def ytitle(self): return self._ytitle @ytitle.setter def ytitle(self,val): self._ytitle = val self.addattr('ytitle') @property def foreground(self): return self._foreground @foreground.setter def foreground(self,val): if not isinstance(val, vector): raise TypeError('foreground must be a vector') self._foreground = vector(value) self.addattr('foreground') @property def background(self): return self._background @background.setter def background(self,val): if not isinstance(val,vector): raise TypeError('background must be a vector') self._background = vector(value) self.addattr('background') @property def xmin(self): return self._xmin @xmin.setter def xmin(self,val): self._xmin = val self.addattr('xmin') @property def xmax(self): return self._xmax @xmax.setter def xmax(self,val): self._xmax = val self.addattr('xmax') @property def ymin(self): return self._ymin @ymin.setter def ymin(self,val): self._ymin = val self.addattr('ymin') @property def ymax(self): return self._ymax @ymax.setter def ymax(self,val): self._ymax = val self.addattr('ymax') @property def logx(self): return self._logx @logx.setter def logx(self,val): self._logx = val self.addattr('logx') @property def logy(self): return self._logy @logx.setter def logy(self,val): self._logy = val self.addattr('logy') def delete(self): self.addmethod('delete','None') def _ipython_display_(self): # don't print something when making an (anonymous) graph pass # def __del__(self): # cmd = {"cmd": "delete", "idx": self.idx} # self.appendcmd(cmd) # super(graph, self).__del__() class faces(object): def __init__(self, **args): raise NameError('faces is no longer supported; use vertex with triangle or quad') class label(standardAttributes): def __init__(self, **args): args['_objName'] = 'label' args['_default_size'] = None self._xoffset = 0 self._yoffset = 0 self._text = " " self._font = "sans" self._height = 13 self._background = None self._border = 5 self._align = None self._box = True self._line = True self._linecolor = None self._linewidth = 1 self._space = 0 self._pixel_pos = False super(label, self).setup(args) @property def xoffset(self): return self._xoffset @xoffset.setter def xoffset(self,value): self._xoffset = value if not self._constructing: self.addattr('xoffset') @property def yoffset(self): return self._yoffset @yoffset.setter def yoffset(self,value): self._yoffset = value if not self._constructing: self.addattr('yoffset') @property def text(self): return self._text @text.setter def text(self,value): self._text = print_to_string(value) if not self._constructing: self.addattr("text") @property def align(self): return self._align @align.setter def align(self,value): self._align = value if not self._constructing: self.addattr('align') @property def font(self): return self._font @font.setter def font(self,value): self._font = value if not self._constructing: self.addattr('font') @property def height(self): return self._height @height.setter def height(self,value): self._height = value if not self._constructing: self.addattr('height') @property def background(self): return self._background @background.setter def background(self,value): if isinstance(value, vector): self._background.value = value else: raise TypeError('background must be a vector') if not self._constructing: self.addattr('background') @property def border(self): return self._border @border.setter def border(self,value): self._border = value if not self._constructing: self.addattr('border') @property def box(self): return self._box @box.setter def box(self,value): self._box = value if not self._constructing: self.addattr('box') @property def line(self): return self._line @line.setter def line(self,value): self._line = value if not self._constructing: self.addattr('line') @property def linewidth(self): return self._linewidth @linewidth.setter def linewidth(self,value): self._linewidth = value if not self._constructing: self.addattr('linewidth') @property def linecolor(self): return self._linecolor @linecolor.setter def linecolor(self,value): if isinstance(value, vector): self._linecolor = vector(value) else: raise TypeError('linecolor must be a vector') if not self._constructing: self.addattr('linecolor') @property def space(self): return self._space @space.setter def space(self,value): self._space = value if not self._constructing: self.addattr('space') @property def pixel_pos(self): return self._pixel_pos @pixel_pos.setter def pixel_pos(self,value): self._pixel_pos = value if not self._constructing: self.addattr('pixel_pos') class frame(object): def __init__(self, **args): raise NameError('frame is not yet implemented') class Mouse(baseObj): def __init__(self, canvas): self._pos = None self._ray = None self._alt = False self._ctrl = False self._shift = False self._canvas = canvas self._pick = None super(Mouse, self).__init__() @property def pos(self): if self._pos is None: # can be none if mouse has never been inside canvas self._pos = vector(0,0,0) return self._pos @pos.setter def pos(self,value): raise AttributeError('Cannot set position of the mouse') @property def ray(self): if self._ray is None: # can be none if mouse has never been inside canvas self._ray = vector(0,0,-1) return self._ray @ray.setter def ray(self,value): raise AttributeError('Cannot set ray') @property def ctrl(self): return self._ctrl @ctrl.setter def ctrl(self, value): raise AttributeError('Cannot set mouse.ctrl') @property def shift(self): return self._shift @shift.setter def shift(self, value): raise AttributeError('Cannot set mouse.shift') @property def alt(self): return self._alt @alt.setter def alt(self, value): raise AttributeError('Cannot set mouse.alt') @property def pick(self): #self.appendcmd({"val":self._canvas.idx, "method":"pick", "idx":1 }) self.addmethod('pick', self._canvas.idx) _wait(self._canvas) # wait for render to finish and call setpick return self._pick @pick.setter def pick(self, value): raise AttributeError('Cannot set mouse.pick') def setpick(self, value): # value is the entire event p = value['pick'] if p is not None: po = baseObj.object_registry[p] if 'segment' in value: po.segment = value['segment'] self._pick = po else: self._pick = None def project(self, **args): if 'normal' not in args: raise AttributeError('scene.mouse.project() must specify a normal') normal = norm(args['normal']) dist = 0 if 'd' in args: dist = args['d'] elif 'point' in args: point = args['point'] dist = dot(normal, point) ndc = dot(normal, self._canvas.camera.pos) - dist ndr = dot(normal, self._ray) if ndr == 0: return None t = -ndc/ndr return self._canvas.camera.pos + t*self._ray class Camera(object): def __init__(self, canvas): self._canvas = canvas self._followthis = None self._pos = None @property def canvas(self): return self._canvas @canvas.setter def canvas(self, value): raise AttributeError('Cannot assign camera to a different canvas') @property def pos(self): c = self._canvas return c.center-(norm(c.axis)*(c.range / tan(c.fov/2))) @pos.setter def pos(self, value): c = self._canvas c.center = value+self.axis @property def axis(self): c = self._canvas return norm(c._axis)*( c.range / tan(c.fov/2) ) @axis.setter def axis(self, value): c = self._canvas c.center = self.pos+value # use current self.pos before it is changed by change in c.axis c.axis = norm(value) c.range = mag(value)*tan(c.fov/2) @property def up(self): ## but really this should not exist: should be scene.up return self._canvas.up @up.setter def up(self, value): self._canvas.up = value def rotate(self, angle=0, axis=None, origin=None): if angle == 0: return c = self._canvas if axis is None: axis = c.up if origin is not None and origin != self.pos: origin = self.pos + (self.pos-origin).rotate(angle=angle, axis=axis) else: origin = self.pos if c._axis.diff_angle(axis) > 1e-6: c.axis = c._axis.rotate(angle=angle, axis=axis) c.up = c._up.rotate(angle=angle, axis=axis) c.center = origin + self.axis class meta_canvas(object): @property def selected(self): return getattr(self, '_selected') @selected.setter def selected(self, value): self._selected = value class canvas(baseObj): selected = None hasmouse = None maxVertices = 65535 ## 2^16 - 1 due to GS weirdness def __init__(self, **args): baseObj._canvas_constructing = True if _isnotebook: from IPython.display import display, HTML, Javascript display(HTML("""
""")) display(Javascript("""if (typeof Jupyter !== "undefined") { window.__context = { glowscript_container: $("#glowscript").removeAttr("id")};}else{ element.textContent = ' ';}""")) super(canvas, self).__init__() ## get idx, attrsupdt self._constructing = True canvas.selected = self if 'lights' in args: raise AttributeError("Lights for a canvas can be assigned only after the canvas has been created.") self._objz = set() self.vertexCount = 0 self._visible = True self._background = vector(0,0,0) self._ambient = vector(0.2, 0.2, 0.2) self._height = 400 # to match the GlowScript default self._width = 640 self._align = 'none' self._fov = pi/3 self._resizable = True # The following determine the view: self._range = 1 # user can alter with zoom self._axis = vector(0,0,-1) # user can alter with spin self._forward = vector(0,0,-1) # self.axis is primal internally; self._forward is now a synonym self._up = vector(0,1,0) # user with touch screen can rotate around z self._autoscale = True # set False if user zooms self._center = vector(0,0,0) # cannot be altered by user # Reject JavaScript canvas_update user values immediately following Python setting of values: self._set_range = False self._set_forward = False self._set_center = False self._set_up = False self._set_autoscale = False self._userzoom = True self._userspin = True self._userpan = True self._pixel_to_world = 0 self._title = '' self._caption = '' self._mouse = Mouse(self) self._binds = {'mousedown':[], 'mouseup':[], 'mousemove':[],'click':[], 'mouseenter':[], 'mouseleave':[], 'keydown':[], 'keyup':[], 'redraw':[], 'draw_complete':[], 'resize':[]} #'_compound':[]} # no key events unless notebook command mode can be disabled self._camera = Camera(self) self.title_anchor = [self.idx, 1] ## used by buttons etc. self.caption_anchor = [self.idx, 2] cmd = {"cmd": "canvas", "idx": self.idx} # send only nondefault values to GlowScript canvasVecAttrs = ['background', 'ambient','forward','up', 'center'] canvasNonVecAttrs = ['visible', 'height', 'width', 'title','fov', 'range','align', 'autoscale', 'userzoom', 'userspin', 'userpan', 'title', 'caption'] for a in canvasNonVecAttrs: if a in args: if args[a] is not None: setattr(self, '_'+a, args[a]) cmd[a]= args[a] del args[a] for a in canvasVecAttrs: if a in args: aval = args[a] if not isinstance(aval, vector): raise TypeError(a, 'must be a vector') setattr(self, '_'+a, vector(aval)) cmd[a] = aval.value del args[a] # set values of user-defined attributes for key, value in args.items(): # Assign all other properties setattr(self, key, value) self._forward.on_change = self._on_forward_change self._up.on_change = self._on_up_change self._center.on_change = self._on_center_change self.appendcmd(cmd) self._constructing = False self._camera.follow = self.follow self.lights = [] # delete all lights created by glowcomm.js # Add the standard lighting (these lights will be added to self._lights): distant_light(direction=vector( 0.22, 0.44, 0.88), color=color.gray(0.8)) distant_light(direction=vector(-0.88, -0.22, -0.44), color=color.gray(0.3)) baseObj._canvas_constructing = False def follow(self, obj): ## should allow a function also self.addmethod('follow', obj.idx) def select(self): canvas.selected = self self.addmethod('select','None') @classmethod def get_selected(cls): return cls.selected def delete(self): self.addmethod('delete','None') @property def title(self): return self._title @title.setter def title(self,value): self._title = value if not self._constructing: self.appendcmd({'title':value}) @property def caption(self): return self._caption @caption.setter def caption(self,value): self._caption = value if not self._constructing: self.appendcmd({'caption':value}) def append_to_title(self, *args): t = print_to_string(*args) self._title += t if not self._constructing: self.appendcmd({'append_to_title':t}) def append_to_caption(self, *args): t = print_to_string(*args) self._caption += t if not self._constructing: self.appendcmd({'append_to_caption':t}) @property def mouse(self): return self._mouse @mouse.setter def mouse(self,value): raise AttributeError('Cannot set scene.mouse') @property def camera(self): return self._camera @camera.setter def camera(self,value): raise AttributeError('Cannot set scene.camera') @property def visible(self): return self._visible @visible.setter def visible(self,value): self._visible = value if not self._constructing: self.addattr('visible') @property def resizable(self): return self._resizable @resizable.setter def resizable(self,value): self._resizable = value if not self._constructing: self.addattr('resizable') @property def background(self): return self._background @background.setter def background(self,value): self._background = value if not self._constructing: self.appendcmd({"background":value.value}) @property def ambient(self): return self._ambient @ambient.setter def ambient(self,value): self._ambient = vector(value) if not self._constructing: self.addattr('ambient') @property def width(self): return self._width @width.setter def width(self,value): self._width = value if not self._constructing: self.appendcmd({"width":value}) @property def height(self): return self._height @height.setter def height(self,value): self._height = value if not self._constructing: self.appendcmd({"height":value}) @property def align(self): return self._align @align.setter def align(self,val): if not (val == 'left' or val == 'right' or val == 'none'): raise NameError("align must be 'left', 'right', or 'none' (the default).") self._align = val self.appendcmd({"align":val}) @property def center(self): return self._center @center.setter def center(self,value): if isinstance(value, vector): self._center = self._set_center = vector(value) if not self._constructing: self.appendcmd({"center":value.value}) else: raise TypeError('center must be a vector') @property def axis(self): return self._axis @axis.setter def axis(self,value): self._axis = self._set_forward = vector(value) if not self._constructing: self.appendcmd({"forward":value.value}) @property def forward(self): # scene.forward is an external synonym for scene.axis return self._axis @forward.setter def forward(self,value): self._axis = self._set_forward = vector(value) if not self._constructing: self.appendcmd({"forward":value.value}) @property def range(self): return self._range @range.setter def range(self,value): self._range = self._set_range = value if not self._constructing: self.appendcmd({"range":value}) @property def up(self): return self._up @up.setter def up(self,value): self._up = self._set_up = value if not self._constructing: self.appendcmd({"up":value.value}) @property def autoscale(self): return self._autoscale @autoscale.setter def autoscale(self,value): self._autoscale = self._set_autoscale = value if not self._constructing: self.appendcmd({"autoscale":value}) @property def fov(self): return self._fov @fov.setter def fov(self,value): self._fov = value if not self._constructing: self.appendcmd({"fov":value}) @property def userzoom(self): return self._userzoom @userzoom.setter def userzoom(self,value): self._userzoom = value if not self._constructing: self.appendcmd({"userzoom":value}) @property def userspin(self): return self._userspin @userspin.setter def userspin(self,value): self._userspin = value if not self._constructing: self.appendcmd({"userspin":value}) @property def userpan(self): return self._userpan @userpan.setter def userpan(self,value): self._userpan = value if not self._constructing: self.appendcmd({"userpan":value}) @property def lights(self): return self._lights @lights.setter def lights(self, value): if type(value) is list and len(value) == 0: # JSON doesn't like an empty list self._lights = [] self.appendcmd({"lights":'empty_list'}) # don't encode this unusual statement else: raise AttributeError("canvas.lights can be set only to [].") @property def pixel_to_world(self): return self._pixel_to_world @pixel_to_world.setter def pixel_to_world(self, value): raise AttributeError('pixel_to_world is read-only') def capture(self, filename): if not isinstance(filename, str): raise AttributeError('A capture file name must be a string.') if '.png' not in filename: filename += '.png' self.addmethod('capture', filename) @property def objects(self): obs = [] for ob in self._objz: if ob.visible: obs.append(ob) return obs @objects.setter def objects(self, *args1, **args ): raise AttributeError('objects is read-only') def objz(self, obj, operation): try: ii = (obj.idx > 0) ## invalid object will not have .idx attribute if operation == 'add': self._objz.add(obj) elif operation == 'delete': self._objz.remove(obj) except: raise TypeError(obj + ' is not an object belonging to a canvas') ## key events conflict with notebook command mode; not permitted for now def handle_event(self, evt): ## events and scene info updates global keysdownlist ev = evt['event'] if ev == 'pick': self.mouse.setpick( evt ) self._waitfor = True # what pick is looking for elif ev == '_compound': # compound, text, extrusion obj = self._compound p = evt['pos'] if obj._objName == 'text': obj._length = p[0] obj._descender = p[1] obj._up.value = list_to_vec(evt['up']) else: # Set attribute_vector.value, which avoids nullifying the # on_change functions that detect changes in e.g. obj.pos.y obj._pos.value = list_to_vec(p) s = evt['size'] obj._size.value = obj._size0 = list_to_vec(s) obj._axis.value = obj._size._x*norm(obj._axis) obj._up.value = list_to_vec(evt['up']) self._waitfor = True # what compound and text and extrusion are looking for in _wait() elif ev == 'resize': if self.resizable and ('resize' in self._binds): self.width = evt['width'] self.height = evt['height'] del evt['width'] del evt['height'] for fct in self._binds['resize']: # inspect the bound function and see what it's expecting if _ispython3: # Python 3 a = signature(fct) if str(a) != '()': fct( evt ) else: fct() else: # Python 2 a = getargspec(fct) if len(a.args) > 0: fct( evt ) else: fct() else: # pause/waitfor, update_canvas if 'pos' in evt: pos = evt['pos'] evt['pos'] = list_to_vec(pos) self.mouse._pos = evt['pos'] if 'ray' in evt: ray = evt['ray'] evt['ray'] = list_to_vec(ray) self.mouse._ray = evt['ray'] canvas.hasmouse = self if ev != 'update_canvas': ## mouse events bound to functions, and pause/waitfor evt['canvas'] = self if ev[:3] != 'key': # not a key event self.mouse._alt = evt['alt'] self.mouse._shift = evt['shift'] self.mouse._ctrl = evt['ctrl'] evt1 = event_return(evt) ## turn it into an object for fct in self._binds[ev]: # inspect the bound function and see what it's expecting if _ispython3: # Python 3 a = signature(fct) if str(a) != '()': fct( evt1 ) else: fct() else: # Python 2 a = getargspec(fct) if len(a.args) > 0: fct( evt1 ) else: fct() self._waitfor = evt1 # what pause and waitfor are looking for else: ## user can change forward (spin), range/autoscale (zoom), up (touch), center (pan) if 'forward' in evt and self.userspin and not self._set_forward: fwd = evt['forward'] self._forward = list_to_vec(fwd) self._set_forward = False if 'up' in evt and self.userspin and not self._set_up: cup = evt['up'] self._up = list_to_vec(cup) self._set_up = False if 'center' in evt and self.userpan and not self._set_center: center = evt['center'] self._center = list_to_vec(center) self._set_center = False if 'range' in evt and self.userzoom and not self._set_range: self._range = evt['range'] self._set_range = False if 'autoscale' in evt and self.userzoom and not self._set_autoscale: self._autoscale = evt['autoscale'] self._set_autoscale = False if 'keysdown' in evt: keysdownlist = evt['keysdown'] def bind(self, eventtype, whattodo): evts = eventtype.split() for evt in evts: if evt in self._binds: self._binds[evt].append(whattodo) else: raise TypeError(evt + ' is an illegal event type') self.addmethod('bind', eventtype) def unbind(self, eventtype, whatnottodo): evts = eventtype.split() for evt in evts: if evt in self._binds and whatnottodo in self._binds[evt]: self._binds[evt].remove(whatnottodo) self.addmethod('unbind', eventtype) def waitfor(self, eventtype): if 'textures' in eventtype: # textures are local; little need to wait eventtype = eventtype.replace('textures', '') if eventtype == '': return evts = ['redraw', 'draw_complete'] # wait for a render if eventtype in evts: baseObj.sent = False while baseObj.sent is False: rate(60) else: self.addmethod('waitfor', eventtype) _wait(self) return self._waitfor def pause(self,*s): if len(s) > 0: s = s[0] self.addmethod('pause', s) else: self.addmethod('pause', '') _wait(self) return self._waitfor def _on_forward_change(self): self.addattr('forward') def _on_up_change(self): self.addattr('up') def _on_center_change(self): self.addattr('center') def _ipython_display_(self): # don't print something when making an (anonymous) canvas pass class event_return(object): def __init__(self, args): self.canvas = args['canvas'] self.event = args['event'] self.which = args['which'] self.shift = args['shift'] self.ctrl = args['ctrl'] self.alt = args['alt'] if self.event[:3] == 'key': self.key = args['key'] else: self.pos = args['pos'] self.press = args['press'] self.release = args['release'] class local_light(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) args['_objName'] = "local_light" super(local_light, self).setup(args) if (canvas.get_selected() is not None): canvas.get_selected()._lights.append(self) class distant_light(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) args['_objName'] = "distant_light" self._direction = vector(0,0,1) super(distant_light, self).setup(args) if (canvas.get_selected() is not None): canvas.get_selected()._lights.append(self) @property def direction(self): return self._direction @direction.setter def direction(self, value): self._direction = vector(value) if not self._constructing: self.addattr('direction') ## title_anchor = [canvas.idx, 1] and caption_anchor = [canvas.idx, 2] are attributes of canvas print_anchor = 3 ## problematic -- intended to point at print area class wtext(standardAttributes): def __init__(self, **args): super(wtext, self).__init__() ## get idx, attrsupdt from baseObj argsToSend = ['location', 'text'] self._constructing = True objName = 'wtext' self._text = '' if 'text' in args: self._text = str(args['text']) self.location = None if 'pos' in args: self.location = args['pos'] if self.location == print_anchor: #self.location = [-1, print_anchor] raise AttributeError('wtext: Cannot specify "print_anchor" in VPython 7.') argsToSend.append('location') cmd = {"cmd": objName, "idx": self.idx, "text":self._text} if self.location is not None: cmd['location'] = self.location self.appendcmd(cmd) self._constructing = False @property def text(self): return self._text @text.setter def text(self, value): self._text = str(value) if not self._constructing: self.addattr('text') class controls(baseObj): attrlists = { 'button': ['text', 'color', 'textcolor', 'background', 'disabled'], 'checkbox':['checked', 'text', 'disabled'], 'radio':['checked', 'text', 'disabled'], 'menu':['selected', 'choices', 'index', 'disabled'], 'slider':['vertical', 'min', 'max', 'step', 'value', 'length', 'width', 'left', 'right', 'top', 'bottom', 'align', 'disabled'], 'winput':['width', 'height', 'text', 'type', 'disabled'] } def setup(self, args): super(controls, self).__init__() ## get idx, attrsupdt from baseObj ## default values of common attributes self._constructing = True argsToSend = [] objName = args['_objName'] del args['_objName'] if 'pos' in args: self.location = args['pos'] if self.location == print_anchor: #self.location = [-1, print_anchor] raise AttributeError(objName+': Cannot specify "print_anchor" in VPython 7.') argsToSend.append('location') del args['pos'] if 'canvas' in args: ## specified in constructor self.canvas = args['canvas'] del args['canvas'] else: self.canvas = canvas.get_selected() if 'bind' in args: self._bind = args['bind'] del args['bind'] else: raise AttributeError('bind missing') ## override default vector attributes vectorAttributes = ['color', 'textcolor', 'background'] for a in vectorAttributes: if a in args: val = args[a] if a == 'textcolor': del args[a] a = 'color' # textcolor is a legacy attribute; now use color argsToSend.append(a) if isinstance(val, vector): setattr(self, '_'+a, val) else: raise AttributeError(a+' must be a vector') ## override default scalar attributes for a,val in args.items(): if a in controls.attrlists[objName]: argsToSend.append(a) setattr(self, '_'+a, val) else: setattr(self, a, val) cmd = {"cmd": objName, "idx": self.idx} cmd["canvas"] = self.canvas.idx ## send only args specified in constructor for a in argsToSend: ## all shared attributes are scalars aval = getattr(self,a) if isinstance(aval, vector): aval = aval.value cmd[a] = aval self.appendcmd(cmd) self._constructing = False @property def bind(self): return self._bind @bind.setter def bind(self, value): raise AttributeError('bind cannot be changed') @property def pos(self): raise AttributeError(objName+' pos attribute is not available.') @pos.setter def pos(self, value): raise AttributeError(objName+' pos attribute cannot be changed.') @property def disabled(self): return self._disabled @disabled.setter def disabled(self, value): self._disabled = value if not self._constructing: self.addattr('disabled') def delete(self): self.addmethod('delete', 'None') def _ipython_display_(self): # don't print something when making an (anonymous) object pass class button(controls): def __init__(self, **args): args['_objName'] = 'button' self._text = "" self._color = color.black self._background = color.white self._disabled = False super(button, self).setup(args) @property def text(self): return self._text @text.setter def text(self, value): self._text = value if not self._constructing: self.addattr('text') @property def textcolor(self): # legacy; now use color instead of textcolor return self._color @textcolor.setter def textcolor(self, value): self._color = vector(value) if not self._constructing: self.addattr('color') @property def color(self): # legacy; now use color instead of textcolor return self._color @color.setter def color(self, value): self._color = vector(value) if not self._constructing: self.addattr('color') @property def background(self): return self._background @background.setter def background(self, value): self._background = vector(value) if not self._constructing: self.addattr('background') class checkbox(controls): def __init__(self, **args): args['_objName'] = 'checkbox' self._checked = False self._text = '' super(checkbox, self).setup(args) @property def text(self): return self._text @text.setter def text(self, value): self._text = value if not self._constructing: self.addattr('text') @property def checked(self): return self._checked @checked.setter def checked(self, value): self._checked = value if not self._constructing: self.addattr('checked') class radio(controls): def __init__(self, **args): args['_objName'] = 'radio' self._checked = False self._text = '' super(radio, self).setup(args) @property def text(self): return self._text @text.setter def text(self, value): self._text = value if not self._constructing: self.addattr('text') @property def checked(self): return self._checked @checked.setter def checked(self, value): self._checked = value if not self._constructing: self.addattr('checked') class winput(controls): def __init__(self, **args): args['_objName'] = 'winput' self._checked = False self._text = '' self._type = 'numeric' self._number = None self._width = 100 self._height = 20 super(winput, self).setup(args) @property def text(self): return self._text @text.setter def text(self, value): self._text = value if not self._constructing: self.addattr('text') @property def number(self): return self._number @number.setter def number(self, value): raise AttributeError('Cannot change the winput number attribute.') @property def width(self): return self._width @width.setter def width(self, value): raise AttributeError('Cannot change the winput width attribute.') @property def type(self): return self._type @type.setter def type(self, value): raise AttributeError('Cannot change the winput type attribute.') class menu(controls): def __init__(self, **args): args['_objName'] = 'menu' self._selected = "None" self._choices = args['choices'] if 'index' in args: args['selected'] = self._choices[ args['index'] ] del args['index'] super(menu, self).setup(args) @property def choices(self): return self._choices @choices.setter def choices(self, value): raise AttributeError('choices cannot be modified after a menu is created') @property def index(self): if self._selected == "None": return None else: return self._choices.index(self._selected) @index.setter def index(self, value): self.selected = self._choices[value] @property def selected(self): if self._selected == "None": return None else: return self._selected @selected.setter def selected(self, value): if value is None: value = "None" self._selected = value if not self._constructing: self.addattr('selected') class slider(controls): def __init__(self, **args): args['_objName'] = 'slider' self._vertical = False if 'min' in args: ## set here in order to set step self._min = args['min'] else: self._min = 0 if 'max' in args: self._max = args['max'] else: self._max = 1 self._step = 0.001*(self._max - self._min) self._value = self._min self._length = 400 self._width = 10 self._left = 0 self._right = 0 self._top = 0 self._bottom = 0 self._align = 'left' super(slider, self).setup(args) @property def vertical(self): return self._vertical @vertical.setter def vertical(self, value): raise AttributeError('vertical cannot be changed after creating a slider') @property def min(self): return self._min @min.setter def min(self, value): raise AttributeError('min cannot be changed after creating a slider') @property def max(self): return self._max @max.setter def max(self, value): raise AttributeError('max cannot be changed after creating a slider') @property def step(self): return self._step @step.setter def step(self, value): raise AttributeError('step cannot be changed after creating a slider') @property def value(self): return self._value @value.setter def value(self, val): self._value = val if not self._constructing: self.addattr('value') @property def length(self): return self._length @length.setter def length(self, value): raise AttributeError('length cannot be changed after creating a slider') @property def width(self): return self._width @width.setter def width(self, value): raise AttributeError('width cannot be changed after creating a slider') @property def left(self): return self._left @left.setter def left(self, value): raise AttributeError('left cannot be changed after creating a slider') @property def right(self): return self._right @right.setter def right(self, value): raise AttributeError('right cannot be changed after creating a slider') @property def top(self): return self._top @top.setter def top(self, value): raise AttributeError('top cannot be changed after creating a slider') @property def bottom(self): return self._bottom @bottom.setter def bottom(self, value): raise AttributeError('bottom cannot be changed after creating a slider') @property def align(self): return self._align @align.setter def align(self, value): raise AttributeError('align cannot be changed after creating a slider') class extrusion(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) # to keep standardAttributes happy args['_objName'] = "extrusion" if 'path' not in args: raise AttributeError("An extrusion must have a path.") if 'shape' not in args: raise AttributeError("An extrusion must have a shape.") self.check_shape(args['shape']) savesize = None if 'size' in args: savesize = args['size'] del args['size'] self._shape = [ ] pozz = args['path'] npozz = [] for pp in pozz: npozz.append(pp.value) ## convert vectors to lists args['path'] = npozz[:] self._show_start_face = True self._show_end_face = True if 'color' in args: self._start_face_color = args['color'] self._end_face_color = args['color'] super(extrusion, self).setup(args) self.canvas._compound = self # used by event handler to update pos and size _wait(self.canvas) if savesize is not None: self._size = savesize @property def axis(self): return self._axis @axis.setter def axis(self,value): # extrusion axis and size don't interact self._save_oldaxis = adjust_up(self._axis, value, self._up, self._save_oldaxis) # this sets self._axis and self._up if not self._constructing: # must update both axis and up when either is changed self.addattr('axis') self.addattr('up') @property def size(self): return self._size @size.setter def size(self,value): # extrusion axis and size don't interact self._size.value = value if not self._constructing: self.addattr('size') def level(self,a): # Determine format of shape specification: # return 1 if a = [S, S, ...] where S is [x,y] # return 2 if a = [ [S, S, ... ], [S, S, ...], [S, S, ...] ] # return 3 if a = [ [ [S, S, ... ], [S, S, ...], [S, S, ...] ], # [ [S, S, ... ], [S, S, ...], [S, S, ...] ], ... ] if not (type(a) is list or type(a) is tuple): raise AttributeError("A shape must be a list of [x,y] elements.") if not (type(a[0]) is list or type(a[0]) is tuple): raise AttributeError("A shape must be a list of [x,y] elements.") if type(a[0][0]) is int or type(a[0][0]) is float: return 1 if type(a[0][0][0]) is int or type(a[0][0][0]) is float: return 2 if type(a[0][0][0][0]) is int or type(a[0][0][0][0]) is float: return 3 raise AttributeError("A shape must be a list of lists of [x,y] elements.") def check_shape(self, a): # check that all shapes are closed n = self.level(a) if n == 1: p1 = a[0] p2 = a[-1] if not (p1[0] == p2[0] and p1[1] == p2[1]): raise AttributeError("An extrusion shape must be closed.") elif n == 2: for b in a: p1 = b[0] p2 = b[-1] if not (p1[0] == p2[0] and p1[1] == p2[1]): raise AttributeError("An extrusion shape must be closed.") else: for c in a: for b in c: p1 = b[0] p2 = b[-1] if not (p1[0] == p2[0] and p1[1] == p2[1]): raise AttributeError("An extrusion shape must be closed.") @property def path(self): if self._constructing: return self._path else: return None @path.setter def path(self,value): raise AttributeError('path cannot be changed after extrusion is created') @property def shape(self): if self._constructing: return self._shape else: return None @shape.setter def shape(self, value): raise AttributeError('shape cannot be changed after extrusion is created') @property def show_start_face(self): if self._constructing: return self._show_start_face else: return None @show_start_face.setter def show_start_face(self,value): raise AttributeError('show_start_face cannot be changed after extrusion is created') @property def show_end_face(self): if self._constructing: return self._show_end_face else: return None @show_end_face.setter def show_end_face(self,value): raise AttributeError('show_end_face cannot be changed after extrusion is created') @property def start_face_color(self): if self._constructing: return self._start_face_color else: return None @start_face_color.setter def start_face_color(self,value): raise AttributeError('start_face_color cannot be changed after extrusion is created') @property def end_face_color(self): if self._constructing: return self._end_face_color else: return None @end_face_color.setter def end_face_color(self,value): raise AttributeError('end_face_color cannot be changed after extrusion is created') class text(standardAttributes): def __init__(self, **args): args['_default_size'] = vector(1,1,1) # to keep standardAttributes happy args['_objName'] = "text" self._height = 1 ## not derived from size self._length = 1 ## calculated from actual object and returned to vpython self._depth = 0.2 ## default is 0.2*height self._align = "left" self._text = "" self._font = "sans" self._billboard = False self._start_face_color = vector(1,1,1) self._end_face_color = vector(1,1,1) self._show_start_face = True self._show_end_face = True self._descender = 0.3 self._vertical_spacing = 1.3 self._upper_left = vector(0,0,0) self._upper_right = vector(0,0,0) self._lower_left = vector(0,0,0) self._lower_right = vector(0,0,0) self._start = vector(0,0,0) self._end = vector(0,0,0) _special_clone = None if ('size' in args): raise AttributeError("The text object has no size attribute.") if '_cloneid' in args: _special_clone = args['_cloneid'] self._lines = len(args['_text'].split('\n')) else: if 'text' in args: self._lines = len(args['text'].split('\n')) else: raise AttributeError('A text object must have a text attribute') if 'color' in args: self._start_face_color = args['color'] self._end_face_color = args['color'] super(text, self).setup(args) if _special_clone is None: self.canvas._compound = self # used by event handler to update pos and size _wait(self.canvas) # sets _length, _descender, up @property def size(self): raise AttributeError("The text object has no size attribute.") @size.setter def size(self, val): raise AttributeError("The text object has no size attribute.") @property def axis(self): return self._axis @axis.setter def axis(self,value): # changing axis does not affect size oldaxis = vector(self.axis) u = self.up self._axis.value = value self._save_oldaxis = adjust_up(norm(oldaxis), self._axis, self._up, self._save_oldaxis) self.addattr('axis') self.addattr('up') @property def length(self): return self._length @length.setter def length(self, val): if self._constructing: raise AttributeError("text length can't be set when creating a text object") else: self._length = val self.addattr('length') @property def height(self): return self._height @height.setter def height(self, val): if self._constructing: self._height = val else: if val == self._height: return self._height = val self.addattr('height') @property def depth(self): return self._depth @depth.setter def depth(self, val): # sign issue ?? if self._constructing: self._depth = val else: if abs(val) < 0.01*self._height: if val < 0: val = -0.01*self._height else: val = 0.01*self._height self._depth = value self.addattr('depth') @property def align(self): return self._align @align.setter def align(self,val): self._align = val if not self._constructing: self.addattr('align') @property def font(self): return self._font @font.setter def font(self, val): if not (val=='sans' or val=='serif'): raise AttributeError("text font must be either 'sans' or 'serif' ") self._font = val if not self._constructing: self.addattr('font') @property def text(self): return self._text @text.setter def text(self, val): self._text = val if not self._constructing: self.addattr('text') @property def billboard(self): return self._billboard @billboard.setter def billboard(self, val): raise AttributeError("billboard cannot be modified") @property def start_face_color(self): return self._start_face_color @start_face_color.setter def start_face_color(self, val): raise AttributeError("start_face_color cannot be modified") @property def end_face_color(self): return self._end_face_color @end_face_color.setter def end_face_color(self, val): raise AttributeError("end_face_color cannot be modified") @property def show_start_face(self): return self._show_start_face @show_start_face.setter def show_start_face(self,val): raise AttributeError("show_start_face cannot be modified") @property def show_end_face(self): return self._show_end_face @show_end_face.setter def show_end_face(self,val): raise AttributeError("show_end_face cannot be modified") @property def descender(self): return self._descender @descender.setter def descender(self, val): raise AttributeError("descender cannot be modified") @property def upper_left(self): if self.align == "right": dx = -self.length elif self.align == "center": dx = -self.length/2 else: dx = 0 ul = self.pos + (self.height * norm(self.up)) + dx*norm(self.axis) return ul @upper_left.setter def upper_left(self, val): raise AttributeError("upper_left cannot be modified") @property def upper_right(self): return self.upper_left + norm(self.axis)*self.length @upper_right.setter def upper_right(self, val): raise AttributeError("upper_right cannot be modified") @property def lower_left(self): return self.upper_left + norm(self.up)*(-self.height -self._descender - 1.5*self.height*(self._lines-1) ) @lower_left.setter def lower_left(self, val): raise AttributeError("lower_left cannot be modified") @property def lower_right(self): return self.lower_left + norm(self.axis)*self.length @lower_right.setter def lower_right(self, val): raise AttributeError("lower_right cannot be modified") @property def start(self): return self.upper_left - norm(self.up)*self.height @start.setter def start(self, val): raise AttributeError("start cannot be modified") @property def end(self): return self.upper_right - norm(self.up)*self.height @end.setter def end(self, val): raise AttributeError("end cannot be modified") @property def vertical_spacing(self): return 1.5*self.height @vertical_spacing.setter def vertical_spacing(self, val): raise AttributeError("vertical_spacing cannot be modified") def combin(x, y): # combin(x,y) = factorial(x)/[factorial(y)*factorial(x-y)] z = x-y num = 1.0 if y > z: y,z = z,y nn = z+1 ny = 1 while nn <= x: num = num*nn/ny nn += 1 ny += 1 if nn != x+1: raise ValueError('Illegal arguments (%d, %d) for combin function' % (x, y)) return num def sleep(dt): # don't use time.sleep because it delays output queued up before the call to sleep t = clock()+dt while clock() < t: rate(60) def print_to_string(*args): # treatment of
vs. \n not quite right here s = '' for a in args: s += str(a)+' ' s = s[:-1] return(s) def keysdown(): global keysdownlist keys = [] for k in keysdownlist: # return a copy of keysdownlist keys.append(k) return keys