Repository navigation
Conversation
…r=...) Four new `scene_objects` primitives wrapping the corresponding Three.js objects, all available via the chainable `scene.<name>()` factory: - `Polyline` — connects a sequence of 3D points with optional per-vertex colors and GPU-dashed `LineDashedMaterial` (defaults `dash_size=3`, `gap_size=1`, matching `LineDashedMaterial`'s own defaults). - `Lathe` — surface of revolution generated by spinning a 2D profile around the y axis (wireframe-capable like other geometry primitives). - `ArrowHelper` — wraps Three.js' `ArrowHelper` with optional radial segments for a smoother cone head and a `line_width` hint (documented as commonly clamped to 1 by WebGL). - `PolarGridHelper` — circular reference grid in the XZ plane. `Object3D.rotate(...)` gains an optional intrinsic Euler `order` kwarg matching `THREE.Euler(rx, ry, rz, order)` — one of `'XYZ'`, `'XZY'`, `'YXZ'`, `'YZX'`, `'ZXY'`, `'ZYX'` (default `'XYZ'`, preserving the previous behavior). The rotation matrix is composed Python-side so that the rotation is stored in `self.R` and survives a re-send to the client (reconnect, page revisit, etc.) like every other `rotate_R` call. `rotation_matrix_from_euler` accepts the same `order` argument and delegates to a small generic `_matmul3` helper. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…e, add polar_grid scene parameter Reshape the slice into a coherent "missing common geometry primitives + polar grid floor" PR: - Drop ArrowHelper and PolarGridHelper. They were buggy (ArrowHelper passed Python None through to Three.js as null, defeating the constructor's `=== undefined` default-handling and producing invisible / mis-sized arrow heads) and they are helpers, not geometries -- belong in a separate slice alongside Box3Helper / PlaneHelper / light helpers. - Add Plane, Cone, Torus, Capsule wrapping the corresponding Three.js geometries via the existing generic dispatch in scene.js. - Add a `polar_grid: tuple[float, int, int] | None` scene constructor kwarg that replaces the rectangular floor with a circular ground + PolarGridHelper (mutually exclusive with `grid`; polar takes precedence). - Validate `len(colors) == len(points)` in Polyline at the API boundary instead of letting Three.js silently render extra points black. - Move EULER_ORDERS to the top of Object3D alongside other class-level state. - Drop a redundant `import pytest as _pytest` shim in the test file. - Add a parametrized smoke test covering Plane / Cone / Torus / Capsule that asserts each dispatches to the expected Three.js geometry class -- guards against silent removals or renames in future Three.js upgrades. - Add a `test_polar_grid` integration test for the scene kwarg. - Rework the docs demo to showcase the geometry primitives and add a standalone Polar Grid demo.
polar_grid scene parameter, and rotate(order=...)
Inserting polar_grid between `grid` (3rd positional) and `camera` (was 4th) silently shifted every later parameter by one slot, so any caller passing `camera` (or anything after it) positionally would have started binding a `tuple[float, int, int] | None` to a `SceneCamera | None` arg and crashed later in JS. The established convention from recent param additions (`control_type`, `fps`, `show_stats`) is to append to the end of the signature, leaving the deprecated positional zone untouched until the NiceGUI 4.0 keyword-only enforcement promised by the inline DEPRECATED comment lands. The :param docstring entry moves to match.
…hness, polyline guard
- Replace incorrect THREE.Euler claim on rotation_matrix_from_euler / rotate
with an accurate description (leftmost letter rotates first about world frame)
and pin the per-order expected matrix as an explicit dict in the test.
- Drop the dead add_rename('polar_grid', 'polar-grid') line; the kwarg never
had a pre-rename history.
- Expose the PolarGridHelper smoothness as an optional 4th tuple element
(radius, sectors, rings, divisions) defaulting to 64; cover the new path
with a parametrized test that pins the helper vertex count.
- Reject Polyline with fewer than 2 points at the API boundary.
We confirmed during development that Object3D.rotate(...) does not match THREE.Euler(rx, ry, rz, order) semantics. The demo's docstring kept saying it did, which would mislead readers. Just describe the kwarg.
The previous demos showed primitives on a square grid and polar grid with three plain spheres separately. Combining them into a single demo gives readers all the new geometry types arranged around a circular floor with distinct colors — a richer visual that fits both features in one frame, and removes the redundancy of the separate Polar Grid demo.
… loads The previous STL handling computed `EdgesGeometry` synchronously against an empty `BufferGeometry` placeholder, then assigned the loaded geometry onto `mesh.geometry` from the loader callback. This crashed in `BufferGeometryUtils.mergeVertices` with "Cannot read properties of undefined (reading 'count')" and never produced wireframe edges for the real geometry. Move STL out of the geometric-primitives branch into its own block that mirrors the GLTF flow: create a `THREE.Group` placeholder marked `userData.isStl`, then build the `LineSegments` (wireframe) or `Mesh` child inside the loader callback once the actual geometry is available. Replay any `material()` call queued via `pendingMaterialInfo` once loaded, matching the GLTF deferral. Extend `material()` to treat `isStl` like `isGltf` — defer until loaded, and traverse both `isMesh` and `isLine` children when applying material props.
Stacks on zauberzeug#5989 (scene primitives + STL Group restructure). The STL clipping-plane deferral relies on 5989's STL Group + userData.loaded structure: when set_clipping_planes is called before the STL geometry loads, the planes are stashed on userData.pendingClippingPlanes and flushed from the loader callback (mirroring pendingMaterialInfo). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks @Jepson2k — really nice work here. The feature code is correct (I re-derived the rotation math against numpy for all 6 orders + legacy 3-arg, and checked every new primitive's args against its Three.js constructor), the tests are a cut above, and CI's green. Two notes: 1. The STL-loader rework fixes a real crash — let's get it out ahead of the restNice catch reworking the STL path (empty- Minimal repro (12-triangle cube STL, from nicegui import app, ui
@ui.page('/')
def index():
app.add_static_file(local_file='cube.stl', url_path='/cube.stl')
with ui.scene() as scene:
scene.stl('/cube.stl', wireframe=True)
ui.run()
Same app, same
On Because this is a genuine bugfix to an existing, broken public-API path (
Ready-to-use regression test (crashes on def test_stl_wireframe(screen: Screen):
"""A wireframe STL must render as edges: a LineSegments whose geometry is EdgesGeometry."""
scene = None
obj = None
@ui.page('/')
def page():
nonlocal scene, obj
app.add_static_file(local_file=TEST_DIR / 'media' / 'cube.stl', url_path='/cube.stl')
with ui.scene() as scene:
obj = scene.stl('/cube.stl', wireframe=True)
screen.open('/')
screen.wait(1.0)
result = screen.selenium.execute_script(
f'const o = getElement({scene.id}).objects.get("{obj.id}");'
'const child = o.children && o.children[0];'
'return {'
' root_type: o.type,'
' child_geometry: child ? child.geometry.type : null,'
' edge_count: (child && child.geometry.attributes.position) ? child.geometry.attributes.position.count : 0,'
'};'
)
assert result['root_type'] == 'Group', f'expected a Group wrapper, got {result}'
assert result['child_geometry'] == 'EdgesGeometry', f'expected EdgesGeometry child, got {result}'
assert result['edge_count'] > 0, f'expected non-empty edges, got {result}'2.
|
|
@falkoschindler You may want to look at the real bug fixed also by this PR and see how to proceed, in this PR or break-out |
The implementation composes world-frame (extrinsic) rotations; the demo called it intrinsic, steering users to the wrong order string. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wireframe-STL crash fix is split out per review; the STL and material() regions now match main exactly so the fixed version merges in cleanly once zauberzeug#6137 lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@evnchn @falkoschindler the STL fix was split out to #6137 and docs updated as well. |
|
Thanks @Jepson2k — that closes out both notes cleanly:
Feature code was already correct on my end, so with the docs squared away this PR is in good shape from my side. Merge-order/timing between this and #6137 I'll leave to @falkoschindler. |
Port the six primitives out of the deprecated scene_objects.py into per-object components under objects/ (geometry classes via create_geometry, Polyline via create_mesh), registered in objects/__init__.py and the Scene alias block. rotate(order=) and polar_grid re-apply unchanged; the old create-dispatcher branches are superseded by the component files. Screen tests read .mesh from the object registry records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkVPPMQg33tFuGCVLkyrJQ
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # nicegui/elements/scene/scene_object3d.py


Motivation
ui.sceneis missing several commonly-used Three.js primitives (Plane,Cone,Torus,Capsule), aLine-with-vertex-colors-and-dashing primitive (Polyline), and a surface-of-revolution primitive (Lathe). Many 3D visualizations also benefit from a circular polar floor instead of a rectangular grid.This PR fills those gaps and generalizes
Object3D.rotate(...)to support all six intrinsic Euler orders rather than only the implicit'XYZ'.Implementation
Polyline(with optional per-vertex colors and dashed material),Lathe,Plane,Cone(withopen_ended/theta_start/theta_length),Torus(with optional partialarc),Capsule. Each is a module inobjects/like the existing primitives:Plane,Cone,Torus,CapsuleandLathereturn their Three.js geometry fromcreate_geometry, andPolylinebuilds its ownLineincreate_meshso it can carry per-vertex colors and dashing.Scene(polar_grid=(radius, sectors, rings))or(radius, sectors, rings, divisions), wheredivisionssets how smooth each ring is (default 64). Replaces the rectangular grid withPolarGridHelper. Mutually exclusive withgrid(polar takes precedence).Object3D.rotate(..., order='ZYX')accepts any of{'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'}. Default'XYZ'is bit-for-bit unchanged. Matrix is composed Python-side so the rotation survives a re-send to the client.Progress