Add TransformControls and hover support to Scene#5990
Draft
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Draft
Add TransformControls and hover support to Scene#5990Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Conversation
**TransformControls** lets the user grab and drag a 3D object via a
gizmo. Methods are hung off `Object3D` (chain-style) so callers never
see the underlying string ID:
```python
box = scene.box()
box.enable_transform_controls(mode='rotate', visible_axes=['Z'], rotation_snap=math.radians(5))
box.set_transform_mode('translate')
box.disable_transform_controls()
```
The corresponding events arrive on the parent `ui.scene` via
`on_transform`, `on_transform_start`, and `on_transform_end` callbacks
typed as `SceneTransformEventArguments` (with both local `x/y/z` and
world `wx/wy/wz` populated uniformly across all three event types).
**Hover overlay**: any object marked `.hoverable()` gets a back-face
glow clone that mirrors its mesh descendants and follows the cursor.
The whole detection/render path runs on the client; tune the appearance
via `hover_color`, `hover_opacity`, `hover_scale` on `ui.scene(...)`.
Notable internals:
- `userOrbitEnabled` latch tracks the user's intended orbit-controls
state. `set_orbit_enabled(False)` writes the latch *and* disables
controls; the dragging-changed handler restores `controls.enabled`
to the latch value on drag end (not unconditionally `true`), so a
TransformControls drag can no longer silently re-enable orbit.
- `Object3D.data` appends `hoverable_` only when truthy; the JS
destructuring reader treats a missing trailing field as `undefined`,
so older payloads stay readable.
- The hover-glow per-frame sync runs inside the existing render loop
and skips the O(n) world-transform refresh when the hovered root's
`matrixWorld.elements` hash is unchanged.
- TransformControls axis-lock state lives in a module-scoped `WeakMap`
rather than `tc.userData`, avoiding collisions with user code.
- TransformControls axis recoloring (which required reaching into the
three.js private `_gizmo`) is dropped from the public API — no caller
needed it and the private-property access was a maintenance hazard.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- Drop the spurious `add_rename` calls for the new `hover-*` props
(they would have emitted deprecation warnings for a brand-new API,
and the source names were camelCase rather than snake_case anyway).
- Make `hover_color` a string (`'#ffffff'`), matching `background_color`
and the rest of NiceGUI's color API. Mirrors the JS-side
`hoverColor: String` declaration; three.js's `MeshBasicMaterial`
already accepts CSS color strings.
- Bundle `space` and `rotation_snap` into the JS
`enable_transform_controls(...)` signature so the gizmo is configured
in a single round-trip — no transient default-state flicker on slow
connections. Drop the implicit `if (mode === "translate") setSpace("world")`
shortcut since three.js's TransformControls already defaults to
`'world'`, and the asymmetric apply-on-enable-only behavior was
surprising for `set_transform_mode`.
- `Scene.set_orbit_enabled` now returns `Self` for chain-style use,
matching the surrounding `on_transform*` methods.
- Tests: replace blind `screen.wait(0.5)` / `screen.wait(0.3)` sleeps
in the new tests with explicit polls on `is_initialized` and
`has_transform_controls`. Drop the dead "Simulate drag" button in
`test_set_orbit_enabled_survives_transform_drag` (it referenced a
non-existent `eval` method and was never clicked). Replace the
hardcoded `len(plain.data) == 17` magic number with a
length-relative assertion that survives future additions to
`Object3D.data`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 interactivity features that come up constantly in 3D-visualization apps but aren't yet in
ui.scene:This is one of several small slices being carved out of #5964 per the splitting discussion on that thread.
Implementation
Public API (
nicegui/elements/scene/scene_object3d.py,nicegui/elements/scene/scene.py,nicegui/events.py):Methods are hung off
Object3D(chain-style) so callers never need the underlying string ID:Object3D.enable_transform_controls(*, mode, size, visible_axes, space, rotation_snap)anddisable_transform_controls().Object3D.set_transform_mode/size/space/rotation_snap(...).Object3D.hoverable(value=True).Scenegainson_transform,on_transform_start,on_transform_endconstructor kwargs and corresponding chain methods, plusset_orbit_enabled(value),hover_color,hover_opacity,hover_scale.SceneTransformEventArgumentscarriestype,mode,object_id,object_name, localx/y/z, localrx/ry/rz, and worldwx/wy/wz— populated uniformly across all three event types so callers don't have to test forNone.Notable internals (
nicegui/elements/scene/scene.js,nicegui/elements/scene/src/index.mjs):userOrbitEnabledlatch tracks the user's intended orbit-controls state.set_orbit_enabled(False)writes the latch and disablescontrols.enabled; thedragging-changedhandler restorescontrols.enabled = userOrbitEnabledon drag end (not unconditionallytrue). Locks in the regression where a TransformControls drag silently re-enabled OrbitControls even after the user explicitly disabled them. Tested viatest_set_orbit_enabled_survives_transform_drag.WeakMap<TransformControls, axis>rather thantc.userData, avoiding collisions with user code.Object3D.dataappendshoverable_only when truthy. The JS destructuring reader treats a missing trailing field asundefined, so older payloads remain readable. Tested viatest_hoverable_serialization_only_when_truthy.matrixWorld.elementshash is unchanged. Thepointermoveraycaster also early-exits when no object is hoverable._gizmo) are not included. No upstream caller needed them and the private-property access was a maintenance hazard.Bundle:
TransformControlsre-exported fromnicegui/elements/scene/src/index.mjs.package.jsondoes not change (TransformControlsis a transitive dep of the existingthreepin). Bundle rebuilt.Tests (
tests/test_scene.py):test_transform_controls_enable_disable— asserts the JS-side controls map gains and loses an entry as enable/disable methods are called.test_transform_controls_mode_change— assertstc.modeactually changes (no implicit-pass).test_set_orbit_enabled_survives_transform_drag— disables orbit, simulates a TransformControls drag-start + drag-end viadragging-changed, assertscontrols.enabledstaysfalse.test_hoverable_serialization_only_when_truthy— asserts plain objects'datalength stays at 17 and only hoverable objects append the trailingTrue.Documentation (
website/documentation/content/scene_documentation.py):A "TransformControls and hover overlay" demo wires both features into a single scene with mode-switching buttons.
Progress
Object3D.datagains an optional trailing field; the JS reader is length-tolerant.)