Add clipping planes and named click intersections to Scene - #5993
Draft
Jepson2k wants to merge 21 commits into
Draft
Add clipping planes and named click intersections to Scene#5993Jepson2k wants to merge 21 commits into
Jepson2k wants to merge 21 commits into
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.
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.
6 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends NiceGUI’s ui.scene stack with clipping-plane support on Object3D, named plane intersections in scene click events, and a runtime-watched raycaster_threshold. In the overall codebase, these changes expand the public 3D scene API across Python event types, scene/object state serialization, frontend scene behavior, tests, and docs.
Changes:
- Add new scene/event API types for 3D points, named intersection planes, and clipping planes.
- Add
Object3D.set_clipping_planes(...)/clear_clipping_planes()plus scene-side click intersection handling and watchedraycaster_threshold. - Add scene docs/examples and test coverage for clipping planes, click intersections, and runtime raycaster updates.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
website/documentation/content/scene_documentation.py |
Adds demos for clipping planes and named click-plane intersections. |
tests/test_scene.py |
Adds scene tests for clipping state, click intersections, and runtime threshold updates. |
nicegui/events.py |
Introduces new public event dataclasses and extends scene click event payloads. |
nicegui/elements/scene/scene_object3d.py |
Stores clipping plane state on objects and exposes new clipping methods. |
nicegui/elements/scene/scene.py |
Extends Scene API/props and maps frontend click intersections into Python event args. |
nicegui/elements/scene/scene.js |
Implements frontend clipping application, named plane intersections, and watched raycaster threshold behavior. |
falkoschindler
self-requested a review
May 5, 2026 12:54
… 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.
Jepson2k
added a commit
to Jepson2k/nicegui
that referenced
this pull request
May 5, 2026
…ing deferral The 5989 half (STL Group + isStl material handling) shipped on scene-primitives-rotate as 0b7f676. The set_clipping_planes deferral half moves to its own entry under zauberzeug#5993, since that's where the function is introduced.
Jepson2k
marked this pull request as draft
May 6, 2026 15:14
Contributor
Author
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>
Jepson2k
force-pushed
the
scene-clipping-axes
branch
from
May 6, 2026 20:09
59cbb79 to
67b97d8
Compare
Jepson2k
added a commit
to Jepson2k/nicegui
that referenced
this pull request
May 13, 2026
ViewHelper.render() implicitly clears the framebuffer; without this guard the main scene gets wiped every frame after the helper renders, leaving an empty canvas. The original feature/additional_scene_features branch had this guard; it was lost in the simplification that split this PR out of zauberzeug#5993.
6 tasks
Per CONTRIBUTING.md. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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
…e-clipping-axes Port clipping and intersections onto the modular object system: set_clipping_planes awaits get_object so async loaders (GLTF/STL) resolve before the traversal — both pendingClippingPlanes hacks are gone. The deprecated data-array no longer carries clipping state; _resend() re-sends it after context loss. Intersection planes and the raycaster threshold port unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkVPPMQg33tFuGCVLkyrJQ
Clipping state no longer rides the deprecated Object3D.data tuple; _resend() replays it when the scene remounts. Assert that contract end-to-end: apply planes, lose the WebGL context, re-initialize, and wait for the planes to come back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkVPPMQg33tFuGCVLkyrJQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Two additions that come up constantly in 3D-visualization apps:
Plus
raycaster_thresholdbecomes a real watched prop instead of init-only.Implementation
SceneClipPlane(nx, ny, nz, d)plus chainableObject3D.set_clipping_planes(...)/clear_clipping_planes(). Applies asmaterial.clippingPlaneson every mesh descendant; the renderer'slocalClippingEnabledflag flips on first use. State lives onObject3Dand is replayed throughinit_objectson reconnect, with a pending-planes path sogltfobjects pick up clipping once their async loader resolves.Scene(intersection_planes=[SceneIntersectionPlane(name, axis, offset), ...])— every configured plane appears ine.intersectionson every click; the value isNonewhen the ray misses, distinguishing "not configured" from "didn't intersect" without.get()ambiguity.raycaster_thresholdis now a real watched prop.Progress