Skip to content

Active Elements

Active elements are high-level convenience classes that create interactive 3D entities and register their own interaction handlers automatically. They simplify the common case of "add a draggable X to the scene."

Available Active Elements

Class Entity Interaction Page
ActPoint Draggable Point Left-drag on four constraint planes ActPoint
ActRectangle2D Rectangle2D body + square ActPoint handles Corner handles resize, centre handle translates ActRectangle2D

Common Behaviour

All active elements inherit from ActSceneObject and share:

Feature Description
Auto-registration Triggers and handlers are set up automatically by viz.add()
Custom handler Optional callback invoked before default movement; returns bool to signal full handling
Click handler Optional on_click callback invoked on a click (press-and-release without dragging)
Standard drag triggers View-plane, XY, XZ, YZ with Shift/Ctrl modifiers (left mouse button)
Self-contained flush Default handler calls update() + flush() after moving
Labels viz.add(ap, label=...) creates an attached label, removed together with the entity

Enable / Disable

Every active element can be disabled at runtime — it stops responding to hover/drag/click/scroll (the interaction config is re-pushed with enabled=False):

ap.disable()          # stop interaction
ap.enable()           # re-enable
ap.set_enabled(False) # explicit

Usage Pattern

from pytanga.viz import ActPoint, Visualizer
from pytanga.geometry import Point

viz = Visualizer()

# Without custom handler — just drag the point:
ap = ActPoint(Point(1, 2, 3))
viz.add(ap, color="#ff4444")

# With custom handler — update other entities on every drag:
async def on_move(event, ap):
    # event.world_position is a Point
    # ap.point is the current position
    # ap.viz_handle gives access to the scene for updates
    return False  # let ActPoint move the point and flush

ap = ActPoint(Point(0, 0, 2), handler=on_move)
viz.add(ap)

viz.run()

Custom Handler Contract

ActHandler = Callable[[DragEvent, ActSceneObject], Awaitable[bool]]
Return value Behaviour
True Fully handled — no default movement, no automatic flush (handler is responsible)
False Default behaviour runs: entity is moved to event.world_position, update() + flush() are called

Drag Lifecycle Handlers

In addition to the move-phase handler, an active element accepts two notification callbacks for the start and end of a drag:

ActEventHandler = Callable[[DragEvent, ActSceneObject], Awaitable[None]]

ap = ActPoint(
    Point(0, 0, 2),
    handler=on_move,
    on_drag_start=on_start,   # called on DRAG_START
    on_drag_end=on_end,       # called on DRAG_END
)

on_drag_start / on_drag_end receive the same (event, ap) arguments as the move handler, but their return value is ignored — they observe the drag lifecycle and never override the default movement.

Click Handler

An active element can also accept an on_click notification callback, fired on a press-and-release without dragging:

ActClickHandler = Callable[[ClickEvent, ActSceneObject], Awaitable[None]]

ap = ActPoint(Point(0, 0, 2), on_click=on_click)

event.world_position reports the element's ideal point (its centre), not the ray/mesh hit. The return value is ignored.

Writing Custom Active Elements

Subclass ActSceneObject and implement three properties:

from pytanga.viz import ActSceneObject, InteractionConfig, MouseButton
from pytanga.viz._active import _default_drag_triggers
from pytanga.geometry import Sphere, Point

class ActSphere(ActSceneObject):
    def __init__(
        self,
        sphere,
        *,
        handler=None,
        on_drag_start=None,
        on_drag_end=None,
        on_click=None,
    ):
        super().__init__(
            handler=handler,
            on_drag_start=on_drag_start,
            on_drag_end=on_drag_end,
            on_click=on_click,
        )
        self._sphere = sphere

    @property
    def entity(self):
        return self._sphere

    @property
    def interaction_config(self):
        return InteractionConfig(
            enabled=True,
            triggers=_default_drag_triggers(MouseButton.LEFT),
            throttle_ms=40,
        )

    def _move_to(self, pos: Point):
        self._sphere = Sphere(pos, self._sphere.radius)

Then viz.add(ActSphere(...)) will work automatically.

Optionally override drag_anchor(ray_origin, ray_direction) -> Point to return the nearest point on the entity's ideal geometry to the picking ray; the base raises NotImplementedError (which keeps the mesh-surface anchor). This is what makes ActPoint snap to its centre, and what a future torus/circle or sphere active element would implement to keep its rendered extent out of the drag.

See Also