Skip to content

Viewer Navigation

Everything on this page is read directly from the event handlers in glyphviz_gl/viewport.py (the Viewport QOpenGLWidget subclass) and the shortcuts wired up in glyphviz_gl/main_window.py. Citations point at the function/line where each binding is implemented.

Camera controls

The camera is orbit/target/distance based (_cam_azimuth, _cam_elevation, _cam_target, _cam_distanceviewport.py:126-129). Which mouse button does what depends on whether a Move/Rotate/Size gizmo mode is active (see Node manipulation below) — the bindings below describe plain camera-only interaction, i.e. no gizmo mode selected. They also assume Camera Mode is off; C swaps them all for free flight (see Camera Mode).

Action Binding Source
Orbit Left-click + drag (or Right-click + drag) mouseMoveEvent, _orbit_cameraviewport.py:1502-1515, 1533-1535
Orbit + zoom together, around the selected object Left + Right together + drag _orbit_zoom_camera, _maybe_snap_orbit_target
Pan Middle-click + drag mouseMoveEventviewport.py:1517-1528
Zoom Scroll wheel (×0.88 per notch in, ×1.14 per notch out) wheelEventviewport.py:1569-1573
Reset / reframe view View → Reset Camera menu item _reset_cameramain_window.py:1802-1803

Orbit sensitivity is 0.4° of azimuth/elevation per pixel of drag; elevation is clamped to ±89° (_orbit_camera, viewport.py:1533-1535). Pan speed scales with the current camera distance (viewport.py:1525-1528).

The Left + Right combined gesture is ANTz's: horizontal drag orbits, vertical drag zooms. Pressing both buttons also re-centers the orbit pivot on the selected node, so it orbits that object rather than whatever the camera happened to be aimed at.

Camera smoothing (optional inertia)

By default the camera tracks the mouse exactly. The Camera group in the Properties panel adds an optional glide: the view eases toward where you drag instead of arriving instantly.

Control What it does
Smoothing slider The amount, as a time constant in seconds. 0 (the default) is off.
Off / ANTz buttons Presets: no smoothing, and ANTz's own feel (0.33 s).
Applies to Left+Right drag only (the default, matching ANTz) or All camera navigation (also single-button orbit, pan and wheel zoom).

This exists because ANTz always smoothed its camera and never let you turn it off. Two groups asked for it back:

  • Former ANTz users, who found the glide made live demonstrations look noticeably smoother.
  • Users with tremor. Dr. Dave Warner reported that patients with movement disorders such as Parkinson's found ANTz's smoothing genuinely helpful, because it buffers tremor out of the camera. All camera navigation is the setting to use for that — a tremor doesn't confine itself to one gesture.

GlyphViz's version does not swing the target out of frame. That was the part of ANTz's implementation people disliked, and it wasn't the smoothing itself: ANTz eased the camera's position while applying the orbit angle and the zoom instantly on the same frame, so the eye crawled toward a solution the aim had already left. GlyphViz eases azimuth, elevation, distance and target together under one time constant, so what you're orbiting stays framed for the whole glide. The filter is also frame-rate correct, so a given setting feels the same at 30 fps and at 144 — ANTz's approximation did not, which is why its smoothing felt so much heavier in its early, slower years.

Two further notes:

  • It's a lag, not momentum. Releasing the mouse stops the camera easing to a halt; nothing overshoots or keeps spinning.
  • Camera Mode (free flight) is never smoothed. Rate flight already integrates a held cursor offset once per frame, so it has no per-event jitter to filter.

Camera jumps stay instant whatever the setting: reference-view presets, double-click focus, Reset Camera, and scene loading. Only drags glide.

Follow Selected Node re-pins the camera's target on the followed node every frame without smoothing — following a moving node is a source to track, not a hand tremor to filter, and easing it would leave the camera permanently trailing. Orbiting and zooming around a followed node still glide normally.

The setting is per session — it resets to Off when GlyphViz restarts.

Reset Camera re-fits the camera's target and distance to the bounding box of all loaded nodes (the same auto-framing Viewport.set_nodes() does on load) — it does not reset azimuth/elevation back to any default angle, since it just calls set_nodes() again (main_window.py:1802-1803, viewport.py:223-244).

Camera Mode (free flight)

Press C, click Fly on the Camera toolbar, or use View → Camera Mode (Free Flight) to swap the orbit gestures for flight. The orbit camera is always anchored to a target — to get a fresh angle you pan away, re-orient, pan again. Camera Mode drops the anchor so you can simply fly to the shot, which is what it exists for: composing screen-capture angles.

Action Binding Source
Fly forward / back (hold above / below the press point) Left-click + hold, cursor above/below the click _apply_fly_drag
Turn left / right (hold beside the press point) Left-click + hold, cursor left/right of the click _apply_fly_drag
Look around from where the camera stands Right-click + drag _look_around_camera
Slide sideways / up / down without turning Left + Right together + drag _fly_strafe_camera
Fly forward / back Scroll wheel wheelEvent
Pan Middle-click + drag (unchanged) mouseMoveEvent

Left-button flight is rate-based, matching ANTz (but on ANTz's opposite button — in ANTz the right button flies and the left looks around; here it's reversed). The point where you press becomes the bearings origin, and nothing moves until the cursor leaves a small dead zone around it. Holding the cursor above the origin flies forward at a speed proportional to the held offset — a slight offset gives a slight but constant speed for as long as the button is down, even with the mouse stationary — and bringing the cursor back to the origin stops the flight; holding below it flies backward, and holding left/right of it turns at a rate proportional to the sideways offset. Flight is locked to the horizontal plane it starts on: the camera translates along its heading with the pitch component dropped, so altitude never changes no matter where the aim points (use right-drag look-around, the wheel, or a Left+Right slide to change altitude).

Speeds scale with the camera's arm length (_FLY_RATE_PER_PX_S), so holding the same offset covers about the same fraction of the view whether you're circling a whole scene or nosing around one glyph. Pitch is clamped to ±89°, as with the orbit camera.

While Camera Mode is on:

  • Clicking still selects, and Shift+drag still rubber-band selects — but Move/Rotate/Size gizmo drags are suspended until it's switched back off.
  • Follow Selected Node is switched off (it re-pins the camera target every frame, which would undo flight immediately).
  • The fixed +X/+Y/+Z reference views can't fly. Turning Camera Mode on switches to the Main camera; picking a reference view turns it off.

Double-clicking a node also moves the camera: it selects the node and zooms in to frame it with headroom (mouseDoubleClickEventfocus_on_nodeviewport.py:1474-1479, 306-310). The Node Table's double-click does the same thing (_on_table_double_clickmain_window.py:1796-1800).

Selection

Action Binding Source
Select a node Left-click on it (replaces selection) mouseReleaseEventviewport.py:1453-1469
Add/remove one node Ctrl + Left-click on it mouseReleaseEventnodeClickedAdditiveviewport.py:1461-1466
Rubber-band select Shift + Left-click-drag a rectangle mousePressEvent/mouseReleaseEventviewport.py:1412-1425, 1434-1451
Deselect all Left-click empty space (no Ctrl held) mouseReleaseEventviewport.py:1467-1469
Select/deselect all Ctrl+A (toggles: selects all unless everything is already selected, then deselects) QShortcut("Ctrl+A")_on_select_all_togglemain_window.py:109-111, 1258-1263

Rubber-band region select is disabled while Stereo 3D (Cross-Eye) view is active — a single screen-space rectangle can't be mapped back to one node set when the two eyes show different projections of the same scene. Single-node click/double-click picking still works in stereo (mousePressEvent comment — viewport.py:1413-1418).

Node Table row selection and Viewer selection are the same selection state — picking in one updates the other (_on_table_selection, _on_viewport_pick*main_window.py:1267-1284). See Node Panel.

Keyboard shortcuts

Shortcuts handled directly in the Viewer (Viewport.keyPressEvent/event, viewport.py:1341-1390) — active whenever the 3D view has keyboard focus:

Key Action Source
↑ (Up) Select the current node's parent navParent_nav_parentviewport.py:1356-1358, main_window.py:1844-1852
↓ (Down) Select the current node's first child navChild_nav_childviewport.py:1359-1361, main_window.py:1854-1865
Tab Select next sibling (wraps; at branch level 0 this cycles all root nodes) navNextSibling_nav_next_siblingviewport.py:1345-1348, main_window.py:1867-1891
Shift+Tab Select previous sibling (wraps) navPrevSibling_nav_prev_siblingviewport.py:1349-1352, main_window.py:1893-1906
N New node (child of selection if a non-root node is selected, else a new root); steps by the Placement Step, wrapping to a new translate_y row at 360 createNode_on_create_nodeviewport.py:1362-1366, main_window.py:1910-1913
Shift+N New child node under the current (single) selection, in the next free slot of the same wrapped step lattice createChildNode_create_child_nodeviewport.py:1363-1364
\ (backslash) Halve the draw limit (progressively hides nodes — perf aid for huge scenes) _adjust_draw_limit(0.5)viewport.py:1368-1374, 1392-1408
Shift+\ or | Double the draw limit (restores toward showing everything) _adjust_draw_limit(2.0)viewport.py:1368-1374
T Toggle tag-label display viewport.py:1375-1379
B Toggle background black/white bgToggleRequestedviewport.py:1380-1382
8 Cycle the scene-wide render/blend mode renderModeCycleRequestedviewport.py:1383-1385
K Quick-save — writes fresh, timestamped node + tag CSVs immediately (no dialog), next to the running executable quickSaveRequested_quick_saveviewport.py:1386-1388, main_window.py:1093-1106
C Toggle Camera Mode (free flight) on/off — see above toggle_fly_mode
D Toggle Draw Mode (paint objects onto a World Grid) — see below; Esc also exits toggle_draw_mode
Shift+C Step to the next camera (Main → +X → +Y → +Z → Main) cycle_camera
O / Shift+O Cycle each selected node's geometry forward/back one entry (ANTz-style; the step is per node, so a mixed-geometry selection stays mixed). Works on Link nodes too — a link renders whatever geometry it carries, stretched between its two ends (Geometries) geoCycleRequested_cycle_selected_geometry
J / Shift+J Cycle each selected node's topology forward/back one entry (same per-node rule) topoCycleRequested_cycle_selected_topology

Shortcuts registered at the main-window level (QShortcut, still fire while the Viewer has focus, since Qt shortcuts aren't widget-scoped here):

Key Action Source
Ctrl+A Select all / deselect all (see above) main_window.py:109-111
U Open the selected node's Link field (URL or local file) in the OS default handler _on_open_linkmain_window.py:112, 1166-1181
Delete / Backspace Delete the selected node(s) and their descendants _delete_selectedmain_window.py:113-114
Ctrl+B Show/hide the yellow selection bounding box without changing the selection — for clean screengrabs of a selected state. (ANTz uses the spacebar for this; in GlyphViz the spacebar drives Channels playback.) View → Show Selection Box_set_selection_box

File/Edit/View menu actions carry their own accelerators (Ctrl+O, Ctrl+S, Ctrl+Shift+S, Ctrl+Alt+S, F12, Ctrl+C, Ctrl+V, Ctrl+G, Ctrl+B) — see Menus. Ctrl+Alt+S writes the current selection to a file of its own; see Save Selection.

Node manipulation

The Manipulate toolbar (built in _build_manipulate_toolbar, main_window.py:241-279) sits above the Viewer and is always visible. It has three mutually-exclusive mode buttons — Move, Rotate, Size — plus X/Y/Z axis-confinement checkboxes (all checked by default). The buttons are disabled until at least one node is selected.

Clicking a mode button a second time returns to camera-only dragging (_on_mode_button_clickedmain_window.py:281-283). While a mode is active:

  • Left-click + drag moves/rotates/scales the selection along the first enabled axis (and the second, if two or more axes are checked).
  • Right-click + drag manipulates the third enabled axis, but only when all three X/Y/Z boxes are checked — otherwise Right-click still just orbits the camera.

(mouseMoveEvent, _apply_axis_deltas.) Move drag speed matches camera-pan speed (_MOVE_WORLD_PER_PX); Rotate drag speed matches orbit sensitivity (_ROTATE_DEG_PER_PX, 0.4° per pixel); Size drag applies a per-pixel multiplier (_SIZE_FACTOR_PER_PX), floored at 0.001.

Transform handles

Arming a mode also puts red/green/blue X/Y/Z handles on the selected node — arrows for Move, rings for Rotate, capped bars for Size. They hold a constant on-screen size however far away the node is, highlight as the cursor comes over them, and are drawn on top of the scene so a handle stays grabbable even when the glyph is larger than it is.

Dragging a handle constrains the edit to that one axis and tracks the cursor: pull the red arrow 60 pixels along itself and the node moves 60 pixels' worth of world distance, however the node's parent is scaled or rotated. Dragging across a handle does nothing. On a multi-selection the handles sit on the first selected node and every other selected node takes the same field change.

The handles are purely additive — a press that misses every one of them still gets the free-form axis drag described above, so the existing gesture is unchanged. View → Show Transform Handles turns the handles off without disarming the mode.

Which direction a handle points is per-mode, because the three field groups do not act in the same frame (glyphviz_core/gizmo.py):

Mode Handles point along Why
Move the direction each translate_x/y/z field actually pushes the node translate is a placement coordinate in the parent's topology, so on a Sphere parent the red arrow is the longitude tangent, not world X
Rotate the axis each rotate_x/y/z field spins the node about under ANTz's Heading/Tilt/Roll convention rotate_y and rotate_z both drive Z rotations, so the rings are derived from the rotation matrix rather than assumed
Size the node's own local axes scale_x/y/z genuinely stretch those

Handles are not drawn on Link nodes (a link's own transform is inert), in Camera Mode (which owns every drag button), or in stereo mode (where the scene is drawn twice and a cursor measured against the whole widget would grab at neither) — the armed mode and its free-form drags still work in all three cases.

Scaling uniformly

Lock X/Y/Z together, in the Properties panel's Selected Node group, also governs Size mode: with it ticked, dragging any bar — or any free-form Size drag — applies the same factor to all three axes, and a pale triangle joins the three bar ends to show they are tied. A drag shares the factor rather than forcing the three fields equal the way the panel's spin boxes do, so a deliberately 3:1:1 glyph keeps its proportions instead of snapping to a cube on the first pixel.

When two rotate rings coincide

Each ring is a true circle in the plane perpendicular to its own axis — the path a point on the glyph actually travels. Two rings landing on top of each other is Euler gimbal lock, reported honestly: near rotate_y = ±90° in Euler XYZ mode, rotate_x and rotate_z really do spin the node about the same axis, so there is nothing left for one of them to do independently. (Heading/Tilt/Roll has its own lock, at tilt 0° / 180°.) Dragging still works — the hover highlight shows which ring you have — and the rings separate again as you move the middle angle away from the singularity.

Draw Mode (painting objects)

Press D or click the Properties panel's Draw Mode button (Create group) to paint new objects into the scene with the mouse:

  • Left click stamps one new object — built from the New Object Defaults (geometry, topology, scale, color) — exactly where the cursor meets the World Grid's plane.
  • Left click + drag stamps a trail of them, one every Stamp Spacing pixels of cursor travel (default 30 px; 0 stamps on every mouse move for maximum density).
  • Draw Z (Properties panel, next to the Draw Mode button) sets each stamped object's translate_z — its elevation above the grid plane. The object always lands directly under the cursor at that elevation.

The brush doesn't have to be a single object. The Stamp combo (Create group) switches between Single Object and any loaded Glyph Composer template — with a template selected, every click or drag-stamp plants the whole hyperglyph (its root lands on the cursor, its levels hang beneath it as usual). Get a template into the combo either way:

  • Load… next to the combo opens a saved composer template (.json), or
  • Tools → Glyph Composer → Use as Draw Brush sends the template you're editing straight over and turns Draw Mode on — compose a tree, press, paint a forest. (It registers a snapshot; press it again after edits to update the brush.) Loaded templates accumulate in the combo, so you can alternate brushes stroke to stroke.

Two jitter controls give strokes an organic, hand-scattered feel; both draw one random value per stamp (so a hyperglyph stays coherent), and both default to 0 = off:

  • Heading Jitter ±° spins each stamp by a random heading (added to the root's rotate_z, which turns the whole subtree).
  • Scale Jitter ±% grows or shrinks each stamp by one random factor on all three axes, preserving its proportions.

Every stamped object is a real child of the grid (parent_id = grid.id, translate_x/y = its longitude/latitude in the grid's extents window), so a drawn scene saves, reloads, and merges like any other — and moving, rotating, or scaling the grid afterward carries the whole drawing with it. Drawing targets the selected grid when one is selected, else the scene's first grid; entering Draw Mode in a gridless scene creates a default whole-earth grid automatically. Globe-form grids aren't drawable yet — switch the grid's Shape to Flat first.

While Draw Mode is on, the left button belongs to the brush: plain click-select is suspended (Ctrl+click still adds to the selection, Shift+drag still rubber-band selects — handy for grabbing a misplaced stroke to Delete it). Right-drag still orbits, the wheel still zooms, and the middle button still pans, so you can reposition the camera mid-drawing. Esc or D exits; Draw Mode and Camera Mode displace each other, since both claim the left button outright.

Screen-to-grid math lives in glyphviz_core/draw_mode.py (the click ray is intersected with the grid's plane in the grid's own frame, so drawing is exact under any grid translate/rotate/scale); gesture wiring in viewport.py (set_draw_mode, draw_plane_hit), node creation in main_window.py (_on_draw_stamp). Tests: tests/test_draw_mode.py.

Next step

See Menus for the menu-driven equivalents of these actions, or Properties Panel to see what happens after you select a node.