lavavu-osmesa 1.9.9__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lavavu/LavaVuPython.py +561 -0
- lavavu/_LavaVuPython.cpython-313-x86_64-linux-gnu.so +0 -0
- lavavu/__init__.py +15 -0
- lavavu/__main__.py +12 -0
- lavavu/amalgamate.py +15 -0
- lavavu/aserver.py +359 -0
- lavavu/control.py +1731 -0
- lavavu/convert.py +888 -0
- lavavu/dict.json +2528 -0
- lavavu/font.bin +0 -0
- lavavu/html/LavaVu-amalgamated.css +282 -0
- lavavu/html/OK-min.js +99 -0
- lavavu/html/baseviewer.js +307 -0
- lavavu/html/control.css +104 -0
- lavavu/html/control.js +340 -0
- lavavu/html/dat-gui-light-theme.css +68 -0
- lavavu/html/dat.gui.min.js +2 -0
- lavavu/html/draw.js +2259 -0
- lavavu/html/drawbox.js +1039 -0
- lavavu/html/emscripten-template.js +184 -0
- lavavu/html/emscripten.css +92 -0
- lavavu/html/favicon.ico +0 -0
- lavavu/html/gl-matrix-min.js +47 -0
- lavavu/html/gui.css +25 -0
- lavavu/html/menu.js +615 -0
- lavavu/html/server.js +226 -0
- lavavu/html/stats.min.js +5 -0
- lavavu/html/styles.css +58 -0
- lavavu/html/webview-template.html +43 -0
- lavavu/html/webview.html +43 -0
- lavavu/lavavu.py +6200 -0
- lavavu/osmesa/LavaVuPython.py +561 -0
- lavavu/osmesa/_LavaVuPython.cpython-313-x86_64-linux-gnu.so +0 -0
- lavavu/osmesa/__init__.py +0 -0
- lavavu/points.py +191 -0
- lavavu/server.py +343 -0
- lavavu/shaders/default.frag +14 -0
- lavavu/shaders/default.vert +17 -0
- lavavu/shaders/fontShader.frag +20 -0
- lavavu/shaders/fontShader.vert +18 -0
- lavavu/shaders/lineShader.frag +39 -0
- lavavu/shaders/lineShader.vert +26 -0
- lavavu/shaders/pointShader.frag +127 -0
- lavavu/shaders/pointShader.vert +53 -0
- lavavu/shaders/triShader.frag +153 -0
- lavavu/shaders/triShader.vert +49 -0
- lavavu/shaders/volumeShader.frag +400 -0
- lavavu/shaders/volumeShader.vert +5 -0
- lavavu/tracers.py +207 -0
- lavavu/vutils.py +211 -0
- lavavu_osmesa-1.9.9.dist-info/METADATA +323 -0
- lavavu_osmesa-1.9.9.dist-info/RECORD +65 -0
- lavavu_osmesa-1.9.9.dist-info/WHEEL +5 -0
- lavavu_osmesa-1.9.9.dist-info/entry_points.txt +2 -0
- lavavu_osmesa-1.9.9.dist-info/licenses/LICENSE.md +179 -0
- lavavu_osmesa-1.9.9.dist-info/top_level.txt +1 -0
- lavavu_osmesa.libs/libLLVM-17-51492e70.so +0 -0
- lavavu_osmesa.libs/libOSMesa-f6a8f160.so.8.0.0 +0 -0
- lavavu_osmesa.libs/libdrm-b0291a67.so.2.4.0 +0 -0
- lavavu_osmesa.libs/libffi-3a37023a.so.6.0.2 +0 -0
- lavavu_osmesa.libs/libglapi-520b284c.so.0.0.0 +0 -0
- lavavu_osmesa.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- lavavu_osmesa.libs/libselinux-d0805dcb.so.1 +0 -0
- lavavu_osmesa.libs/libtinfo-3a2cb85b.so.6.1 +0 -0
- lavavu_osmesa.libs/libzstd-76b78bac.so.1.4.4 +0 -0
lavavu/aserver.py
ADDED
@@ -0,0 +1,359 @@
|
|
1
|
+
import urllib
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
import errno
|
5
|
+
import weakref
|
6
|
+
import base64
|
7
|
+
import json
|
8
|
+
import socket
|
9
|
+
|
10
|
+
#ASYNC
|
11
|
+
import asyncio
|
12
|
+
import aiohttp
|
13
|
+
from aiohttp import web
|
14
|
+
import logging
|
15
|
+
|
16
|
+
from urllib.parse import unquote
|
17
|
+
|
18
|
+
"""
|
19
|
+
HTTP Server interface
|
20
|
+
"""
|
21
|
+
|
22
|
+
def _get_viewer(ref):
|
23
|
+
#Get from weak reference, if deleted raise exception
|
24
|
+
lv = ref()
|
25
|
+
if not lv:
|
26
|
+
raise(Exception("Viewer not found"))
|
27
|
+
return lv
|
28
|
+
|
29
|
+
def _execute(lv, cmds):
|
30
|
+
if len(cmds) and cmds[0] == '_':
|
31
|
+
#base64 encoded commands or JSON state
|
32
|
+
cmds = str(base64.b64decode(cmds).decode('utf-8'))
|
33
|
+
#cmds = str(base64.b64decode(cmds), 'utf-8')
|
34
|
+
|
35
|
+
#Object to select can be provided in preceding angle brackets
|
36
|
+
selobj = None
|
37
|
+
if cmds[0] == '<':
|
38
|
+
pos = cmds.find('>')
|
39
|
+
selobj = lv.objects[cmds[1:pos]]
|
40
|
+
cmds = cmds[pos+1:]
|
41
|
+
|
42
|
+
#Execute commands via python API by preceding with '.'
|
43
|
+
done = False
|
44
|
+
if cmds[0] == '.':
|
45
|
+
attr = cmds.split()[0][1:]
|
46
|
+
pos = cmds.find(' ')
|
47
|
+
params = cmds[pos+1:]
|
48
|
+
if selobj:
|
49
|
+
#Call on Object
|
50
|
+
func = getattr(selobj, attr)
|
51
|
+
if func and callable(func):
|
52
|
+
func(params)
|
53
|
+
done = True
|
54
|
+
else:
|
55
|
+
#Call on Viewer
|
56
|
+
func = getattr(lv, attr)
|
57
|
+
if func and callable(func):
|
58
|
+
func(params)
|
59
|
+
done = True
|
60
|
+
elif cmds[0] == '$':
|
61
|
+
#Requests prefixed by '$' are sent
|
62
|
+
#from property collection controls
|
63
|
+
#format is $ID KEY VALUE
|
64
|
+
# - ID is the python id() of the properties object
|
65
|
+
# All properties collections are stored on their parent
|
66
|
+
# object using this id in the _collections dict
|
67
|
+
# - KEY is the property name key to set
|
68
|
+
# - VALUE is a json string containing the value to set
|
69
|
+
S = cmds.split()
|
70
|
+
target = S[0][1:]
|
71
|
+
if target in lv._collections:
|
72
|
+
#Get from _collections by id (weakref)
|
73
|
+
props = lv._collections[target]()
|
74
|
+
props[S[1]] = json.loads(S[2])
|
75
|
+
#Check for callback - if provided, call with updated props
|
76
|
+
func = getattr(props, 'callback')
|
77
|
+
if func and callable(func):
|
78
|
+
func(props)
|
79
|
+
|
80
|
+
#Default, call via lv.commands() scripting API
|
81
|
+
if not done:
|
82
|
+
if selobj:
|
83
|
+
selobj.select()
|
84
|
+
lv.commands(cmds)
|
85
|
+
|
86
|
+
headers = {'Access-Control-Allow-Origin' : '*',
|
87
|
+
'x-colab-notebook-cache-control' : 'no-cache'} #Colab: disable offline access cache
|
88
|
+
|
89
|
+
def img_response(lv, query={}):
|
90
|
+
global headers
|
91
|
+
resp = None
|
92
|
+
if 'width' in query and 'height' in query:
|
93
|
+
resp = lv.jpeg(resolution=(int(query['width']), int(query['height'])))
|
94
|
+
elif 'width' in query:
|
95
|
+
resp = lv.jpeg(resolution=(int(query['width']), 0))
|
96
|
+
else:
|
97
|
+
resp = lv.jpeg()
|
98
|
+
|
99
|
+
#Ensure the response is valid before serving
|
100
|
+
if resp is not None:
|
101
|
+
return web.Response(body=resp, content_type='image/jpeg', headers=headers)
|
102
|
+
else:
|
103
|
+
return web.Response(text='', headers=headers)
|
104
|
+
|
105
|
+
async def handle_get(request):
|
106
|
+
global headers
|
107
|
+
lv = _get_viewer(request.app['viewer'])
|
108
|
+
|
109
|
+
#Default to empty OK 200 response
|
110
|
+
response = web.Response(text='', headers=headers)
|
111
|
+
|
112
|
+
#print(request.path)
|
113
|
+
#for q in request.query:
|
114
|
+
# print(q, request.query[q])
|
115
|
+
|
116
|
+
if request.path == '/':
|
117
|
+
#Index request returns full screen interactive view
|
118
|
+
lv = _get_viewer(request.app['viewer'])
|
119
|
+
w = lv.control.Window(align=None, wrapper=None, fullscreen=True)
|
120
|
+
code = lv.control.show(True, filename="")
|
121
|
+
response = web.Response(text=code, headers=headers, content_type='text/html')
|
122
|
+
|
123
|
+
elif request.path.startswith('/image'):
|
124
|
+
response = img_response(lv, request.query)
|
125
|
+
|
126
|
+
elif request.path.startswith('/command=') or request.path.startswith('/icommand='):
|
127
|
+
pos1 = request.path.find('=')
|
128
|
+
pos2 = request.path.find('?')
|
129
|
+
if pos2 < 0: pos2 = len(request.path)
|
130
|
+
cmds = unquote(request.path[pos1+1:pos2])
|
131
|
+
|
132
|
+
#Run viewer commands
|
133
|
+
_execute(lv, cmds)
|
134
|
+
|
135
|
+
#Serve image or just respond 200
|
136
|
+
if request.path.startswith('/icommand='):
|
137
|
+
response = img_response(lv, request.query)
|
138
|
+
|
139
|
+
elif request.path.startswith('/getstate'):
|
140
|
+
state = lv.app.getState()
|
141
|
+
response = web.Response(text=state, headers=headers, content_type='application/json')
|
142
|
+
elif request.path.startswith('/connect'):
|
143
|
+
if 'url' in request.query:
|
144
|
+
#Save first valid connection URL on the viewer
|
145
|
+
url = request.query['url']
|
146
|
+
if len(lv._url) == 0:
|
147
|
+
lv._url = url
|
148
|
+
uid = id(lv)
|
149
|
+
response = web.Response(text=str(uid), headers=headers)
|
150
|
+
elif request.path.startswith('/key='):
|
151
|
+
pos2 = request.path.find('&')
|
152
|
+
cmds = unquote(request.path[1:pos2])
|
153
|
+
lv.commands('key ' + cmds, True)
|
154
|
+
elif request.path.startswith('/mouse='):
|
155
|
+
pos2 = request.path.find('&')
|
156
|
+
cmds = unquote(request.path[1:pos2])
|
157
|
+
lv.commands('mouse ' + cmds, True)
|
158
|
+
elif request.path.startswith('/db'):
|
159
|
+
#Send the database
|
160
|
+
db = bytes(lv.app.serialize())
|
161
|
+
response = web.Response(body=db, headers=headers, content_type='application/octet-stream')
|
162
|
+
else:
|
163
|
+
#Serve other urls as files if available
|
164
|
+
#print("UNKNOWN - ", request.path)
|
165
|
+
path = request.path
|
166
|
+
if os.path.exists(path):
|
167
|
+
#OK to always serve files in cwd?
|
168
|
+
response = web.FileResponse(path)
|
169
|
+
else:
|
170
|
+
#Serve files from lavavu html dir
|
171
|
+
#print(' - not found in cwd')
|
172
|
+
if path[0] == '/': path = path[1:]
|
173
|
+
path2 = os.path.join(lv.htmlpath, path)
|
174
|
+
if os.path.exists(path2) and os.path.isfile(path2):
|
175
|
+
#print(' - found in htmlpath')
|
176
|
+
response = web.FileResponse(path2)
|
177
|
+
|
178
|
+
return response
|
179
|
+
|
180
|
+
async def handle_post(request):
|
181
|
+
lv = _get_viewer(request.app['viewer'])
|
182
|
+
#print("POST", request.path)
|
183
|
+
#text = await request.text()
|
184
|
+
text = ''
|
185
|
+
#Always interpret post data as commands
|
186
|
+
#(can perform other actions too based on path later if we want)
|
187
|
+
if request.body_exists:
|
188
|
+
body = await request.read()
|
189
|
+
#body = await request.text()
|
190
|
+
|
191
|
+
cmds = str(body, 'utf-8') #python3 only
|
192
|
+
#from urllib.parse import unquote
|
193
|
+
#data_string = unquote(body)
|
194
|
+
#cmds = str(data_string.decode('utf-8'))
|
195
|
+
|
196
|
+
#Run viewer commands
|
197
|
+
_execute(lv, cmds)
|
198
|
+
|
199
|
+
return web.Response(text='', headers=headers)
|
200
|
+
|
201
|
+
|
202
|
+
"""
|
203
|
+
HTTP Server manager class
|
204
|
+
"""
|
205
|
+
class Server(object):
|
206
|
+
def __init__(self, viewer, port=8080, ipv6=False, retries=100):
|
207
|
+
#Allows viewer to be garbage collected
|
208
|
+
self.viewer = weakref.ref(viewer)
|
209
|
+
self.ipv6 = ipv6
|
210
|
+
self.retries = retries
|
211
|
+
self._closing = False
|
212
|
+
self._lock = asyncio.Lock()
|
213
|
+
|
214
|
+
#Get port/socket before running server in synchronous code
|
215
|
+
self.socket, self.port = _listen(port, self.ipv6, self.retries)
|
216
|
+
|
217
|
+
async def run(self):
|
218
|
+
#Server run!
|
219
|
+
#Lock until the port is retrieved
|
220
|
+
async with self._lock:
|
221
|
+
await _serve(self.viewer, self.socket)
|
222
|
+
|
223
|
+
#ONLY NEED THIS IF NO LOOP EXISTS AND WE ARE MANAGING OUR OWN
|
224
|
+
#while not self._closing:
|
225
|
+
# await asyncio.sleep(3600) # sleep forever
|
226
|
+
# #await asyncio.sleep(1) # sleep forever
|
227
|
+
# print('_', end='')
|
228
|
+
|
229
|
+
#To stop, call cleanup (TODO: put this somewhere in closedown code)
|
230
|
+
#await runner.cleanup()
|
231
|
+
|
232
|
+
def _listen(port, ipv6, retries):
|
233
|
+
#Get port/socket before running server in synchronous code
|
234
|
+
#avoids race conditions over port number with subsequent code that
|
235
|
+
#tries to use server.port before it is confirmed/opened
|
236
|
+
hostidx = 0
|
237
|
+
for i in range(retries):
|
238
|
+
try:
|
239
|
+
hosts = []
|
240
|
+
socktype = socket.AF_INET
|
241
|
+
if ipv6:
|
242
|
+
hosts = ['::', 'localhost', '::1']
|
243
|
+
host = hosts[hostidx]
|
244
|
+
socktype = socket.AF_INET6
|
245
|
+
else:
|
246
|
+
hosts = ['0.0.0.0', 'localhost', '127.0.0.1']
|
247
|
+
host = hosts[hostidx]
|
248
|
+
|
249
|
+
#https://github.com/aio-libs/aiohttp/issues/1987#issuecomment-309401600
|
250
|
+
sock = socket.socket(socktype)
|
251
|
+
sock.bind((host, port))
|
252
|
+
# Aiohttp will call 'listen' inside.
|
253
|
+
# But it must be called before we actually use the port,
|
254
|
+
# any attempts to connect before the 'listen' call will
|
255
|
+
# be rejected.
|
256
|
+
sock.listen(128)
|
257
|
+
params = sock.getsockname()
|
258
|
+
port = params[1]
|
259
|
+
#print("Socket ready on host %s port %s" % (host, port))
|
260
|
+
return sock, port
|
261
|
+
|
262
|
+
except (Exception) as e:
|
263
|
+
#Try another port
|
264
|
+
if e.errno == errno.EADDRINUSE: #98
|
265
|
+
port += 1
|
266
|
+
#Try again...
|
267
|
+
elif e.errno == errno.EAFNOSUPPORT: #97 : Address family not supported by protocol
|
268
|
+
#Try next host name/address
|
269
|
+
hostidx += 1
|
270
|
+
if hostidx > 2:
|
271
|
+
#Try again without ipv6?
|
272
|
+
if ipv6:
|
273
|
+
ipv6 = False
|
274
|
+
else:
|
275
|
+
ipv6 = True
|
276
|
+
hostidx = 0
|
277
|
+
#Try again...
|
278
|
+
else:
|
279
|
+
print("Socket open failed: ", e, e.errno, host, port)
|
280
|
+
|
281
|
+
print("Failed to open socket, max retries reached")
|
282
|
+
return None, 0
|
283
|
+
|
284
|
+
async def _serve(viewer, sock):
|
285
|
+
try:
|
286
|
+
#Create web application manager
|
287
|
+
app = web.Application()
|
288
|
+
#https://github.com/aio-libs/aiohttp/issues/3287
|
289
|
+
#app.logger.manager.disable = 100
|
290
|
+
app.logger.setLevel(logging.CRITICAL)
|
291
|
+
|
292
|
+
#Store viewer
|
293
|
+
app["viewer"] = viewer
|
294
|
+
#Add routes
|
295
|
+
app.router.add_post('/{tail:.*}', handle_post),
|
296
|
+
app.router.add_get('/{tail:.*}', handle_get)
|
297
|
+
|
298
|
+
#Static routes? https://docs.aiohttp.org/en/stable/web_advanced.html
|
299
|
+
#app.add_routes([web.static('/', path_to_static_folder)])
|
300
|
+
#routes.static('/prefix', path_to_static_folder)
|
301
|
+
|
302
|
+
#app.add_routes([web.get('/', handler),
|
303
|
+
# web.post('/post', post_handler),
|
304
|
+
# web.put('/put', put_handler)])
|
305
|
+
|
306
|
+
#Can't use this, is blocking
|
307
|
+
#web.run_app(app, sock=sock)
|
308
|
+
|
309
|
+
# set up aiohttp - like run_app, but non-blocking - socket provided version
|
310
|
+
runner = aiohttp.web.AppRunner(app, access_log=None)
|
311
|
+
await runner.setup()
|
312
|
+
site = aiohttp.web_runner.SockSite(runner, sock=sock)
|
313
|
+
await site.start()
|
314
|
+
|
315
|
+
"""
|
316
|
+
#This works but have to wait for the port to be allocated before using
|
317
|
+
# passing socket as above gets port in synchronous code
|
318
|
+
|
319
|
+
# set up aiohttp - like run_app, but non-blocking
|
320
|
+
runner = aiohttp.web.AppRunner(app)
|
321
|
+
await runner.setup()
|
322
|
+
site = aiohttp.web.TCPSite(runner, host=host, port=port, reuse_address=False)
|
323
|
+
await site.start()
|
324
|
+
|
325
|
+
#Get port from first entry in list of active connections
|
326
|
+
for s in runner.sites:
|
327
|
+
#print(s.name, s._port)
|
328
|
+
return s._port #Get actual port allocated
|
329
|
+
return 0
|
330
|
+
"""
|
331
|
+
|
332
|
+
except (Exception) as e:
|
333
|
+
print("Server start failed: ", e)
|
334
|
+
|
335
|
+
#Ignore SIGPIPE altogether (does not apply on windows)
|
336
|
+
#import sys
|
337
|
+
#if sys.platform != 'win32':
|
338
|
+
# from signal import signal, SIGPIPE, SIG_IGN
|
339
|
+
# signal(SIGPIPE, SIG_IGN)
|
340
|
+
|
341
|
+
def serve(viewer, port=None, ipv6=False, retries=100):
|
342
|
+
s = Server(viewer, port, ipv6, retries)
|
343
|
+
#Attach to event loop
|
344
|
+
loop = asyncio.get_event_loop()
|
345
|
+
loop.create_task(s.run())
|
346
|
+
return s
|
347
|
+
|
348
|
+
"""
|
349
|
+
Main entry point - run server and open browser interface
|
350
|
+
"""
|
351
|
+
if __name__ == '__main__':
|
352
|
+
import lavavu
|
353
|
+
import asyncio
|
354
|
+
lv = lavavu.Viewer()
|
355
|
+
|
356
|
+
lv.browser()
|
357
|
+
lv.app.loop()
|
358
|
+
#lv.interactive()
|
359
|
+
|