Examples¶
Ready-to-run code for the most common scenarios. Copy, paste, and adapt.
Minimal overlay¶
The simplest possible integration — a NavCube floating in the corner of a window:
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from navcube import NavCubeOverlay
app = QApplication(sys.argv)
win = QMainWindow()
win.setWindowTitle("Minimal NaviCube")
win.resize(800, 600)
win.show()
cube = NavCubeOverlay(parent=win)
cube.move(win.width() - 150, 10)
cube.show()
cube.viewOrientationRequested.connect(
lambda dx, dy, dz, ux, uy, uz:
print(f"Dir: ({dx:+.3f}, {dy:+.3f}, {dz:+.3f}) "
f"Up: ({ux:+.3f}, {uy:+.3f}, {uz:+.3f})")
)
sys.exit(app.exec())
Inline / dock mode¶
Embed the cube as a regular widget inside a layout — no overlay window flags:
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QHBoxLayout,
QVBoxLayout, QLabel, QDockWidget,
)
from PySide6.QtCore import Qt
from navcube import NavCubeOverlay, NavCubeStyle
app = QApplication(sys.argv)
win = QMainWindow()
win.setWindowTitle("Inline NaviCube Demo")
win.resize(900, 600)
# Main content area
central = QLabel("3D Viewport Area")
central.setAlignment(Qt.AlignCenter)
win.setCentralWidget(central)
# Dock widget with an inline navicube
dock = QDockWidget("Orientation", win)
dock_widget = QWidget()
dock_layout = QVBoxLayout(dock_widget)
# overlay=False makes it a normal QWidget
cube = NavCubeOverlay(parent=None, overlay=False,
style=NavCubeStyle(size=100, inactive_opacity=1.0))
dock_layout.addWidget(cube)
dock_layout.addStretch()
dock.setWidget(dock_widget)
win.addDockWidget(Qt.RightDockWidgetArea, dock)
cube.viewOrientationRequested.connect(
lambda dx, dy, dz, ux, uy, uz:
central.setText(f"Dir: ({dx:+.2f}, {dy:+.2f}, {dz:+.2f})")
)
win.show()
sys.exit(app.exec())
Custom colors¶
A dark blue theme with orange hover highlights:
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from navcube import NavCubeOverlay, NavCubeStyle
app = QApplication(sys.argv)
win = QMainWindow()
win.resize(800, 600)
win.show()
style = NavCubeStyle(
theme="dark",
size=140,
# Dark blue faces
face_color_dark=(35, 55, 110),
edge_color_dark=(25, 40, 85),
corner_color_dark=(18, 28, 60),
# Light text
text_color_dark=(220, 230, 255),
# Dark borders
border_color_dark=(8, 12, 30),
border_secondary_color_dark=(12, 18, 40),
# Orange hover highlight
hover_color_dark=(255, 140, 0, 235),
hover_text_color_dark=(255, 255, 255),
# Subtle semi-transparent controls
control_color_dark=(25, 45, 90, 100),
control_rim_color_dark=(15, 30, 60, 150),
# Stronger shadow
shadow_color_dark=(0, 0, 0, 100),
shadow_offset_x=2.5,
shadow_offset_y=3.0,
)
cube = NavCubeOverlay(parent=win, style=style)
cube.move(win.width() - 170, 10)
cube.show()
win.show()
sys.exit(app.exec())
Localized labels¶
German¶
from navcube import NavCubeStyle
german_style = NavCubeStyle(
labels={
"TOP": "OBEN",
"FRONT": "VORNE",
"LEFT": "LINKS",
"BACK": "HINTEN",
"RIGHT": "RECHTS",
"BOTTOM": "UNTEN",
},
)
Japanese¶
from navcube import NavCubeStyle
japanese_style = NavCubeStyle(
font_family="Noto Sans CJK JP",
font_fallback="SansSerif",
font_weight="bold",
label_max_width_ratio=0.60, # CJK characters need more width
label_max_height_ratio=0.55,
labels={
"TOP": "\u4e0a", # 上
"FRONT": "\u524d", # 前
"LEFT": "\u5de6", # 左
"BACK": "\u5f8c", # 後
"RIGHT": "\u53f3", # 右
"BOTTOM": "\u4e0b", # 下
},
)
Russian¶
from navcube import NavCubeStyle
russian_style = NavCubeStyle(
font_family="DejaVu Sans",
labels={
"TOP": "\u0412\u0415\u0420\u0425", # ВЕРХ
"FRONT": "\u041f\u0415\u0420\u0415\u0414", # ПЕРЕД
"LEFT": "\u041b\u0415\u0412\u041e", # ЛЕВО
"BACK": "\u0417\u0410\u0414", # ЗАД
"RIGHT": "\u041f\u0420\u0410\u0412\u041e", # ПРАВО
"BOTTOM": "\u041d\u0418\u0417", # НИЗ
},
)
Y-up engine¶
For engines that use Y-up coordinates (Three.js, Unity, glTF viewers):
import sys
import numpy as np
from PySide6.QtWidgets import QApplication, QMainWindow
from navcube import NavCubeOverlay
class YUpNaviCube(NavCubeOverlay):
"""NaviCube configured for Y-up coordinate systems."""
_WORLD_ROT = np.array([
[1, 0, 0],
[0, 0, -1],
[0, 1, 0],
], dtype=float)
app = QApplication(sys.argv)
win = QMainWindow()
win.resize(800, 600)
win.show()
cube = YUpNaviCube(parent=win)
cube.move(win.width() - 150, 10)
cube.show()
def on_orient(dx, dy, dz, ux, uy, uz):
# These are in Y-up world space — no conversion needed
print(f"Y-up Dir: ({dx:+.3f}, {dy:+.3f}, {dz:+.3f})")
print(f"Y-up Up: ({ux:+.3f}, {uy:+.3f}, {uz:+.3f})")
cube.viewOrientationRequested.connect(on_orient)
# Push Y-up camera vectors directly
# Example: looking from front, direction = (0, 0, -1), up = (0, 1, 0)
cube.push_camera(0, 0, -1, 0, 1, 0)
sys.exit(app.exec())
OCC integration¶
Full working integration with pythonocc:
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import Qt
from OCC.Display.SimpleGui import init_display
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from navcube import NavCubeOverlay
from navcube.connectors.occ import OCCNavCubeSync
class OCCViewerWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("OCC + NaviCube")
self.resize(1024, 768)
# Initialize OCC display (replace with your own OCC widget setup)
display, start_display, add_menu, add_function_to_menu = init_display()
self._display = display
# Add some geometry
box = BRepPrimAPI_MakeBox(50, 30, 20).Shape()
display.DisplayShape(box, update=True)
# Get the V3d_View
view = display.GetView()
# Create and connect the navicube
canvas = display.GetWidget()
self.navicube = NavCubeOverlay(parent=canvas)
self.navicube.show()
# The connector handles everything else
self.sync = OCCNavCubeSync(view, self.navicube)
self.navicube.move(canvas.width() - 150, 10)
def closeEvent(self, event):
self.sync.teardown()
super().closeEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = OCCViewerWindow()
win.show()
sys.exit(app.exec())
VTK integration¶
Full working integration with VTK:
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
import vtkmodules.all as vtk
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from navcube import NavCubeOverlay
from navcube.connectors.vtk import VTKNavCubeSync
class VTKViewerWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("VTK + NaviCube")
self.resize(1024, 768)
# VTK setup
self.vtk_widget = QVTKRenderWindowInteractor(self)
self.setCentralWidget(self.vtk_widget)
renderer = vtk.vtkRenderer()
renderer.SetBackground(0.2, 0.2, 0.2)
self.vtk_widget.GetRenderWindow().AddRenderer(renderer)
# Add a cube actor
source = vtk.vtkCubeSource()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
renderer.AddActor(actor)
renderer.ResetCamera()
# Create the navicube
self.navicube = NavCubeOverlay(parent=self.vtk_widget)
self.navicube.show()
self.navicube.move(self.vtk_widget.width() - 150, 10)
# Connect via VTK sync
self.sync = VTKNavCubeSync(renderer, self.navicube)
self.vtk_widget.Initialize()
self.vtk_widget.Start()
def closeEvent(self, event):
self.sync.teardown()
super().closeEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = VTKViewerWindow()
win.show()
sys.exit(app.exec())
Runtime theme switching¶
Toggle between light and dark themes with a button:
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget,
)
from navcube import NavCubeOverlay, NavCubeStyle
app = QApplication(sys.argv)
win = QMainWindow()
win.setWindowTitle("Theme Switching")
win.resize(800, 600)
central = QWidget()
layout = QVBoxLayout(central)
win.setCentralWidget(central)
cube = NavCubeOverlay(parent=win)
cube.show()
cube.move(win.width() - 150, 10)
is_dark = [False]
light_style = NavCubeStyle(theme="light")
dark_style = NavCubeStyle(
theme="dark",
face_color_dark=(60, 60, 70),
edge_color_dark=(45, 45, 55),
corner_color_dark=(35, 35, 42),
hover_color_dark=(0, 180, 100, 235),
)
def toggle_theme():
is_dark[0] = not is_dark[0]
cube.set_style(dark_style if is_dark[0] else light_style)
btn.setText("Switch to Light" if is_dark[0] else "Switch to Dark")
btn = QPushButton("Switch to Dark")
btn.clicked.connect(toggle_theme)
layout.addWidget(btn)
layout.addStretch()
win.show()
sys.exit(app.exec())
Custom home orientation¶
Set the orientation the Home button snaps back to:
import math
from navcube import NavCubeOverlay
cube = NavCubeOverlay(parent=viewport)
cube.show()
# Home = looking straight at the FRONT face
# Inward direction: (0, 1, 0) — toward +Y (FRONT in Z-up)
# Up vector: (0, 0, 1) — Z is up
cube.set_home(0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
# Or set home to an isometric view
d = 1.0 / math.sqrt(3.0)
cube.set_home(-d, d, -d, 0.0, 0.0, 1.0)
XYZ gizmo¶
Show the axis gizmo with custom colors:
from navcube import NavCubeOverlay, NavCubeStyle
style = NavCubeStyle(
show_gizmo=True,
gizmo_x_color=(255, 50, 50), # Bright red
gizmo_y_color=(50, 255, 50), # Bright green
gizmo_z_color=(50, 100, 255), # Bright blue
gizmo_font_size=10,
)
cube = NavCubeOverlay(parent=viewport, style=style)
cube.show()
Custom connector skeleton¶
A minimal template for writing your own connector. Replace the placeholder API calls with whatever your engine provides:
"""
Connector template for NavCubeOverlay with a custom 3D engine.
Replace the placeholder method calls with your engine's API.
"""
import math
from PySide6.QtCore import QTimer
class CustomNaviCubeSync:
_TICK_MS = 16
_INTERACTION_TICKS = 1
_IDLE_TICKS = 4
def __init__(self, engine, navicube):
self._engine = engine
self._navicube = navicube
self._active = False
self._tick_count = 0
navicube.viewOrientationRequested.connect(self._on_orient)
self._timer = QTimer()
self._timer.timeout.connect(self._poll)
self._timer.start(self._TICK_MS)
def set_interaction_active(self, active: bool):
self._active = bool(active)
self._tick_count = 0
if self._navicube:
self._navicube.set_interaction_active(active)
def teardown(self):
self._timer.stop()
try:
if self._navicube:
self._navicube.viewOrientationRequested.disconnect(self._on_orient)
except Exception:
pass
self._engine = None
self._navicube = None
def _poll(self):
if not self._engine or not self._navicube:
return
rate = self._INTERACTION_TICKS if self._active else self._IDLE_TICKS
self._tick_count += 1
if self._tick_count < rate:
return
self._tick_count = 0
try:
# Replace with your engine's camera API:
cam_dir = self._engine.get_camera_direction() # INWARD (eye -> scene)
cam_up = self._engine.get_camera_up()
self._navicube.push_camera(
cam_dir[0], cam_dir[1], cam_dir[2],
cam_up[0], cam_up[1], cam_up[2],
)
except Exception:
pass
def _on_orient(self, dx, dy, dz, ux, uy, uz):
if not self._engine:
return
mag = math.sqrt(dx*dx + dy*dy + dz*dz)
if mag < 1e-6:
return
try:
# Replace with your engine's camera API:
focal = self._engine.get_focal_point()
dist = self._engine.get_camera_distance()
s = dist / mag
self._engine.set_camera_position(
focal[0] + dx*s, focal[1] + dy*s, focal[2] + dz*s,
)
self._engine.set_camera_up(ux, uy, uz)
self._engine.render()
except Exception:
pass