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 CHANGED
@@ -1,6 +1,7 @@
1
1
  from .clipping import Clipping
2
2
  from .colormap import Colormap, Colorbar
3
3
  from .font import Font
4
+ from .gizmo import CoordinateAxes, NavigationCube
4
5
  from .renderer import Renderer
5
6
  from .scene import Scene
6
7
  from .labels import Labels
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.1.dev0'
22
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev0')
21
+ __version__ = version = '1.2.3.dev0'
22
+ __version_tuple__ = version_tuple = (1, 2, 3, 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'g7d909a7c5'
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
- pos = self.map_point(self._center)
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.translate(pos[0], pos[1], pos[2])
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=None)
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
- # self.uniforms.rot_mat[:] = self.transform._rot_mat.flatten()
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
- t_last: float | None = None
18
- timer: threading.Timer | None = None
19
-
20
-
21
- def debounce(arg=None):
22
- def decorator(func):
23
- # Render only once every 1/_TARGET_FPS seconds
24
- @functools.wraps(func)
25
- def debounced(obj, *args, **kwargs):
26
- if not hasattr(obj, "_debounce_data"):
27
- obj._debounce_data = {}
28
-
29
- fname = func.__name__
30
- if obj._debounce_data.get(fname, None) is None:
31
- obj._debounce_data[fname] = _DebounceData(None, None)
32
-
33
- data = obj._debounce_data[fname]
34
-
35
- # check if we already have a render scheduled
36
- if platform.is_pyodide:
37
- if data.timer is not None and not data.timer.done():
38
- return
39
- else:
40
- if data.timer is not None:
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
- def f():
44
- # clear the timer, so we can schedule a new one with the next function call
45
- t = time.time()
46
- data.timer = None
47
- if platform.is_pyodide:
48
- # due to async nature, we need to update t_last before calling func
49
- data.t_last = t
50
- func(obj, *args, **kwargs)
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
- data.t_last = t
53
- func(obj, *args, **kwargs)
73
+ pending = (args, kwargs)
74
+ run_now = False
54
75
 
55
- if data.timer is not None:
56
- return
76
+ if timer is None:
77
+ schedule(max(0.0, interval - elapsed))
57
78
 
58
- if data.t_last is None:
59
- # first call -> just call the function immediately
60
- data.t_last = time.time()
61
- f()
62
- return
79
+ if run_now:
80
+ fn(*args, **kwargs)
63
81
 
64
- t_wait = max(1 / target_fps - (time.time() - data.t_last), 0)
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
- target_fps = _TARGET_FPS
81
- return decorator(arg)
82
- else:
83
- target_fps = arg
84
- return decorator
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
- if self._resize_observer is not None:
151
- self._resize_observer.disconnect()
152
- if self._intersection_observer is not None:
153
- self._intersection_observer.disconnect()
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