Dash_vtk: Display XYZ axes in 3D scene (orientation marker / coordinate helper)

Thanks for the pointer, but unfortunately, this hasn’t solved the issue. While the Plotly go.Figure approach works for static vector visualization, it doesn’t solve the core requirement: a real-time orientation marker that behaves like the native vtkOrientationMarkerWidget.

After several deep-dives and implementation attempts, here is the “Real Truth” regarding the current limitations of dash-vtk for this specific feature:

  1. The “Dead Axis” Problem: In a standard VTK environment, the orientation marker is a slave to the main renderer. In dash-vtk, even when using a Python callback to sync cameraPosition, there is a significant lag. The axes are either “fixed” (don’t move) or they stutter because the camera data has to travel from the browser to the Python server and back for every single degree of rotation.

  2. Missing Client-Side Sync: Without a native OrientationMarker component in the dash-vtk library, we are forced to use two separate dash_vtk.View components. Because these components don’t share a single internal JavaScript state, they cannot be perfectly synced without a Client-Side Callback (which requires writing custom React/JS, defeating the purpose of a pure Python Dash implementation).

  3. Label Rendering Issues: Desktop VTK (as shown in my earlier screenshots) uses vtkAxesActor with high-quality 2D text labels. In dash-vtk, trying to render 3D text actors or even HTML overlays results in “length of null” errors or labels that don’t rotate correctly with the coordinate system.

  4. Camera Auto-Reset: Every time the child geometry of the axis-view is updated via a callback, the view attempts to reset.

    import dash
    from dash import html, dcc, Input, Output, State
    import dash_vtk
    import numpy as np
    
    app = dash.Dash(_name_)
    
    def build_axes():
    l = 1.0
    configs = [
    {‘d’: [l, 0, 0], ‘c’: [1, 0, 0]},  # X: Red
    
    
    
    
    {‘d’: [0, l, 0], ‘c’: [0, 1, 0]},  # Y: Green
    {‘d’: [0, 0, l], ‘c’: [0, 0, 1]},  # Z: Blue
    ]
    geoms = []
    for cfg in configs:
    geoms.append(dash_vtk.GeometryRepresentation(
    children=[dash_vtk.PolyData(points=[0, 0, 0, *cfg[‘d’]], lines=[2, 0, 1])],
    property={“color”: cfg[‘c’], “lineWidth”: 5}
    ))
    geoms.append(dash_vtk.GeometryRepresentation(
    children=[dash_vtk.Algorithm(
    vtkClass=“vtkConeSource”,
    state={“direction”: cfg[‘d’], “center”: cfg[‘d’], “height”: 0.2, “radius”: 0.08}
    )],
    property={“color”: cfg[‘c’]}
    ))
    return geoms
    
    app.layout = html.Div([
    html.Div([
    
    MAIN VIEW
    
    dash_vtk.View(
    id=“main-view”,
    background=[0.1, 0.1, 0.1],
    cameraPosition=[3, 3, 3],
    cameraViewUp=[0, 0, 1],
    children=[
    dash_vtk.GeometryRepresentation(
    id=“obj-repr”,
    children=[
    
    Placeholder for your OBJ
    
    dash_vtk.Algorithm(vtkClass=“vtkConeSource”, state={“height”: 2.0, “radius”: 1.0})
    ],
    )
    ],
    style={“width”: “100%”, “height”: “90vh”},
    ),
    
        # AXIS WIDGET
        html.Div(\[
            dash_vtk.View(
                id="axis-view",
                cameraPosition=\[3, 3, 3\],
                cameraViewUp=\[0, 0, 1\],
                cameraParallelProjection=True,
                interactorSettings=\[{"button": 1, "action": "None"}\],  # Follower only
                children=build_axes(),
                style={"width": "100%", "height": "100%"},
            ),
            # Absolute labels relative to the mini-box
            html.Div("X", style={"position": "absolute", "right": "10px", "top": "45%", "color": "red",
                                 "fontWeight": "bold"}),
            html.Div("Y", style={"position": "absolute", "left": "45%", "top": "10px", "color": "green",
                                 "fontWeight": "bold"}),
            html.Div("Z", style={"position": "absolute", "left": "10px", "bottom": "10px", "color": "blue",
                                 "fontWeight": "bold"}),
        \], style={
            "position": "absolute", "bottom": "20px", "left": "20px",
            "width": "140px", "height": "140px",
            "border": "2px solid #555", "background": "rgba(0,0,0,0.4)",
            "borderRadius": "8px", "pointerEvents": "none"
        }),
    \], style={"position": "relative"}),
    
    # Hidden component to keep the camera synced even if no sliders are used
    dcc.Interval(id="sync-trigger", interval=200)
    
    ])
    
    @app.callbackapp.callbackapp.callbackapp.callback(
    Output(“axis-view”, “cameraPosition”),
    Output(“axis-view”, “cameraViewUp”),
    Input(“main-view”, “cameraPosition”),
    Input(“main-view”, “cameraViewUp”),
    
    We use the interval as a backup to catch movement if the view doesn’t fire
    
    Input(“sync-trigger”, “n_intervals”),
    State(“main-view”, “cameraPosition”),
    State(“main-view”, “cameraViewUp”),
    )
    def sync_camera(p_in, u_in, n, p_state, u_state):
    
    Use state if inputs are null to avoid the ‘length’ error
    
    pos = p_in or p_state or [3, 3, 3]
    up = u_in or u_state or [0, 0, 1]
    
    # Normalize to a unit vector so the axes don't zoom
    mag = np.linalg.norm(pos)
    if mag == 0: mag = 1
    
    new_pos = \[(p / mag) \* 4.0 for p in pos\]  # Fixed distance 4.0
    return new_pos, up
    
    if _name_ == “_main_”:
    app.run(debug=True)
    

Conclusion: It seems this is a fundamental limitation of the current dash-vtk wrapper. Unless there is a way to link two views at the React/WebGL layer without a Python round-trip, a true “Movable Orientation Marker” isn’t feasible for professional use-cases in Dash right now.