Add clipping planes, axes inset, and named click intersections to Scene#5993
Draft
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Draft
Add clipping planes, axes inset, and named click intersections to Scene#5993Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Conversation
Three related Scene additions: **Clipping planes** (`Object3D.set_clipping_planes` / `clear_clipping_planes`): Apply arbitrary `nx*x + ny*y + nz*z + d = 0` clip planes to an object and all of its mesh descendants. Useful for "see inside an articulated mesh" views. Backed by Three.js' per-material `clippingPlanes`; the renderer's `localClippingEnabled` flag is flipped on first use. New `SceneClipPlane` dataclass for the plane spec. **Axes inset overlay** (`Scene.set_axes_inset` / `set_axes_labels`): Toggleable orientation gizmo (Three.js' `ViewHelper`) anchored to a configurable corner with explicit pixel size and margins. The inset is positioned via manual scissor + viewport math inside the existing render loop (compatible with the current `^0.180.0` three.js pin — no need for the `ViewHelper.location.left/right/top/bottom` field added in r184). Custom labels honored via `setLabels`; the documented (but previously unimplemented) `font` / `color_x/y/z` / `size` knobs are intentionally not exposed because Three.js' ViewHelper doesn't surface them through a stable public API. **Named click intersections** (`Scene(intersection_planes=[...])`, `SceneClickEventArguments.intersections`): the host application declares named planes (axis + offset along that axis) and reads the click ray's intersection with each plane on every click event as `e.intersections[name]`. Each entry is a `ScenePoint` (with `.x/.y/.z`) when the ray hits the plane, or `None` when it misses. Empty plane list means an empty dict — no per-click cost for users who don't opt in. Generalizes the common "click ground to place" workflow without hard-coding a single ground plane. `raycaster_threshold` is also now a real prop with a Vue watcher, so runtime changes to the line/points hit-test threshold take effect without remounting the scene. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- handleClick: forward canvas pointerdown to viewHelper.handleClick via a capture-phase listener with synthesized event coords (so r180's hardcoded dim=128 / bottom-right math works for any anchor/size), stopImmediate- Propagation so OrbitControls/DragControls don't race on the same pointerdown, and suppress click3d emission for the consumed pointerdown. - setLabelStyle: extend Scene.set_axes_labels with font/color/radius kwargs forwarded to viewHelper.setLabelStyle (public, JSDoc-documented on r180); cache opts and re-apply after viewHelper rebuild on enable/disable cycles. - Review fixes: drop spurious add_rename calls for the new props, make set_axes_inset/set_axes_labels return Self, drop defensive .get() in _handle_click, move per-frame viewport reset into the inset block (no cost when inset is disabled), drop redundant Object.assign merge, clone shared axis-normal vectors, replace fixed mount-wait sleeps with a deterministic _wait_for_scene_ready helper polling renderer readiness. - Tests: add test_axes_inset_handle_click_snaps_camera, extend test_set_axes_inset_and_labels with style + rebuild persistence assertions. - Docs: axes_inset demo now shows click-to-snap and color/radius kwargs.
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
Three related Scene additions that come up constantly in 3D-visualization apps and round out the interactivity story:
Plus
raycaster_thresholdbecomes a real watched prop instead of an init-only value.This is one of several small slices being carved out of #5964 per the splitting discussion on that thread.
Implementation
Clipping planes (
nicegui/elements/scene/scene_object3d.py,nicegui/elements/scene/scene.js,nicegui/events.py):SceneClipPlane(nx, ny, nz, d)dataclass.Object3D.set_clipping_planes(planes: list[SceneClipPlane])andclear_clipping_planes()chained methods.THREE.Planeper spec, applies asmaterial.clippingPlanesto every mesh descendant, and flips the renderer'slocalClippingEnabledflag on first use.clipIntersection = falsemeans clipping is the union across planes.Axes inset (
Scene.set_axes_inset,Scene.set_axes_labels):dict[str, Any]):ViewHelper, then composites it into the configured corner inside the existing render loop using manual scissor + viewport math (renderer.setScissor/renderer.setViewport). This works on the^0.180.0three.js pin — no need for theViewHelper.location.left/right/top/bottomfield added in r184.set_axes_labelshonors thelabelstuple viaviewHelper.setLabels(...)and forwardsfont/color/radiustoviewHelper.setLabelStyle(...)(a public, JSDoc-documented method on r180 with defaults'24px Arial'/'#000000'/14). Toggling the inset off then on re-applies the cached labels and style on the rebuiltViewHelper.viewHelper.handleClick(event)via a capture-phase listener. r180'shandleClickhas a hardcodeddim = 128and assumes the inset is at the canvas's bottom-right, so we synthesize event coordinates that make its math compute the correct NDC for our actual inset rect (works for any anchor/size). When the click hits an axis, the listener callsevent.stopImmediatePropagation()so OrbitControls/DragControls don't also see the click and start a drag-rotate. Subsequentclick3devent emission is suppressed for that pointerdown so apps don't see a stray click on whatever the underlying scene happens to be at the inset's screen rect.Named click intersections (
Scene(intersection_planes=[...]),SceneClickEventArguments.intersections):SceneIntersectionPlane(name, axis: 'x' | 'y' | 'z', offset)dataclass andScenePoint(x, y, z)dataclass.Sceneconstructor gainsintersection_planes: list[SceneIntersectionPlane] | None = None. Empty list / None → empty intersections dict on every click → zero per-click cost for opted-out users.THREE.Planeper spec at mount and on prop change (with cloned axis-normal vectors so the per-axis singleton is never mutated), then on every click doesraycaster.ray.intersectPlane(plane, point)per configured plane.SceneClickEventArguments.intersections: dict[str, ScenePoint | None]— every configured plane appears in the dict on every click; the value isNonewhen the ray misses, so callers can distinguish "plane not configured" from "plane didn't intersect" without.get()ambiguity.Raycaster threshold (
Scene(raycaster_threshold=...)):Tests (
tests/test_scene.py):test_set_clipping_planes— apply + clear, assert each material'sclippingPlanesarray is populated then cleared.test_set_axes_inset_and_labels— toggle on, set custom labels with non-defaultfont/color/radius, assert the cached opts are reflected on the JS side, toggle off then on, assert the cached labels/style survive the rebuild.test_axes_inset_handle_click_snaps_camera— dispatch apointerdownover the +X axis sprite at its computed screen position, assertviewHelper.animatingflips on, then off when the snap-animation completes.test_intersection_planes_in_click_event— configure'ground'and'wall'planes, click the canvas, assert both names appear ine.intersections.test_raycaster_threshold_runtime_change— change the prop after mount and assert the raycaster'sLine.thresholdupdates.Documentation (
website/documentation/content/scene_documentation.py): three new demos covering clipping planes, the axes inset (now showing the click-to-snap interaction andfont/color/radius), and click-plane intersections.Progress
SceneClickEventArgumentsgains anintersectionsfield with a default-factory empty dict.)