webgpu 1.2.1.dev0__py3-none-any.whl → 1.2.3.dev0__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 +1 -0
- webgpu/_version.py +3 -3
- webgpu/camera.py +16 -9
- webgpu/canvas.py +171 -66
- webgpu/gizmo.py +304 -0
- webgpu/input_handler.py +12 -12
- webgpu/labels.py +59 -24
- webgpu/link/base.py +26 -6
- webgpu/link/websocket.py +38 -4
- webgpu/platform.py +29 -0
- webgpu/renderer.py +121 -24
- webgpu/scene.py +51 -15
- webgpu/shaders/gizmo_edges.wgsl +61 -0
- webgpu/shaders/gizmo_mesh.wgsl +62 -0
- webgpu/shaders/light.wgsl +2 -1
- webgpu/shaders/shapes.wgsl +32 -3
- webgpu/shaders/text.wgsl +89 -17
- webgpu/shapes.py +39 -1
- webgpu/uniforms.py +14 -9
- webgpu/vectors.py +3 -10
- {webgpu-1.2.1.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/METADATA +1 -1
- webgpu-1.2.3.dev0.dist-info/RECORD +47 -0
- webgpu-1.2.1.dev0.dist-info/RECORD +0 -44
- {webgpu-1.2.1.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/WHEEL +0 -0
- {webgpu-1.2.1.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/licenses/LICENSE +0 -0
- {webgpu-1.2.1.dev0.dist-info → webgpu-1.2.3.dev0.dist-info}/top_level.txt +0 -0
webgpu/__init__.py
CHANGED
webgpu/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 2,
|
|
21
|
+
__version__ = version = '1.2.3.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 3, 'dev0')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g3663824c4'
|
webgpu/camera.py
CHANGED
|
@@ -97,10 +97,10 @@ class Transform:
|
|
|
97
97
|
|
|
98
98
|
def reset_xy(self, flip: bool = False):
|
|
99
99
|
"""Reset to a view looking along +Z onto the XY plane, optionally flipped."""
|
|
100
|
-
|
|
100
|
+
s = np.linalg.norm(self._mat[:3, :3], axis=0)[0] # current uniform scale
|
|
101
101
|
self._mat = np.identity(4)
|
|
102
102
|
self.translate(-self._center[0], -self._center[1], -self._center[2])
|
|
103
|
-
self.
|
|
103
|
+
self.scale(s)
|
|
104
104
|
if flip:
|
|
105
105
|
self.rotate(0, 180)
|
|
106
106
|
|
|
@@ -212,12 +212,12 @@ class Camera:
|
|
|
212
212
|
self._get_position_function = get_position_function
|
|
213
213
|
|
|
214
214
|
def register_callbacks(self, input_handler):
|
|
215
|
-
input_handler.on_mousedown(self._on_mousedown)
|
|
216
|
-
input_handler.on_mouseup(self._on_mouseup)
|
|
217
|
-
input_handler.on_mouseout(self._on_mouseup)
|
|
218
|
-
input_handler.on_mousemove(self._on_mousemove)
|
|
219
|
-
input_handler.on_dblclick(self._on_dblclick)
|
|
220
|
-
input_handler.on_wheel(self._on_wheel, shift=
|
|
215
|
+
input_handler.on_mousedown(self._on_mousedown, ctrl=False, shift=False, alt=False)
|
|
216
|
+
input_handler.on_mouseup(self._on_mouseup, ctrl=False, shift=False, alt=False)
|
|
217
|
+
input_handler.on_mouseout(self._on_mouseup, ctrl=False, shift=False, alt=False)
|
|
218
|
+
input_handler.on_mousemove(self._on_mousemove, ctrl=False, shift=False, alt=False)
|
|
219
|
+
input_handler.on_dblclick(self._on_dblclick, ctrl=False, shift=False, alt=False)
|
|
220
|
+
input_handler.on_wheel(self._on_wheel, ctrl=False, shift=False, alt=False)
|
|
221
221
|
|
|
222
222
|
def unregister_callbacks(self, input_handler):
|
|
223
223
|
input_handler.unregister("mousedown", self._on_mousedown)
|
|
@@ -326,7 +326,14 @@ class Camera:
|
|
|
326
326
|
self.uniforms.model_view[:] = model_view.transpose().flatten()
|
|
327
327
|
self.uniforms.model_view_projection[:] = model_view_proj.transpose().flatten()
|
|
328
328
|
self.uniforms.normal_mat[:] = normal_mat.flatten()
|
|
329
|
-
#
|
|
329
|
+
# Extract pure rotation from model_view (upper-left 3x3, normalize columns to remove scale)
|
|
330
|
+
mv33 = model_view[:3, :3]
|
|
331
|
+
col_norms = np.linalg.norm(mv33, axis=0)
|
|
332
|
+
col_norms[col_norms == 0] = 1.0
|
|
333
|
+
rot33 = mv33 / col_norms
|
|
334
|
+
rot_mat4 = np.identity(4)
|
|
335
|
+
rot_mat4[:3, :3] = rot33
|
|
336
|
+
self.uniforms.rot_mat[:] = rot_mat4.transpose().flatten()
|
|
330
337
|
self.uniforms.width = self.canvas.width
|
|
331
338
|
self.uniforms.height = self.canvas.height
|
|
332
339
|
self.uniforms.update_buffer()
|
webgpu/canvas.py
CHANGED
|
@@ -8,80 +8,183 @@ import pathlib
|
|
|
8
8
|
from . import platform
|
|
9
9
|
from .utils import get_device, read_texture, Lock
|
|
10
10
|
from .webgpu_api import *
|
|
11
|
+
from functools import wraps
|
|
11
12
|
|
|
12
13
|
_TARGET_FPS = 60
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
@dataclass
|
|
16
|
-
class _DebounceData:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
16
|
+
# @dataclass
|
|
17
|
+
# class _DebounceData:
|
|
18
|
+
# t_last_frame: float = 0
|
|
19
|
+
# t_last_call: float = 0
|
|
20
|
+
# timer: threading.Timer | None = None
|
|
21
|
+
# lock: Lock = None
|
|
22
|
+
# running: bool = False
|
|
23
|
+
# pending: bool = False
|
|
24
|
+
|
|
25
|
+
def debounce(arg=None, *, rate_hz=60):
|
|
26
|
+
|
|
27
|
+
def _rate_limited(fn, rate_hz):
|
|
28
|
+
interval = 1.0 / rate_hz
|
|
29
|
+
lock = threading.RLock()
|
|
30
|
+
last_call = 0.0
|
|
31
|
+
timer = None
|
|
32
|
+
pending = None
|
|
33
|
+
|
|
34
|
+
def schedule(delay):
|
|
35
|
+
nonlocal timer
|
|
36
|
+
timer = threading.Timer(delay, run_pending)
|
|
37
|
+
timer.daemon = True
|
|
38
|
+
timer.start()
|
|
39
|
+
|
|
40
|
+
def run_pending():
|
|
41
|
+
nonlocal last_call, timer, pending
|
|
42
|
+
|
|
43
|
+
with lock:
|
|
44
|
+
if pending is None:
|
|
45
|
+
timer = None
|
|
41
46
|
return
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
args, kwargs = pending
|
|
49
|
+
pending = None
|
|
50
|
+
# print("call frequency = ", 1.0 / (time.monotonic() - last_call))
|
|
51
|
+
last_call = time.monotonic()
|
|
52
|
+
|
|
53
|
+
fn(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
with lock:
|
|
56
|
+
timer = None
|
|
57
|
+
if pending is not None:
|
|
58
|
+
delay = max(0.0, interval - (time.monotonic() - last_call))
|
|
59
|
+
schedule(delay)
|
|
60
|
+
|
|
61
|
+
@wraps(fn)
|
|
62
|
+
def wrapper(*args, **kwargs):
|
|
63
|
+
nonlocal last_call, pending
|
|
64
|
+
|
|
65
|
+
with lock:
|
|
66
|
+
now = time.monotonic()
|
|
67
|
+
elapsed = now - last_call
|
|
68
|
+
if elapsed >= interval and timer is None:
|
|
69
|
+
# print("call frequency = ", 1.0 / elapsed if elapsed > 0 else float('inf'))
|
|
70
|
+
last_call = now
|
|
71
|
+
run_now = True
|
|
51
72
|
else:
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
pending = (args, kwargs)
|
|
74
|
+
run_now = False
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
if timer is None:
|
|
77
|
+
schedule(max(0.0, interval - elapsed))
|
|
57
78
|
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
data.t_last = time.time()
|
|
61
|
-
f()
|
|
62
|
-
return
|
|
79
|
+
if run_now:
|
|
80
|
+
fn(*args, **kwargs)
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
if platform.is_pyodide:
|
|
66
|
-
import asyncio
|
|
67
|
-
async def _runner():
|
|
68
|
-
if t_wait > 0:
|
|
69
|
-
await asyncio.sleep(t_wait)
|
|
70
|
-
f()
|
|
71
|
-
data.timer = asyncio.create_task(_runner())
|
|
72
|
-
else:
|
|
73
|
-
data.timer = threading.Timer(t_wait, f)
|
|
74
|
-
data.timer.start()
|
|
75
|
-
|
|
76
|
-
debounced._original = func
|
|
77
|
-
return debounced
|
|
82
|
+
return wrapper
|
|
78
83
|
|
|
79
84
|
if callable(arg):
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
return _rate_limited(arg, rate_hz)
|
|
86
|
+
|
|
87
|
+
if arg is not None:
|
|
88
|
+
rate_hz = arg
|
|
89
|
+
|
|
90
|
+
def decorate(fn):
|
|
91
|
+
return _rate_limited(fn, rate_hz)
|
|
92
|
+
return decorate
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# def debounce(arg=None):
|
|
97
|
+
# def decorator(func):
|
|
98
|
+
# # Render only once every 1/_TARGET_FPS seconds
|
|
99
|
+
# @functools.wraps(func)
|
|
100
|
+
# def debounced(obj, *args, **kwargs):
|
|
101
|
+
# if not hasattr(obj, "_debounce_data"):
|
|
102
|
+
# obj._debounce_data = {}
|
|
103
|
+
|
|
104
|
+
# fname = func.__name__
|
|
105
|
+
# if obj._debounce_data.get(fname, None) is None:
|
|
106
|
+
# obj._debounce_data[fname] = _DebounceData(0, 0, None, Lock())
|
|
107
|
+
|
|
108
|
+
# data = obj._debounce_data[fname]
|
|
109
|
+
# frame_time = 1.0 / target_fps
|
|
110
|
+
|
|
111
|
+
# def run():
|
|
112
|
+
# while True:
|
|
113
|
+
# # Call func OUTSIDE the lock to avoid deadlocks
|
|
114
|
+
# func(obj, *args, **kwargs)
|
|
115
|
+
|
|
116
|
+
# with data.lock:
|
|
117
|
+
# if not data.pending:
|
|
118
|
+
# data.running = False
|
|
119
|
+
# return
|
|
120
|
+
# data.pending = False
|
|
121
|
+
# elapsed = time.time() - data.t_last_frame
|
|
122
|
+
# t_wait = frame_time - elapsed
|
|
123
|
+
# if t_wait > 0:
|
|
124
|
+
# # Schedule deferred re-run to respect frame rate
|
|
125
|
+
# if platform.is_pyodide:
|
|
126
|
+
# import asyncio
|
|
127
|
+
# async def _rerun():
|
|
128
|
+
# await asyncio.sleep(t_wait)
|
|
129
|
+
# with data.lock:
|
|
130
|
+
# data.t_last_frame = time.time()
|
|
131
|
+
# run()
|
|
132
|
+
# asyncio.create_task(_rerun())
|
|
133
|
+
# else:
|
|
134
|
+
# def _deferred():
|
|
135
|
+
# with data.lock:
|
|
136
|
+
# data.t_last_frame = time.time()
|
|
137
|
+
# run()
|
|
138
|
+
# data.timer = threading.Timer(t_wait, _deferred)
|
|
139
|
+
# data.timer.start()
|
|
140
|
+
# return
|
|
141
|
+
# data.t_last_frame = time.time()
|
|
142
|
+
|
|
143
|
+
# def f():
|
|
144
|
+
# with data.lock:
|
|
145
|
+
# if t_call != data.t_last_call and t_call - data.t_last_frame < frame_time:
|
|
146
|
+
# return
|
|
147
|
+
# if data.running:
|
|
148
|
+
# data.pending = True
|
|
149
|
+
# return
|
|
150
|
+
# data.running = True
|
|
151
|
+
# data.t_last_frame = time.time()
|
|
152
|
+
|
|
153
|
+
# run()
|
|
154
|
+
|
|
155
|
+
# with data.lock:
|
|
156
|
+
# t_call = time.time()
|
|
157
|
+
# data.t_last_call = t_call
|
|
158
|
+
# t_wait = frame_time - (t_call - data.t_last_frame)
|
|
159
|
+
|
|
160
|
+
# if t_wait <= 0:
|
|
161
|
+
# if data.running:
|
|
162
|
+
# data.pending = True
|
|
163
|
+
# return
|
|
164
|
+
# data.running = True
|
|
165
|
+
# data.t_last_frame = time.time()
|
|
166
|
+
# if data.timer is not None:
|
|
167
|
+
# data.timer.cancel()
|
|
168
|
+
# data.timer = None
|
|
169
|
+
# else:
|
|
170
|
+
# if data.timer is not None:
|
|
171
|
+
# data.timer.cancel()
|
|
172
|
+
# if platform.is_pyodide:
|
|
173
|
+
# import asyncio
|
|
174
|
+
# async def _runner():
|
|
175
|
+
# await asyncio.sleep(t_wait)
|
|
176
|
+
# f()
|
|
177
|
+
# asyncio.create_task(_runner())
|
|
178
|
+
# else:
|
|
179
|
+
# data.timer = threading.Timer(t_wait, f)
|
|
180
|
+
# data.timer.start()
|
|
181
|
+
# return
|
|
182
|
+
|
|
183
|
+
# run()
|
|
184
|
+
|
|
185
|
+
# debounced._original = func
|
|
186
|
+
# return debounced
|
|
187
|
+
|
|
85
188
|
|
|
86
189
|
|
|
87
190
|
def init_webgpu(html_canvas):
|
|
@@ -147,10 +250,12 @@ class Canvas:
|
|
|
147
250
|
self.update_html_canvas(canvas)
|
|
148
251
|
|
|
149
252
|
def __del__(self):
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
253
|
+
disconnect = getattr(self._resize_observer, "disconnect", None)
|
|
254
|
+
if callable(disconnect):
|
|
255
|
+
disconnect()
|
|
256
|
+
disconnect = getattr(self._intersection_observer, "disconnect", None)
|
|
257
|
+
if callable(disconnect):
|
|
258
|
+
disconnect()
|
|
154
259
|
|
|
155
260
|
def update_html_canvas(self, html_canvas):
|
|
156
261
|
"""Reconfigure the canvas with the current HTML canvas element. This is necessary when the HTML canvas element changes, disappears (e.g. when switching a tab) and appears again."""
|
webgpu/gizmo.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Coordinate axes (3D arrows) and navigation cube renderers.
|
|
2
|
+
|
|
3
|
+
Both render in fixed screen corners, rotated by the camera's rotation matrix.
|
|
4
|
+
Uses the Labels renderer for text (overlay mode) and minimal custom renderers
|
|
5
|
+
for the mesh and edge geometry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .labels import Labels
|
|
11
|
+
from .renderer import MultipleRenderer, Renderer, RenderOptions
|
|
12
|
+
from .utils import (
|
|
13
|
+
BufferBinding,
|
|
14
|
+
UniformBinding,
|
|
15
|
+
buffer_from_array,
|
|
16
|
+
uniform_from_array,
|
|
17
|
+
read_shader_file,
|
|
18
|
+
)
|
|
19
|
+
from .webgpu_api import PrimitiveTopology
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Geometry helpers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _rotation_to_direction(direction):
|
|
27
|
+
"""3x3 rotation mapping +Z to *direction*."""
|
|
28
|
+
z = np.asarray(direction, dtype=np.float64)
|
|
29
|
+
z = z / np.linalg.norm(z)
|
|
30
|
+
up = np.array([1, 0, 0]) if abs(z[0]) < 0.9 else np.array([0, 1, 0])
|
|
31
|
+
x = np.cross(up, z)
|
|
32
|
+
x /= np.linalg.norm(x)
|
|
33
|
+
y = np.cross(z, x)
|
|
34
|
+
return np.column_stack([x, y, z])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cylinder_tris(segs, r, z0, z1, rot):
|
|
38
|
+
verts, norms = [], []
|
|
39
|
+
angles = np.linspace(0, 2 * np.pi, segs, endpoint=False)
|
|
40
|
+
for i in range(segs):
|
|
41
|
+
j = (i + 1) % segs
|
|
42
|
+
c0, s0 = np.cos(angles[i]), np.sin(angles[i])
|
|
43
|
+
c1, s1 = np.cos(angles[j]), np.sin(angles[j])
|
|
44
|
+
p00 = rot @ [r * c0, r * s0, z0]
|
|
45
|
+
p10 = rot @ [r * c1, r * s1, z0]
|
|
46
|
+
p01 = rot @ [r * c0, r * s0, z1]
|
|
47
|
+
p11 = rot @ [r * c1, r * s1, z1]
|
|
48
|
+
n0 = rot @ [c0, s0, 0.0]
|
|
49
|
+
n1 = rot @ [c1, s1, 0.0]
|
|
50
|
+
verts += [p00, p10, p01, p10, p11, p01]
|
|
51
|
+
norms += [n0, n1, n0, n1, n1, n0]
|
|
52
|
+
return verts, norms
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cone_tris(segs, r, z0, z1, rot):
|
|
56
|
+
verts, norms = [], []
|
|
57
|
+
angles = np.linspace(0, 2 * np.pi, segs, endpoint=False)
|
|
58
|
+
tip = rot @ [0, 0, z1]
|
|
59
|
+
slope = r / (z1 - z0)
|
|
60
|
+
base_center = rot @ [0, 0, z0]
|
|
61
|
+
base_n = rot @ [0, 0, -1.0]
|
|
62
|
+
for i in range(segs):
|
|
63
|
+
j = (i + 1) % segs
|
|
64
|
+
c0, s0 = np.cos(angles[i]), np.sin(angles[i])
|
|
65
|
+
c1, s1 = np.cos(angles[j]), np.sin(angles[j])
|
|
66
|
+
p0 = rot @ [r * c0, r * s0, z0]
|
|
67
|
+
p1 = rot @ [r * c1, r * s1, z0]
|
|
68
|
+
raw0 = np.array([c0, s0, slope])
|
|
69
|
+
raw1 = np.array([c1, s1, slope])
|
|
70
|
+
n0 = rot @ raw0 / np.linalg.norm(raw0)
|
|
71
|
+
n1 = rot @ raw1 / np.linalg.norm(raw1)
|
|
72
|
+
nt = (n0 + n1)
|
|
73
|
+
nt /= np.linalg.norm(nt)
|
|
74
|
+
verts += [p0, p1, tip]
|
|
75
|
+
norms += [n0, n1, nt]
|
|
76
|
+
verts += [base_center, p1, p0]
|
|
77
|
+
norms += [base_n, base_n, base_n]
|
|
78
|
+
return verts, norms
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _generate_arrows():
|
|
82
|
+
"""Return (positions, normals, colors) flat float32 arrays for 3 arrows."""
|
|
83
|
+
axes = [
|
|
84
|
+
([1, 0, 0], [0.85, 0.20, 0.20, 1.0]),
|
|
85
|
+
([0, 1, 0], [0.20, 0.75, 0.20, 1.0]),
|
|
86
|
+
([0, 0, 1], [0.25, 0.35, 0.90, 1.0]),
|
|
87
|
+
]
|
|
88
|
+
segs = 12
|
|
89
|
+
shaft_r, shaft_len = 0.018, 0.65
|
|
90
|
+
head_r, head_len = 0.055, 0.28
|
|
91
|
+
all_v, all_n, all_c = [], [], []
|
|
92
|
+
for d, col in axes:
|
|
93
|
+
rot = _rotation_to_direction(d)
|
|
94
|
+
sv, sn = _cylinder_tris(segs, shaft_r, 0, shaft_len, rot)
|
|
95
|
+
cv, cn = _cone_tris(segs, head_r, shaft_len, shaft_len + head_len, rot)
|
|
96
|
+
n = len(sv) + len(cv)
|
|
97
|
+
all_v += sv + cv
|
|
98
|
+
all_n += sn + cn
|
|
99
|
+
all_c += [col] * n
|
|
100
|
+
return (
|
|
101
|
+
np.array(all_v, dtype=np.float32).flatten(),
|
|
102
|
+
np.array(all_n, dtype=np.float32).flatten(),
|
|
103
|
+
np.array(all_c, dtype=np.float32).flatten(),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _generate_cube_faces(h=0.45):
|
|
108
|
+
"""Return (positions, normals, colors) for 6 coloured cube faces."""
|
|
109
|
+
face_defs = [
|
|
110
|
+
([[h, -h, -h], [h, h, -h], [h, h, h], [h, -h, h]], [1, 0, 0], [0.82, 0.55, 0.55, 1.0]),
|
|
111
|
+
([[-h, -h, h], [-h, h, h], [-h, h, -h], [-h, -h, -h]], [-1, 0, 0], [0.58, 0.40, 0.40, 1.0]),
|
|
112
|
+
([[-h, h, -h], [h, h, -h], [h, h, h], [-h, h, h]], [0, 1, 0], [0.55, 0.82, 0.55, 1.0]),
|
|
113
|
+
([[h, -h, -h], [-h, -h, -h], [-h, -h, h], [h, -h, h]], [0, -1, 0], [0.40, 0.58, 0.40, 1.0]),
|
|
114
|
+
([[-h, -h, h], [h, -h, h], [h, h, h], [-h, h, h]], [0, 0, 1], [0.55, 0.55, 0.82, 1.0]),
|
|
115
|
+
([[h, -h, -h], [-h, -h, -h], [-h, h, -h], [h, h, -h]], [0, 0, -1], [0.40, 0.40, 0.58, 1.0]),
|
|
116
|
+
]
|
|
117
|
+
all_v, all_n, all_c = [], [], []
|
|
118
|
+
for corners, normal, color in face_defs:
|
|
119
|
+
c = [np.array(p, dtype=np.float64) for p in corners]
|
|
120
|
+
all_v += [c[0], c[1], c[2], c[0], c[2], c[3]]
|
|
121
|
+
all_n += [normal] * 6
|
|
122
|
+
all_c += [color] * 6
|
|
123
|
+
return (
|
|
124
|
+
np.array(all_v, dtype=np.float32).flatten(),
|
|
125
|
+
np.array(all_n, dtype=np.float32).flatten(),
|
|
126
|
+
np.array(all_c, dtype=np.float32).flatten(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _generate_cube_edges(h=0.45):
|
|
131
|
+
"""Return flat float32 array of edge pairs (p1 xyz, p2 xyz) x 12 edges."""
|
|
132
|
+
v = [
|
|
133
|
+
[-h, -h, -h], [h, -h, -h], [h, h, -h], [-h, h, -h],
|
|
134
|
+
[-h, -h, h], [h, -h, h], [h, h, h], [-h, h, h],
|
|
135
|
+
]
|
|
136
|
+
edges = [
|
|
137
|
+
(0, 1), (1, 2), (2, 3), (3, 0),
|
|
138
|
+
(4, 5), (5, 6), (6, 7), (7, 4),
|
|
139
|
+
(0, 4), (1, 5), (2, 6), (3, 7),
|
|
140
|
+
]
|
|
141
|
+
data = []
|
|
142
|
+
for a, b in edges:
|
|
143
|
+
data += v[a] + v[b]
|
|
144
|
+
return np.array(data, dtype=np.float32)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Renderers
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class GizmoMeshRenderer(Renderer):
|
|
153
|
+
"""Renders a triangle mesh in a fixed corner, rotated by camera."""
|
|
154
|
+
|
|
155
|
+
select_entry_point: str = ""
|
|
156
|
+
|
|
157
|
+
def __init__(self, positions, normals, colors, corner, scale, label="GizmoMesh"):
|
|
158
|
+
super().__init__(label=label)
|
|
159
|
+
self._raw_pos = positions
|
|
160
|
+
self._raw_nrm = normals
|
|
161
|
+
self._raw_col = colors
|
|
162
|
+
self._corner = corner
|
|
163
|
+
self._scale = scale
|
|
164
|
+
self._pos_buf = None
|
|
165
|
+
self._nrm_buf = None
|
|
166
|
+
self._col_buf = None
|
|
167
|
+
self._uni_buf = None
|
|
168
|
+
|
|
169
|
+
def update(self, options: RenderOptions):
|
|
170
|
+
self.n_vertices = len(self._raw_pos) // 3
|
|
171
|
+
self._pos_buf = buffer_from_array(self._raw_pos, label="gizmo_pos", reuse=self._pos_buf)
|
|
172
|
+
self._nrm_buf = buffer_from_array(self._raw_nrm, label="gizmo_nrm", reuse=self._nrm_buf)
|
|
173
|
+
self._col_buf = buffer_from_array(self._raw_col, label="gizmo_col", reuse=self._col_buf)
|
|
174
|
+
uni = np.array([self._corner[0], self._corner[1], self._scale, 0], dtype=np.float32)
|
|
175
|
+
self._uni_buf = uniform_from_array(uni, label="gizmo_uni", reuse=self._uni_buf)
|
|
176
|
+
|
|
177
|
+
def get_shader_code(self):
|
|
178
|
+
return read_shader_file("gizmo_mesh.wgsl")
|
|
179
|
+
|
|
180
|
+
def get_bindings(self):
|
|
181
|
+
return [
|
|
182
|
+
BufferBinding(90, self._pos_buf),
|
|
183
|
+
BufferBinding(91, self._nrm_buf),
|
|
184
|
+
BufferBinding(92, self._col_buf),
|
|
185
|
+
UniformBinding(93, self._uni_buf),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
def get_bounding_box(self):
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class GizmoEdgesRenderer(Renderer):
|
|
193
|
+
"""Renders thick lines in a fixed corner, rotated by camera."""
|
|
194
|
+
|
|
195
|
+
n_vertices: int = 4
|
|
196
|
+
topology: PrimitiveTopology = PrimitiveTopology.triangle_strip
|
|
197
|
+
select_entry_point: str = ""
|
|
198
|
+
|
|
199
|
+
def __init__(self, edge_data, corner, scale, thickness=0.003, label="GizmoEdges"):
|
|
200
|
+
super().__init__(label=label)
|
|
201
|
+
self._raw_edges = edge_data
|
|
202
|
+
self._corner = corner
|
|
203
|
+
self._scale = scale
|
|
204
|
+
self._thickness = thickness
|
|
205
|
+
self._edge_buf = None
|
|
206
|
+
self._uni_buf = None
|
|
207
|
+
|
|
208
|
+
def update(self, options: RenderOptions):
|
|
209
|
+
self.n_instances = len(self._raw_edges) // 6
|
|
210
|
+
self._edge_buf = buffer_from_array(self._raw_edges, label="gizmo_edges", reuse=self._edge_buf)
|
|
211
|
+
uni = np.array([self._corner[0], self._corner[1], self._scale, self._thickness], dtype=np.float32)
|
|
212
|
+
self._uni_buf = uniform_from_array(uni, label="gizmo_edge_uni", reuse=self._uni_buf)
|
|
213
|
+
|
|
214
|
+
def get_shader_code(self):
|
|
215
|
+
return read_shader_file("gizmo_edges.wgsl")
|
|
216
|
+
|
|
217
|
+
def get_bindings(self):
|
|
218
|
+
return [
|
|
219
|
+
BufferBinding(90, self._edge_buf),
|
|
220
|
+
UniformBinding(93, self._uni_buf),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
def get_bounding_box(self):
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Public API
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
_ARROW_COLORS = [
|
|
232
|
+
[0.85, 0.20, 0.20, 1.0],
|
|
233
|
+
[0.20, 0.75, 0.20, 1.0],
|
|
234
|
+
[0.25, 0.35, 0.90, 1.0],
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
_AXES_CORNER = (-0.78, -0.78)
|
|
238
|
+
_AXES_SCALE = 0.15
|
|
239
|
+
_CUBE_CORNER = (0.78, -0.78)
|
|
240
|
+
_CUBE_SCALE = 0.13
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CoordinateAxes(MultipleRenderer):
|
|
244
|
+
"""3D arrows with X/Y/Z labels in the bottom-left corner."""
|
|
245
|
+
|
|
246
|
+
def __init__(self):
|
|
247
|
+
pos, nrm, col = _generate_arrows()
|
|
248
|
+
tip_offset = 1.12
|
|
249
|
+
self.arrows = GizmoMeshRenderer(pos, nrm, col, _AXES_CORNER, _AXES_SCALE, "Arrows")
|
|
250
|
+
self.labels = Labels(
|
|
251
|
+
labels=["X", "Y", "Z"],
|
|
252
|
+
positions=[[tip_offset, 0, 0], [0, tip_offset, 0], [0, 0, tip_offset]],
|
|
253
|
+
colors=_ARROW_COLORS,
|
|
254
|
+
overlay={"corner": _AXES_CORNER, "scale": _AXES_SCALE},
|
|
255
|
+
h_align="center",
|
|
256
|
+
v_align="center",
|
|
257
|
+
font_size=16,
|
|
258
|
+
)
|
|
259
|
+
super().__init__([self.arrows, self.labels])
|
|
260
|
+
|
|
261
|
+
def get_bounding_box(self):
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class NavigationCube(MultipleRenderer):
|
|
266
|
+
"""Coloured orientation cube with edge wireframe and face labels."""
|
|
267
|
+
|
|
268
|
+
FACE_VIEWS = ["yz", "yz_flip", "xz", "xz_flip", "xy", "xy_flip"]
|
|
269
|
+
|
|
270
|
+
def __init__(self):
|
|
271
|
+
h = 0.45
|
|
272
|
+
fp, fn, fc = _generate_cube_faces(h)
|
|
273
|
+
edges = _generate_cube_edges(h)
|
|
274
|
+
self.faces = GizmoMeshRenderer(fp, fn, fc, _CUBE_CORNER, _CUBE_SCALE, "CubeFaces")
|
|
275
|
+
self.faces.select_entry_point = "fragment_select"
|
|
276
|
+
self.edges = GizmoEdgesRenderer(edges, _CUBE_CORNER, _CUBE_SCALE, thickness=0.003, label="CubeEdges")
|
|
277
|
+
|
|
278
|
+
label_offset = h + 0.08
|
|
279
|
+
self.labels = Labels(
|
|
280
|
+
labels=["X", "x", "Y", "y", "Z", "z"],
|
|
281
|
+
positions=[
|
|
282
|
+
[label_offset, 0, 0], [-label_offset, 0, 0],
|
|
283
|
+
[0, label_offset, 0], [0, -label_offset, 0],
|
|
284
|
+
[0, 0, label_offset], [0, 0, -label_offset],
|
|
285
|
+
],
|
|
286
|
+
normals=[
|
|
287
|
+
[1, 0, 0], [-1, 0, 0],
|
|
288
|
+
[0, 1, 0], [0, -1, 0],
|
|
289
|
+
[0, 0, 1], [0, 0, -1],
|
|
290
|
+
],
|
|
291
|
+
colors=[
|
|
292
|
+
[0.75, 0.15, 0.15, 1.0], [0.45, 0.25, 0.25, 1.0],
|
|
293
|
+
[0.15, 0.65, 0.15, 1.0], [0.25, 0.45, 0.25, 1.0],
|
|
294
|
+
[0.15, 0.25, 0.80, 1.0], [0.25, 0.25, 0.45, 1.0],
|
|
295
|
+
],
|
|
296
|
+
overlay={"corner": _CUBE_CORNER, "scale": _CUBE_SCALE},
|
|
297
|
+
h_align="center",
|
|
298
|
+
v_align="center",
|
|
299
|
+
font_size=14,
|
|
300
|
+
)
|
|
301
|
+
super().__init__([self.faces, self.edges, self.labels])
|
|
302
|
+
|
|
303
|
+
def get_bounding_box(self):
|
|
304
|
+
return None
|