webgpu 0.0.1__py3-none-any.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.
- webgpu/__init__.py +12 -0
- webgpu/_version.py +21 -0
- webgpu/camera.py +189 -0
- webgpu/canvas.py +144 -0
- webgpu/clipping.py +137 -0
- webgpu/colormap.py +325 -0
- webgpu/draw.py +35 -0
- webgpu/font.py +162 -0
- webgpu/fonts.json +52 -0
- webgpu/gpu.py +191 -0
- webgpu/input_handler.py +81 -0
- webgpu/jupyter.py +159 -0
- webgpu/jupyter_pyodide.py +363 -0
- webgpu/labels.py +132 -0
- webgpu/light.py +12 -0
- webgpu/lilgui.py +73 -0
- webgpu/link/__init__.py +3 -0
- webgpu/link/base.py +431 -0
- webgpu/link/link.js +431 -0
- webgpu/link/proxy.py +81 -0
- webgpu/link/websocket.py +115 -0
- webgpu/main.py +177 -0
- webgpu/platform.py +129 -0
- webgpu/render_object.py +155 -0
- webgpu/scene.py +201 -0
- webgpu/shaders/__init__.py +0 -0
- webgpu/shaders/camera.wgsl +21 -0
- webgpu/shaders/clipping.wgsl +35 -0
- webgpu/shaders/colormap.wgsl +60 -0
- webgpu/shaders/font.wgsl +53 -0
- webgpu/shaders/light.wgsl +9 -0
- webgpu/shaders/text.wgsl +57 -0
- webgpu/shaders/triangulation.wgsl +34 -0
- webgpu/shaders/vector.wgsl +118 -0
- webgpu/triangles.py +66 -0
- webgpu/uniforms.py +111 -0
- webgpu/utils.py +379 -0
- webgpu/vectors.py +101 -0
- webgpu/webgpu_api.py +1731 -0
- webgpu-0.0.1.dist-info/METADATA +32 -0
- webgpu-0.0.1.dist-info/RECORD +44 -0
- webgpu-0.0.1.dist-info/WHEEL +5 -0
- webgpu-0.0.1.dist-info/licenses/LICENSE +504 -0
- webgpu-0.0.1.dist-info/top_level.txt +1 -0
webgpu/jupyter.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import itertools
|
|
3
|
+
import os
|
|
4
|
+
import pickle
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from . import platform, utils
|
|
8
|
+
from .canvas import Canvas
|
|
9
|
+
from .lilgui import LilGUI
|
|
10
|
+
from .link import js_code as _link_js_code
|
|
11
|
+
from .render_object import *
|
|
12
|
+
from .scene import Scene
|
|
13
|
+
from .triangles import *
|
|
14
|
+
from .utils import init_device_sync
|
|
15
|
+
from .webgpu_api import *
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_package_zip(module_name="webgpu"):
|
|
19
|
+
"""
|
|
20
|
+
Creates a zip file containing all files in the specified Python package.
|
|
21
|
+
"""
|
|
22
|
+
import importlib.util
|
|
23
|
+
import os
|
|
24
|
+
import tempfile
|
|
25
|
+
import zipfile
|
|
26
|
+
|
|
27
|
+
spec = importlib.util.find_spec(module_name)
|
|
28
|
+
if spec is None or spec.origin is None:
|
|
29
|
+
raise ValueError(f"Package {module_name} not found.")
|
|
30
|
+
|
|
31
|
+
package_dir = os.path.dirname(spec.origin)
|
|
32
|
+
|
|
33
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
34
|
+
output_filename = os.path.join(temp_dir, f"{module_name}.zip")
|
|
35
|
+
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
36
|
+
for root, _, files in os.walk(package_dir):
|
|
37
|
+
for file in files:
|
|
38
|
+
file_path = os.path.join(root, file)
|
|
39
|
+
arcname = os.path.relpath(file_path, start=os.path.dirname(package_dir))
|
|
40
|
+
zipf.write(file_path, arcname)
|
|
41
|
+
|
|
42
|
+
return open(output_filename, "rb").read()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_id_counter = itertools.count()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _init_html(scene, width, height):
|
|
49
|
+
from IPython.display import HTML, display
|
|
50
|
+
|
|
51
|
+
if isinstance(scene, RenderObject):
|
|
52
|
+
scene = [scene]
|
|
53
|
+
if isinstance(scene, list):
|
|
54
|
+
scene = Scene(scene)
|
|
55
|
+
|
|
56
|
+
id_ = f"__webgpu_{next(_id_counter)}_"
|
|
57
|
+
|
|
58
|
+
display(
|
|
59
|
+
HTML(
|
|
60
|
+
f"""
|
|
61
|
+
<div id='{id_}root'
|
|
62
|
+
style="display: flex; justify-content: space-between;"
|
|
63
|
+
>
|
|
64
|
+
<canvas
|
|
65
|
+
id='{id_}canvas'
|
|
66
|
+
style='background-color: #d0d0d0; flex: 3; width: {width}px; height: {height}px;'
|
|
67
|
+
>
|
|
68
|
+
</canvas>
|
|
69
|
+
<div id='{id_}lilgui'
|
|
70
|
+
style='flex: 1;'
|
|
71
|
+
|
|
72
|
+
></div>
|
|
73
|
+
</div>
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return scene, id_
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _draw_scene(scene: Scene, width, height, id_):
|
|
82
|
+
html_canvas = platform.js.document.getElementById(f"{id_}canvas")
|
|
83
|
+
|
|
84
|
+
while html_canvas is None:
|
|
85
|
+
html_canvas = platform.js.document.getElementById(f"{id_}canvas")
|
|
86
|
+
html_canvas.width = width
|
|
87
|
+
html_canvas.height = height
|
|
88
|
+
gui_element = platform.js.document.getElementById(f"{id_}lilgui")
|
|
89
|
+
|
|
90
|
+
canvas = Canvas(utils.get_device(), html_canvas)
|
|
91
|
+
scene.gui = LilGUI(gui_element, scene)
|
|
92
|
+
scene.init(canvas)
|
|
93
|
+
scene.render()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _DrawPyodide(b64_data: str):
|
|
97
|
+
data = base64.b64decode(b64_data.encode("utf-8"))
|
|
98
|
+
id_, scene, width, height = pickle.loads(data)
|
|
99
|
+
|
|
100
|
+
_draw_scene(scene, width, height, id_)
|
|
101
|
+
return scene
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _DrawHTML(
|
|
105
|
+
scene: Scene | list[RenderObject] | RenderObject,
|
|
106
|
+
width=640,
|
|
107
|
+
height=640,
|
|
108
|
+
):
|
|
109
|
+
"""Draw a scene using display(Javascrip()) with all information in the HTML
|
|
110
|
+
This way, data is kept in the converted html when running nbconvert
|
|
111
|
+
The scene object is unpickled and drawn within a pyodide instance in the browser when the html is opened
|
|
112
|
+
"""
|
|
113
|
+
from IPython.display import Javascript, display
|
|
114
|
+
|
|
115
|
+
scene, id_ = _init_html(scene, width, height)
|
|
116
|
+
|
|
117
|
+
data = pickle.dumps((id_, scene, width, height))
|
|
118
|
+
b64_data = base64.b64encode(data).decode("utf-8")
|
|
119
|
+
|
|
120
|
+
display(Javascript(f"window.draw_scene('{b64_data}');"))
|
|
121
|
+
return scene
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def Draw(
|
|
125
|
+
scene: Scene | list[RenderObject] | RenderObject,
|
|
126
|
+
width=640,
|
|
127
|
+
height=640,
|
|
128
|
+
):
|
|
129
|
+
scene, id_ = _init_html(scene, width, height)
|
|
130
|
+
_draw_scene(scene, width, height, id_)
|
|
131
|
+
return scene
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if not platform.is_pyodide:
|
|
135
|
+
from IPython.display import Javascript, display
|
|
136
|
+
|
|
137
|
+
is_exporting = "WEBGPU_EXPORTING" in os.environ
|
|
138
|
+
|
|
139
|
+
if is_exporting:
|
|
140
|
+
Draw = _DrawHTML
|
|
141
|
+
webgpu_module = create_package_zip("webgpu")
|
|
142
|
+
webgpu_module_b64 = base64.b64encode(webgpu_module).decode("utf-8")
|
|
143
|
+
js_code = _link_js_code
|
|
144
|
+
js_code += f"\nconst _webgpu_code = '{webgpu_module_b64}';"
|
|
145
|
+
js_code += f"\nwindow.pyodide_ready = init_pyodide(_webgpu_code);"
|
|
146
|
+
display(Javascript(js_code))
|
|
147
|
+
else:
|
|
148
|
+
# Not exporting and not running in pyodide -> Start a websocket server and wait for the client to connect
|
|
149
|
+
while not platform.websocket_server:
|
|
150
|
+
time.sleep(0.1)
|
|
151
|
+
port = platform.websocket_server.port
|
|
152
|
+
host = f"ws://localhost:{port}"
|
|
153
|
+
js_code = _link_js_code + f"WebsocketLink('{host}');"
|
|
154
|
+
|
|
155
|
+
display(Javascript(js_code))
|
|
156
|
+
|
|
157
|
+
platform.init()
|
|
158
|
+
|
|
159
|
+
device = init_device_sync()
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import pickle
|
|
3
|
+
|
|
4
|
+
from .draw import Draw as DrawPyodide
|
|
5
|
+
from .lilgui import LilGUI
|
|
6
|
+
from .render_object import RenderObject, _render_objects
|
|
7
|
+
from .scene import Scene
|
|
8
|
+
from .utils import _is_pyodide, reload_package
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_package_zip(module_name="webgpu"):
|
|
12
|
+
"""
|
|
13
|
+
Creates a zip file containing all files in the specified Python package.
|
|
14
|
+
"""
|
|
15
|
+
import importlib.util
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
import zipfile
|
|
19
|
+
|
|
20
|
+
spec = importlib.util.find_spec(module_name)
|
|
21
|
+
if spec is None or spec.origin is None:
|
|
22
|
+
raise ValueError(f"Package {module_name} not found.")
|
|
23
|
+
|
|
24
|
+
package_dir = os.path.dirname(spec.origin)
|
|
25
|
+
|
|
26
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
27
|
+
output_filename = os.path.join(temp_dir, f"{module_name}.zip")
|
|
28
|
+
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
29
|
+
for root, _, files in os.walk(package_dir):
|
|
30
|
+
for file in files:
|
|
31
|
+
file_path = os.path.join(root, file)
|
|
32
|
+
arcname = os.path.relpath(file_path, start=os.path.dirname(package_dir))
|
|
33
|
+
zipf.write(file_path, arcname)
|
|
34
|
+
|
|
35
|
+
return open(output_filename, "rb").read()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_package_b64 = base64.b64encode(create_package_zip()).decode("utf-8")
|
|
39
|
+
|
|
40
|
+
_init_js_code = (
|
|
41
|
+
r"""
|
|
42
|
+
const SNAPSHOT_URL = 'https://cdn.jsdelivr.net/gh/mhochsteger/ngsolve_pyodide@webgpu1/snapshot.bin.gz';
|
|
43
|
+
|
|
44
|
+
function decodeB64(base64String) {
|
|
45
|
+
const binaryString = atob(base64String);
|
|
46
|
+
const len = binaryString.length;
|
|
47
|
+
const bytes = new Uint8Array(len);
|
|
48
|
+
for (let i = 0; i < len; i++) {
|
|
49
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
50
|
+
}
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchSnapshot() {
|
|
55
|
+
const blob = await (await fetch(SNAPSHOT_URL)).blob();
|
|
56
|
+
const decompressor = new DecompressionStream('gzip');
|
|
57
|
+
const stream = blob.stream().pipeThrough(decompressor);
|
|
58
|
+
const response = new Response(stream);
|
|
59
|
+
return await response.arrayBuffer();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function initLilGUI() {
|
|
63
|
+
// In generated html files, requirejs is imported before lil-gui is loaded.
|
|
64
|
+
// Thus, we must load lil-gui using require, use import otherwise.
|
|
65
|
+
const lil_url = "https://cdn.jsdelivr.net/npm/lil-gui@0.20";
|
|
66
|
+
if(window.define === undefined){
|
|
67
|
+
import(lil_url);
|
|
68
|
+
} else {
|
|
69
|
+
require([lil_url], (module) => {
|
|
70
|
+
window.lil = module;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
if(window.webgpu_ready === undefined) {
|
|
77
|
+
initLilGUI();
|
|
78
|
+
const pyodide_module = await import("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.mjs");
|
|
79
|
+
window.pyodide = await pyodide_module.loadPyodide( {
|
|
80
|
+
// _loadSnapshot: await fetchSnapshot(),
|
|
81
|
+
// lockFileURL: 'https://cdn.jsdelivr.net/gh/mhochsteger/ngsolve_pyodide@webgpu2/pyodide-lock.json',
|
|
82
|
+
// indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/",
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
pyodide.setDebug(true);
|
|
86
|
+
window.pyodide_ready = pyodide.loadPackage(['micropip', 'numpy', 'packaging']);
|
|
87
|
+
await window.pyodide_ready;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
await webgpu_ready;
|
|
91
|
+
}
|
|
92
|
+
const webgpu_b64 = `"""
|
|
93
|
+
+ _package_b64
|
|
94
|
+
+ r"""`;
|
|
95
|
+
const webpgu_zip = decodeB64(webgpu_b64);
|
|
96
|
+
await pyodide.unpackArchive(webpgu_zip, 'zip');
|
|
97
|
+
await pyodide.runPythonAsync('import webgpu.utils');
|
|
98
|
+
await pyodide.runPythonAsync('await webgpu.utils.init_device()');
|
|
99
|
+
}
|
|
100
|
+
window.webgpu_ready = main();
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _encode_data(data):
|
|
107
|
+
binary_chunk = pickle.dumps(data)
|
|
108
|
+
return base64.b64encode(binary_chunk).decode("utf-8")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _decode_data(data):
|
|
112
|
+
binary_chunk = base64.b64decode(data.encode("utf-8"))
|
|
113
|
+
return pickle.loads(binary_chunk)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _encode_function(func):
|
|
117
|
+
import inspect
|
|
118
|
+
|
|
119
|
+
return [func.__name__, inspect.getsource(func)]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _decode_function(encoded_func):
|
|
123
|
+
import __main__
|
|
124
|
+
|
|
125
|
+
func_name, func_str = encoded_func
|
|
126
|
+
symbols = __main__.__dict__
|
|
127
|
+
exec(func_str, symbols, symbols)
|
|
128
|
+
return symbols[func_name]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _init(canvas_id="canvas"):
|
|
132
|
+
import js
|
|
133
|
+
|
|
134
|
+
from webgpu.canvas import init_webgpu
|
|
135
|
+
|
|
136
|
+
return init_webgpu(js.document.getElementById(canvas_id))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_render_canvas(canvas_id):
|
|
140
|
+
return _render_canvases[canvas_id]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _draw_client(canvas_id, scene, assets, globs):
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
import js
|
|
147
|
+
import pyodide.ffi
|
|
148
|
+
|
|
149
|
+
from webgpu.jupyter import _decode_data, _decode_function
|
|
150
|
+
|
|
151
|
+
assets = _decode_data(assets)
|
|
152
|
+
|
|
153
|
+
for module_data in assets.get("modules", {}).values():
|
|
154
|
+
# extract zipfile from binary chunk
|
|
155
|
+
import io
|
|
156
|
+
import zipfile
|
|
157
|
+
|
|
158
|
+
zipf = zipfile.ZipFile(io.BytesIO(module_data))
|
|
159
|
+
zipf.extractall()
|
|
160
|
+
|
|
161
|
+
for file_name, file_data in assets.get("files", {}).items():
|
|
162
|
+
with open(file_name, "wb") as f:
|
|
163
|
+
f.write(file_data)
|
|
164
|
+
|
|
165
|
+
for module_name in assets.get("modules", {}):
|
|
166
|
+
reload_package(module_name)
|
|
167
|
+
|
|
168
|
+
canvas = _init(canvas_id)
|
|
169
|
+
scene = _decode_data(scene)
|
|
170
|
+
|
|
171
|
+
if "init_function" in assets:
|
|
172
|
+
func = _decode_function(assets["init_function"])
|
|
173
|
+
func(canvas, **scene)
|
|
174
|
+
elif "init_function_name" in assets:
|
|
175
|
+
func = globs[assets["init_function_name"]]
|
|
176
|
+
func(canvas, **scene)
|
|
177
|
+
else:
|
|
178
|
+
scene.init(canvas)
|
|
179
|
+
DrawPyodide(scene, canvas)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
_draw_js_code_template = r"""
|
|
183
|
+
async function draw() {{
|
|
184
|
+
var canvas = document.createElement('canvas');
|
|
185
|
+
var canvas_id = "{canvas_id}";
|
|
186
|
+
canvas.id = canvas_id;
|
|
187
|
+
canvas.width = {width};
|
|
188
|
+
canvas.height = {height};
|
|
189
|
+
canvas.style = "background-color: #d0d0d0";
|
|
190
|
+
element.appendChild(canvas);
|
|
191
|
+
await window.webgpu_ready;
|
|
192
|
+
await window.pyodide.runPythonAsync('import webgpu.jupyter; webgpu.jupyter._draw_client("{canvas_id}", "{data}", "{assets}", globals())');
|
|
193
|
+
}}
|
|
194
|
+
draw();
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
if not _is_pyodide:
|
|
198
|
+
from IPython.core.magic import register_cell_magic
|
|
199
|
+
from IPython.display import HTML, Javascript, display
|
|
200
|
+
|
|
201
|
+
display(Javascript(_init_js_code))
|
|
202
|
+
|
|
203
|
+
_call_counter = 0
|
|
204
|
+
|
|
205
|
+
def _get_canvas_id():
|
|
206
|
+
global _call_counter
|
|
207
|
+
_call_counter += 1
|
|
208
|
+
return f"canvas_{_call_counter}"
|
|
209
|
+
|
|
210
|
+
def _run_js_code(data, assets, width, height):
|
|
211
|
+
display(
|
|
212
|
+
Javascript(
|
|
213
|
+
_draw_js_code_template.format(
|
|
214
|
+
canvas_id=_get_canvas_id(),
|
|
215
|
+
data=_encode_data(data),
|
|
216
|
+
assets=_encode_data(assets),
|
|
217
|
+
width=width,
|
|
218
|
+
height=height,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
html_code = r"""
|
|
224
|
+
<div id="{canvas_id}_row" style="display: flex; justify-content: space-between;">
|
|
225
|
+
<canvas id="{canvas_id}" style="flex: 3; margin-right: 10px; padding: 10px; height: {height}px; width: {width}px; background-color: #d0d0d0;"></canvas>
|
|
226
|
+
<div id="{canvas_id}_gui" style="flex: 1; margin-left: 10px; padding: 10px;"></div>
|
|
227
|
+
</div>
|
|
228
|
+
"""
|
|
229
|
+
js_code = r"""
|
|
230
|
+
async function draw() {{
|
|
231
|
+
await window.webgpu_ready;
|
|
232
|
+
var gui_element = document.getElementById('{canvas_id}' + '_gui');
|
|
233
|
+
console.log('gui_element =', gui_element);
|
|
234
|
+
if(window.lil_guis === undefined) {{
|
|
235
|
+
window.lil_guis = new Object();
|
|
236
|
+
}}
|
|
237
|
+
window.lil_guis['{canvas_id}'] = new lil.GUI({{container: gui_element}});
|
|
238
|
+
// var canvas2 = document.createElement('canvas');
|
|
239
|
+
// console.log("canvas2 =", canvas2);
|
|
240
|
+
var canvas = document.getElementById("{canvas_id}");
|
|
241
|
+
console.log('canvas size', canvas.clientWidth, canvas.clientHeight);
|
|
242
|
+
console.log(canvas);
|
|
243
|
+
canvas.width = Math.floor(canvas.clientWidth/32)*32;
|
|
244
|
+
canvas.height = Math.floor(canvas.clientHeight/32)*32;
|
|
245
|
+
canvas.style = "background-color: #d0d0d0; max-width: {width}px; max-height: {height}px;";
|
|
246
|
+
await window.pyodide.runPythonAsync('import webgpu.jupyter; webgpu.jupyter._draw_client("{canvas_id}", "{scene}", "{assets}", globals())');
|
|
247
|
+
}}
|
|
248
|
+
draw();
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def Draw(
|
|
252
|
+
scene: Scene | list[RenderObject] | RenderObject,
|
|
253
|
+
width=608,
|
|
254
|
+
height=608,
|
|
255
|
+
modules=[],
|
|
256
|
+
):
|
|
257
|
+
if isinstance(scene, RenderObject):
|
|
258
|
+
scene = [scene]
|
|
259
|
+
if isinstance(scene, list):
|
|
260
|
+
scene = Scene(scene)
|
|
261
|
+
canvas_id = _get_canvas_id()
|
|
262
|
+
scene.gui = LilGUI(canvas_id, scene._id)
|
|
263
|
+
assets = {"modules": {module: create_package_zip(module) for module in modules}}
|
|
264
|
+
display(
|
|
265
|
+
HTML(html_code.format(canvas_id=canvas_id, width=width, height=height)),
|
|
266
|
+
Javascript(
|
|
267
|
+
js_code.format(
|
|
268
|
+
canvas_id=canvas_id,
|
|
269
|
+
scene=_encode_data(scene),
|
|
270
|
+
assets=_encode_data(assets),
|
|
271
|
+
width=width,
|
|
272
|
+
height=height,
|
|
273
|
+
)
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return scene
|
|
278
|
+
|
|
279
|
+
def DrawCustom(
|
|
280
|
+
client_function,
|
|
281
|
+
kwargs={},
|
|
282
|
+
modules: list[str] = [],
|
|
283
|
+
files: list[str] = [],
|
|
284
|
+
width=608,
|
|
285
|
+
height=608,
|
|
286
|
+
):
|
|
287
|
+
assets = {
|
|
288
|
+
"modules": {module: create_package_zip(module) for module in modules},
|
|
289
|
+
"files": {f: open(f, "rb").read() for f in files},
|
|
290
|
+
}
|
|
291
|
+
if isinstance(client_function, str):
|
|
292
|
+
assets["init_function_name"] = client_function
|
|
293
|
+
else:
|
|
294
|
+
assets["init_function"] = _encode_function(client_function)
|
|
295
|
+
canvas_id = _get_canvas_id()
|
|
296
|
+
display(
|
|
297
|
+
HTML(html_code.format(canvas_id=canvas_id, height=height, width=width)),
|
|
298
|
+
Javascript(
|
|
299
|
+
js_code.format(
|
|
300
|
+
canvas_id=canvas_id,
|
|
301
|
+
scene=_encode_data(kwargs),
|
|
302
|
+
assets=_encode_data(assets),
|
|
303
|
+
width=width,
|
|
304
|
+
height=height,
|
|
305
|
+
)
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def run_code_in_pyodide(code: str):
|
|
310
|
+
display(
|
|
311
|
+
Javascript(
|
|
312
|
+
f"window.webgpu_ready.then(() => {{ window.pyodide.runPythonAsync(`{code}`) }});"
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@register_cell_magic
|
|
317
|
+
def pyodide(line, cell):
|
|
318
|
+
run_code_in_pyodide(str(cell))
|
|
319
|
+
|
|
320
|
+
@register_cell_magic
|
|
321
|
+
def pyodide_and_kernel(line, cell):
|
|
322
|
+
run_code_in_pyodide(str(cell))
|
|
323
|
+
ip = get_ipython()
|
|
324
|
+
exec(cell, ip.user_global_ns)
|
|
325
|
+
|
|
326
|
+
del pyodide
|
|
327
|
+
|
|
328
|
+
class Pyodide:
|
|
329
|
+
def __setattr__(self, key, value):
|
|
330
|
+
data = _encode_data(value)
|
|
331
|
+
display(
|
|
332
|
+
Javascript(
|
|
333
|
+
f"window.webgpu_ready.then(() => {{ window.pyodide.runPythonAsync(`import webgpu.jupyter; {key} = webgpu.jupyter._decode_data('{data}')`) }});"
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
pyodide = Pyodide()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def pyodide_install_packages(packages):
|
|
341
|
+
if not _is_pyodide:
|
|
342
|
+
display(
|
|
343
|
+
Javascript(
|
|
344
|
+
f"window.webgpu_ready = window.webgpu_ready.then(() => {{ return window.pyodide.loadPackage({packages}); }})"
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def update_render_object(id, **kwargs):
|
|
350
|
+
if _is_pyodide:
|
|
351
|
+
obj = _render_objects[id]
|
|
352
|
+
obj.update(**kwargs)
|
|
353
|
+
else:
|
|
354
|
+
kwargs = _encode_data(kwargs)
|
|
355
|
+
run_code_in_pyodide(
|
|
356
|
+
f"import webgpu.jupyter; webgpu.jupyter.update_render_object('{id}', **webgpu.jupyter._decode_data('{kwargs}'));"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def redraw_canvas(canvas_id: str):
|
|
361
|
+
run_code_in_pyodide(
|
|
362
|
+
f"import webgpu.draw, js; js.requestAnimationFrame(webgpu.draw._canvas_id_to_gpu['{canvas_id}'].input_handler.render_function)"
|
|
363
|
+
)
|
webgpu/labels.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .font import Font
|
|
4
|
+
from .render_object import RenderObject
|
|
5
|
+
from .uniforms import Binding
|
|
6
|
+
from .utils import BufferBinding, read_shader_file
|
|
7
|
+
from .webgpu_api import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Labels(RenderObject):
|
|
11
|
+
vertex_entry_point: str = "vertexText"
|
|
12
|
+
fragment_entry_point: str = "fragmentFont"
|
|
13
|
+
n_vertices: int = 6
|
|
14
|
+
|
|
15
|
+
"""Render a list of strings on screen
|
|
16
|
+
@param labels: list of strings to render
|
|
17
|
+
@param positions: list of positions to render the labels at
|
|
18
|
+
@param apply_camera: whether to apply the camera transformation to the labels
|
|
19
|
+
@param h_align: horizontal alignment of the labels. Can be one of: left, l, center, c, right, r
|
|
20
|
+
@param v_align: horizontal alignment of the labels. Can be one of: bottom, b, center, c, top, t
|
|
21
|
+
@param font_size: font size
|
|
22
|
+
|
|
23
|
+
If any of apply_camera, h_align, or v_align is a list, it must have the same length as labels.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
labels: list[str],
|
|
29
|
+
positions: list[tuple],
|
|
30
|
+
apply_camera: bool | list[bool] = False,
|
|
31
|
+
h_align: str | list[str] = "left",
|
|
32
|
+
v_align: str | list[str] = "bottom",
|
|
33
|
+
font_size=20,
|
|
34
|
+
):
|
|
35
|
+
self.labels = labels
|
|
36
|
+
self.positions = positions
|
|
37
|
+
self.font_size = font_size
|
|
38
|
+
self.apply_camera = apply_camera
|
|
39
|
+
self.h_align = h_align
|
|
40
|
+
self.v_align = v_align
|
|
41
|
+
|
|
42
|
+
def update(self, timestamp):
|
|
43
|
+
if timestamp == self._timestamp:
|
|
44
|
+
return
|
|
45
|
+
self._timestamp = timestamp
|
|
46
|
+
n_chars = sum(len(label) for label in self.labels)
|
|
47
|
+
n_labels = len(self.labels)
|
|
48
|
+
self.n_vertices = 6
|
|
49
|
+
self.n_instances = n_chars
|
|
50
|
+
char_t = np.dtype(
|
|
51
|
+
[
|
|
52
|
+
("itext", np.uint32),
|
|
53
|
+
("ichar", np.uint16),
|
|
54
|
+
("char", np.uint8),
|
|
55
|
+
("padding", np.uint8),
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
char_data = np.zeros(n_chars, dtype=char_t)
|
|
59
|
+
text_t = np.dtype(
|
|
60
|
+
[
|
|
61
|
+
("pos", np.float32, 3),
|
|
62
|
+
("length", np.uint16),
|
|
63
|
+
("apply_camera", np.uint8),
|
|
64
|
+
("alignment", np.uint8),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
text_data = np.zeros(n_labels, dtype=text_t)
|
|
68
|
+
|
|
69
|
+
align_map = {
|
|
70
|
+
"c": 1,
|
|
71
|
+
"center": 1,
|
|
72
|
+
"r": 2,
|
|
73
|
+
"right": 2,
|
|
74
|
+
"t": 2,
|
|
75
|
+
"top": 2,
|
|
76
|
+
"b": 0,
|
|
77
|
+
"bottom": 0,
|
|
78
|
+
"l": 0,
|
|
79
|
+
"left": 0,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ichar = 0
|
|
83
|
+
for i, label, pos in zip(range(len(self.labels)), self.labels, self.positions):
|
|
84
|
+
h_align = self.h_align if isinstance(self.h_align, str) else self.h_align[i]
|
|
85
|
+
v_align = self.v_align if isinstance(self.v_align, str) else self.v_align[i]
|
|
86
|
+
align = align_map[h_align] + 4 * align_map[v_align]
|
|
87
|
+
apply_camera = (
|
|
88
|
+
self.apply_camera if isinstance(self.apply_camera, bool) else self.apply_camera[i]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if len(pos) == 2:
|
|
92
|
+
pos = (*pos, 0)
|
|
93
|
+
|
|
94
|
+
text_data[i]["pos"] = pos
|
|
95
|
+
text_data[i]["length"] = len(label)
|
|
96
|
+
text_data[i]["apply_camera"] = apply_camera
|
|
97
|
+
text_data[i]["alignment"] = align
|
|
98
|
+
|
|
99
|
+
i0 = ichar
|
|
100
|
+
for c in label:
|
|
101
|
+
char_data[ichar]["itext"] = i
|
|
102
|
+
char_data[ichar]["ichar"] = ichar - i0
|
|
103
|
+
char_data[ichar]["char"] = ord(c)
|
|
104
|
+
ichar += 1
|
|
105
|
+
|
|
106
|
+
self.font = Font(self.canvas, self.font_size)
|
|
107
|
+
|
|
108
|
+
data = (
|
|
109
|
+
np.array([len(self.labels)], dtype=np.uint32).tobytes()
|
|
110
|
+
+ text_data.tobytes()
|
|
111
|
+
+ char_data.tobytes()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self.buffer = self.device.createBuffer(
|
|
115
|
+
len(data),
|
|
116
|
+
usage=BufferUsage.STORAGE | BufferUsage.COPY_DST,
|
|
117
|
+
)
|
|
118
|
+
self.device.queue.writeBuffer(self.buffer, 0, data)
|
|
119
|
+
self.create_render_pipeline()
|
|
120
|
+
|
|
121
|
+
def get_shader_code(self):
|
|
122
|
+
shader_code = read_shader_file("text.wgsl", __file__)
|
|
123
|
+
shader_code += self.font.get_shader_code()
|
|
124
|
+
shader_code += self.options.camera.get_shader_code()
|
|
125
|
+
return shader_code
|
|
126
|
+
|
|
127
|
+
def get_bindings(self):
|
|
128
|
+
return [
|
|
129
|
+
*self.font.get_bindings(),
|
|
130
|
+
*self.options.camera.get_bindings(),
|
|
131
|
+
BufferBinding(Binding.TEXT, self.buffer),
|
|
132
|
+
]
|