jswidget 0.1.0__tar.gz

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.
@@ -0,0 +1,17 @@
1
+ # Build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ src/*.egg-info/
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *.so
11
+
12
+ # Jupyter
13
+ .ipynb_checkpoints/
14
+
15
+ # Environment
16
+ .venv/
17
+ venv/
jswidget-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DannyRuijters
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: jswidget
3
+ Version: 0.1.0
4
+ Summary: A lightweight anywidget-based IPython widget that renders arbitrary JavaScript inside Jupyter notebooks
5
+ Project-URL: Homepage, https://github.com/DannyRuijters/JSWidget
6
+ Project-URL: Repository, https://github.com/DannyRuijters/JSWidget
7
+ Author: DannyRuijters
8
+ License: BSD 3-Clause License
9
+
10
+ Copyright (c) 2026, DannyRuijters
11
+
12
+ Redistribution and use in source and binary forms, with or without
13
+ modification, are permitted provided that the following conditions are met:
14
+
15
+ 1. Redistributions of source code must retain the above copyright notice, this
16
+ list of conditions and the following disclaimer.
17
+
18
+ 2. Redistributions in binary form must reproduce the above copyright notice,
19
+ this list of conditions and the following disclaimer in the documentation
20
+ and/or other materials provided with the distribution.
21
+
22
+ 3. Neither the name of the copyright holder nor the names of its
23
+ contributors may be used to endorse or promote products derived from
24
+ this software without specific prior written permission.
25
+
26
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
30
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
31
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
32
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
33
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
34
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ License-File: LICENSE
37
+ Keywords: anywidget,ipython,javascript,jupyter,widget
38
+ Classifier: Development Status :: 4 - Beta
39
+ Classifier: Framework :: Jupyter
40
+ Classifier: Framework :: Jupyter :: JupyterLab
41
+ Classifier: Intended Audience :: Developers
42
+ Classifier: Intended Audience :: Science/Research
43
+ Classifier: License :: OSI Approved :: BSD License
44
+ Classifier: Programming Language :: JavaScript
45
+ Classifier: Programming Language :: Python :: 3
46
+ Classifier: Topic :: Multimedia :: Graphics
47
+ Requires-Python: >=3.8
48
+ Requires-Dist: anywidget
49
+ Requires-Dist: numpy
50
+ Requires-Dist: traitlets
51
+ Description-Content-Type: text/markdown
52
+
53
+ # JSWidget
54
+
55
+ A lightweight [anywidget](https://anywidget.dev/)-based IPython widget that renders arbitrary JavaScript (Canvas 2D, WebGL, etc.) inside Jupyter notebooks. It passes JSON data and binary buffers (mesh vertices, normals, indices) over the ipywidgets comm API, with live-update callbacks so Python can push new data without re-executing the cell.
56
+
57
+ Works in **VS Code**, **JupyterLab**, and **classic Jupyter notebooks** — no extra frontend build step required.
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install jswidget
63
+ ```
64
+
65
+ Or install from source in development mode:
66
+
67
+ ```bash
68
+ git clone https://github.com/DannyRuijters/JSWidget.git
69
+ cd JSWidget
70
+ pip install -e .
71
+ ```
72
+
73
+ ## Quick Start
74
+
75
+ ```python
76
+ from jswidget import JSWidget
77
+
78
+ w = JSWidget(width=600, height=400)
79
+ w.js_code = '''
80
+ const canvas = document.createElement("canvas");
81
+ canvas.width = opts.width;
82
+ canvas.height = opts.height;
83
+ el.appendChild(canvas);
84
+ const ctx = canvas.getContext("2d");
85
+ ctx.fillStyle = "#e94560";
86
+ ctx.font = "24px sans-serif";
87
+ ctx.fillText("Hello from JSWidget!", 20, 50);
88
+ '''
89
+ w.show()
90
+ ```
91
+
92
+ ## JavaScript API
93
+
94
+ The JavaScript code passed to `js_code` receives the following variables:
95
+
96
+ | Variable | Description |
97
+ |---|---|
98
+ | `el` | Container DOM element to render into |
99
+ | `data` | JSON data dict set from Python via `w.data` |
100
+ | `getBuffer(name)` | Returns a named binary buffer as an `ArrayBuffer` |
101
+ | `opts` | `{width, height}` of the widget |
102
+ | `setState(obj)` | Save state that persists across re-renders |
103
+ | `getState()` | Retrieve previously saved state |
104
+ | `onData(fn)` | Register a callback for live `data` updates from Python |
105
+ | `onBuffers(fn)` | Register a callback for live buffer updates from Python |
106
+
107
+ ## Passing Data
108
+
109
+ ### JSON data
110
+
111
+ ```python
112
+ w.data = {'values': [10, 40, 80], 'color': '#4ecdc4'}
113
+ # Update later (triggers onData callbacks in JS):
114
+ w.send_data({'values': [90, 20, 55], 'color': '#ff6b6b'})
115
+ ```
116
+
117
+ ### Binary buffers
118
+
119
+ ```python
120
+ import numpy as np
121
+
122
+ vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
123
+ normals = np.array([[0,0,1],[0,0,1],[0,0,1]], dtype=np.float32)
124
+ indices = np.array([0, 1, 2], dtype=np.uint32)
125
+
126
+ w.set_buffers(vertices=vertices, normals=normals, indices=indices)
127
+ ```
128
+
129
+ Buffers are accessible in JavaScript via `getBuffer('vertices')`, etc., and return an `ArrayBuffer` that can be wrapped in a typed array (e.g. `new Float32Array(getBuffer('vertices'))`).
130
+
131
+ ## Demo Notebook
132
+
133
+ See [JSWidget_demo.ipynb](JSWidget_demo.ipynb) for full examples including:
134
+
135
+ 1. **Canvas 2D drawing** — gradient backgrounds and text rendering
136
+ 2. **Bar chart with live updates** — pass data from Python via `send_data()` and re-draw with `onData()`
137
+ 3. **Interactive WebGL mesh viewer** — sphere and torus rendering with mouse-drag rotation and scroll zoom, using binary buffers for mesh data
138
+
139
+ ## Files
140
+
141
+ | File | Description |
142
+ |---|---|
143
+ | `jswidget.py` | The `JSWidget` class (anywidget-based DOMWidget with ESM frontend) |
144
+ | `JSWidget_demo.ipynb` | Demo notebook with Canvas 2D, bar chart, and WebGL examples |
145
+ | `LICENSE` | BSD 3-Clause License |
146
+
147
+ ## License
148
+
149
+ BSD 3-Clause — see [LICENSE](LICENSE).
@@ -0,0 +1,97 @@
1
+ # JSWidget
2
+
3
+ A lightweight [anywidget](https://anywidget.dev/)-based IPython widget that renders arbitrary JavaScript (Canvas 2D, WebGL, etc.) inside Jupyter notebooks. It passes JSON data and binary buffers (mesh vertices, normals, indices) over the ipywidgets comm API, with live-update callbacks so Python can push new data without re-executing the cell.
4
+
5
+ Works in **VS Code**, **JupyterLab**, and **classic Jupyter notebooks** — no extra frontend build step required.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install jswidget
11
+ ```
12
+
13
+ Or install from source in development mode:
14
+
15
+ ```bash
16
+ git clone https://github.com/DannyRuijters/JSWidget.git
17
+ cd JSWidget
18
+ pip install -e .
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```python
24
+ from jswidget import JSWidget
25
+
26
+ w = JSWidget(width=600, height=400)
27
+ w.js_code = '''
28
+ const canvas = document.createElement("canvas");
29
+ canvas.width = opts.width;
30
+ canvas.height = opts.height;
31
+ el.appendChild(canvas);
32
+ const ctx = canvas.getContext("2d");
33
+ ctx.fillStyle = "#e94560";
34
+ ctx.font = "24px sans-serif";
35
+ ctx.fillText("Hello from JSWidget!", 20, 50);
36
+ '''
37
+ w.show()
38
+ ```
39
+
40
+ ## JavaScript API
41
+
42
+ The JavaScript code passed to `js_code` receives the following variables:
43
+
44
+ | Variable | Description |
45
+ |---|---|
46
+ | `el` | Container DOM element to render into |
47
+ | `data` | JSON data dict set from Python via `w.data` |
48
+ | `getBuffer(name)` | Returns a named binary buffer as an `ArrayBuffer` |
49
+ | `opts` | `{width, height}` of the widget |
50
+ | `setState(obj)` | Save state that persists across re-renders |
51
+ | `getState()` | Retrieve previously saved state |
52
+ | `onData(fn)` | Register a callback for live `data` updates from Python |
53
+ | `onBuffers(fn)` | Register a callback for live buffer updates from Python |
54
+
55
+ ## Passing Data
56
+
57
+ ### JSON data
58
+
59
+ ```python
60
+ w.data = {'values': [10, 40, 80], 'color': '#4ecdc4'}
61
+ # Update later (triggers onData callbacks in JS):
62
+ w.send_data({'values': [90, 20, 55], 'color': '#ff6b6b'})
63
+ ```
64
+
65
+ ### Binary buffers
66
+
67
+ ```python
68
+ import numpy as np
69
+
70
+ vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
71
+ normals = np.array([[0,0,1],[0,0,1],[0,0,1]], dtype=np.float32)
72
+ indices = np.array([0, 1, 2], dtype=np.uint32)
73
+
74
+ w.set_buffers(vertices=vertices, normals=normals, indices=indices)
75
+ ```
76
+
77
+ Buffers are accessible in JavaScript via `getBuffer('vertices')`, etc., and return an `ArrayBuffer` that can be wrapped in a typed array (e.g. `new Float32Array(getBuffer('vertices'))`).
78
+
79
+ ## Demo Notebook
80
+
81
+ See [JSWidget_demo.ipynb](JSWidget_demo.ipynb) for full examples including:
82
+
83
+ 1. **Canvas 2D drawing** — gradient backgrounds and text rendering
84
+ 2. **Bar chart with live updates** — pass data from Python via `send_data()` and re-draw with `onData()`
85
+ 3. **Interactive WebGL mesh viewer** — sphere and torus rendering with mouse-drag rotation and scroll zoom, using binary buffers for mesh data
86
+
87
+ ## Files
88
+
89
+ | File | Description |
90
+ |---|---|
91
+ | `jswidget.py` | The `JSWidget` class (anywidget-based DOMWidget with ESM frontend) |
92
+ | `JSWidget_demo.ipynb` | Demo notebook with Canvas 2D, bar chart, and WebGL examples |
93
+ | `LICENSE` | BSD 3-Clause License |
94
+
95
+ ## License
96
+
97
+ BSD 3-Clause — see [LICENSE](LICENSE).
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jswidget"
7
+ version = "0.1.0"
8
+ description = "A lightweight anywidget-based IPython widget that renders arbitrary JavaScript inside Jupyter notebooks"
9
+ readme = "README.md"
10
+ license = {file = "LICENSE"}
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "DannyRuijters"},
14
+ ]
15
+ keywords = ["jupyter", "widget", "javascript", "anywidget", "ipython"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Framework :: Jupyter",
19
+ "Framework :: Jupyter :: JupyterLab",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: Science/Research",
22
+ "License :: OSI Approved :: BSD License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: JavaScript",
25
+ "Topic :: Multimedia :: Graphics",
26
+ ]
27
+ dependencies = [
28
+ "anywidget",
29
+ "numpy",
30
+ "traitlets",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/DannyRuijters/JSWidget"
35
+ Repository = "https://github.com/DannyRuijters/JSWidget"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/jswidget"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = [
42
+ "src/jswidget/",
43
+ "README.md",
44
+ "LICENSE",
45
+ "pyproject.toml",
46
+ ]
@@ -0,0 +1,21 @@
1
+ """
2
+ JSWidget - A lightweight anywidget-based IPython widget that renders
3
+ arbitrary JavaScript inside Jupyter notebooks.
4
+
5
+ Usage:
6
+ from jswidget import JSWidget
7
+
8
+ w = JSWidget(width=800, height=600)
9
+ w.js_code = '''
10
+ const canvas = document.createElement("canvas");
11
+ canvas.width = opts.width;
12
+ canvas.height = opts.height;
13
+ el.appendChild(canvas);
14
+ '''
15
+ w.show()
16
+ """
17
+
18
+ from jswidget.jswidget import JSWidget
19
+
20
+ __all__ = ["JSWidget"]
21
+ __version__ = "0.1.0"
@@ -0,0 +1,233 @@
1
+ """
2
+ Custom DOMWidget that renders arbitrary JavaScript and passes data
3
+ (including user-defined binary buffers) over the ipywidgets comm API.
4
+
5
+ Uses anywidget for reliable rendering in VS Code, JupyterLab, and classic notebooks.
6
+ No external dependencies beyond anywidget and numpy.
7
+
8
+ Usage:
9
+ from jswidget import JSWidget
10
+ import numpy as np
11
+
12
+ w = JSWidget(width=800, height=600)
13
+
14
+ # Set binary buffers (any name, created dynamically)
15
+ vertices = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
16
+ indices = np.array([0,1,2], dtype=np.uint32)
17
+ w.set_buffers(vertices=vertices, normals=normals, indices=indices)
18
+
19
+ # Set JSON data
20
+ w.data = {'opacity': 0.8, 'color': [1, 0, 0]}
21
+
22
+ # Set JavaScript code and display
23
+ w.js_code = '''
24
+ const canvas = document.createElement('canvas');
25
+ canvas.width = opts.width;
26
+ canvas.height = opts.height;
27
+ el.appendChild(canvas);
28
+ const ctx = canvas.getContext('2d');
29
+
30
+ // Access binary buffers
31
+ const verts = new Float32Array(getBuffer('vertices'));
32
+
33
+ // Access JSON data
34
+ const opacity = data.opacity;
35
+
36
+ // Register for live updates from Python
37
+ onData((newData) => { ... });
38
+ onBuffers((newMeta) => { ... });
39
+
40
+ // Save state to survive re-renders
41
+ setState({rotX: 0.3, rotY: 0.5});
42
+ '''
43
+ w.show()
44
+ """
45
+
46
+ import numpy as np
47
+ import traitlets
48
+ import anywidget
49
+
50
+ # The ESM frontend module - provides the same JS API as jswidget.py
51
+ _ESM = """
52
+ function render({ model, el }) {
53
+ // Create container
54
+ const container = document.createElement('div');
55
+ container.style.width = model.get('width') + 'px';
56
+ container.style.height = model.get('height') + 'px';
57
+ container.style.overflow = 'hidden';
58
+ container.style.position = 'relative';
59
+ el.appendChild(container);
60
+
61
+ let cleanupFn = null;
62
+ const _id = Math.random().toString(36).slice(2, 14);
63
+
64
+ function executeCode() {
65
+ const code = model.get('js_code');
66
+ if (!code) return;
67
+
68
+ // Cleanup previous execution
69
+ if (cleanupFn) {
70
+ try { cleanupFn(); } catch(e) {}
71
+ cleanupFn = null;
72
+ }
73
+ container.innerHTML = '';
74
+
75
+ // State management
76
+ window.__jsw_state = window.__jsw_state || {};
77
+ function setState(obj) { window.__jsw_state[_id] = Object.assign(window.__jsw_state[_id] || {}, obj); }
78
+ function getState() { return window.__jsw_state[_id] || {}; }
79
+
80
+ // opts
81
+ const opts = { width: model.get('width'), height: model.get('height') };
82
+
83
+ // data
84
+ let currentData = model.get('data') || {};
85
+
86
+ // getBuffer
87
+ function getBuffer(name) {
88
+ const raw = model.get('_buf_' + name);
89
+ if (!raw) return null;
90
+ if (raw instanceof DataView) {
91
+ if (raw.byteLength === 0) return null;
92
+ return raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength);
93
+ }
94
+ if (raw instanceof ArrayBuffer) {
95
+ return raw.byteLength === 0 ? null : raw;
96
+ }
97
+ if (raw.buffer instanceof ArrayBuffer) {
98
+ if (raw.byteLength === 0) return null;
99
+ return raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength);
100
+ }
101
+ return null;
102
+ }
103
+
104
+ // Callback registries
105
+ const _dataCallbacks = [];
106
+ const _bufferCallbacks = [];
107
+ function onData(fn) { _dataCallbacks.push(fn); }
108
+ function onBuffers(fn) { _bufferCallbacks.push(fn); }
109
+
110
+ // Wire up model change events to callbacks
111
+ function _onDataChange() {
112
+ currentData = model.get('data') || {};
113
+ _dataCallbacks.forEach(fn => fn(currentData));
114
+ }
115
+ function _onBuffersChange() {
116
+ const meta = model.get('_buffers_metadata') || [];
117
+ _bufferCallbacks.forEach(fn => fn(meta));
118
+ }
119
+ model.on('change:data', _onDataChange);
120
+ model.on('change:_buffers_metadata', _onBuffersChange);
121
+
122
+ // Register cleanup to remove listeners
123
+ cleanupFn = () => {
124
+ model.off('change:data', _onDataChange);
125
+ model.off('change:_buffers_metadata', _onBuffersChange);
126
+ };
127
+
128
+ try {
129
+ const fn = new Function('el', 'data', 'getBuffer', 'opts', 'setState', 'getState', 'onData', 'onBuffers', code);
130
+ fn(container, currentData, getBuffer, opts, setState, getState, onData, onBuffers);
131
+ } catch(e) {
132
+ const errDiv = document.createElement('pre');
133
+ errDiv.style.color = 'red';
134
+ errDiv.style.padding = '10px';
135
+ errDiv.textContent = 'JS Error: ' + e.message + '\\n' + e.stack;
136
+ container.appendChild(errDiv);
137
+ console.error('JSWidget execution error:', e);
138
+ }
139
+ }
140
+
141
+ // Execute initial code
142
+ executeCode();
143
+
144
+ // Re-execute when js_code changes
145
+ model.on('change:js_code', executeCode);
146
+
147
+ // Update container size
148
+ model.on('change:width', () => {
149
+ container.style.width = model.get('width') + 'px';
150
+ });
151
+ model.on('change:height', () => {
152
+ container.style.height = model.get('height') + 'px';
153
+ });
154
+
155
+ return () => {
156
+ if (cleanupFn) {
157
+ try { cleanupFn(); } catch(e) {}
158
+ }
159
+ };
160
+ }
161
+ export default { render };
162
+ """
163
+
164
+
165
+ class JSWidget(anywidget.AnyWidget):
166
+ """A DOMWidget that renders arbitrary JavaScript with binary data.
167
+ Uses anywidget for reliable comm-based rendering in all environments.
168
+
169
+ The JavaScript code receives:
170
+ el - the container DOM element
171
+ data - the JSON data dict
172
+ getBuffer(n) - get named buffer as ArrayBuffer
173
+ opts - {width, height}
174
+ setState(obj) - save state that persists across re-renders
175
+ getState() - retrieve previously saved state
176
+ onData(fn) - register callback for data updates
177
+ onBuffers(fn) - register callback for buffer updates
178
+ """
179
+
180
+ _esm = _ESM
181
+
182
+ # User-provided JavaScript code to execute in the widget
183
+ js_code = traitlets.Unicode('').tag(sync=True)
184
+
185
+ # JSON-serializable data dict passed to JavaScript
186
+ data = traitlets.Dict({}).tag(sync=True)
187
+
188
+ # Widget dimensions
189
+ width = traitlets.Int(800).tag(sync=True)
190
+ height = traitlets.Int(600).tag(sync=True)
191
+
192
+ # Metadata about binary buffers (triggers onBuffers callback in JS)
193
+ # Binary buffer traits (_buf_<name>) are created dynamically by set_buffers().
194
+ _buffers_metadata = traitlets.List([]).tag(sync=True)
195
+
196
+ def set_buffers(self, **named_buffers):
197
+ """Set binary data buffers for the JavaScript frontend.
198
+
199
+ Each keyword argument should be a numpy array, bytes, or bytearray.
200
+
201
+ Example:
202
+ w.set_buffers(vertices=vertices, normals=normals, indices=indices)
203
+ """
204
+ metadata = []
205
+ with self.hold_sync():
206
+ for name, buf in named_buffers.items():
207
+ trait_name = f'_buf_{name}'
208
+ if not self.has_trait(trait_name):
209
+ self.add_traits(**{trait_name: traitlets.Bytes(b'').tag(sync=True)})
210
+
211
+ if isinstance(buf, np.ndarray):
212
+ metadata.append({'name': name, 'dtype': str(buf.dtype), 'shape': list(buf.shape)})
213
+ setattr(self, trait_name, buf.tobytes())
214
+ elif isinstance(buf, (bytes, bytearray, memoryview)):
215
+ metadata.append({'name': name, 'dtype': 'bytes', 'shape': [len(buf)]})
216
+ setattr(self, trait_name, bytes(buf))
217
+ else:
218
+ raise TypeError(f"Buffer '{name}' must be numpy array, bytes, or bytearray")
219
+
220
+ self._buffers_metadata = metadata
221
+
222
+ def send_data(self, data_dict):
223
+ """Update the data dict and push to JavaScript."""
224
+ self.data = data_dict
225
+
226
+ def execute(self, js_code):
227
+ """Update the JavaScript code and re-render."""
228
+ self.js_code = js_code
229
+
230
+ def show(self):
231
+ """Display the widget."""
232
+ from IPython.display import display
233
+ display(self)