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/control.py
ADDED
@@ -0,0 +1,1731 @@
|
|
1
|
+
"""
|
2
|
+
LavaVu python interface
|
3
|
+
|
4
|
+
Interactive HTML UI controls library
|
5
|
+
These functions can be called from the ControlFactory provided on
|
6
|
+
|
7
|
+
- lavavu.Viewer()
|
8
|
+
- lavavu.Object()
|
9
|
+
- lavavu.Properties()
|
10
|
+
|
11
|
+
Some work only on the viewer and some only to objects and some to both.
|
12
|
+
Each will provide a control that allows modifying visualisation properties or executing commands interactively
|
13
|
+
in an IPython notebook environment
|
14
|
+
|
15
|
+
Example
|
16
|
+
-------
|
17
|
+
|
18
|
+
Create a Button control to execute a rotation command:
|
19
|
+
|
20
|
+
>>> lv = lavavu.Viewer()
|
21
|
+
>>> lv.control.Button('rotate x 1')
|
22
|
+
|
23
|
+
Create a Range control to adjust the pointsize property:
|
24
|
+
|
25
|
+
>>> lv = lavavu.Viewer()
|
26
|
+
>>> pts = lv.points()
|
27
|
+
>>> pts.control.Range('pointsize', range=[1, 10])
|
28
|
+
|
29
|
+
For convenience, controls to set visualisation properties can often be created by calling control directly with the property name
|
30
|
+
This will create a control of the default type for for that property if one is defined, e.g.
|
31
|
+
|
32
|
+
>>> lv = lavavu.Viewer()
|
33
|
+
>>> pts = lv.points()
|
34
|
+
>>> pts.control('pointsize')
|
35
|
+
"""
|
36
|
+
|
37
|
+
__all__ = ['Button', 'Checkbox', 'Colour', 'ColourMapList', 'ColourMaps', 'Command', 'Divider', 'DualRange', 'Entry', 'File', 'Filter', 'Gradient', 'List', 'Number', 'Number2D', 'Number3D', 'ObjectList', 'ObjectSelect', 'Panel', 'Range', 'Range2D', 'Range3D', 'Rotation', 'Tabs', 'TimeStepper', 'Window']
|
38
|
+
|
39
|
+
import os
|
40
|
+
import sys
|
41
|
+
import time
|
42
|
+
import datetime
|
43
|
+
import json
|
44
|
+
from vutils import is_ipython, is_notebook
|
45
|
+
import weakref
|
46
|
+
import string
|
47
|
+
import re
|
48
|
+
from random import Random
|
49
|
+
|
50
|
+
#Register of windows (viewer instances)
|
51
|
+
windows = []
|
52
|
+
#Window unique ids
|
53
|
+
winids = []
|
54
|
+
#Generate unique strings for controls and windows
|
55
|
+
id_random = Random() #Ensure we use our own default seed in case set in notebook
|
56
|
+
def gen_id(length=10):
|
57
|
+
alphabet = string.ascii_letters + string.digits
|
58
|
+
try:
|
59
|
+
return ''.join(id_random.choices(alphabet, k=length))
|
60
|
+
except:
|
61
|
+
#Python 3.5
|
62
|
+
return ''.join(id_random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(length))
|
63
|
+
|
64
|
+
basehtml = """
|
65
|
+
<html>
|
66
|
+
|
67
|
+
<head>
|
68
|
+
<title>LavaVu Interface</title>
|
69
|
+
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
71
|
+
|
72
|
+
<style>
|
73
|
+
html, body, table, form {
|
74
|
+
padding: 0; margin: 0;
|
75
|
+
border: none;
|
76
|
+
font-size: 9pt;
|
77
|
+
font-family: sans-serif;
|
78
|
+
}
|
79
|
+
p {margin: 0px 2px; }
|
80
|
+
|
81
|
+
.canvas {
|
82
|
+
width: 100%; height: 100%;
|
83
|
+
}
|
84
|
+
|
85
|
+
canvas {
|
86
|
+
z-index: 0; margin: 0px; padding:0px;
|
87
|
+
border: none;
|
88
|
+
}
|
89
|
+
|
90
|
+
</style>
|
91
|
+
|
92
|
+
---SCRIPTS---
|
93
|
+
|
94
|
+
</head>
|
95
|
+
|
96
|
+
<body onload="---INIT---" oncontextmenu="return false;">
|
97
|
+
---CONTENT---
|
98
|
+
</body>
|
99
|
+
|
100
|
+
</html>
|
101
|
+
"""
|
102
|
+
|
103
|
+
inlinehtml = """
|
104
|
+
---SCRIPTS---
|
105
|
+
<div id="---ID---" style="display: block; width: ---WIDTH---px; height: ---HEIGHT---px;"></div>
|
106
|
+
---CONTENT---
|
107
|
+
|
108
|
+
<script>
|
109
|
+
---INIT---
|
110
|
+
</script>
|
111
|
+
"""
|
112
|
+
|
113
|
+
#Some common elements (TODO: dynamically create these when not found)
|
114
|
+
hiddenhtml = """
|
115
|
+
<div id="progress" class="popup" style="display: none; width: 310px; height: 32px;">
|
116
|
+
<span id="progressmessage"></span><span id="progressstatus"></span>
|
117
|
+
<div id="progressbar" style="width: 300px; height: 10px; background: #58f;"></div>
|
118
|
+
</div>
|
119
|
+
|
120
|
+
<div id="hidden" style="display: none">
|
121
|
+
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAPCAYAAAA2yOUNAAAAj0lEQVQokWNIjHT8/+zZs//Pnj37/+TJk/9XLp/+f+bEwf9HDm79v2Prqv9aKrz/GUYVEaeoMDMQryJXayWIoi0bFmFV1NWS+z/E1/Q/AwMDA0NVcez/LRsWoSia2luOUAADVcWx/xfO6/1/5fLp/1N7y//HhlmhKoCBgoyA/w3Vyf8jgyyxK4CBUF8zDAUAAJRXY0G1eRgAAAAASUVORK5CYII=" id="slider">
|
122
|
+
<canvas id="gradient" width="2048" height="1"></canvas>
|
123
|
+
<canvas id="palette" width="512" height="24" class="palette checkerboard"></canvas>
|
124
|
+
</div>
|
125
|
+
"""
|
126
|
+
|
127
|
+
#Global data (script) - set from lavavu
|
128
|
+
jsglobals = ''
|
129
|
+
|
130
|
+
#Static HTML location
|
131
|
+
htmlpath = ""
|
132
|
+
|
133
|
+
def _isviewer(target):
|
134
|
+
"""Return true if target is a viewer"""
|
135
|
+
#Can't use isinstance without class in scope
|
136
|
+
#return not hasattr(target, "parent")
|
137
|
+
return target.__class__.__name__ == 'Viewer' or any(['lavavu.Viewer' in str(b) for b in target.__class__.__bases__])
|
138
|
+
|
139
|
+
def _isobject(target):
|
140
|
+
"""Return true if target is a lavavu object"""
|
141
|
+
return target.__class__.__name__ == 'Object'
|
142
|
+
|
143
|
+
def _getviewer(target):
|
144
|
+
"""Return its viewer if target is vis object
|
145
|
+
otherwise just return target as if is a viewer"""
|
146
|
+
if not _isviewer(target):
|
147
|
+
return target.parent
|
148
|
+
return target
|
149
|
+
|
150
|
+
def _getproperty(target, propname):
|
151
|
+
"""Return value of property from target
|
152
|
+
if not defined, return the default value for this property
|
153
|
+
"""
|
154
|
+
_lv = _getviewer(target)
|
155
|
+
if propname in target:
|
156
|
+
return target[propname]
|
157
|
+
elif propname in _lv.properties:
|
158
|
+
#Get property default
|
159
|
+
prop = _lv.properties[propname]
|
160
|
+
return prop["default"]
|
161
|
+
else:
|
162
|
+
return None
|
163
|
+
|
164
|
+
_file_cache = dict()
|
165
|
+
|
166
|
+
def _webglcode(shaders, css, scripts, menu=True, lighttheme=True, stats=False):
|
167
|
+
"""
|
168
|
+
Returns base WebGL code, by default using full WebGL renderer (draw.js)
|
169
|
+
Pass 'drawbox.js' for box interactor only
|
170
|
+
"""
|
171
|
+
code = shaders
|
172
|
+
if menu:
|
173
|
+
scripts[1] += ['menu.js']
|
174
|
+
#Following scripts need require disabled
|
175
|
+
scripts[0] += ['dat.gui.min.js']
|
176
|
+
#Stats module for WebVR
|
177
|
+
if stats:
|
178
|
+
scripts[0] += ['stats.min.js']
|
179
|
+
if lighttheme:
|
180
|
+
css.append("dat-gui-light-theme.css")
|
181
|
+
#Some css tweaks
|
182
|
+
css.append("gui.css")
|
183
|
+
|
184
|
+
else:
|
185
|
+
#DAT.gui disabled
|
186
|
+
scripts[0] += ['!GUI_OFF']
|
187
|
+
|
188
|
+
#First scripts list needs require.js disabled
|
189
|
+
code += _getjslibs(['!REQUIRE_OFF'] + scripts[0] + ['!REQUIRE_ON'] + scripts[1])
|
190
|
+
|
191
|
+
code += _getcss(css)
|
192
|
+
code += '<script>\n' + jsglobals + '\n</script>\n'
|
193
|
+
return code
|
194
|
+
|
195
|
+
def _webglviewcode(shaderpath, menu=True, lighttheme=True):
|
196
|
+
"""
|
197
|
+
Returns WebGL base code for an interactive visualisation window
|
198
|
+
"""
|
199
|
+
jslibs = [['gl-matrix-min.js'], ['OK-min.js', 'draw.js']]
|
200
|
+
return _webglcode(_getshaders(shaderpath), ['styles.css'], jslibs, menu=menu, lighttheme=lighttheme)
|
201
|
+
|
202
|
+
def _webglboxcode(menu=True, lighttheme=True):
|
203
|
+
"""
|
204
|
+
Returns WebGL base code for the bounding box interactor window
|
205
|
+
"""
|
206
|
+
jslibs = [['gl-matrix-min.js'], ['OK-min.js', 'control.js', 'drawbox.js']]
|
207
|
+
return _webglcode('', ['control.css', 'styles.css'], jslibs, menu=menu, lighttheme=lighttheme)
|
208
|
+
|
209
|
+
def _connectcode(target):
|
210
|
+
"""
|
211
|
+
Returns base code to open a connection only, for obtaining correct connection url
|
212
|
+
(Only required if no interaction windows have been opened yet)
|
213
|
+
"""
|
214
|
+
#If connection via window already opened, we don't need this
|
215
|
+
if len(winids) == 0:
|
216
|
+
initjs = '\n_wi[0] = new WindowInteractor(0, {uid}, {port});\n;'.format(uid=id(target), port=target.port)
|
217
|
+
#return _readfilehtml('control.js') + initjs
|
218
|
+
s = '<script>\n' + _readfilehtml('control.js') + initjs + '</script>\n'
|
219
|
+
return s
|
220
|
+
else:
|
221
|
+
return ""
|
222
|
+
|
223
|
+
def _emscriptencode(menu=False, lighttheme=True):
|
224
|
+
"""
|
225
|
+
Returns WebGL base code for an interactive visualisation window
|
226
|
+
"""
|
227
|
+
jslibs = [[], ['emscripten.js', 'LavaVu.js']]
|
228
|
+
return _webglcode('', ['emscripten.css'], jslibs, menu=menu, lighttheme=lighttheme)
|
229
|
+
|
230
|
+
def _getcss(files=["styles.css"]):
|
231
|
+
#Load stylesheets to inline tag
|
232
|
+
return _filestohtml(files, tag="style")
|
233
|
+
|
234
|
+
def _getjslibs(files):
|
235
|
+
#Load a set of combined javascript libraries in script tags
|
236
|
+
return _filestohtml(files, tag="script")
|
237
|
+
|
238
|
+
def _filestohtml(files, tag="script"):
|
239
|
+
#Load a set of files into a string enclosed in specified html tag
|
240
|
+
code = '<' + tag + '>\n'
|
241
|
+
for f in files:
|
242
|
+
code += _readfilehtml(f)
|
243
|
+
code += '\n</' + tag + '>\n'
|
244
|
+
return code
|
245
|
+
|
246
|
+
def _readfilehtml(filename):
|
247
|
+
#HACK: Need to disable require.js to load libraries from inline script tags
|
248
|
+
if filename == "!REQUIRE_OFF":
|
249
|
+
return '_backup_define = window.define; window.define = undefined;'
|
250
|
+
elif filename == "!REQUIRE_ON":
|
251
|
+
return 'window.define = _backup_define; delete _backup_define;'
|
252
|
+
elif filename == "!GUI_OFF":
|
253
|
+
return 'window.dat = undefined;'
|
254
|
+
|
255
|
+
#Read a file from the htmlpath (or cached copy)
|
256
|
+
global _file_cache
|
257
|
+
if not filename in _file_cache:
|
258
|
+
_file_cache[filename] = _readfile(os.path.join(htmlpath, filename))
|
259
|
+
return _file_cache[filename]
|
260
|
+
|
261
|
+
def _readfile(filename):
|
262
|
+
#Read a text file and return contents
|
263
|
+
data = ""
|
264
|
+
with open(filename, 'r') as f:
|
265
|
+
data = f.read()
|
266
|
+
f.close()
|
267
|
+
return data
|
268
|
+
|
269
|
+
def _getshaders(path, shaders=['points', 'lines', 'triangles', 'volume']):
|
270
|
+
#Load combined shaders
|
271
|
+
src = '<script>\nvar shaders = {};\n'
|
272
|
+
sdict = {'points' : 'point', 'lines' : 'line', 'triangles' : 'tri', 'volume' : 'volume'};
|
273
|
+
for key in shaders:
|
274
|
+
vs = _readfile(os.path.join(path, sdict[key] + 'Shader.vert'))
|
275
|
+
fs = _readfile(os.path.join(path, sdict[key] + 'Shader.frag'))
|
276
|
+
vs = vs.replace('"', '\\"').replace('\n', '\\n')
|
277
|
+
fs = fs.replace('"', '\\"').replace('\n', '\\n')
|
278
|
+
#vs = re.sub("^\s*in ", 'attribute ', vs, flags=re.MULTILINE);
|
279
|
+
#vs = re.sub("^\s*out ", 'varying ', vs, flags=re.MULTILINE)
|
280
|
+
#fs = re.sub("^\s*in ", 'varying ', fs, flags=re.MULTILINE);
|
281
|
+
src += 'shaders["' + key + '-vs"] = "' + vs + '";'
|
282
|
+
src += 'shaders["' + key + '-fs"] = "' + fs + '";'
|
283
|
+
src += '\n</script>\n\n'
|
284
|
+
return src
|
285
|
+
|
286
|
+
def _getshaders_as_scripts(path, shaders=['points', 'lines', 'triangles', 'volume']):
|
287
|
+
#Load shaders into script tags, not compatible with colab for some reason
|
288
|
+
src = ''
|
289
|
+
sdict = {'points' : 'point', 'lines' : 'line', 'triangles' : 'tri', 'volume' : 'volume'};
|
290
|
+
for key in shaders:
|
291
|
+
src += '<script id="' + key + '-vs" type="x-shader/x-vertex">\n'
|
292
|
+
src += _readfile(os.path.join(path, sdict[key] + 'Shader.vert'))
|
293
|
+
src += '</script>\n\n'
|
294
|
+
src += '<script id="' + key + '-fs" type="x-shader/x-fragment">\n'
|
295
|
+
src += _readfile(os.path.join(path, sdict[key] + 'Shader.frag'))
|
296
|
+
src += '</script>\n\n'
|
297
|
+
return src
|
298
|
+
|
299
|
+
class _Action(object):
|
300
|
+
"""Base class for an action triggered by a control
|
301
|
+
|
302
|
+
Default action is to run a command
|
303
|
+
"""
|
304
|
+
|
305
|
+
def __init__(self, target, command=None, readproperty=None):
|
306
|
+
self.target = target
|
307
|
+
self.command = command
|
308
|
+
if not hasattr(self, "property"):
|
309
|
+
self.property = readproperty
|
310
|
+
self.lastvalue = 0
|
311
|
+
self.index = None
|
312
|
+
|
313
|
+
def script(self):
|
314
|
+
#Return script action for HTML export
|
315
|
+
if self.command is None or not len(self.command): return ""
|
316
|
+
#Run a command with a value argument
|
317
|
+
return self.command + ' ---VAL---'
|
318
|
+
|
319
|
+
class _PropertyAction(_Action):
|
320
|
+
"""Property change action triggered by a control
|
321
|
+
"""
|
322
|
+
|
323
|
+
def __init__(self, target, prop, command=None, index=None):
|
324
|
+
self.property = prop
|
325
|
+
#Default action after property set is redraw, can be set to provided
|
326
|
+
#if command is None:
|
327
|
+
# command = "redraw"
|
328
|
+
self.command = command
|
329
|
+
super(_PropertyAction, self).__init__(target, command)
|
330
|
+
self.index = index
|
331
|
+
|
332
|
+
def script(self):
|
333
|
+
#Return script action for HTML export
|
334
|
+
#Set a property
|
335
|
+
#Check for index (3D prop)
|
336
|
+
propset = self.property + '=---VAL---'
|
337
|
+
if self.index is not None:
|
338
|
+
propset = self.property + '[' + str(self.index) + ']=---VAL---'
|
339
|
+
# - Globally
|
340
|
+
script = ''
|
341
|
+
if _isviewer(self.target):
|
342
|
+
script = 'select; ' + propset
|
343
|
+
# - on an object selector, select the object
|
344
|
+
elif type(self.target).__name__ == 'ObjectSelect':
|
345
|
+
script = propset
|
346
|
+
# - on a properties collection
|
347
|
+
elif type(self.target).__name__ == 'Properties':
|
348
|
+
script = '$' + str(id(self.target)) + ' ' + self.property + ' ---VAL--- '
|
349
|
+
# - On an object
|
350
|
+
else:
|
351
|
+
script = '<' + self.target["name"] + '>' + propset
|
352
|
+
#Add any additional commands
|
353
|
+
return script + '; ' + super(_PropertyAction, self).script()
|
354
|
+
|
355
|
+
|
356
|
+
class _CommandAction(_Action):
|
357
|
+
"""Command action triggered by a control, with object select before command
|
358
|
+
"""
|
359
|
+
|
360
|
+
def __init__(self, target, command, readproperty):
|
361
|
+
self.command = command
|
362
|
+
super(_CommandAction, self).__init__(target, command, readproperty)
|
363
|
+
|
364
|
+
def script(self):
|
365
|
+
#Return script action for HTML export
|
366
|
+
#Set a property
|
367
|
+
# - Globally
|
368
|
+
script = ''
|
369
|
+
if _isviewer(self.target):
|
370
|
+
script = 'select; '
|
371
|
+
# - on an object selector, select the object
|
372
|
+
elif type(self.target).__name__ == 'ObjectSelect':
|
373
|
+
script = ''
|
374
|
+
# - On an object
|
375
|
+
else:
|
376
|
+
script = '<' + self.target["name"] + '>'
|
377
|
+
#Add the commands
|
378
|
+
return script + super(_CommandAction, self).script()
|
379
|
+
|
380
|
+
class _FilterAction(_PropertyAction):
|
381
|
+
"""Filter property change action triggered by a control
|
382
|
+
"""
|
383
|
+
def __init__(self, target, findex, prop, command=None):
|
384
|
+
self.findex = findex
|
385
|
+
if command is None: command = "redraw"
|
386
|
+
self.command = command
|
387
|
+
super(_FilterAction, self).__init__(target, prop)
|
388
|
+
|
389
|
+
def script(self):
|
390
|
+
#Return script action for HTML export
|
391
|
+
#Set a filter range
|
392
|
+
cmd = "filtermin" if self.property == "minimum" else "filtermax"
|
393
|
+
return 'select ' + self.target["name"] + '; ' + cmd + ' ' + str(self.findex) + ' ---VAL---'
|
394
|
+
#propset = "filters=" + json.dumps()
|
395
|
+
#script = 'select ' + self.target["name"] + '; ' + propset
|
396
|
+
|
397
|
+
class _HTML(object):
|
398
|
+
"""A class to output HTML controls
|
399
|
+
"""
|
400
|
+
|
401
|
+
#Parent class for container types
|
402
|
+
def __init__(self, label):
|
403
|
+
self.label = label
|
404
|
+
self.uniqueid()
|
405
|
+
|
406
|
+
def html(self):
|
407
|
+
"""Return the HTML code"""
|
408
|
+
return ''
|
409
|
+
|
410
|
+
def uniqueid(self):
|
411
|
+
#Get a unique control identifier
|
412
|
+
self.id = gen_id()
|
413
|
+
self.elid = "lvctrl_" + self.id
|
414
|
+
|
415
|
+
def labelhtml(self):
|
416
|
+
#Default label
|
417
|
+
html = ""
|
418
|
+
if self.label:
|
419
|
+
html += '<p>' + self.label + ':</p>\n'
|
420
|
+
return html
|
421
|
+
|
422
|
+
|
423
|
+
class _Container(_HTML):
|
424
|
+
"""A container for a set of controls
|
425
|
+
"""
|
426
|
+
#Parent class for container types
|
427
|
+
def __init__(self, viewer, label=""):
|
428
|
+
self.viewer = viewer
|
429
|
+
self._content = []
|
430
|
+
self._current = 0
|
431
|
+
super(_Container, self).__init__(label)
|
432
|
+
|
433
|
+
def add(self, ctrl):
|
434
|
+
self._content.append(ctrl)
|
435
|
+
return ctrl
|
436
|
+
|
437
|
+
def controls(self):
|
438
|
+
return self.html()
|
439
|
+
|
440
|
+
def __iter__(self):
|
441
|
+
return self
|
442
|
+
|
443
|
+
def __next__(self):
|
444
|
+
if self._current > len(self._content)-1:
|
445
|
+
self._current = 0
|
446
|
+
raise StopIteration
|
447
|
+
else:
|
448
|
+
self._current += 1
|
449
|
+
return self._content[self._current-1]
|
450
|
+
|
451
|
+
def html(self):
|
452
|
+
html = self.labelhtml()
|
453
|
+
html += '<div style="padding:0px; margin: 0px; position: relative;" class="lvctrl">\n'
|
454
|
+
for i in range(len(self._content)):
|
455
|
+
html += self._content[i].controls()
|
456
|
+
html += '</div>\n'
|
457
|
+
return html
|
458
|
+
|
459
|
+
def scripts(self):
|
460
|
+
#Returns script dictionary
|
461
|
+
d = {}
|
462
|
+
for i in range(len(self._content)):
|
463
|
+
d.update(self._content[i].scripts())
|
464
|
+
return d
|
465
|
+
|
466
|
+
class Window(_Container):
|
467
|
+
"""
|
468
|
+
Creates an interaction window with an image of the viewer frame
|
469
|
+
and webgl controller for rotation/translation
|
470
|
+
|
471
|
+
Parameters
|
472
|
+
----------
|
473
|
+
align : str
|
474
|
+
Set to "left/right" to align viewer window, default is left. Set to None to skip.
|
475
|
+
wrapper : str
|
476
|
+
Set the style of the wrapper div, default is empty string so wrapper is enabled with no custom style
|
477
|
+
Set to None to disable wrapper
|
478
|
+
"""
|
479
|
+
def __init__(self, viewer, resolution=None, align="left", wrapper="", fullscreen=False):
|
480
|
+
super(Window, self).__init__(viewer)
|
481
|
+
self.align = align
|
482
|
+
self.wrapper = wrapper
|
483
|
+
self.fullscreen = fullscreen
|
484
|
+
if resolution is None:
|
485
|
+
#Default to largest of 640x480 or output res / 2
|
486
|
+
resolution = [0,0]
|
487
|
+
resolution[0] = max(viewer.output_resolution[0]//2, 640)
|
488
|
+
resolution[1] = int(resolution[0] * viewer.output_resolution[1]/viewer.output_resolution[0])
|
489
|
+
elif isinstance(resolution, int):
|
490
|
+
#Passed interger - interpert as width
|
491
|
+
resolution = [resolution,0]
|
492
|
+
|
493
|
+
if resolution[1] == 0:
|
494
|
+
#Width only, set height based on output aspect ratio
|
495
|
+
resolution[1] = int(resolution[0] * viewer.output_resolution[1]/viewer.output_resolution[0])
|
496
|
+
|
497
|
+
viewer.output_resolution = resolution
|
498
|
+
|
499
|
+
def html(self):
|
500
|
+
#print(self.viewer["resolution"], self.viewer.output_resolution)
|
501
|
+
#w = self.viewer["resolution"][0]
|
502
|
+
#h = self.viewer["resolution"][1]
|
503
|
+
w = self.viewer.output_resolution[0]
|
504
|
+
h = self.viewer.output_resolution[1]
|
505
|
+
style = f'height: {h}px; width: {w}px; min-height: 50px; min-width: 50px; position: relative; display: inline-block;'
|
506
|
+
style_img = 'height: 100%; width: 100%; position: relative; margin: 0px; display: inline-block;'
|
507
|
+
if self.align is not None:
|
508
|
+
style += ' float: ' + self.align + ';'
|
509
|
+
if self.wrapper is not None:
|
510
|
+
style += ' margin-right: 10px;'
|
511
|
+
classes = ""
|
512
|
+
if self.fullscreen:
|
513
|
+
style += ' height: 100%; width: 100%;'
|
514
|
+
else:
|
515
|
+
classes = "resizer"
|
516
|
+
style +=" border: 1px solid #aaa;"
|
517
|
+
|
518
|
+
html = ""
|
519
|
+
html += '<div class="' + classes + '" style="' + style + '">\n'
|
520
|
+
#html += '<img id="imgtarget_---VIEWERID---" draggable=false style="margin: 0px; border: 1px solid #aaa; display: inline-block;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAAPUlEQVR42u3OMQEAAAgDINe/iSU1xh5IQPamKgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLtwAMBsGqBDct9xQAAAABJRU5ErkJggg==">\n'
|
521
|
+
html += '<img id="imgtarget_---VIEWERID---" draggable=false style="' + style_img + '" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAAPUlEQVR42u3OMQEAAAgDINe/iSU1xh5IQPamKgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLtwAMBsGqBDct9xQAAAABJRU5ErkJggg==">\n'
|
522
|
+
html += """
|
523
|
+
<div style="display: none; z-index: 200; position: absolute; top: 5px; right: 5px;">
|
524
|
+
<select onchange="_wi['---VIEWERID---'].box.mode = this.value;">
|
525
|
+
<option>Rotate</option>
|
526
|
+
<option>Translate</option>
|
527
|
+
<option>Zoom</option>
|
528
|
+
</select>
|
529
|
+
<input type="button" value="Reset" onclick="_wi['---VIEWERID---'].execute('reset');">
|
530
|
+
</div>"""
|
531
|
+
html += '</div>\n'
|
532
|
+
|
533
|
+
#Display any contained controls
|
534
|
+
if self.wrapper is not None:
|
535
|
+
html += '<div style="' + self.wrapper + '" class="lvctrl">\n'
|
536
|
+
html += super(Window, self).html()
|
537
|
+
if self.wrapper is not None:
|
538
|
+
html += '</div>\n'
|
539
|
+
#html += '<div style="clear: both;">\n'
|
540
|
+
return html
|
541
|
+
|
542
|
+
class Panel(Window):
|
543
|
+
"""Creates a control panel with an interactive viewer window and a set of controls
|
544
|
+
placed to the left with the viewer aligned to the right
|
545
|
+
"""
|
546
|
+
def __init__(self, *args, **kwargs):
|
547
|
+
super(Panel, self).__init__(*args, align="right", wrapper=None, **kwargs)
|
548
|
+
|
549
|
+
class Tabs(_Container):
|
550
|
+
"""Creates a group of controls with tabs that can be shown or hidden
|
551
|
+
|
552
|
+
Parameters
|
553
|
+
----------
|
554
|
+
buttons : boolean
|
555
|
+
Display the tab buttons for switching tabs
|
556
|
+
"""
|
557
|
+
def __init__(self, target, buttons=True):
|
558
|
+
self.tabs = []
|
559
|
+
self.buttons = buttons
|
560
|
+
super(Tabs, self).__init__(target)
|
561
|
+
|
562
|
+
def tab(self, label=""):
|
563
|
+
"""Add a new tab, any controls appending will appear in the new tab
|
564
|
+
Parameters
|
565
|
+
----------
|
566
|
+
label : str
|
567
|
+
Label for the tab, if omitted will be blank
|
568
|
+
"""
|
569
|
+
self.tabs.append(label)
|
570
|
+
|
571
|
+
def add(self, ctrl):
|
572
|
+
if not len(self.tabs): self.tab()
|
573
|
+
self._content.append(ctrl)
|
574
|
+
ctrl.tab = len(self.tabs)-1
|
575
|
+
|
576
|
+
def html(self):
|
577
|
+
html = """
|
578
|
+
<script>
|
579
|
+
function openTab_---ELID---(el, tabName) {
|
580
|
+
var i;
|
581
|
+
var x = document.getElementsByClassName("---ELID---");
|
582
|
+
for (i = 0; i < x.length; i++)
|
583
|
+
x[i].style.display = "none";
|
584
|
+
document.getElementById("---ELID---_" + tabName).style.display = "block";
|
585
|
+
|
586
|
+
tabs = document.getElementsByClassName("tab_---ELID---");
|
587
|
+
for (i = 0; i < x.length; i++)
|
588
|
+
tabs[i].className = tabs[i].className.replace(" lvseltab", "");
|
589
|
+
el.className += " lvseltab";
|
590
|
+
}
|
591
|
+
</script>
|
592
|
+
"""
|
593
|
+
if self.buttons:
|
594
|
+
html += "<div class='lvtabbar'>\n"
|
595
|
+
for t in range(len(self.tabs)):
|
596
|
+
#Add header items
|
597
|
+
classes = 'lvbutton lvctrl tab_---ELID---'
|
598
|
+
if t == 0: classes += ' lvseltab'
|
599
|
+
html += '<a class="' + classes + '" onclick="openTab_---ELID---(this, this.innerHTML)">---LABEL---</a>'
|
600
|
+
html = html.replace('---LABEL---', self.tabs[t])
|
601
|
+
html += "</div>\n"
|
602
|
+
for t in range(len(self.tabs)):
|
603
|
+
#Add control wrappers
|
604
|
+
style = ''
|
605
|
+
if t != 0: style = 'display: none;'
|
606
|
+
html += '<div id="---ELID---_---LABEL---" style="' + style + '" class="lvtab lvctrl ---ELID---">\n'
|
607
|
+
for ctrl in self._content:
|
608
|
+
if ctrl.tab == t:
|
609
|
+
html += ctrl.controls()
|
610
|
+
html += '</div>\n'
|
611
|
+
html = html.replace('---LABEL---', self.tabs[t])
|
612
|
+
html = html.replace('---ELID---', self.elid)
|
613
|
+
return html
|
614
|
+
|
615
|
+
class _Control(_HTML):
|
616
|
+
"""
|
617
|
+
_Control object
|
618
|
+
|
619
|
+
Parameters
|
620
|
+
----------
|
621
|
+
target : Obj or Viewer
|
622
|
+
Add a control to an object to control object properties
|
623
|
+
Add a control to a viewer to control global proerties and run commands
|
624
|
+
property : str
|
625
|
+
Property to modify
|
626
|
+
command : str
|
627
|
+
Command to run
|
628
|
+
value : any type
|
629
|
+
Initial value of the controls
|
630
|
+
label : str
|
631
|
+
Descriptive label for the control
|
632
|
+
readproperty : str
|
633
|
+
Property to read control value from on update (but not modified)
|
634
|
+
"""
|
635
|
+
|
636
|
+
def __init__(self, target, property=None, command=None, value=None, label=None, index=None, readproperty=None):
|
637
|
+
super(_Control, self).__init__(label)
|
638
|
+
self.label = label
|
639
|
+
|
640
|
+
#Can either set a property directly or run a command
|
641
|
+
self.property = readproperty
|
642
|
+
self.target = target
|
643
|
+
self.action = None
|
644
|
+
if property:
|
645
|
+
#Property set
|
646
|
+
self.action = _PropertyAction(target, property, command, index)
|
647
|
+
if label is None:
|
648
|
+
self.label = property.capitalize()
|
649
|
+
self.property = property
|
650
|
+
elif command:
|
651
|
+
#Command only
|
652
|
+
self.action = _CommandAction(target, command, readproperty)
|
653
|
+
if label is None:
|
654
|
+
self.label = command.capitalize()
|
655
|
+
else:
|
656
|
+
#Assume derived class will fill out the action, this is just a placeholder
|
657
|
+
self.action = _Action(target)
|
658
|
+
|
659
|
+
if not self.label:
|
660
|
+
self.label = ""
|
661
|
+
|
662
|
+
#Get value from target or default if not provided
|
663
|
+
if property is not None:
|
664
|
+
if value is None:
|
665
|
+
value = _getproperty(target, property)
|
666
|
+
else:
|
667
|
+
#2d/3D value?
|
668
|
+
if index is not None:
|
669
|
+
V = target[property]
|
670
|
+
V[index] = value
|
671
|
+
target[property][index] = V #Set the provided value
|
672
|
+
else:
|
673
|
+
target[property] = value #Set the provided value
|
674
|
+
|
675
|
+
#Append reload command from prop dict if no command provided
|
676
|
+
_lv = _getviewer(target)
|
677
|
+
if property in _lv.properties:
|
678
|
+
prop = _lv.properties[property]
|
679
|
+
#TODO: support higher reload values
|
680
|
+
cmd = ""
|
681
|
+
if prop["redraw"] > 1 and not "reload" in str(command):
|
682
|
+
cmd = "reload"
|
683
|
+
elif prop["redraw"] > 0 and not "redraw" in str(command):
|
684
|
+
cmd = "reload"
|
685
|
+
#Append if command exists
|
686
|
+
if command is None:
|
687
|
+
command = cmd
|
688
|
+
else:
|
689
|
+
#Pass the value to command if provided
|
690
|
+
command += " ---VAL--- ; " + cmd
|
691
|
+
|
692
|
+
self.value = value
|
693
|
+
self._value = value #Store passed initial value
|
694
|
+
|
695
|
+
def onchange(self):
|
696
|
+
return "_wi['---VIEWERID---'].do_action('" + str(self.id) + "', this.value, this);"
|
697
|
+
|
698
|
+
def show(self):
|
699
|
+
#Show only this control
|
700
|
+
html = '<div style="" class="lvctrl">\n'
|
701
|
+
html += self.html()
|
702
|
+
html += '</div>\n'
|
703
|
+
|
704
|
+
def html(self):
|
705
|
+
return self.controls()
|
706
|
+
|
707
|
+
def controls(self, type='number', attribs={}, onchange=""):
|
708
|
+
#Input control
|
709
|
+
html = '<input id="---ELID---_{0}" class="---ELID---" type="{0}"'.format(type)
|
710
|
+
#Set custom attribute for property controls
|
711
|
+
if not "step" in attribs or attribs["step"] is None:
|
712
|
+
attribs["step"] = "any"
|
713
|
+
#if self.value:
|
714
|
+
# attribs["step"] = abs(self.value) / 100.
|
715
|
+
#else:
|
716
|
+
# attribs["step"] = "any"
|
717
|
+
for key in attribs:
|
718
|
+
html += key + '="' + str(attribs[key]) + '" '
|
719
|
+
html += self.attribs()
|
720
|
+
html += 'value="' + str(self.value) + '" '
|
721
|
+
#Onchange event
|
722
|
+
onchange += self.onchange()
|
723
|
+
html += 'onchange="' + onchange + '" '
|
724
|
+
html += '>\n'
|
725
|
+
html = html.replace('---ELID---', self.elid)
|
726
|
+
return html
|
727
|
+
|
728
|
+
def attribs(self):
|
729
|
+
html = ""
|
730
|
+
if self.property:
|
731
|
+
#Set custom attribute for property controls
|
732
|
+
if _isobject(self.target):
|
733
|
+
html += 'data-target="' + str(self.target["name"]) + '" '
|
734
|
+
html += 'data-property="' + self.property + '" '
|
735
|
+
if self.action.index is not None:
|
736
|
+
html += 'data-index="' + str(self.action.index) + '" '
|
737
|
+
return html
|
738
|
+
|
739
|
+
def scripts(self):
|
740
|
+
#Returns script dictionary
|
741
|
+
return {self.id : self.action.script()}
|
742
|
+
|
743
|
+
class _MultiControl(_Control):
|
744
|
+
"""
|
745
|
+
_Control object that holds multiple integrated Controls
|
746
|
+
"""
|
747
|
+
|
748
|
+
def __init__(self, *args, **kwargs):
|
749
|
+
super(_MultiControl, self).__init__(*args, **kwargs)
|
750
|
+
self._content = []
|
751
|
+
self._current = 0
|
752
|
+
|
753
|
+
def add(self, ctrl):
|
754
|
+
self._content.append(ctrl)
|
755
|
+
return ctrl
|
756
|
+
|
757
|
+
def controls(self):
|
758
|
+
return self.html()
|
759
|
+
|
760
|
+
def __iter__(self):
|
761
|
+
return self
|
762
|
+
|
763
|
+
def __next__(self):
|
764
|
+
if self._current > len(self._content)-1:
|
765
|
+
self._current = 0
|
766
|
+
raise StopIteration
|
767
|
+
else:
|
768
|
+
self._current += 1
|
769
|
+
return self._content[self._current-1]
|
770
|
+
|
771
|
+
def html(self):
|
772
|
+
html = self.labelhtml()
|
773
|
+
for i in range(len(self._content)):
|
774
|
+
html += self._content[i].controls()
|
775
|
+
return html
|
776
|
+
|
777
|
+
def scripts(self):
|
778
|
+
#Returns script dictionary
|
779
|
+
d = {}
|
780
|
+
for i in range(len(self._content)):
|
781
|
+
d[self._content[i].id] = self._content[i].action.script()
|
782
|
+
return d
|
783
|
+
|
784
|
+
class Divider(_Control):
|
785
|
+
"""A divider element
|
786
|
+
"""
|
787
|
+
|
788
|
+
def controls(self):
|
789
|
+
return '<hr style="clear: both;">\n'
|
790
|
+
|
791
|
+
class Number(_Control):
|
792
|
+
"""A basic numerical input control
|
793
|
+
"""
|
794
|
+
def __init__(self, target=None, property=None, command=None, value=None, label=None, index=None, step=None, readproperty=None):
|
795
|
+
super(Number, self).__init__(target, property, command, value, label, index, readproperty)
|
796
|
+
|
797
|
+
#Get step defaults from prop dict
|
798
|
+
_lv = _getviewer(target)
|
799
|
+
if step is None and property is not None and property in _lv.properties:
|
800
|
+
prop = _lv.properties[property]
|
801
|
+
#Check for integer type, set default step to 1
|
802
|
+
T = prop["type"]
|
803
|
+
ctrl = prop["control"]
|
804
|
+
#Whole number step size?
|
805
|
+
if "integer" in T:
|
806
|
+
step = 1
|
807
|
+
elif len(ctrl) > 1 and len(ctrl[1]) == 3:
|
808
|
+
step = ctrl[1][2]
|
809
|
+
elif "real" in T:
|
810
|
+
r = 1.0
|
811
|
+
#Use given range if any provided
|
812
|
+
if len(ctrl) > 1 and len(ctrl[1]) > 1:
|
813
|
+
r = ctrl[1][1] - ctrl[1][0]
|
814
|
+
step = r / 100.0
|
815
|
+
|
816
|
+
self.step = step
|
817
|
+
|
818
|
+
def controls(self):
|
819
|
+
html = self.labelhtml()
|
820
|
+
attribs = {"step" : self.step}
|
821
|
+
html += super(Number, self).controls('number', attribs)
|
822
|
+
return html + '<br>\n'
|
823
|
+
|
824
|
+
class Number2D(_MultiControl):
|
825
|
+
"""A set of two numeric controls for adjusting a 2D value
|
826
|
+
"""
|
827
|
+
def __init__(self, target, property, label=None, step=None, *args, **kwargs):
|
828
|
+
super(Number2D, self).__init__(target, property, label=label, *args, **kwargs)
|
829
|
+
if self._value is None:
|
830
|
+
self._value = _getproperty(target, property)
|
831
|
+
self.ctrlX = self.add(Number(target=target, property=property, value=self._value[0], step=step, label="", index=0))
|
832
|
+
self.ctrlY = self.add(Number(target=target, property=property, value=self._value[1], step=step, label="", index=1))
|
833
|
+
|
834
|
+
def controls(self):
|
835
|
+
html = self.labelhtml() + self.ctrlX.controls() + self.ctrlY.controls()
|
836
|
+
return html.replace('<br>', '') + '<br>'
|
837
|
+
|
838
|
+
class Number3D(_MultiControl):
|
839
|
+
"""A set of three numeric controls for adjusting a 3D value
|
840
|
+
"""
|
841
|
+
def __init__(self, target, property, label=None, step=None, *args, **kwargs):
|
842
|
+
super(Number3D, self).__init__(target, property, label=label, *args, **kwargs)
|
843
|
+
if self._value is None:
|
844
|
+
self._value = _getproperty(target, property)
|
845
|
+
self.ctrlX = self.add(Number(target=target, property=property, value=self._value[0], step=step, label="", index=0))
|
846
|
+
self.ctrlY = self.add(Number(target=target, property=property, value=self._value[1], step=step, label="", index=1))
|
847
|
+
self.ctrlZ = self.add(Number(target=target, property=property, value=self._value[2], step=step, label="", index=2))
|
848
|
+
|
849
|
+
def controls(self):
|
850
|
+
html = self.labelhtml() + self.ctrlX.controls() + self.ctrlY.controls() + self.ctrlZ.controls()
|
851
|
+
return html.replace('<br>', '') + '<br>'
|
852
|
+
|
853
|
+
class Rotation(_MultiControl):
|
854
|
+
"""A set of six buttons for adjusting a 3D rotation
|
855
|
+
"""
|
856
|
+
def __init__(self, target, *args, **kwargs):
|
857
|
+
self.label="Rotation"
|
858
|
+
super(Rotation, self).__init__(target, *args, **kwargs)
|
859
|
+
|
860
|
+
self.ctrlX0 = self.add(Button(target=target, command="rotate x -1", label="-X"))
|
861
|
+
self.ctrlX1 = self.add(Button(target=target, command="rotate x 1", label="+X"))
|
862
|
+
self.ctrlY0 = self.add(Button(target=target, command="rotate y -1", label="-Y"))
|
863
|
+
self.ctrlY1 = self.add(Button(target=target, command="rotate y 1", label="+Y"))
|
864
|
+
self.ctrlZ0 = self.add(Button(target=target, command="rotate z -1", label="-Z"))
|
865
|
+
self.ctrlZ1 = self.add(Button(target=target, command="rotate z 1", label="+Z"))
|
866
|
+
|
867
|
+
def controls(self):
|
868
|
+
html = self.labelhtml() + self.ctrlX0.controls() + self.ctrlX1.controls() + self.ctrlY0.controls() + self.ctrlY1.controls() + self.ctrlZ0.controls() + self.ctrlZ1.controls()
|
869
|
+
return html.replace('<br>', '') + '<br>'
|
870
|
+
|
871
|
+
|
872
|
+
class Checkbox(_Control):
|
873
|
+
"""A checkbox control for a boolean value
|
874
|
+
"""
|
875
|
+
def labelhtml(self):
|
876
|
+
return '' #'<br>\n'
|
877
|
+
|
878
|
+
def controls(self):
|
879
|
+
attribs = {}
|
880
|
+
if self.value: attribs = {"checked" : "checked"}
|
881
|
+
html = "<label>\n"
|
882
|
+
html += super(Checkbox, self).controls('checkbox', attribs)
|
883
|
+
html += " " + self.label + "</label><br>\n"
|
884
|
+
return html
|
885
|
+
|
886
|
+
def onchange(self):
|
887
|
+
return "; _wi['---VIEWERID---'].do_action('" + str(self.id) + "', this.checked ? 1 : 0, this);"
|
888
|
+
|
889
|
+
class Range(_Control):
|
890
|
+
"""A slider control for a range of values
|
891
|
+
|
892
|
+
Parameters
|
893
|
+
----------
|
894
|
+
range : list or tuple
|
895
|
+
Min/max values for the range
|
896
|
+
"""
|
897
|
+
def __init__(self, target=None, property=None, command=None, value=None, label=None, index=None, range=None, step=None, readproperty=None):
|
898
|
+
super(Range, self).__init__(target, property, command, value, label, index, readproperty)
|
899
|
+
|
900
|
+
#Get range & step defaults from prop dict
|
901
|
+
_lv = _getviewer(target)
|
902
|
+
defrange = [0., 1., 0.]
|
903
|
+
T = None
|
904
|
+
if property is not None and property in _lv.properties:
|
905
|
+
prop = _lv.properties[property]
|
906
|
+
#Check for integer type, set default step to 1
|
907
|
+
T = prop["type"]
|
908
|
+
if "integer" in T:
|
909
|
+
defrange[2] = 1
|
910
|
+
ctrl = prop["control"]
|
911
|
+
if len(ctrl) > 1 and len(ctrl[1]) == 3:
|
912
|
+
defrange = ctrl[1]
|
913
|
+
|
914
|
+
if range is None:
|
915
|
+
range = defrange[0:2]
|
916
|
+
if step is None:
|
917
|
+
step = defrange[2]
|
918
|
+
|
919
|
+
self.range = range
|
920
|
+
self.step = step
|
921
|
+
if not step:
|
922
|
+
#Whole number step size?
|
923
|
+
r = range[1] - range[0]
|
924
|
+
#Has no type from property, or type is not real: Assume a step size of 1 if range max-min > 5 and both are integers
|
925
|
+
if (T is None or not "real" in T) and (r >= 5 and range[0] - int(range[0]) == 0 and range[1] - int(range[1]) == 0):
|
926
|
+
self.step = 1
|
927
|
+
#Range is less than 5 or range values indicate real numbers
|
928
|
+
else:
|
929
|
+
self.step = r / 100.0
|
930
|
+
|
931
|
+
def controls(self):
|
932
|
+
attribs = {"min" : self.range[0], "max" : self.range[1], "step" : self.step}
|
933
|
+
html = self.labelhtml()
|
934
|
+
html += super(Range, self).controls('number', attribs, onchange='this.nextElementSibling.value=this.value; ')
|
935
|
+
html += super(Range, self).controls('range', attribs, onchange='this.previousElementSibling.value=this.value; ')
|
936
|
+
return html + '<br>\n'
|
937
|
+
|
938
|
+
class Button(_Control):
|
939
|
+
"""A push button control to execute a defined command
|
940
|
+
"""
|
941
|
+
def __init__(self, target, command, label=None):
|
942
|
+
super(Button, self).__init__(target, None, command, None, label)
|
943
|
+
|
944
|
+
def onchange(self):
|
945
|
+
return "_wi['---VIEWERID---'].do_action('" + str(self.id) + "', '', this);"
|
946
|
+
|
947
|
+
def labelhtml(self):
|
948
|
+
return ''
|
949
|
+
|
950
|
+
def controls(self):
|
951
|
+
html = self.labelhtml()
|
952
|
+
html = '<input class="---ELID---" type="button" value="' + str(self.label) + '" '
|
953
|
+
#Onclick event
|
954
|
+
html += 'onclick="' + self.onchange() + '" '
|
955
|
+
html += '><br>\n'
|
956
|
+
html = html.replace('---ELID---', self.elid)
|
957
|
+
return html
|
958
|
+
|
959
|
+
class Entry(_Control):
|
960
|
+
"""A generic input control for string values
|
961
|
+
"""
|
962
|
+
def controls(self):
|
963
|
+
html = self.labelhtml()
|
964
|
+
html += '<input class="---ELID---" type="text" value="" '
|
965
|
+
html += self.attribs()
|
966
|
+
html += ' onkeypress="if (event.keyCode == 13) { _wi[\'---VIEWERID---\'].do_action(\'---ID---\', this.value.trim(), this); };"><br>\n'
|
967
|
+
html = html.replace('---ELID---', self.elid)
|
968
|
+
return html.replace('---ID---', self.id)
|
969
|
+
|
970
|
+
class Command(_Control):
|
971
|
+
"""A generic input control for executing command strings
|
972
|
+
"""
|
973
|
+
def __init__(self, *args, **kwargs):
|
974
|
+
super(Command, self).__init__(command=" ", label="Command", *args, **kwargs)
|
975
|
+
|
976
|
+
def controls(self):
|
977
|
+
html = self.labelhtml()
|
978
|
+
html += """
|
979
|
+
<input class="---ELID---" type="text" value=""
|
980
|
+
onkeypress="if (event.keyCode == 13) { var cmd=this.value.trim();
|
981
|
+
_wi['---VIEWERID---'].do_action('---ID---', cmd ? cmd : 'repeat', this); this.value=''; };"><br>\n
|
982
|
+
"""
|
983
|
+
html = html.replace('---ELID---', self.elid)
|
984
|
+
return html.replace('---ID---', self.id)
|
985
|
+
|
986
|
+
class File(_Control):
|
987
|
+
"""A file picker control
|
988
|
+
|
989
|
+
Unfortunately there is no way to get the file path
|
990
|
+
"""
|
991
|
+
def __init__(self, command="file", directory=False, multiple=False, accept="", *args, **kwargs):
|
992
|
+
self.options = ""
|
993
|
+
if directory:
|
994
|
+
self.options += 'webkitdirectory directory '
|
995
|
+
elif multiple:
|
996
|
+
self.options += 'multiple '
|
997
|
+
if accept:
|
998
|
+
#Comma separated file types list, e.g. image/*,audio/*,video/*,.pdf
|
999
|
+
self.options += 'accept="' + accept + '"'
|
1000
|
+
|
1001
|
+
super(File, self).__init__(command=command, label="Load File", *args, **kwargs)
|
1002
|
+
|
1003
|
+
def controls(self):
|
1004
|
+
html = self.labelhtml()
|
1005
|
+
html += """
|
1006
|
+
<div style="border:solid #aaa 1px; padding:10px; margin-bottom: 5px;">
|
1007
|
+
<input class="---ELID---" type="file" id="file_selector_---ELID---" ---OPTIONS--- name="files[]"/>
|
1008
|
+
<output id="list"></output>
|
1009
|
+
</div>
|
1010
|
+
<script>
|
1011
|
+
function fileSelected_---ELID---(event) {
|
1012
|
+
var output = [];
|
1013
|
+
for (var i = 0; i < event.target.files.length; i++) {
|
1014
|
+
var f = event.target.files.item(i);
|
1015
|
+
output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
|
1016
|
+
f.size, ' bytes, last modified: ',
|
1017
|
+
'</li>');
|
1018
|
+
_wi['---VIEWERID---'].do_action("---ID---", f.name);
|
1019
|
+
}
|
1020
|
+
document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
|
1021
|
+
}
|
1022
|
+
document.getElementById('file_selector_---ELID---').addEventListener('change', fileSelected_---ELID---, false);
|
1023
|
+
</script>
|
1024
|
+
"""
|
1025
|
+
html = html.replace('---ELID---', self.elid)
|
1026
|
+
html = html.replace('---OPTIONS---', self.options)
|
1027
|
+
return html.replace('---ID---', self.id)
|
1028
|
+
|
1029
|
+
class List(_Control):
|
1030
|
+
"""A list of predefined input values to set properties or run commands
|
1031
|
+
|
1032
|
+
Parameters
|
1033
|
+
----------
|
1034
|
+
options: list
|
1035
|
+
List of the available value strings
|
1036
|
+
"""
|
1037
|
+
def __init__(self, target, property=None, options=None, *args, **kwargs):
|
1038
|
+
#Get default options from prop dict
|
1039
|
+
if options is None:
|
1040
|
+
defoptions = []
|
1041
|
+
_lv = _getviewer(target)
|
1042
|
+
if property is not None and property in _lv.properties:
|
1043
|
+
prop = _lv.properties[property]
|
1044
|
+
ctrl = prop["control"]
|
1045
|
+
if len(ctrl) > 2 and len(ctrl[2]):
|
1046
|
+
defoptions = ctrl[2]
|
1047
|
+
options = defoptions
|
1048
|
+
self.options = options
|
1049
|
+
super(List, self).__init__(target, property, *args, **kwargs)
|
1050
|
+
|
1051
|
+
def controls(self):
|
1052
|
+
html = self.labelhtml()
|
1053
|
+
html += '<select class="---ELID---" id="---ELID---" value="" '
|
1054
|
+
html += self.attribs()
|
1055
|
+
html += 'onchange="' + self.onchange() + '">\n'
|
1056
|
+
for opt in self.options:
|
1057
|
+
#Each element of options list can be:
|
1058
|
+
# - dict {"label" : label, "value" : value, "selected" : True/False}
|
1059
|
+
# - list [value, label, selected]
|
1060
|
+
# - value only
|
1061
|
+
if isinstance(opt, dict):
|
1062
|
+
selected = "selected" if opt.selected else ""
|
1063
|
+
html += '<option value="' + str(opt["value"]) + '" ' + selected + '>' + opt["label"] + '</option>\n'
|
1064
|
+
elif isinstance(opt, list) or isinstance(opt, tuple):
|
1065
|
+
selected = "selected" if len(opt) > 2 and opt[2] else ""
|
1066
|
+
html += '<option value="' + str(opt[0]) + '" ' + selected + '>' + str(opt[1]) + '</option>\n'
|
1067
|
+
else:
|
1068
|
+
html += '<option>' + str(opt) + '</option>\n'
|
1069
|
+
html += '</select><br>\n'
|
1070
|
+
html = html.replace('---ELID---', self.elid)
|
1071
|
+
return html
|
1072
|
+
|
1073
|
+
class Colour(_Control):
|
1074
|
+
"""A colour picker for setting colour properties
|
1075
|
+
"""
|
1076
|
+
def __init__(self, *args, **kwargs):
|
1077
|
+
self.style = ""
|
1078
|
+
super(Colour, self).__init__(command="", *args, **kwargs)
|
1079
|
+
|
1080
|
+
def controls(self):
|
1081
|
+
html = self.labelhtml()
|
1082
|
+
html += """
|
1083
|
+
<div class="colourbg checkerboard" style="---STYLE---">
|
1084
|
+
<div id="---ELID---" ---ATTRIBS--- class="colour ---ELID---" onclick="
|
1085
|
+
var col = new Colour(this.style.backgroundColor);
|
1086
|
+
var offset = [this.getBoundingClientRect().left, this.getBoundingClientRect().top];
|
1087
|
+
var el = this;
|
1088
|
+
var savefn = function(val) {
|
1089
|
+
var c = new Colour(0);
|
1090
|
+
c.setHSV(val);
|
1091
|
+
el.style.backgroundColor = c.html();
|
1092
|
+
_wi['---VIEWERID---'].do_action('---ID---', c.html(), el);
|
1093
|
+
}
|
1094
|
+
el.picker = new ColourPicker(savefn);
|
1095
|
+
el.picker.pick(col, offset[0], offset[1]);">
|
1096
|
+
</div>
|
1097
|
+
</div>
|
1098
|
+
<script>
|
1099
|
+
var el = document.getElementById("---ELID---");
|
1100
|
+
//Set the initial colour
|
1101
|
+
var col = new Colour('---VALUE---');
|
1102
|
+
el.style.backgroundColor = col.html();
|
1103
|
+
</script>
|
1104
|
+
"""
|
1105
|
+
html = html.replace('---VALUE---', str(self.value))
|
1106
|
+
html = html.replace('---ELID---', self.elid)
|
1107
|
+
html = html.replace('---ATTRIBS---', self.attribs())
|
1108
|
+
html = html.replace('---STYLE---', self.style)
|
1109
|
+
return html.replace('---ID---', self.id)
|
1110
|
+
|
1111
|
+
class ColourIndicator(Colour):
|
1112
|
+
"""A small indicator for showing/setting the colour of the object
|
1113
|
+
"""
|
1114
|
+
def __init__(self, *args, **kwargs):
|
1115
|
+
super(ColourIndicator, self).__init__(*args, **kwargs)
|
1116
|
+
#Custom div style:
|
1117
|
+
self.style="border: #888 1px solid; display: inline-block; width: 20px; height: 20px;"
|
1118
|
+
self.label = ""
|
1119
|
+
#Set the colour from cached data so at least the indicated colour is correct
|
1120
|
+
if not self.target["colour"]:
|
1121
|
+
self.target["colour"] = self.target.colour.toString()
|
1122
|
+
|
1123
|
+
class Gradient(_Control):
|
1124
|
+
"""A colourmap editor
|
1125
|
+
"""
|
1126
|
+
def __init__(self, target, *args, **kwargs):
|
1127
|
+
super(Gradient, self).__init__(target, property="colourmap", command="", *args, **kwargs)
|
1128
|
+
#Get and save the map id of target object
|
1129
|
+
if _isviewer(target):
|
1130
|
+
raise(Exception("Gradient control requires an Object target, not Viewer"))
|
1131
|
+
self.maps = target.parent.state["colourmaps"]
|
1132
|
+
self.map = None
|
1133
|
+
for m in self.maps:
|
1134
|
+
if m["name"] == self.value:
|
1135
|
+
self.map = m
|
1136
|
+
self.selected = -1;
|
1137
|
+
|
1138
|
+
def controls(self):
|
1139
|
+
html = self.labelhtml()
|
1140
|
+
html += """
|
1141
|
+
<canvas id="---ELID---_canvas" ---ATTRIBS--- width="512" height="24" class="palette checkerboard">
|
1142
|
+
</canvas>
|
1143
|
+
<script>
|
1144
|
+
var el = document.getElementById("---ELID---_canvas"); //Get the canvas
|
1145
|
+
//Store the maps
|
1146
|
+
el.colourmaps = ---COLOURMAPS---;
|
1147
|
+
el.currentmap = ---COLOURMAP---;
|
1148
|
+
el.selectedIndex = ---SELID---;
|
1149
|
+
if (!el.gradient) {
|
1150
|
+
//Create the gradient editor
|
1151
|
+
el.gradient = new GradientEditor(el, function(obj, id) {
|
1152
|
+
//Gradient updated
|
1153
|
+
//var colours = obj.palette.toJSON()
|
1154
|
+
el.currentmap = obj.palette.get(el.currentmap);
|
1155
|
+
_wi['---VIEWERID---'].do_action('---ID---', JSON.stringify(el.currentmap.colours));
|
1156
|
+
//_wi['---VIEWERID---'].do_action('---ID---', obj.palette.toJSON(), el);
|
1157
|
+
|
1158
|
+
//Update stored maps list by name
|
1159
|
+
if (el.selectedIndex >= 0)
|
1160
|
+
el.colourmaps[el.selectedIndex].colours = el.currentmap.colours; //el.gradient.palette.get();
|
1161
|
+
}
|
1162
|
+
, true); //Enable premultiply
|
1163
|
+
}
|
1164
|
+
//Load the initial colourmap
|
1165
|
+
el.gradient.read(el.currentmap.colours);
|
1166
|
+
</script>
|
1167
|
+
"""
|
1168
|
+
mapstr = json.dumps(self.maps)
|
1169
|
+
html = html.replace('---COLOURMAPS---', mapstr)
|
1170
|
+
if self.map:
|
1171
|
+
html = html.replace('---COLOURMAP---', json.dumps(self.map))
|
1172
|
+
else:
|
1173
|
+
html = html.replace('---COLOURMAP---', '"black white"')
|
1174
|
+
html = html.replace('---SELID---', str(self.selected))
|
1175
|
+
html = html.replace('---ELID---', self.elid)
|
1176
|
+
html = html.replace('---ATTRIBS---', self.attribs())
|
1177
|
+
return html.replace('---ID---', self.id)
|
1178
|
+
|
1179
|
+
class ColourMapList(List):
|
1180
|
+
"""A colourmap list selector, populated by the default colour maps
|
1181
|
+
"""
|
1182
|
+
def __init__(self, target, selection=None, *args, **kwargs):
|
1183
|
+
if _isviewer(target):
|
1184
|
+
raise(Exception("ColourMapList control requires an Object target, not Viewer"))
|
1185
|
+
#Load maps list
|
1186
|
+
if selection is None:
|
1187
|
+
selection = target.parent.defaultcolourmaps()
|
1188
|
+
options = [''] + selection
|
1189
|
+
#Also add the matplotlib colourmaps if available
|
1190
|
+
try:
|
1191
|
+
#Load maps list
|
1192
|
+
import matplotlib
|
1193
|
+
import matplotlib.pyplot as plt
|
1194
|
+
sel = matplotlib.pyplot.colormaps()
|
1195
|
+
options += matplotlib.pyplot.colormaps()
|
1196
|
+
except:
|
1197
|
+
pass
|
1198
|
+
|
1199
|
+
#Preceding command with '.' calls via python API, allowing use of matplotlib maps
|
1200
|
+
super(ColourMapList, self).__init__(target, options=options, command=".colourmap", label="Load Colourmap", *args, **kwargs)
|
1201
|
+
|
1202
|
+
class ColourMaps(_MultiControl):
|
1203
|
+
"""A colourmap list selector, populated by the available colour maps,
|
1204
|
+
combined with a colourmap editor for the selected colour map
|
1205
|
+
"""
|
1206
|
+
def __init__(self, target, *args, **kwargs):
|
1207
|
+
#Load maps list
|
1208
|
+
if _isviewer(target):
|
1209
|
+
raise(Exception("ColourMaps control requires an Object target, not Viewer"))
|
1210
|
+
self.maps = target.parent.state["colourmaps"]
|
1211
|
+
options = [["", "None"]]
|
1212
|
+
sel = target["colourmap"]
|
1213
|
+
if sel is None: sel = ""
|
1214
|
+
for m in range(len(self.maps)):
|
1215
|
+
options.append([self.maps[m]["name"], self.maps[m]["name"]])
|
1216
|
+
#Mark selected
|
1217
|
+
if sel == self.maps[m]["name"]:
|
1218
|
+
options[-1].append(True)
|
1219
|
+
sel = m
|
1220
|
+
|
1221
|
+
super(ColourMaps, self).__init__(target, *args, **kwargs)
|
1222
|
+
self.list = self.add(List(target, options=options, command="", property="colourmap", label="", *args, **kwargs))
|
1223
|
+
|
1224
|
+
self.gradient = Gradient(target)
|
1225
|
+
self.gradient.selected = sel #gradient editor needs to know selection index
|
1226
|
+
self.gradient.label = self.label
|
1227
|
+
self.add(self.gradient) #Add control list
|
1228
|
+
|
1229
|
+
def controls(self):
|
1230
|
+
html = self.list.controls() + self.gradient.controls()
|
1231
|
+
html = html.replace('---PALLID---', str(self.gradient.elid))
|
1232
|
+
return html + '<br>'
|
1233
|
+
|
1234
|
+
class TimeStepper(Range):
|
1235
|
+
"""A time step selection range control with up/down buttons
|
1236
|
+
"""
|
1237
|
+
def __init__(self, viewer, *args, **kwargs):
|
1238
|
+
#Acts as a command setter with some additional controls
|
1239
|
+
super(TimeStepper, self).__init__(target=viewer, label="Timestep", command="timestep", readproperty="timestep", *args, **kwargs)
|
1240
|
+
|
1241
|
+
self.timesteps = viewer.timesteps()
|
1242
|
+
self.step = 1
|
1243
|
+
if len(self.timesteps):
|
1244
|
+
self.range = (self.timesteps[0], self.timesteps[-1])
|
1245
|
+
#Calculate step gap
|
1246
|
+
self.step = self.timesteps[1] - self.timesteps[0]
|
1247
|
+
else:
|
1248
|
+
self.range = (0,0)
|
1249
|
+
self.value = 0
|
1250
|
+
|
1251
|
+
def controls(self):
|
1252
|
+
html = Range.controls(self)
|
1253
|
+
#Note: unicode symbol escape must use double slash to be
|
1254
|
+
# passed through to javascript or python will process them
|
1255
|
+
html += """
|
1256
|
+
<script>
|
1257
|
+
var timer_---ELID--- = -1;
|
1258
|
+
function startTimer_---ELID---() {
|
1259
|
+
if (timer_---ELID--- >= 0) {
|
1260
|
+
if (timer_---ELID--- > 0)
|
1261
|
+
window.cancelAnimationFrame(timer_---ELID---);
|
1262
|
+
timer_---ELID--- = window.requestAnimationFrame(nextStep_---ELID---);
|
1263
|
+
}
|
1264
|
+
}
|
1265
|
+
function nextStep_---ELID---() {
|
1266
|
+
el = document.getElementById('---ELID---_number');
|
1267
|
+
if (el) {
|
1268
|
+
//Call again on image load - pass callback
|
1269
|
+
var V = _wi['---VIEWERID---'];
|
1270
|
+
if (!V.box.canvas.mouse.isdown && !V.box.zoomTimer && (!V.box.gui || V.box.gui.closed))
|
1271
|
+
V.execute("next", startTimer_---ELID---);
|
1272
|
+
else
|
1273
|
+
setTimeout(nextStep_---ELID---, 100);
|
1274
|
+
}
|
1275
|
+
}
|
1276
|
+
function playPause_---ELID---(btn) {
|
1277
|
+
if (timer_---ELID--- >= 0) {
|
1278
|
+
btn.value="\\u25BA";
|
1279
|
+
window.cancelAnimationFrame(timer_---ELID---);
|
1280
|
+
timer_---ELID--- = -1;
|
1281
|
+
} else {
|
1282
|
+
timer_---ELID--- = 0;
|
1283
|
+
startTimer_---ELID---();
|
1284
|
+
btn.value="\\u25FE";
|
1285
|
+
}
|
1286
|
+
}
|
1287
|
+
</script>
|
1288
|
+
"""
|
1289
|
+
html += '<input type="button" style="width: 50px;" onclick="var el = document.getElementById(\'---ELID---_range\'); el.stepDown(); el.onchange()" value="←" />'
|
1290
|
+
html += '<input type="button" style="width: 50px;" onclick="var el = document.getElementById(\'---ELID---_range\'); el.stepUp(); el.onchange()" value="→" />'
|
1291
|
+
html += '<input type="button" style="width: 60px;" onclick="playPause_---ELID---(this);" value="►" />'
|
1292
|
+
html = html.replace('---ELID---', self.elid)
|
1293
|
+
return html
|
1294
|
+
|
1295
|
+
class DualRange(_MultiControl):
|
1296
|
+
"""A set of two range slider controls for adjusting a minimum and maximum range
|
1297
|
+
|
1298
|
+
Parameters
|
1299
|
+
----------
|
1300
|
+
range : list or tuple
|
1301
|
+
Min/max values for the range
|
1302
|
+
"""
|
1303
|
+
def __init__(self, target, properties, values=[None,None], label=None, range=(0.,1.), step=None):
|
1304
|
+
super(DualRange, self).__init__(target)
|
1305
|
+
self.label = label
|
1306
|
+
|
1307
|
+
self.ctrlmin = self.add(Range(target=target, property=properties[0], step=step, value=values[0], range=range, label=""))
|
1308
|
+
self.ctrlmax = self.add(Range(target=target, property=properties[1], step=step, value=values[1], range=range, label=""))
|
1309
|
+
|
1310
|
+
class Range2D(_MultiControl):
|
1311
|
+
"""A set of two range slider controls for adjusting a 2D value
|
1312
|
+
|
1313
|
+
Parameters
|
1314
|
+
----------
|
1315
|
+
range : list or tuple
|
1316
|
+
Min/max values for the ranges
|
1317
|
+
"""
|
1318
|
+
def __init__(self, target, property, label=None, value=None, range=(0.,1.), step=None, *args, **kwargs):
|
1319
|
+
super(Range2D, self).__init__(target, property=property, command=None, value=value, label=label, *args, **kwargs)
|
1320
|
+
|
1321
|
+
if self._value is None:
|
1322
|
+
self._value = _getproperty(target, property)
|
1323
|
+
|
1324
|
+
self.ctrlX = self.add(Range(target=target, property=property, step=step, value=self._value[0], range=range, label="", index=0))
|
1325
|
+
self.ctrlY = self.add(Range(target=target, property=property, step=step, value=self._value[1], range=range, label="", index=1))
|
1326
|
+
|
1327
|
+
class Range3D(_MultiControl):
|
1328
|
+
"""A set of three range slider controls for adjusting a 3D value
|
1329
|
+
|
1330
|
+
Parameters
|
1331
|
+
----------
|
1332
|
+
range : list or tuple
|
1333
|
+
Min/max values for the ranges
|
1334
|
+
"""
|
1335
|
+
def __init__(self, target, property, label=None, value=None, range=(0.,1.), step=None, *args, **kwargs):
|
1336
|
+
super(Range3D, self).__init__(target, property=property, command=None, value=value, label=label, *args, **kwargs)
|
1337
|
+
|
1338
|
+
if self._value is None:
|
1339
|
+
self._value = _getproperty(target, property)
|
1340
|
+
|
1341
|
+
self.ctrlX = self.add(Range(target=target, property=property, step=step, value=self._value[0], range=range, label="", index=0))
|
1342
|
+
self.ctrlY = self.add(Range(target=target, property=property, step=step, value=self._value[1], range=range, label="", index=1))
|
1343
|
+
self.ctrlZ = self.add(Range(target=target, property=property, step=step, value=self._value[2], range=range, label="", index=2))
|
1344
|
+
|
1345
|
+
class Filter(DualRange):
|
1346
|
+
"""A set of two range slider controls for adjusting a minimum and maximum filter range
|
1347
|
+
|
1348
|
+
Parameters
|
1349
|
+
----------
|
1350
|
+
range : list or tuple
|
1351
|
+
Min/max values for the filter range
|
1352
|
+
"""
|
1353
|
+
def __init__(self, target, filteridx, label=None, range=None, step=None):
|
1354
|
+
self.label = label
|
1355
|
+
if len(target["filters"]) <= filteridx:
|
1356
|
+
print("Filter index out of range: ", filteridx, len(target["filters"]))
|
1357
|
+
return None
|
1358
|
+
self.filter = target["filters"][filteridx]
|
1359
|
+
|
1360
|
+
#Default label - data set name
|
1361
|
+
if label is None:
|
1362
|
+
label = self.filter['by'].capitalize()
|
1363
|
+
|
1364
|
+
#Get the default range limits from the matching data source
|
1365
|
+
fname = self.filter['by']
|
1366
|
+
if not target["data"] or not "minimum" in target["data"][fname]:
|
1367
|
+
#Attempt to update
|
1368
|
+
target.reload()
|
1369
|
+
target.parent.render()
|
1370
|
+
|
1371
|
+
if not target["data"]:
|
1372
|
+
raise(ValueError("target data list empty, can't use filter"))
|
1373
|
+
if not fname in target['data']:
|
1374
|
+
raise(ValueError(fname + " not found in target data list, can't use filter"))
|
1375
|
+
self.data = target["data"][fname]
|
1376
|
+
if not range:
|
1377
|
+
#self.range = (self.filter["minimum"], self.filter["maximum"])
|
1378
|
+
if self.filter["map"]:
|
1379
|
+
range = (0.,1.)
|
1380
|
+
else:
|
1381
|
+
range = (self.data["minimum"]*0.99, self.data["maximum"]*1.01)
|
1382
|
+
|
1383
|
+
#Setup DualRange using filter min/max
|
1384
|
+
super(Filter, self).__init__(_getviewer(target), properties=[None, None], values=[self.filter["minimum"], self.filter["maximum"]], label=label, range=range)
|
1385
|
+
|
1386
|
+
#Replace actions on the controls
|
1387
|
+
self.ctrlmin.action = _FilterAction(target, filteridx, "minimum")
|
1388
|
+
self.ctrlmax.action = _FilterAction(target, filteridx, "maximum")
|
1389
|
+
|
1390
|
+
class ObjectList(_MultiControl):
|
1391
|
+
"""A set of checkbox controls for controlling visibility of all visualisation objects
|
1392
|
+
"""
|
1393
|
+
def __init__(self, viewer, *args, **kwargs):
|
1394
|
+
super(ObjectList, self).__init__(viewer, label="Objects", *args, **kwargs)
|
1395
|
+
for obj in viewer.objects.list:
|
1396
|
+
self.add(ColourIndicator(obj, "colour"))
|
1397
|
+
self.add(Checkbox(obj, "visible", label=obj["name"]))
|
1398
|
+
|
1399
|
+
#TODO: broken
|
1400
|
+
class ObjectSelect(_Container):
|
1401
|
+
"""A list selector of all visualisation objects that can be used to
|
1402
|
+
choose the target of a set of controls
|
1403
|
+
|
1404
|
+
Parameters
|
1405
|
+
----------
|
1406
|
+
objects : list
|
1407
|
+
A list of objects to display, by default all available objects are added
|
1408
|
+
"""
|
1409
|
+
def __init__(self, viewer, objects=None, *args, **kwargs):
|
1410
|
+
if not _isviewer(viewer):
|
1411
|
+
print("Can't add ObjectSelect control to an Object, must add to Viewer")
|
1412
|
+
return
|
1413
|
+
self.parent = viewer
|
1414
|
+
if objects is None:
|
1415
|
+
objects = viewer.objects.list
|
1416
|
+
|
1417
|
+
#Load maps list
|
1418
|
+
options = [(0, "None")]
|
1419
|
+
for o in range(len(objects)):
|
1420
|
+
obj = objects[o]
|
1421
|
+
options += [(o+1, obj["name"])]
|
1422
|
+
|
1423
|
+
#The list control
|
1424
|
+
self._list = List(target=viewer, label="", options=options, command="select", *args, **kwargs)
|
1425
|
+
|
1426
|
+
#Init container
|
1427
|
+
super(ObjectSelect, self).__init__(viewer) #, label="Objects", options=options, command="select", *args, **kwargs)
|
1428
|
+
|
1429
|
+
#Holds a control factory so controls can be added with this as a target
|
1430
|
+
self.control = _ControlFactory(self)
|
1431
|
+
|
1432
|
+
#def onchange(self):
|
1433
|
+
# #Update the control values on change
|
1434
|
+
# #return super(ObjectSelect, self).onchange()
|
1435
|
+
# return self._list.onchange()
|
1436
|
+
|
1437
|
+
def __contains__(self, key):
|
1438
|
+
#print "CONTAINS",key
|
1439
|
+
obj = self._list.action.lastvalue
|
1440
|
+
#print "OBJECT == ",obj,(key in self.parent.objects.list[obj-1])
|
1441
|
+
return obj > 0 and key in self.parent.objects.list[obj-1]
|
1442
|
+
|
1443
|
+
def __getitem__(self, key):
|
1444
|
+
#print "GETITEM",key
|
1445
|
+
obj = self._list.action.lastvalue
|
1446
|
+
if obj > 0:
|
1447
|
+
#Passthrough: Get from selected object
|
1448
|
+
return self.parent.objects.list[obj-1][key]
|
1449
|
+
return None
|
1450
|
+
|
1451
|
+
def __setitem__(self, key, value):
|
1452
|
+
obj = self._list.action.lastvalue
|
1453
|
+
#print "SETITEM",key,value
|
1454
|
+
if obj > 0:
|
1455
|
+
#Passtrough: Set on selected object
|
1456
|
+
self.parent.objects.list[obj-1][key] = value
|
1457
|
+
|
1458
|
+
#Undefined method call - pass call to target
|
1459
|
+
def __getattr__(self, key):
|
1460
|
+
#__getattr__ called if no attrib/method found
|
1461
|
+
def any_method(*args, **kwargs):
|
1462
|
+
#If member function exists on target, call it
|
1463
|
+
obj = self._list.action.lastvalue
|
1464
|
+
if obj > 0:
|
1465
|
+
method = getattr(self.parent.objects.list[obj-1], key, None)
|
1466
|
+
if method and callable(method):
|
1467
|
+
return method(*args, **kwargs)
|
1468
|
+
return any_method
|
1469
|
+
|
1470
|
+
def html(self):
|
1471
|
+
html = '<div style="border: #888 1px solid; display: inline-block; padding: 6px;" class="lvctrl">\n'
|
1472
|
+
html += self._list.controls()
|
1473
|
+
html += '<hr>\n'
|
1474
|
+
html += super(ObjectSelect, self).html()
|
1475
|
+
html += '</div>\n'
|
1476
|
+
return html
|
1477
|
+
|
1478
|
+
def controls(self):
|
1479
|
+
return self.html()
|
1480
|
+
|
1481
|
+
class _ControlFactory(object):
|
1482
|
+
"""
|
1483
|
+
Create and manage sets of controls for interaction with a Viewer or Object
|
1484
|
+
Controls can run commands or change properties
|
1485
|
+
"""
|
1486
|
+
#Creates a control factory used to generate controls for a specified target
|
1487
|
+
def __init__(self, target):
|
1488
|
+
self._target = weakref.ref(target)
|
1489
|
+
self.clear()
|
1490
|
+
self.interactor = False
|
1491
|
+
#self.output = ""
|
1492
|
+
|
1493
|
+
#Save types of all control/containers
|
1494
|
+
def all_subclasses(cls):
|
1495
|
+
return cls.__subclasses__() + [g for s in cls.__subclasses__() for g in all_subclasses(s)]
|
1496
|
+
|
1497
|
+
#_Control contructor shortcut methods
|
1498
|
+
#(allows constructing controls directly from the factory object)
|
1499
|
+
#Use a closure to define a new method to call constructor and add to controls
|
1500
|
+
def addmethod(constr):
|
1501
|
+
def method(*args, **kwargs):
|
1502
|
+
#Return the new control and add it to the list
|
1503
|
+
newctrl = constr(self._target(), *args, **kwargs)
|
1504
|
+
self.add(newctrl)
|
1505
|
+
return newctrl
|
1506
|
+
return method
|
1507
|
+
|
1508
|
+
self._control_types = all_subclasses(_Control)
|
1509
|
+
self._container_types = all_subclasses(_Container)
|
1510
|
+
for constr in self._control_types + self._container_types:
|
1511
|
+
key = constr.__name__
|
1512
|
+
method = addmethod(constr)
|
1513
|
+
#Set docstring (+ _Control docs)
|
1514
|
+
if constr in self._control_types:
|
1515
|
+
method.__doc__ = constr.__doc__ + _Control.__doc__
|
1516
|
+
else:
|
1517
|
+
method.__doc__ = constr.__doc__
|
1518
|
+
self.__setattr__(key, method)
|
1519
|
+
|
1520
|
+
def __call__(self, property, *args, **kwargs):
|
1521
|
+
"""
|
1522
|
+
Calling with a property name creates the default control for that property
|
1523
|
+
"""
|
1524
|
+
_lv = _getviewer(self._target())
|
1525
|
+
if property is not None and property in _lv.properties:
|
1526
|
+
#Get control info from prop dict
|
1527
|
+
prop = _lv.properties[property]
|
1528
|
+
T = prop["type"]
|
1529
|
+
ctrl = prop["control"]
|
1530
|
+
if len(ctrl) > 2 and len(ctrl[2]) > 1:
|
1531
|
+
#Has selections
|
1532
|
+
return self.List(property, *args, **kwargs)
|
1533
|
+
if "integer" in T or "real" in T:
|
1534
|
+
hasrange = len(ctrl) > 1 and len(ctrl[1]) == 3
|
1535
|
+
#Multi-dimensional?
|
1536
|
+
if "[2]" in T:
|
1537
|
+
return self.Range2D(property, *args, **kwargs) if hasrange else self.Number2D(property, *args, **kwargs)
|
1538
|
+
elif "[3]" in T:
|
1539
|
+
return self.Range3D(property, *args, **kwargs) if hasrange else self.Number3D(property, *args, **kwargs)
|
1540
|
+
else:
|
1541
|
+
return self.Range(property, *args, **kwargs) if hasrange else self.Number(property, *args, **kwargs)
|
1542
|
+
elif T == "string":
|
1543
|
+
return self.Entry(property, *args, **kwargs)
|
1544
|
+
elif T == "boolean":
|
1545
|
+
return self.Checkbox(property, *args, **kwargs)
|
1546
|
+
elif T == "colour":
|
1547
|
+
return self.Colour(property, *args, **kwargs)
|
1548
|
+
else:
|
1549
|
+
print("Unable to determine control type for property: " + property)
|
1550
|
+
print(prop)
|
1551
|
+
|
1552
|
+
def add(self, ctrl):
|
1553
|
+
"""
|
1554
|
+
Add a control
|
1555
|
+
"""
|
1556
|
+
#Just add to the parent lists if not a viewer
|
1557
|
+
if not _isviewer(self._target()):
|
1558
|
+
self._target().parent.control.add(ctrl)
|
1559
|
+
else:
|
1560
|
+
if type(ctrl) in self._container_types:
|
1561
|
+
#New container, further controls will be added to it
|
1562
|
+
self._containers.append(ctrl)
|
1563
|
+
else:
|
1564
|
+
#Normal control: add to active container
|
1565
|
+
self._containers[-1].add(ctrl)
|
1566
|
+
|
1567
|
+
return ctrl
|
1568
|
+
|
1569
|
+
def show(self, menu=True, filename=None, fallback=None):
|
1570
|
+
"""
|
1571
|
+
Displays all added controls including viewer if any
|
1572
|
+
|
1573
|
+
Parameters
|
1574
|
+
----------
|
1575
|
+
filename : str
|
1576
|
+
Filename to write output HTML
|
1577
|
+
If in a notebook this is not necessary as content is displayed inline
|
1578
|
+
If not provided and run outside IPython, defaults to "control.html"
|
1579
|
+
fallback : function
|
1580
|
+
A function which is called in place of the viewer display method when run outside IPython
|
1581
|
+
"""
|
1582
|
+
#Show all controls in container
|
1583
|
+
target = self._target()
|
1584
|
+
|
1585
|
+
#Creates an interactor to connect javascript/html controls to IPython and viewer
|
1586
|
+
#if no viewer Window() created, it will be a windowless interactor
|
1587
|
+
if _isviewer(target):
|
1588
|
+
#Append the current viewer ref
|
1589
|
+
windows.append(target)
|
1590
|
+
#Get unique ID
|
1591
|
+
winids.append(gen_id())
|
1592
|
+
else:
|
1593
|
+
target.parent.control.show()
|
1594
|
+
return
|
1595
|
+
|
1596
|
+
viewerid = winids[-1]
|
1597
|
+
|
1598
|
+
#Generate the HTML and associated action JS
|
1599
|
+
html = "<form novalidate>"
|
1600
|
+
chtml = ""
|
1601
|
+
actions = {}
|
1602
|
+
for con in self._containers:
|
1603
|
+
html += con.html()
|
1604
|
+
actions.update(con.scripts())
|
1605
|
+
html += "</form>"
|
1606
|
+
#print('\n'.join([str(a)+":"+actions[a] for a in actions]))
|
1607
|
+
|
1608
|
+
#Auto-clear now content generated
|
1609
|
+
#Prevents doubling up if cell executed again
|
1610
|
+
self.clear()
|
1611
|
+
|
1612
|
+
#Set viewer id
|
1613
|
+
html = html.replace('---VIEWERID---', str(viewerid))
|
1614
|
+
#self.output += html
|
1615
|
+
|
1616
|
+
code = _webglboxcode(menu)
|
1617
|
+
|
1618
|
+
#Display HTML inline or export
|
1619
|
+
if is_notebook() and filename is None:
|
1620
|
+
if not target.server:
|
1621
|
+
raise(Exception("LavaVu HTTP Server must be active for interactive controls, set port= parameter to > 0"))
|
1622
|
+
"""
|
1623
|
+
HTTP server mode interaction, rendering in separate render thread:
|
1624
|
+
- This should work in all notebook contexts, colab, jupyterlab etc
|
1625
|
+
- Only problem remaining is port access, from docker or cloud instances etc, need to forward port
|
1626
|
+
- jupyter-server-proxy packaged (available on pip) supports forwarding port via the jupyter server
|
1627
|
+
"""
|
1628
|
+
from IPython.display import display,HTML
|
1629
|
+
#Interaction requires some additional js/css/webgl
|
1630
|
+
#Insert stylesheet, shaders and combined javascript libraries
|
1631
|
+
display(HTML(code))
|
1632
|
+
|
1633
|
+
#Try and prevent this getting cached
|
1634
|
+
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
1635
|
+
html += "<!-- CREATION TIMESTAMP {0} -->".format(timestamp)
|
1636
|
+
|
1637
|
+
#Pass port and object id from server
|
1638
|
+
html += "<script>init('{0}');</script>".format(viewerid)
|
1639
|
+
actionjs = self.export_actions(actions, id(target), target.port)
|
1640
|
+
#Output the controls and start interactor
|
1641
|
+
display(HTML(actionjs + html))
|
1642
|
+
|
1643
|
+
else:
|
1644
|
+
#Export standalone html
|
1645
|
+
#return as string or write output to .html file in current directory
|
1646
|
+
template = basehtml
|
1647
|
+
template = template.replace('---SCRIPTS---', code + self.export_actions(actions)) #Process actions
|
1648
|
+
template = template.replace('---INIT---', 'init(\'{0}\');'.format(viewerid))
|
1649
|
+
template = template.replace('---CONTENT---', html)
|
1650
|
+
full_html = template
|
1651
|
+
|
1652
|
+
if filename == '':
|
1653
|
+
#Just return code
|
1654
|
+
return full_html
|
1655
|
+
else:
|
1656
|
+
#Write the file
|
1657
|
+
if filename is None:
|
1658
|
+
filename = "control.html"
|
1659
|
+
hfile = open(filename, "w")
|
1660
|
+
hfile.write(full_html)
|
1661
|
+
hfile.close()
|
1662
|
+
|
1663
|
+
if callable(fallback):
|
1664
|
+
fallback(target)
|
1665
|
+
|
1666
|
+
def export_actions(self, actions, uid=0, port=0, proxy=False):
|
1667
|
+
#Process actions
|
1668
|
+
topjs = '<script>\n'
|
1669
|
+
if port > 0:
|
1670
|
+
topjs += 'function init(viewerid) {{_wi[viewerid] = new WindowInteractor(viewerid, {uid}, {port});\n'.format(uid=uid, port=port)
|
1671
|
+
else:
|
1672
|
+
topjs += 'function init(viewerid) {{_wi[viewerid] = new WindowInteractor(viewerid, {uid});\n'.format(uid=uid)
|
1673
|
+
|
1674
|
+
topjs += '_wi[viewerid].actions = {\n'
|
1675
|
+
|
1676
|
+
actionjs = ''
|
1677
|
+
for act in actions:
|
1678
|
+
#Add to action functions to list, each takes the value of the control when called
|
1679
|
+
if len(actionjs):
|
1680
|
+
actionjs += ',\n'
|
1681
|
+
actionjs += ' "' + str(act) + '" : "' + actions[act] + '"';
|
1682
|
+
|
1683
|
+
#Add init and finish
|
1684
|
+
return topjs + actionjs + '};\n}\n</script>\n'
|
1685
|
+
|
1686
|
+
def redisplay(self):
|
1687
|
+
"""Update the active viewer image if any
|
1688
|
+
Applies changes made in python to the viewer and forces a redisplay
|
1689
|
+
"""
|
1690
|
+
if not is_notebook():
|
1691
|
+
return
|
1692
|
+
from IPython.display import display,HTML,Javascript
|
1693
|
+
display(Javascript(self.redisplay_call()))
|
1694
|
+
|
1695
|
+
def redisplay_call(self):
|
1696
|
+
"""Update the active viewer image if any
|
1697
|
+
Applies changes made in python to the viewer and forces a redisplay
|
1698
|
+
"""
|
1699
|
+
#Find matching viewer id, redisplay first match
|
1700
|
+
ids = self.active_viewers()
|
1701
|
+
return ';'.join('_wi["{0}"].redisplay()'.format(i) for i in ids)
|
1702
|
+
|
1703
|
+
def active_viewers(self):
|
1704
|
+
"""Return matching active viewer IDs
|
1705
|
+
"""
|
1706
|
+
#Find matching viewer ids, all that match
|
1707
|
+
ids = []
|
1708
|
+
for idx,obj in enumerate(windows):
|
1709
|
+
if obj == self._target():
|
1710
|
+
ids.append(winids[idx])
|
1711
|
+
return ids
|
1712
|
+
|
1713
|
+
def update(self):
|
1714
|
+
"""Update the control values from current viewer data
|
1715
|
+
Applies changes made in python to the UI controls
|
1716
|
+
"""
|
1717
|
+
#NOTE: to do this now, all we need is to trigger a get_state call from interactor by sending any command
|
1718
|
+
#TODO: currently only works for controllers with a window, other controls will not be updated
|
1719
|
+
if not is_notebook():
|
1720
|
+
return
|
1721
|
+
#Find matching viewer id, update all that match
|
1722
|
+
for idx,obj in enumerate(windows):
|
1723
|
+
#if obj == self._target():
|
1724
|
+
viewerid = winids[idx]
|
1725
|
+
from IPython.display import display,HTML
|
1726
|
+
display(HTML('<script>_wi["{0}"].execute(" ");</script>'.format(viewerid)))
|
1727
|
+
|
1728
|
+
def clear(self):
|
1729
|
+
#Initialise with a default control wrapper
|
1730
|
+
self._containers = [_Container(self)]
|
1731
|
+
|