Skip to content

Commit 8b53c41

Browse files
author
Aaron Titus
committed
added support for pyqt browser window for display vpython canvas
1 parent 7a5aad2 commit 8b53c41

File tree

3 files changed

+94
-68
lines changed

3 files changed

+94
-68
lines changed

vpython/no_notebook.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .vpython import GlowWidget, baseObj, vector, canvas
1+
from .vpython import GlowWidget, baseObj, vector, canvas, _browsertype
22
from ._notebook_helpers import _in_spyder, _undo_vpython_import_in_spyder
3+
#from .qtbrowser import createQtBrowser
34

45
from http.server import BaseHTTPRequestHandler, HTTPServer
56
import os
@@ -13,13 +14,18 @@
1314
import txaio
1415
import copy
1516
import socket
17+
import PyQt5.QtCore
18+
import PyQt5.QtWebEngineWidgets
19+
from PyQt5.QtWidgets import QApplication
20+
import multiprocessing
1621

1722
import signal
1823
from urllib.parse import unquote
1924

2025
from .rate_control import rate
2126

2227

28+
2329
# Check for Ctrl+C. SIGINT will also be sent by our code if WServer is closed.
2430
def signal_handler(signal, frame):
2531
stop_server()
@@ -231,10 +237,25 @@ def onClose(self, wasClean, code, reason):
231237
else:
232238
__server = HTTPServer(('', __HTTP_PORT), serveHTTP)
233239
# or webbrowser.open_new_tab()
234-
_webbrowser.open('http://localhost:{}'.format(__HTTP_PORT))
240+
if(_browsertype=='default'): #uses default browser
241+
_webbrowser.open('http://localhost:{}'.format(__HTTP_PORT)) #uses default browser
242+
235243
except:
236244
pass
237245

246+
def start_Qapp(port):
247+
# creates a python browser with PyQt5
248+
# runs qtbrowser.py in a separate process
249+
filepath=os.path.dirname(__file__)
250+
filename=filepath+'/qtbrowser.py'
251+
os.system('python '+filename+' http://localhost:{}'.format(port))
252+
253+
254+
#create a browser in its own process
255+
if(_browsertype=='pyqt'): #uses default browser
256+
__m = multiprocessing.Process(target=start_Qapp, args=(__HTTP_PORT,))
257+
__m.start()
258+
238259
__w = threading.Thread(target=__server.serve_forever)
239260
__w.start()
240261

@@ -302,7 +323,6 @@ def stop_server():
302323
# python/threading.py:_shutdown). Since we just stopped those threads,
303324
# we'll now exit.
304325

305-
306326
GW = GlowWidget()
307327

308328
while not (httpserving and websocketserving): # try to make sure setup is complete

vpython/qtbrowser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
import PyQt5.QtCore
3+
import PyQt5.QtWebEngineWidgets
4+
from PyQt5.QtWidgets import QApplication
5+
6+
print(sys.argv)
7+
8+
if(len(sys.argv)>1):
9+
10+
if sys.argv[1]:
11+
12+
app = QApplication(sys.argv)
13+
14+
web = PyQt5.QtWebEngineWidgets.QWebEngineView()
15+
web.load(PyQt5.QtCore.QUrl(sys.argv[1]))
16+
web.show()
17+
18+
sys.exit(app.exec_())
19+
20+
else:
21+
print("Please give a URL as the first command-line argument when running the program.")
22+
23+
else:
24+
print("Please give a URL as the first command-line argument when running the program.")

vpython/vpython.py

Lines changed: 47 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
'event_return', 'extrusion', 'faces', 'frame', 'gcurve', 'gdots',
2525
'ghbars', 'gobj', 'graph', 'gvbars', 'helix', 'label',
2626
'local_light', 'menu', 'meta_canvas', 'points', 'pyramid',
27-
'quad', 'radio', 'ring', 'simple_sphere', 'sleep', 'slider', 'sphere',
27+
'quad', 'radio', 'ring', 'set_browser', 'simple_sphere', 'sleep', 'slider', 'sphere',
2828
'standardAttributes', 'text', 'textures', 'triangle', 'vertex',
29-
'wtext', 'winput', 'keysdown']
29+
'wtext', 'winput']
3030

3131
__p = platform.python_version()
3232
_ispython3 = (__p[0] == '3')
@@ -40,8 +40,6 @@
4040
version = [__version__, 'jupyter']
4141
GSversion = [__gs_version__, 'glowscript']
4242

43-
keysdownlist = [] # list of keys currently pressed
44-
4543
# To print immediately, do this:
4644
# print(.....)
4745
# sys.stdout.flush()
@@ -97,7 +95,7 @@
9795
'right':'q', 'top':'r', 'bottom':'s', '_cloneid':'t',
9896
'logx':'u', 'logy':'v', 'dot':'w', 'dot_radius':'x',
9997
'markers':'y', 'legend':'z', 'label':'A', 'delta':'B', 'marker_color':'C',
100-
'size_units':'D', 'userpan':'E', 'scroll':'F'}
98+
'size_units':'D', 'userpan':'E'}
10199

102100
# methods are X in {'m': '23X....'}
103101
# pos is normally updated as an attribute, but for interval-based trails, it is updated (multiply) as a method
@@ -524,7 +522,7 @@ class standardAttributes(baseObj):
524522
['visible'],
525523
[]],
526524
'compound':[['pos', 'color', 'trail_color'],
527-
['axis', 'size', 'up', 'origin'],
525+
['axis', 'size', 'up'],
528526
['visible', 'opacity','shininess', 'emissive',
529527
'make_trail', 'trail_type', 'interval', 'texture',
530528
'retain', 'trail_color', 'trail_radius', 'obj_idxs', 'pickable'],
@@ -700,7 +698,7 @@ def setup(self, args):
700698

701699

702700
# set canvas
703-
if self.canvas is None: ## not specified in constructor
701+
if self.canvas == None: ## not specified in constructor
704702
self.canvas = canvas.get_selected()
705703
#cmd["attrs"].append({"attr": 'canvas', "value": self.canvas.idx})
706704
cmd['canvas'] = self.canvas.idx
@@ -713,7 +711,7 @@ def setup(self, args):
713711
if _special_clone is not None: cmd["_cloneid"] = _special_clone
714712
self.appendcmd(cmd)
715713

716-
# if ('frame' in args and args['frame'] is not None):
714+
# if ('frame' in args and args['frame'] != None):
717715
# frame.objects.append(self)
718716
# frame.update_obj_list()
719717

@@ -1001,9 +999,9 @@ def rotate(self, angle=None, axis=None, origin=None):
1001999
saveorigin = origin
10021000
if angle == 0:
10031001
return
1004-
if angle is None:
1002+
if angle == None:
10051003
raise TypeError('You must specify an angle through which to rotate')
1006-
if axis is None:
1004+
if axis == None:
10071005
rotaxis = self.axis
10081006
else:
10091007
rotaxis = axis
@@ -1530,34 +1528,32 @@ def size(self,value): # compound axis and size don't interact
15301528
if not self._constructing:
15311529
self.addattr('size')
15321530

1533-
@property
1534-
def origin(self):
1535-
return self._origin
1536-
@origin.setter
1537-
def origin(self,value): # compound origin cannot be reset
1538-
if not self._constructing:
1539-
raise AttributeError('The compound "origin" attribute is read-only; change "pos" instead.')
1540-
self._origin = value
1531+
def _world_zaxis(self):
1532+
axis = self._axis
1533+
up = norm(self._up)
1534+
if abs(axis.dot(up)) / sqrt(axis.mag2) > 0.98:
1535+
if abs(norm(axis).dot(vector(-1,0,0))) > 0.98:
1536+
z_axis = axis.cross(vector(0,0,1)).norm()
1537+
else:
1538+
z_axis = axis.cross(vector(-1,0,0)).norm()
1539+
else:
1540+
z_axis = axis.cross(up).norm()
1541+
return z_axis
15411542

15421543
def world_to_compound(self, v):
1543-
v = v-self._pos
1544-
x_axis = self._axis.hat
1545-
y_axis = self._up.hat
1546-
z_axis = x_axis.cross(y_axis)
1547-
ox = self._size0.x/self._size.x # _size0 is the original size
1548-
oy = self._size0.y/self._size.y
1549-
oz = self._size0.z/self._size.z
1550-
return self._origin + vector(v.dot(x_axis)*ox, v.dot(y_axis)*oy, v.dot(z_axis)*oz)
1544+
axis = self._axis
1545+
z_axis = self._world_zaxis()
1546+
y_axis = z_axis.cross(axis).norm()
1547+
x_axis = axis.norm()
1548+
v = v - self._pos
1549+
return vector(v.dot(x_axis), v.dot(y_axis), v.dot(z_axis))
15511550

15521551
def compound_to_world(self, v):
1553-
v = v-self._origin
1554-
x_axis = self._axis.hat
1555-
y_axis = self._up.hat
1556-
z_axis = x_axis.cross(y_axis)
1557-
ox = self._size.x/self._size0.x # _size0 is the original size
1558-
oy = self._size.y/self._size0.y
1559-
oz = self._size.z/self._size0.z
1560-
return self._pos + v.x*ox*x_axis + v.y*oy*y_axis + v.z*oz*z_axis
1552+
axis = self._axis
1553+
z_axis = self._world_zaxis()
1554+
y_axis = z_axis.cross(axis).norm()
1555+
x_axis = axis.norm()
1556+
return self._pos+(v.x*x_axis) + (v.y*y_axis) + (v.z*z_axis)
15611557

15621558
class vertex(standardAttributes):
15631559
def __init__(self, **args):
@@ -1965,7 +1961,7 @@ def __init__(self,*args1, **args):
19651961

19661962
super(curveMethods, self).setup(args)
19671963

1968-
if tpos is not None:
1964+
if tpos != None:
19691965
if len(args1) > 0: raise AttributeError('Malformed constructor')
19701966
self.append(tpos)
19711967
if len(args1) > 0:
@@ -1989,7 +1985,7 @@ def __init__(self,*args1, **args):
19891985

19901986
super(curveMethods, self).setup(args)
19911987

1992-
if tpos is not None:
1988+
if tpos != None:
19931989
if len(args1) > 0: raise AttributeError('Malformed constructor')
19941990
self.append(tpos)
19951991
if len(args1) > 0:
@@ -2282,7 +2278,6 @@ def __init__(self, **args):
22822278
self._title = ""
22832279
self._xtitle = ""
22842280
self._ytitle = ""
2285-
self._scroll = False
22862281
argsToSend = []
22872282

22882283
## override default vector attributes
@@ -2297,7 +2292,7 @@ def __init__(self, **args):
22972292

22982293
## override default scalar attributes
22992294
scalarAttributes = ['width', 'height', 'title', 'xtitle', 'ytitle','align',
2300-
'xmin', 'xmax', 'ymin', 'ymax', 'logx', 'logy', 'fast', 'scroll']
2295+
'xmin', 'xmax', 'ymin', 'ymax', 'logx', 'logy', 'fast']
23012296
for a in scalarAttributes:
23022297
if a in args:
23032298
argsToSend.append(a)
@@ -2310,12 +2305,6 @@ def __init__(self, **args):
23102305

23112306
cmd = {"cmd": objName, "idx": self.idx}
23122307

2313-
if self._scroll:
2314-
if not ('xmin' in argsToSend and 'xmax' in argsToSend):
2315-
raise AttributeError("For a scrolling graph, both xmin and xmax must be specified.")
2316-
if self._xmax <= self._xmin:
2317-
raise AttributeError("For a scrolling graph, xmax must be greater than xmin.")
2318-
23192308
## send only args specified in constructor
23202309
for a in argsToSend:
23212310
aval = getattr(self,a)
@@ -2329,16 +2318,11 @@ def __init__(self, **args):
23292318
def fast(self): return self._fast
23302319
@fast.setter
23312320
def fast(self,val):
2321+
# if _isnotebook and not val:
2322+
# raise AttributeError('"fast = False" is currently not available in a Jupyter notebook.')
23322323
self._fast = val
23332324
self.addattr('fast')
23342325

2335-
@property
2336-
def scroll(self): return self._scroll
2337-
@scroll.setter
2338-
def scroll(self,val):
2339-
self._scroll = val
2340-
self.addattr('scroll')
2341-
23422326
@property
23432327
def width(self): return self._width
23442328
@width.setter
@@ -2830,7 +2814,7 @@ def __init__(self, **args):
28302814

28312815
for a in canvasNonVecAttrs:
28322816
if a in args:
2833-
if args[a] is not None:
2817+
if args[a] != None:
28342818
setattr(self, '_'+a, args[a])
28352819
cmd[a]= args[a]
28362820
del args[a]
@@ -3115,13 +3099,11 @@ def objz(self, obj, operation):
31153099

31163100
## key events conflict with notebook command mode; not permitted for now
31173101
def handle_event(self, evt): ## events and scene info updates
3118-
global keysdownlist
31193102
ev = evt['event']
31203103
if ev == 'pick':
31213104
self.mouse.setpick( evt )
31223105
self._waitfor = True # what pick is looking for
31233106
elif ev == '_compound': # compound, text, extrusion
3124-
print('compound event return')
31253107
obj = self._compound
31263108
p = evt['pos']
31273109
if obj._objName == 'text':
@@ -3133,7 +3115,7 @@ def handle_event(self, evt): ## events and scene info updates
31333115
# on_change functions that detect changes in e.g. obj.pos.y
31343116
obj._pos.value = list_to_vec(p)
31353117
s = evt['size']
3136-
obj._size.value = obj._size0 = list_to_vec(s)
3118+
obj._size.value = list_to_vec(s)
31373119
obj._axis.value = obj._size._x*norm(obj._axis)
31383120
obj._up.value = list_to_vec(evt['up'])
31393121
self._waitfor = True # what compound and text and extrusion are looking for in _wait()
@@ -3200,8 +3182,6 @@ def handle_event(self, evt): ## events and scene info updates
32003182
if 'autoscale' in evt and self.userzoom and not self._set_autoscale:
32013183
self._autoscale = evt['autoscale']
32023184
self._set_autoscale = False
3203-
if 'keysdown' in evt: keysdownlist = evt['keysdown']
3204-
32053185

32063186
def bind(self, eventtype, whattodo):
32073187
evts = eventtype.split()
@@ -3275,7 +3255,7 @@ def __init__(self, **args):
32753255
args['_objName'] = "local_light"
32763256
super(local_light, self).setup(args)
32773257

3278-
if (canvas.get_selected() is not None):
3258+
if (canvas.get_selected() != None):
32793259
canvas.get_selected()._lights.append(self)
32803260

32813261
class distant_light(standardAttributes):
@@ -3285,7 +3265,7 @@ def __init__(self, **args):
32853265
self._direction = vector(0,0,1)
32863266
super(distant_light, self).setup(args)
32873267

3288-
if (canvas.get_selected() is not None):
3268+
if (canvas.get_selected() != None):
32893269
canvas.get_selected()._lights.append(self)
32903270

32913271
@property
@@ -4112,9 +4092,11 @@ def print_to_string(*args): # treatment of <br> vs. \n not quite right here
41124092
s = s[:-1]
41134093
return(s)
41144094

4115-
def keysdown():
4116-
global keysdownlist
4117-
keys = []
4118-
for k in keysdownlist: # return a copy of keysdownlist
4119-
keys.append(k)
4120-
return keys
4095+
# global variable for type of web browser to display vpython
4096+
_browsertype = 'default'
4097+
def set_browser(type='default'):
4098+
global _browsertype
4099+
if type=='pyqt':
4100+
_browsertype='pyqt'
4101+
else:
4102+
_browsertype='default'

0 commit comments

Comments
 (0)