Skip to content

Add TransformControls and hover support to Scene#5990

Draft
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Jepson2k:scene-transform-hover
Draft

Add TransformControls and hover support to Scene#5990
Jepson2k wants to merge 2 commits intozauberzeug:mainfrom
Jepson2k:scene-transform-hover

Conversation

@Jepson2k
Copy link
Copy Markdown

Motivation

Two interactivity features that come up constantly in 3D-visualization apps but aren't yet in ui.scene:

  • TransformControls — let the user grab and drag a 3D object via a gizmo (translate / rotate / scale) and stream the resulting transforms back to Python.
  • Hover overlay — give selectable / interactable objects a cheap visual affordance ("this is hoverable, click me") that doesn't round-trip through the server on every mouse move.

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) and disable_transform_controls().
  • Object3D.set_transform_mode/size/space/rotation_snap(...).
  • Object3D.hoverable(value=True).
  • Scene gains on_transform, on_transform_start, on_transform_end constructor kwargs and corresponding chain methods, plus set_orbit_enabled(value), hover_color, hover_opacity, hover_scale.
  • New event type SceneTransformEventArguments carries type, mode, object_id, object_name, local x/y/z, local rx/ry/rz, and world wx/wy/wz — populated uniformly across all three event types so callers don't have to test for None.

Notable internals (nicegui/elements/scene/scene.js, nicegui/elements/scene/src/index.mjs):

  • New userOrbitEnabled latch tracks the user's intended orbit-controls state. set_orbit_enabled(False) writes the latch and disables controls.enabled; the dragging-changed handler restores controls.enabled = userOrbitEnabled on drag end (not unconditionally true). Locks in the regression where a TransformControls drag silently re-enabled OrbitControls even after the user explicitly disabled them. Tested via test_set_orbit_enabled_survives_transform_drag.
  • TransformControls axis-lock state lives in a module-scoped WeakMap<TransformControls, axis> rather than tc.userData, avoiding collisions with user code.
  • Object3D.data appends hoverable_ only when truthy. The JS destructuring reader treats a missing trailing field as undefined, so older payloads remain readable. Tested via test_hoverable_serialization_only_when_truthy.
  • 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. The pointermove raycaster also early-exits when no object is hoverable.
  • TransformControls and ViewHelper-flavored axis recoloring (which required reaching into the three.js private _gizmo) are not included. No upstream caller needed them and the private-property access was a maintenance hazard.

Bundle: TransformControls re-exported from nicegui/elements/scene/src/index.mjs. package.json does not change (TransformControls is a transitive dep of the existing three pin). 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 — asserts tc.mode actually changes (no implicit-pass).
  • test_set_orbit_enabled_survives_transform_drag — disables orbit, simulates a TransformControls drag-start + drag-end via dragging-changed, asserts controls.enabled stays false.
  • test_hoverable_serialization_only_when_truthy — asserts plain objects' data length stays at 17 and only hoverable objects append the trailing True.

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

  • The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc.
  • The implementation is complete. (Otherwise, open a draft PR.)
  • This PR does not address a security issue. (Security fixes must be coordinated via the security advisory process before opening a PR.)
  • Pytests have been added.
  • Documentation has been added.
  • No breaking changes to the public API. (Object3D.data gains an optional trailing field; the JS reader is length-tolerant.)

Jepson2k and others added 2 commits April 24, 2026 09:18
**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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant