Skip to content

Add clipping planes and named click intersections to Scene - #5993

Draft
Jepson2k wants to merge 21 commits into
zauberzeug:mainfrom
Jepson2k:scene-clipping-axes
Draft

Add clipping planes and named click intersections to Scene#5993
Jepson2k wants to merge 21 commits into
zauberzeug:mainfrom
Jepson2k:scene-clipping-axes

Conversation

@Jepson2k

@Jepson2k Jepson2k commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Motivation

Two additions that come up constantly in 3D-visualization apps:

  • Clipping planes to "see inside" articulated geometry against arbitrary plane equations.
  • Named click intersections — declare named planes once, then read each click ray's intersection per click, generalizing the common "click ground to place" workflow.

Plus raycaster_threshold becomes a real watched prop instead of init-only.

Implementation

  • SceneClipPlane(nx, ny, nz, d) plus chainable Object3D.set_clipping_planes(...) / clear_clipping_planes(). Applies as material.clippingPlanes on every mesh descendant; the renderer's localClippingEnabled flag flips on first use. State lives on Object3D and is replayed through init_objects on reconnect, with a pending-planes path so gltf objects pick up clipping once their async loader resolves.
  • Scene(intersection_planes=[SceneIntersectionPlane(name, axis, offset), ...]) — every configured plane appears in e.intersections on every click; the value is None when the ray misses, distinguishing "not configured" from "didn't intersect" without .get() ambiguity.
  • raycaster_threshold is now a real watched prop.

Progress

  • The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc.
  • The implementation is complete.
  • This PR does not address a security issue.
  • Pytests have been added.
  • Documentation has been added.
  • No breaking changes to the public API.

Jepson2k and others added 7 commits April 24, 2026 09:05
…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.
@Jepson2k
Jepson2k marked this pull request as ready for review May 3, 2026 03:24
@evnchn
evnchn requested a review from Copilot May 3, 2026 07:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 watched raycaster_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.

Comment thread tests/test_scene.py Outdated
Comment thread nicegui/events.py
Comment thread nicegui/elements/scene/scene_object3d.py
Comment thread nicegui/elements/scene/scene.py
Comment thread nicegui/elements/scene/scene.js Outdated
Comment thread nicegui/elements/scene/scene.js
Comment thread nicegui/elements/scene/scene.js
@falkoschindler falkoschindler added feature Type/scope: New or intentionally changed behavior review Status: PR is open and needs review labels May 5, 2026
@falkoschindler falkoschindler added this to the Next milestone May 5, 2026
@falkoschindler
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 Jepson2k changed the title Add clipping planes, axes inset, and named click intersections to Scene Add clipping planes and named click intersections to Scene May 6, 2026
@Jepson2k
Jepson2k marked this pull request as draft May 6, 2026 15:14
@Jepson2k

Jepson2k commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

Took this back to a draft until #5989 merges since after some more testing it appears that it'll have STL related issues. Same deferral fix that was done for GLTF is needed for STL but that requires the isSTL flag introduced in #5989.

@falkoschindler falkoschindler added blocked Status: Blocked by another issue or dependency and removed review Status: PR is open and needs review labels May 6, 2026
@falkoschindler falkoschindler removed this from the Next milestone May 6, 2026
Jepson2k and others added 2 commits May 6, 2026 16:04
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
Jepson2k force-pushed the scene-clipping-axes branch from 59cbb79 to 67b97d8 Compare May 6, 2026 20:09
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.
@Jepson2k Jepson2k mentioned this pull request Jun 4, 2026
6 tasks
Per CONTRIBUTING.md. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jepson2k and others added 7 commits July 2, 2026 14:10
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Status: Blocked by another issue or dependency feature Type/scope: New or intentionally changed behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants