webgpu 1.2.4.dev0__py3-none-any.whl → 1.2.7__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
@@ -2,6 +2,8 @@ from .clipping import Clipping
2
2
  from .colormap import Colormap, Colorbar
3
3
  from .font import Font
4
4
  from .gizmo import CoordinateAxes, NavigationCube
5
+ from .export.format import Interaction
6
+ from .gui_param import GuiParam
5
7
  from .renderer import Renderer
6
8
  from .scene import Scene
7
9
  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.4.dev0'
22
- __version_tuple__ = version_tuple = (1, 2, 4, 'dev0')
21
+ __version__ = version = '1.2.7'
22
+ __version_tuple__ = version_tuple = (1, 2, 7)
23
23
 
24
- __commit_id__ = commit_id = 'ge3c4870ec'
24
+ __commit_id__ = commit_id = 'g255121a87'
webgpu/camera.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
 
3
3
  from .uniforms import BaseBinding, Binding, UniformBase, ct
4
- from .utils import read_shader_file
4
+ from .utils import read_shader_file, Lock
5
5
 
6
6
 
7
7
  class CameraUniforms(UniformBase):
@@ -21,6 +21,70 @@ class CameraUniforms(UniformBase):
21
21
  ("padding", ct.c_uint32),
22
22
  ]
23
23
 
24
+ def update(self, transform, canvas):
25
+ """Recompute projection/model-view matrices from transform and canvas dimensions.
26
+
27
+ Returns (model_view_proj, model_view) matrices, or (None, None) if canvas is unavailable.
28
+ """
29
+ if canvas is None or canvas.height == 0:
30
+ return None, None
31
+
32
+ near = 0.1
33
+ far = 10
34
+ fov = 45
35
+ aspect = canvas.width / canvas.height
36
+
37
+ zoom = 1.0
38
+ top = near * (np.tan(np.radians(fov) / 2)) * zoom
39
+ height = 2 * top
40
+ width = aspect * height
41
+ left = -0.5 * width
42
+ right = left + width
43
+ bottom = top - height
44
+
45
+ x = 2 * near / (right - left)
46
+ y = 2 * near / (top - bottom)
47
+
48
+ a = (right + left) / (right - left)
49
+ b = (top + bottom) / (top - bottom)
50
+
51
+ c = -far / (far - near)
52
+ d = (-far * near) / (far - near)
53
+
54
+ proj_mat = np.array(
55
+ [
56
+ [x, 0, a, 0],
57
+ [0, y, b, 0],
58
+ [0, 0, c, d],
59
+ [0, 0, -1, 0],
60
+ ]
61
+ )
62
+
63
+ view_mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, -3], [0, 0, 0, 1]])
64
+ model_view = view_mat @ transform.mat
65
+ model_view_proj = proj_mat @ model_view
66
+ normal_mat = np.linalg.inv(model_view)
67
+
68
+ self.aspect = aspect
69
+ self.view[:] = view_mat.transpose().flatten()
70
+ self.model_view[:] = model_view.transpose().flatten()
71
+ self.model_view_projection[:] = model_view_proj.transpose().flatten()
72
+ self.normal_mat[:] = normal_mat.flatten()
73
+
74
+ # Extract pure rotation from model_view (upper-left 3x3, normalize columns to remove scale)
75
+ mv33 = model_view[:3, :3]
76
+ col_norms = np.linalg.norm(mv33, axis=0)
77
+ col_norms[col_norms == 0] = 1.0
78
+ rot33 = mv33 / col_norms
79
+ rot_mat4 = np.identity(4)
80
+ rot_mat4[:3, :3] = rot33
81
+ self.rot_mat[:] = rot_mat4.transpose().flatten()
82
+ self.width = canvas.width
83
+ self.height = canvas.height
84
+ self.update_buffer()
85
+
86
+ return model_view_proj, model_view
87
+
24
88
 
25
89
  class Transform:
26
90
  """3D transform with translation/rotation/scale around a configurable center."""
@@ -143,95 +207,121 @@ class Transform:
143
207
  def _centering(self, center):
144
208
  return self._CenteringContext(self, center)
145
209
 
210
+
146
211
  class Camera:
147
- """Interactive camera that maps a 3D scene to clip space and updates GPU uniforms."""
212
+ """Interactive camera that holds a transform and notifies registered observers on change."""
148
213
 
149
214
  def __init__(self):
150
- self.uniforms = None
151
- self.canvas = None
152
215
  self.transform = Transform()
153
- self._render_function = None
154
- self._get_position_function = None
216
+ self._observers = []
217
+ self._observers_lock = Lock()
155
218
  self._is_moving = False
156
219
  self._is_rotating = False
220
+ self._registered_handlers = {}
157
221
 
158
222
  def __setstate__(self, state):
159
223
  """Restore pickled camera state (only the transform)."""
160
224
  self.transform = state["transform"]
161
- self.canvas = None
162
- self.uniforms = None
163
- self._render_function = None
164
- self._get_position_function = None
225
+ self._observers = []
226
+ self._observers_lock = Lock()
165
227
  self._is_moving = False
166
228
  self._is_rotating = False
229
+ self._registered_handlers = {}
167
230
 
168
231
  def __getstate__(self):
169
232
  """Return a minimal picklable representation of the camera."""
170
233
  return {"transform": self.transform}
171
234
 
235
+ def register_observer(self, callback):
236
+ """Register a callback to be called when the camera transform changes.
237
+
238
+ Idempotent: registering the same callback twice has no additional effect.
239
+ """
240
+ with self._observers_lock:
241
+ if callback not in self._observers:
242
+ self._observers.append(callback)
243
+
244
+ def unregister_observer(self, callback):
245
+ """Remove a previously registered observer callback."""
246
+ with self._observers_lock:
247
+ old_len = len(self._observers)
248
+ self._observers = [cb for cb in self._observers if cb is not callback]
249
+
250
+ def _notify_observers(self):
251
+ """Notify all registered observers that the transform has changed."""
252
+ with self._observers_lock:
253
+ observers = list(self._observers)
254
+ for cb in observers:
255
+ cb()
256
+
172
257
  def reset(self, pmin, pmax):
173
- """Fit the camera to the axis-aligned box [pmin, pmax] and update uniforms."""
258
+ """Fit the camera to the axis-aligned box [pmin, pmax] and notify observers."""
174
259
  self.transform.init(pmin, pmax)
175
- self._update_uniforms()
260
+ self._notify_observers()
176
261
 
177
262
  def reset_xy(self, flip: bool = False):
178
- """Reset to a top-down XY view of the current center and update uniforms."""
263
+ """Reset to a top-down XY view and notify observers."""
179
264
  self.transform.reset_xy(flip)
180
- self._update_uniforms()
265
+ self._notify_observers()
181
266
 
182
267
  def reset_xz(self, flip: bool = False):
183
- """Reset to an XZ view of the current center and update uniforms."""
268
+ """Reset to an XZ view and notify observers."""
184
269
  self.transform.reset_xz(flip)
185
- self._update_uniforms()
270
+ self._notify_observers()
186
271
 
187
272
  def reset_yz(self, flip: bool = False):
188
- """Reset to a YZ view of the current center and update uniforms."""
273
+ """Reset to a YZ view and notify observers."""
189
274
  self.transform.reset_yz(flip)
190
- self._update_uniforms()
191
-
192
- def set_canvas(self, canvas):
193
- """Attach the camera to a canvas, register resize handling, and allocate uniforms."""
194
- self.canvas = canvas
195
- canvas.on_resize(self._update_uniforms)
196
- if self.uniforms is None:
197
- self.uniforms = CameraUniforms()
198
- self._update_uniforms()
199
-
200
- def get_bindings(self) -> list[BaseBinding]:
201
- return self.uniforms.get_bindings()
275
+ self._notify_observers()
202
276
 
203
277
  def get_shader_code(self):
204
278
  return read_shader_file("camera.wgsl")
205
279
 
206
- def __del__(self):
207
- del self.uniforms
208
-
209
- def set_render_functions(self, redraw_function, get_position_function=None):
210
- """Install callbacks for triggering redraws and mapping screen to world positions."""
211
- self._render_function = redraw_function
212
- self._get_position_function = get_position_function
213
-
214
- def register_callbacks(self, input_handler):
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)
280
+ def register_callbacks(self, input_handler, get_position_fn=None):
281
+ """Register mouse/wheel handlers on the given input_handler.
282
+
283
+ Idempotent: unregisters existing handlers for this input_handler first.
284
+ get_position_fn is used for dblclick center-on-point (per-canvas).
285
+ """
286
+ # Unregister first to avoid duplicates
287
+ self.unregister_callbacks(input_handler)
288
+
289
+ def on_dblclick(ev):
290
+ if get_position_fn:
291
+ p = get_position_fn(ev["canvasX"], ev["canvasY"])
292
+ if p is not None:
293
+ self.transform.set_center(p)
294
+ self._notify_observers()
295
+
296
+ handlers = {
297
+ 'mousedown': self._on_mousedown,
298
+ 'mouseup': self._on_mouseup,
299
+ 'mouseout': self._on_mouseup,
300
+ 'mousemove': self._on_mousemove,
301
+ 'wheel': self._on_wheel,
302
+ 'dblclick': on_dblclick,
303
+ }
304
+ self._registered_handlers[id(input_handler)] = handlers
305
+
306
+ input_handler.on_mousedown(handlers['mousedown'], ctrl=False, shift=False, alt=False)
307
+ input_handler.on_mouseup(handlers['mouseup'], ctrl=False, shift=False, alt=False)
308
+ input_handler.on_mouseout(handlers['mouseout'], ctrl=False, shift=False, alt=False)
309
+ input_handler.on_mousemove(handlers['mousemove'], ctrl=False, shift=False, alt=False)
310
+ input_handler.on_dblclick(handlers['dblclick'], ctrl=False, shift=False, alt=False)
311
+ input_handler.on_wheel(handlers['wheel'], ctrl=False, shift=False, alt=False)
221
312
 
222
313
  def unregister_callbacks(self, input_handler):
223
- input_handler.unregister("mousedown", self._on_mousedown)
224
- input_handler.unregister("mouseup", self._on_mouseup)
225
- input_handler.unregister("mouseout", self._on_mouseup)
226
- input_handler.unregister("mousemove", self._on_mousemove)
227
- input_handler.unregister("dblclick", self._on_dblclick)
228
- input_handler.unregister("wheel", self._on_wheel)
229
-
230
- def _on_dblclick(self, ev):
231
- p = self._get_event_position(ev["canvasX"], ev["canvasY"])
232
- if p is not None:
233
- self.transform.set_center(p)
234
- self._render()
314
+ """Remove previously registered handlers from the given input_handler."""
315
+ key = id(input_handler)
316
+ handlers = self._registered_handlers.pop(key, None)
317
+ if handlers is None:
318
+ return
319
+ input_handler.unregister("mousedown", handlers['mousedown'])
320
+ input_handler.unregister("mouseup", handlers['mouseup'])
321
+ input_handler.unregister("mouseout", handlers['mouseout'])
322
+ input_handler.unregister("mousemove", handlers['mousemove'])
323
+ input_handler.unregister("dblclick", handlers['dblclick'])
324
+ input_handler.unregister("wheel", handlers['wheel'])
235
325
 
236
326
  def _on_mousedown(self, ev):
237
327
  if ev["button"] == 0:
@@ -242,15 +332,10 @@ class Camera:
242
332
  def _on_mouseup(self, _):
243
333
  self._is_moving = False
244
334
  self._is_rotating = False
245
- self._is_zooming = False
246
335
 
247
336
  def _on_wheel(self, ev):
248
- if ev["shiftKey"]:
249
- p = self._get_event_position(ev["canvasX"], ev["canvasY"])
250
- else:
251
- p = self.transform._center
252
- self.transform.scale(1 - ev["deltaY"] / 1000, p)
253
- self._render()
337
+ self.transform.scale(1 - ev["deltaY"] / 1000, self.transform._center)
338
+ self._notify_observers()
254
339
  if hasattr(ev, "preventDefault"):
255
340
  ev.preventDefault()
256
341
 
@@ -258,82 +343,8 @@ class Camera:
258
343
  if self._is_rotating:
259
344
  s = 0.3
260
345
  self.transform.rotate(s * ev["movementY"], s * ev["movementX"])
261
- self._render()
262
- if self._is_moving:
346
+ self._notify_observers()
347
+ elif self._is_moving:
263
348
  s = 0.01
264
349
  self.transform.translate(s * ev["movementX"], -s * ev["movementY"])
265
- self._render()
266
-
267
- def _render(self):
268
- self._update_uniforms()
269
- if self._render_function:
270
- self._render_function()
271
-
272
- def _get_event_position(self, x, y):
273
- if self._get_position_function:
274
- return self._get_position_function(x, y)
275
- return None
276
-
277
- def _update_uniforms(self):
278
- """Recompute projection/model-view matrices and write them into the GPU uniforms."""
279
- if self.canvas is None:
280
- return
281
- if self.uniforms is None:
282
- self.uniforms = CameraUniforms()
283
- near = 0.1
284
- far = 10
285
- fov = 45
286
- if self.canvas.height == 0:
287
- aspect = 800 / 600
288
- else:
289
- aspect = self.canvas.width / self.canvas.height
290
-
291
- zoom = 1.0
292
- top = near * (np.tan(np.radians(fov) / 2)) * zoom
293
- height = 2 * top
294
- width = aspect * height
295
- left = -0.5 * width
296
- right = left + width
297
- bottom = top - height
298
-
299
- x = 2 * near / (right - left)
300
- y = 2 * near / (top - bottom)
301
-
302
- a = (right + left) / (right - left)
303
- b = (top + bottom) / (top - bottom)
304
-
305
- c = -far / (far - near)
306
- d = (-far * near) / (far - near)
307
-
308
- proj_mat = np.array(
309
- [
310
- [x, 0, a, 0],
311
- [0, y, b, 0],
312
- [0, 0, c, d],
313
- [0, 0, -1, 0],
314
- ]
315
- )
316
-
317
- view_mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, -3], [0, 0, 0, 1]])
318
- model_view = view_mat @ self.transform.mat
319
- model_view_proj = proj_mat @ model_view
320
- self.model_view = model_view
321
- normal_mat = np.linalg.inv(model_view)
322
- self.model_view_proj = model_view_proj
323
-
324
- self.uniforms.aspect = aspect
325
- self.uniforms.view[:] = view_mat.transpose().flatten()
326
- self.uniforms.model_view[:] = model_view.transpose().flatten()
327
- self.uniforms.model_view_projection[:] = model_view_proj.transpose().flatten()
328
- self.uniforms.normal_mat[:] = normal_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()
337
- self.uniforms.width = self.canvas.width
338
- self.uniforms.height = self.canvas.height
339
- self.uniforms.update_buffer()
350
+ self._notify_observers()
webgpu/canvas.py CHANGED
@@ -22,73 +22,111 @@ _TARGET_FPS = 60
22
22
  # running: bool = False
23
23
  # pending: bool = False
24
24
 
25
+ class _InstanceDebounce:
26
+ """Descriptor that creates per-instance debounced methods.
27
+
28
+ When used as a decorator on a method, each instance gets its own
29
+ debounce state (lock, timer, last_call, pending) so that calls on
30
+ different instances never interfere with each other.
31
+ """
32
+
33
+ def __init__(self, fn, rate_hz=60):
34
+ self._fn = fn
35
+ self._rate_hz = rate_hz
36
+ self._attr = f"_debounce_{fn.__name__}"
37
+ functools.update_wrapper(self, fn)
38
+
39
+ def __set_name__(self, owner, name):
40
+ self._attr = f"_debounce_{name}"
41
+
42
+ def __get__(self, obj, objtype=None):
43
+ if obj is None:
44
+ return self
45
+ # Return a bound debounced callable for this instance
46
+ state = getattr(obj, self._attr, None)
47
+ if state is None:
48
+ state = _DebounceState(self._fn, obj, self._rate_hz)
49
+ object.__setattr__(obj, self._attr, state)
50
+ return state
51
+
52
+
53
+ class _DebounceState:
54
+ """Per-instance debounce state and callable."""
55
+
56
+ __slots__ = ("_fn", "_obj", "_interval", "_lock", "_last_call", "_timer", "_pending")
57
+
58
+ def __init__(self, fn, obj, rate_hz):
59
+ self._fn = fn
60
+ self._obj = obj
61
+ self._interval = 1.0 / rate_hz
62
+ self._lock = threading.RLock()
63
+ self._last_call = 0.0
64
+ self._timer = None
65
+ self._pending = None
66
+
67
+ def _original(self, *args, **kwargs):
68
+ """Bypass debounce and call the underlying function immediately."""
69
+ return self._fn(self._obj, *args, **kwargs)
70
+
71
+ def _schedule(self, delay):
72
+ self._timer = threading.Timer(delay, self._run_pending)
73
+ self._timer.daemon = True
74
+ self._timer.start()
75
+
76
+ def _run_pending(self):
77
+ with self._lock:
78
+ if self._pending is None:
79
+ self._timer = None
80
+ return
81
+
82
+ args, kwargs = self._pending
83
+ self._pending = None
84
+ self._last_call = time.monotonic()
85
+
86
+ self._fn(self._obj, *args, **kwargs)
87
+
88
+ with self._lock:
89
+ self._timer = None
90
+ if self._pending is not None:
91
+ delay = max(0.0, self._interval - (time.monotonic() - self._last_call))
92
+ self._schedule(delay)
93
+
94
+ def __call__(self, *args, **kwargs):
95
+ with self._lock:
96
+ now = time.monotonic()
97
+ elapsed = now - self._last_call
98
+ if elapsed >= self._interval and self._timer is None:
99
+ self._last_call = now
100
+ run_now = True
101
+ else:
102
+ self._pending = (args, kwargs)
103
+ run_now = False
104
+
105
+ if self._timer is None:
106
+ self._schedule(max(0.0, self._interval - elapsed))
107
+
108
+ if run_now:
109
+ self._fn(self._obj, *args, **kwargs)
110
+
111
+
25
112
  def debounce(arg=None, *, rate_hz=60):
26
113
 
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
46
- return
47
-
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
72
- else:
73
- pending = (args, kwargs)
74
- run_now = False
75
-
76
- if timer is None:
77
- schedule(max(0.0, interval - elapsed))
78
-
79
- if run_now:
80
- fn(*args, **kwargs)
81
-
82
- return wrapper
114
+ if platform.is_pyodide:
115
+ if callable(arg):
116
+ return arg
117
+ def _rate_limited(fn, *args, **kwargs):
118
+ return fn
119
+
120
+ return _rate_limited
83
121
 
84
122
  if callable(arg):
85
- return _rate_limited(arg, rate_hz)
123
+ return _InstanceDebounce(arg, rate_hz)
86
124
 
87
125
  if arg is not None:
88
126
  rate_hz = arg
89
127
 
90
128
  def decorate(fn):
91
- return _rate_limited(fn, rate_hz)
129
+ return _InstanceDebounce(fn, rate_hz)
92
130
  return decorate
93
131
 
94
132
 
@@ -217,6 +255,7 @@ class Canvas:
217
255
 
218
256
  self._on_resize_callbacks = []
219
257
  self._on_update_html_canvas = []
258
+ self._on_visibility_callbacks = []
220
259
 
221
260
  self._resize_observer = None
222
261
  self._intersection_observer = None
@@ -281,8 +320,7 @@ class Canvas:
281
320
  "device": self.device.handle,
282
321
  "format": self.format,
283
322
  "alphaMode": "premultiplied",
284
- "sampleCount": self.multisample.count,
285
- "usage": TextureUsage.RENDER_ATTACHMENT | TextureUsage.COPY_DST,
323
+ "usage": TextureUsage.RENDER_ATTACHMENT | TextureUsage.COPY_DST | TextureUsage.COPY_SRC,
286
324
  }
287
325
  )
288
326
 
@@ -293,6 +331,8 @@ class Canvas:
293
331
  if observer_entry[0].isIntersecting:
294
332
  for func in self._on_resize_callbacks:
295
333
  func()
334
+ for func in self._on_visibility_callbacks:
335
+ func(observer_entry[0].isIntersecting)
296
336
 
297
337
  if self._resize_observer is not None:
298
338
  self._resize_observer.disconnect()
@@ -322,6 +362,10 @@ class Canvas:
322
362
  def on_resize(self, func: Callable):
323
363
  self._on_resize_callbacks.append(func)
324
364
 
365
+ def on_visibility(self, func: Callable):
366
+ """Register callback for visibility changes. Called with True/False."""
367
+ self._on_visibility_callbacks.append(func)
368
+
325
369
  def on_update_html_canvas(self, func: Callable):
326
370
  self._on_update_html_canvas.append(func)
327
371
 
@@ -359,8 +403,9 @@ class Canvas:
359
403
  if canvas is None:
360
404
  return
361
405
  rect = canvas.getBoundingClientRect()
362
- width = int(rect.width)
363
- height = int(rect.height)
406
+ dpr = getattr(platform.js.window, 'devicePixelRatio', 1) or 1
407
+ width = round(rect.width * dpr)
408
+ height = round(rect.height * dpr)
364
409
 
365
410
  if width == self.width and height == self.height:
366
411
  for func in self._on_resize_callbacks:
webgpu/clipping.py CHANGED
@@ -79,20 +79,8 @@ class Clipping(BaseRenderer):
79
79
  return None
80
80
 
81
81
  def __del__(self):
82
- if hasattr(self, "uniforms"):
83
- self.uniforms._buffer.destroy()
84
-
85
- def add_options_to_gui(self, gui):
86
- if gui is None:
87
- return
88
- folder = gui.folder("Clipping", closed=True)
89
- folder.checkbox("enabled", self.mode != self.Mode.DISABLED, self.enable_clipping)
90
- folder.value("x", self.center[0], self.set_x_value)
91
- folder.value("y", self.center[1], self.set_y_value)
92
- folder.value("z", self.center[2], self.set_z_value)
93
- folder.value("nx", self.normal[0], self.set_nx_value)
94
- folder.value("ny", self.normal[1], self.set_ny_value)
95
- folder.value("nz", self.normal[2], self.set_nz_value)
82
+ # Do NOT auto-destroy: the JS engine may still reference this buffer.
83
+ pass
96
84
 
97
85
  def render(self, options: RenderOptions):
98
86
  pass