ply-visualizer 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.
Files changed (64) hide show
  1. ply_visualizer-0.1.0/LICENSE +21 -0
  2. ply_visualizer-0.1.0/PKG-INFO +376 -0
  3. ply_visualizer-0.1.0/README.md +360 -0
  4. ply_visualizer-0.1.0/ply_visualizer/__init__.py +4 -0
  5. ply_visualizer-0.1.0/ply_visualizer/__main__.py +24 -0
  6. ply_visualizer-0.1.0/ply_visualizer/_assets/292.bundle.js +2 -0
  7. ply_visualizer-0.1.0/ply_visualizer/_assets/292.bundle.js.map +1 -0
  8. ply_visualizer-0.1.0/ply_visualizer/_assets/31f44e750ca55f52a561.wasm +0 -0
  9. ply_visualizer-0.1.0/ply_visualizer/_assets/325.bundle.js +2 -0
  10. ply_visualizer-0.1.0/ply_visualizer/_assets/325.bundle.js.map +1 -0
  11. ply_visualizer-0.1.0/ply_visualizer/_assets/367.bundle.js +2 -0
  12. ply_visualizer-0.1.0/ply_visualizer/_assets/367.bundle.js.map +1 -0
  13. ply_visualizer-0.1.0/ply_visualizer/_assets/454.bundle.js +2 -0
  14. ply_visualizer-0.1.0/ply_visualizer/_assets/454.bundle.js.map +1 -0
  15. ply_visualizer-0.1.0/ply_visualizer/_assets/610.bundle.js +2 -0
  16. ply_visualizer-0.1.0/ply_visualizer/_assets/610.bundle.js.map +1 -0
  17. ply_visualizer-0.1.0/ply_visualizer/_assets/630.bundle.js +3 -0
  18. ply_visualizer-0.1.0/ply_visualizer/_assets/630.bundle.js.LICENSE.txt +5 -0
  19. ply_visualizer-0.1.0/ply_visualizer/_assets/630.bundle.js.map +1 -0
  20. ply_visualizer-0.1.0/ply_visualizer/_assets/702.bundle.js +2 -0
  21. ply_visualizer-0.1.0/ply_visualizer/_assets/702.bundle.js.map +1 -0
  22. ply_visualizer-0.1.0/ply_visualizer/_assets/962.bundle.js +2 -0
  23. ply_visualizer-0.1.0/ply_visualizer/_assets/962.bundle.js.map +1 -0
  24. ply_visualizer-0.1.0/ply_visualizer/_assets/apple-touch-icon.png +0 -0
  25. ply_visualizer-0.1.0/ply_visualizer/_assets/bundle.css +601 -0
  26. ply_visualizer-0.1.0/ply_visualizer/_assets/bundle.css.map +1 -0
  27. ply_visualizer-0.1.0/ply_visualizer/_assets/bundle.js +3 -0
  28. ply_visualizer-0.1.0/ply_visualizer/_assets/bundle.js.LICENSE.txt +7 -0
  29. ply_visualizer-0.1.0/ply_visualizer/_assets/bundle.js.map +1 -0
  30. ply_visualizer-0.1.0/ply_visualizer/_assets/examples/camera-path-2026-08-27-14-27-47.json +50 -0
  31. ply_visualizer-0.1.0/ply_visualizer/_assets/examples/example-point-cloud.ply +245080 -0
  32. ply_visualizer-0.1.0/ply_visualizer/_assets/examples/measurement-paths-2026-08-27-15-49-45.json +19 -0
  33. ply_visualizer-0.1.0/ply_visualizer/_assets/index.html +298 -0
  34. ply_visualizer-0.1.0/ply_visualizer/_assets/manifest.webmanifest +26 -0
  35. ply_visualizer-0.1.0/ply_visualizer/_assets/media/icons/measurement/clear.svg +4 -0
  36. ply_visualizer-0.1.0/ply_visualizer/_assets/media/icons/measurement/loop.svg +7 -0
  37. ply_visualizer-0.1.0/ply_visualizer/_assets/media/icons/measurement/measure.svg +9 -0
  38. ply_visualizer-0.1.0/ply_visualizer/_assets/media/icons/measurement/new-free.svg +4 -0
  39. ply_visualizer-0.1.0/ply_visualizer/_assets/media/icons/measurement/undo.svg +6 -0
  40. ply_visualizer-0.1.0/ply_visualizer/_assets/media/ply-icon.png +0 -0
  41. ply_visualizer-0.1.0/ply_visualizer/_assets/media/style.css +1312 -0
  42. ply_visualizer-0.1.0/ply_visualizer/_assets/media/tiff-icon.png +0 -0
  43. ply_visualizer-0.1.0/ply_visualizer/_assets/media/wasm/tiff_wasm.js +1 -0
  44. ply_visualizer-0.1.0/ply_visualizer/_assets/media/wasm/tiff_wasm_bg.wasm +0 -0
  45. ply_visualizer-0.1.0/ply_visualizer/_assets/src/themes/README.md +91 -0
  46. ply_visualizer-0.1.0/ply_visualizer/_assets/src/themes/dark-modern.json +175 -0
  47. ply_visualizer-0.1.0/ply_visualizer/_assets/src/themes/light-modern.json +175 -0
  48. ply_visualizer-0.1.0/ply_visualizer/arrays.py +44 -0
  49. ply_visualizer-0.1.0/ply_visualizer/scenes.py +66 -0
  50. ply_visualizer-0.1.0/ply_visualizer/session.py +360 -0
  51. ply_visualizer-0.1.0/ply_visualizer/training.py +40 -0
  52. ply_visualizer-0.1.0/ply_visualizer.egg-info/PKG-INFO +376 -0
  53. ply_visualizer-0.1.0/ply_visualizer.egg-info/SOURCES.txt +62 -0
  54. ply_visualizer-0.1.0/ply_visualizer.egg-info/dependency_links.txt +1 -0
  55. ply_visualizer-0.1.0/ply_visualizer.egg-info/entry_points.txt +2 -0
  56. ply_visualizer-0.1.0/ply_visualizer.egg-info/requires.txt +3 -0
  57. ply_visualizer-0.1.0/ply_visualizer.egg-info/top_level.txt +1 -0
  58. ply_visualizer-0.1.0/pyproject.toml +29 -0
  59. ply_visualizer-0.1.0/setup.cfg +4 -0
  60. ply_visualizer-0.1.0/setup.py +16 -0
  61. ply_visualizer-0.1.0/tests/test_arrays.py +102 -0
  62. ply_visualizer-0.1.0/tests/test_notebook.py +54 -0
  63. ply_visualizer-0.1.0/tests/test_session.py +83 -0
  64. ply_visualizer-0.1.0/tests/test_training.py +77 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 3D Visualizer Extension
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,376 @@
1
+ Metadata-Version: 2.4
2
+ Name: ply-visualizer
3
+ Version: 0.1.0
4
+ Summary: Local 3D point cloud and mesh viewer for Python and the command line
5
+ Author: Florian Kleinicke
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://3d.f-kleinicke.de
8
+ Project-URL: Repository, https://github.com/kleinicke/ply-visualizer
9
+ Project-URL: Issues, https://github.com/kleinicke/ply-visualizer/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Provides-Extra: notebook
14
+ Requires-Dist: ipython>=8; extra == "notebook"
15
+ Dynamic: license-file
16
+
17
+ # Python and CLI preview — 3D viewer
18
+
19
+ One Python package provides a browser viewer for local 3D files and point
20
+ arrays, plus the `ply-viewer` command. Python 3.10+ and a WebGL-capable browser
21
+ are required. There are no Python runtime dependencies; NumPy arrays work
22
+ without requiring NumPy for users who only open files. Node.js is needed only to
23
+ build the bundled viewer from this repository, not to use an installed wheel.
24
+
25
+ Install the Python package from PyPI as **`ply-visualizer`**. The npm package is
26
+ not published.
27
+
28
+ ## Install with uv (recommended)
29
+
30
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) first. For
31
+ source development, use this repository or a locally built wheel. From the
32
+ repository root, build the browser assets using Node 24:
33
+
34
+ ```sh
35
+ npm ci
36
+ npm run build:python-viewer
37
+ ```
38
+
39
+ ### CLI only
40
+
41
+ For occasional use, run the local package in an isolated tool environment:
42
+
43
+ ```sh
44
+ uvx --from ./packages/python ply-viewer engine/examples/example-point-cloud.ply
45
+ ```
46
+
47
+ For regular use, install its command persistently:
48
+
49
+ ```sh
50
+ uv tool install ./packages/python
51
+ ply-viewer scan.ply
52
+ ```
53
+
54
+ If uv reports that its executable directory is missing from PATH, run
55
+ `uv tool update-shell` and restart your shell. Tool installation makes the CLI
56
+ available, but does not add `ply_visualizer` to your Python project or notebook
57
+ kernel. [uv tools guide](https://docs.astral.sh/uv/guides/tools/).
58
+
59
+ ### Python and local Jupyter notebooks
60
+
61
+ For an existing uv-managed Python project, add the built local package from that
62
+ project's directory (replace the path with your checkout location):
63
+
64
+ ```sh
65
+ uv add /absolute/path/to/ply-visualizer/packages/python
66
+ uv run --with jupyter jupyter lab
67
+ ```
68
+
69
+ Choose the kernel using that project's environment. Then a notebook cell can run
70
+ `from ply_visualizer import show`. The viewer opens in a separate browser tab;
71
+ keep the kernel running. NumPy and PyTorch stay optional: use the versions
72
+ already installed in your project, or add them as needed.
73
+ [uv Jupyter guide](https://docs.astral.sh/uv/guides/integration/jupyter/).
74
+
75
+ For a virtual environment without uv project management, from this repository:
76
+
77
+ ```sh
78
+ # Create this environment only if it does not already exist.
79
+ uv venv packages/python/.venv
80
+ uv pip install --python packages/python/.venv ./packages/python
81
+ packages/python/.venv/bin/ply-viewer engine/examples/example-point-cloud.ply
82
+ ```
83
+
84
+ For notebooks in that environment, install JupyterLab into the same environment
85
+ and launch it directly:
86
+
87
+ ```sh
88
+ uv pip install --python packages/python/.venv jupyterlab
89
+ packages/python/.venv/bin/jupyter lab
90
+ ```
91
+
92
+ ### Installing a wheel
93
+
94
+ A wheel already contains the viewer assets and needs no Node.js build:
95
+
96
+ ```sh
97
+ # CLI only:
98
+ uv tool install /path/to/ply_visualizer-0.1.0-py3-none-any.whl
99
+ # Or, from a uv-managed Python project:
100
+ uv add /path/to/ply_visualizer-0.1.0-py3-none-any.whl
101
+ ```
102
+
103
+ ### Public PyPI installation
104
+
105
+ The public package name is **`ply-visualizer`**:
106
+
107
+ ```sh
108
+ uv add ply-visualizer # Python project
109
+ uv add "ply-visualizer[notebook]" # Notebook display support
110
+ uv pip install ply-visualizer # Existing virtual environment
111
+ uv tool install ply-visualizer # Isolated CLI installation
112
+ uvx --from ply-visualizer ply-viewer scan.ply
113
+ ```
114
+
115
+ The Python import remains `from ply_visualizer import show`. See
116
+ [publishing setup](PUBLISHING.md) for the release workflow.
117
+
118
+ ## Alternative: install with pip from this repository
119
+
120
+ After building the browser assets above:
121
+
122
+ ```sh
123
+ python3 -m venv packages/python/.venv
124
+ packages/python/.venv/bin/python -m pip install ./packages/python
125
+ packages/python/.venv/bin/ply-viewer engine/examples/example-point-cloud.ply
126
+ ```
127
+
128
+ On Windows, replace the environment's `bin/` paths with `Scripts/`, e.g.
129
+ `packages\python\.venv\Scripts\python.exe`, `ply-viewer.exe`, or `jupyter.exe`.
130
+ After activating the environment, the command is simply:
131
+
132
+ ```sh
133
+ ply-viewer scan.ply mesh.stl
134
+ ply-viewer --no-browser scan.ply
135
+ python -m ply_visualizer scan.ply
136
+ ```
137
+
138
+ The command prints a local URL and keeps running until Ctrl+C. `--no-browser`
139
+ allows an agent or another application to open that URL itself. It does not
140
+ render an image or report successful browser rendering to the caller.
141
+
142
+ ## Python
143
+
144
+ ```python
145
+ from ply_visualizer import show
146
+
147
+ # In an interactive Python session or local notebook:
148
+ viewer = show("scan.ply", "mesh.stl")
149
+ print(viewer.url)
150
+ # Later:
151
+ viewer.close()
152
+ ```
153
+
154
+ ```python
155
+ from ply_visualizer import show
156
+
157
+ # Lists, iterables, NumPy arrays, and PyTorch tensors with shape (N, 3) work.
158
+ points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]
159
+ colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255]]
160
+
161
+ # In a script, keep Python alive while the viewer is in use.
162
+ with show(points, colors=colors) as viewer:
163
+ print(viewer.url)
164
+ try:
165
+ viewer.wait()
166
+ except KeyboardInterrupt:
167
+ pass
168
+ ```
169
+
170
+ Coordinates must be finite float32-compatible numbers. Optional colors must
171
+ match the point count and contain integer RGB values in 0..255. Point arrays are
172
+ serialized to a temporary binary PLY, decoded by the existing engine, and
173
+ removed when the session closes. This first implementation serializes arrays row
174
+ by row; it is not a zero-copy transport for very large arrays.
175
+
176
+ ## NumPy and PyTorch
177
+
178
+ Pass arrays and tensors directly, including RGB colors:
179
+
180
+ ```python
181
+ import numpy as np
182
+ from ply_visualizer import show
183
+
184
+ viewer = show(np.random.default_rng(0).normal(size=(1000, 3)))
185
+ ```
186
+
187
+ ```python
188
+ import torch
189
+ from ply_visualizer import show
190
+
191
+ device = "cuda" if torch.cuda.is_available() else (
192
+ "mps" if torch.backends.mps.is_available() else "cpu"
193
+ )
194
+ points = torch.randn(1000, 3, device=device, requires_grad=True)
195
+ rgb = torch.randint(0, 256, (1000, 3), device=device, dtype=torch.uint8)
196
+ viewer = show(points, colors=rgb)
197
+ # No manual .detach(), .cpu(), or .numpy() needed.
198
+ # Keep Python alive and call viewer.close() when finished.
199
+ ```
200
+
201
+ - NumPy arrays can be transposed, sliced, read-only, or non-contiguous.
202
+ - PyTorch inputs can be CPU or GPU tensors, detached or attached to autograd.
203
+ Both coordinates and colors are handled independently, so mixing devices or
204
+ NumPy/PyTorch inputs works. Float16 and bfloat16 tensors are supported.
205
+ - Visualization detaches internally, transfers to CPU, and serializes a
206
+ snapshot. The original device, values, `requires_grad`, and autograd graph are
207
+ unchanged. GPU-to-CPU transfer synchronizes; avoid calling this every training
208
+ step.
209
+ - PyTorch conversion does not depend on NumPy. CPU rows are converted in bounded
210
+ chunks rather than performing a GPU scalar read for every coordinate.
211
+ - Inputs must be real, dense `(N, 3)` data. For `(B, N, 3)` batches, use
212
+ `show(batch[0])`; batches are deliberately not flattened automatically.
213
+ Sparse, quantized, complex, nested, and data-free meta tensors are rejected
214
+ with an error. RGB retains the integer-valued 0..255 convention, including
215
+ floating-point tensors; normalized RGB can be passed as `(rgb * 255).round()`.
216
+
217
+ ## Training previews
218
+
219
+ All updates reuse the same browser tab/inline view and preserve its camera. The
220
+ viewer polls for the newest revision every 500 ms; intermediate revisions can be
221
+ skipped. Publish at a useful training interval, not every forward pass.
222
+ Serialization and GPU transfer are synchronous. Updates replace the scene;
223
+ manual files added to that scene are also replaced on the next update.
224
+
225
+ ```python
226
+ viewer = show(initial_points)
227
+
228
+ # In your training loop, e.g. every 100 steps:
229
+ if step % 100 == 0:
230
+ viewer.update(prediction[0], target=target[0], step=step)
231
+ ```
232
+
233
+ Prediction is orange and target cyan. Supply `colors=` to override prediction
234
+ RGB. Targets may have a different number of points. This is a geometric overlay,
235
+ not a computed nearest-neighbor error metric. Visibility can be toggled in the
236
+ standard file panel. Use **Pause updates** to inspect one revision and **Fit
237
+ scene** to reset the framing explicitly.
238
+
239
+ ### Batch and augmentation inspection
240
+
241
+ ```python
242
+ from ply_visualizer import show_batch
243
+
244
+ viewer = show_batch(batch, target=target_batch)
245
+ viewer.update_batch(next_batch, target=next_targets, step=step)
246
+
247
+ # A list of differently sized point arrays also works:
248
+ viewer = show_batch([original, augmented], labels=["Original", "Augmented"])
249
+ ```
250
+
251
+ The sample selector switches batches in one scene and retains the camera.
252
+ Colors, targets, and vectors (when supplied) must match the batch size. Batch
253
+ snapshots serialize every sample; select a small inspection subset for large
254
+ training batches. Only the chosen sample is loaded by the browser.
255
+
256
+ ### Gradient and displacement arrows
257
+
258
+ ```python
259
+ # After loss.backward(); non-leaf predictions need retain_grad() beforehand.
260
+ viewer.update(
261
+ points,
262
+ vectors=points.grad,
263
+ vector_scale=-learning_rate,
264
+ max_vectors=256,
265
+ step=step,
266
+ )
267
+ ```
268
+
269
+ Arrows are anchored at the corresponding input points. Negative learning-rate
270
+ scaling shows a plain gradient-descent direction; it is not an exact Adam or
271
+ momentum optimizer step. To inspect the actual update, pass measured coordinate
272
+ displacements instead. Nonzero arrows are magenta. Up to `max_vectors` evenly
273
+ spaced vectors are displayed (default 256, maximum 2000). All supplied vector
274
+ rows are validated; no autograd hooks or gradient computation are installed by
275
+ this operation.
276
+
277
+ ### Layer inspection with a removable forward hook
278
+
279
+ ```python
280
+ viewer = show(initial_points)
281
+ with viewer.inspect_layer(model, select=lambda output: output[0], every=100):
282
+ train(model)
283
+ ```
284
+
285
+ For dictionary outputs use a selector such as
286
+ `lambda output: output["points"][0]`. The selector must return `(N, 3)`
287
+ coordinates, not arbitrary feature channels. The hook captures the first forward
288
+ call and then every `every` calls. It returns `None`, preserving the model's
289
+ output. A visualization error disables the hook, emits a warning and is
290
+ accessible as `inspection.error`; it does not invalidate training. The context
291
+ manager removes the hook, while the caller owns the viewer session's lifetime.
292
+ `inspection.close()` also removes it explicitly.
293
+
294
+ Run viewer updates in the main process, on one rank in distributed training, and
295
+ outside compiled model code. GPU-to-CPU transfer introduces synchronization.
296
+ [PyTorch forward hooks](https://docs.pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.register_forward_hook).
297
+
298
+ The server binds only to `127.0.0.1`, uses a random session URL, and serves only
299
+ the explicitly supplied files and bundled viewer assets. Source files remain on
300
+ your machine; the package does not upload them. Keep file paths available for
301
+ the life of the session. Do not share the session URL with untrusted code.
302
+
303
+ ## Inline Jupyter notebooks
304
+
305
+ In a **local** Jupyter notebook, return the viewer as the last expression in a
306
+ cell. Browser auto-opening is disabled when a notebook kernel is detected:
307
+
308
+ ```python
309
+ viewer = show(points)
310
+ viewer # Interactive viewer appears in the output cell.
311
+ ```
312
+
313
+ Or display explicitly, using the notebook extra:
314
+
315
+ ```python
316
+ viewer.display(height=480, ui="collapsed")
317
+ ```
318
+
319
+ Settings are collapsed by default. A compact toolbar keeps **Settings**, **Fit
320
+ scene**, **Pause updates**, and the batch selector accessible. Choose
321
+ `ui="full"` to show settings immediately or `ui="none"` for a presentation-only
322
+ canvas with mouse controls. In UI-free mode, batch selection is unavailable;
323
+ choose the sample in Python before displaying it.
324
+
325
+ `viewer.update(...)` updates every open view, including notebook output. Keep
326
+ the kernel running, call `viewer.close()` when done, and do not call
327
+ `viewer.wait()` in a cell. Notebook output contains a live local iframe, not an
328
+ offline saved scene. Reopen the session after restarting the kernel.
329
+
330
+ This initial inline transport requires the browser and kernel on the same
331
+ machine and a notebook host that permits local iframes. Remote Jupyter, Colab,
332
+ and notebook environments that block local iframe URLs need a widget/proxy
333
+ transport; they are not supported by this transport yet.
334
+
335
+ For a mature widget-based alternative, [K3D](https://k3d-jupyter.org/) supports
336
+ notebook point clouds and other 3D primitives.
337
+ [Rerun](https://rerun.io/examples/feature-showcase/notebook_viewer) is worth
338
+ considering for recorded, time-based diagnostics. This package embeds the
339
+ existing 3D viewer to retain its file formats and interaction controls.
340
+
341
+ ## Current scope
342
+
343
+ - Supported inputs: PLY, XYZ, XYZN, XYZRGB, PCD, PTS, OBJ, STL, OFF, GLB,
344
+ LAS/LAZ, E57, SPZ, SPLAT, KSPLAT, and SOG.
345
+ - Multiple files appear together in one scene.
346
+ - OBJ input currently provides geometry; automatic sidecar material/texture
347
+ resolution and external-resource glTF are outside this preview.
348
+ - No separate image-viewer integration, MCP tools, headless capture, or desktop
349
+ launch integration yet. The shared 3D viewer retains its existing manual
350
+ controls and depth-conversion features.
351
+
352
+ ## Build a distributable wheel
353
+
354
+ ```sh
355
+ npm run build:python-viewer
356
+ packages/python/.venv/bin/python -m pip wheel --no-deps ./packages/python --wheel-dir /tmp/ply-viewer-wheels
357
+ ```
358
+
359
+ The wheel includes the browser engine and its assets. Install that wheel on
360
+ another machine with `python -m pip install /path/to/the.whl`; no Node.js or
361
+ Tauri installation is necessary there. Build assets before packaging.
362
+
363
+ ## Verify
364
+
365
+ ```sh
366
+ npm run test:python-viewer
367
+ # Also test actual NumPy/PyTorch inputs in the isolated environment:
368
+ packages/python/.venv/bin/python -m pip install numpy torch
369
+ packages/python/.venv/bin/python -m unittest discover -s packages/python/tests -v
370
+ cd engine
371
+ npx playwright test local-session.spec.ts --reporter=line
372
+ ```
373
+
374
+ The browser test requires `npm run build:python-viewer` and the existing engine
375
+ test server assets in `engine/dist` (`npm run build --workspace=engine`). Array
376
+ tests skip optional libraries and GPU backends that are unavailable.