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:
-
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 synccameraPosition, 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. -
Missing Client-Side Sync: Without a native
OrientationMarkercomponent in thedash-vtklibrary, we are forced to use two separatedash_vtk.Viewcomponents. 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). -
Label Rendering Issues: Desktop VTK (as shown in my earlier screenshots) uses
vtkAxesActorwith high-quality 2D text labels. Indash-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. -
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.