Home / Graphical interface
Graphical interface
The Raytracer's interactive interface is a runtime plugin
(user_interface.so). It shows the scene, the editing panels and the render viewport, and
lets you manipulate objects with the mouse — down to editing the individual vertices of a mesh.
Overview
The graphical interface is implemented as a runtime plugin
(user_interface.so) loaded by the core's plugin loader. The entry point is the
UserInterface class (which implements IUserInterface), and its plugin type is
PluginType::USER_INTERFACE. The source lives in
srcs/plugins/user_interface/.
The runtime integration cycle is as follows:
- the core loads plugins from
./plugins; - the UI plugin is retrieved by its type
USER_INTERFACE; create(ICoreAccess&)is called;- the UI uses
ICoreAccessto trigger scene and render actions; - on shutdown,
destroy()is called and the plugin instance is unloaded.
UserInterface creates and owns the SFML window event loop, builds the
menus, side panels and render-viewport widgets, forwards user actions to ICoreAccess,
synchronizes viewport selection with panel selection, and displays toasts and auxiliary windows. It
provides:
- scene lifecycle actions (new / open / save / save as / import);
- object creation from the menu (primitives / lights);
- selection and property editing through the panels;
- render status display and render-view controls;
- cluster join / start actions through dedicated windows;
- toast notifications for success and errors.
The UI depends on SFML (graphics, window,
system, network). If the viewport renderer plugin is missing, the UI reports
it through a toast and keeps running.
The Component contract
Every widget derives from a common abstraction, Component, and shares the same
lifecycle:
| Stage | Role |
|---|---|
setFont(...) | Assigns the font used for text. |
layout(...) | Computes geometry (for widgets that own their position/size). |
update(mouse) | Computes the hovered state from the mouse position. |
handleEvent(event, mouse) | Mutates state in reaction to events and fires callbacks. |
draw(window) | Draws the widget into the window. |
The base struct exposes the state fields enabled, hovered and
visible. Viewport projection and picking helpers (including
projectToPixel(...), pickViewportLight(...) and the material-slider filter
isMaterialFloatSlider(name, model)) are grouped in ViewportHelper.
Panels
The panels (in srcs/plugins/user_interface/panels/) form the side inspector column. Each
one rebuilds itself dynamically based on the selected object.
| Panel | Role |
|---|---|
HierarchyPanel | Tree-like camera / lights / primitives list, and selection handling. |
ObjectPanel | Inspector / editor for the selected object's properties. |
MaterialPanel | Material-focused inspector for the selected primitive. |
RendererPanel | Render-texture display and render-tab controls. |
CameraPanel | Editing of the camera resolution, position and rotation. |
HierarchyPanel
Displays the scene as a tree-like list (camera, lights, primitives) and handles selection. It supports
multi-selection, camera selection, and selection sync with viewport picking. A hide
request callback (setOnItemHideRequest) lets an object be hidden, and the panel handles
its own internal scroll and scrollbar.
ObjectPanel
Inspector and editor for the selected object's properties. Its key method
rebuild(const ISceneObject *currentObject) reconfigures the panel dynamically based on the
object's runtime type. It exposes:
- the common transforms — position, rotation and scale;
- for primitives — a material dropdown and float properties converted to sliders;
- for lights — a color picker and an intensity field.
It is also the anchor for vertex editing: the Convert to Mesh button, the
vertex navigator < Point N / total >, the Vertex
field and the - / + buttons of the Size row all appear here
(see Vertex editing).
MaterialPanel
Material-focused inspector for the selected primitives. Its method
rebuild(const ISceneObject *currentObject) builds the controls from the current material
state. It provides:
- a model selector (
PhongvsPBR) through aSegmentedControl; - base-color editing through a
ColorPicker; - dynamic float sliders, filtered by material model through
ViewportHelper::isMaterialFloatSlider(...).
RendererPanel
Displays the render output texture and the top render-tab controls: render comment / status text, a
close-render button, and pixel-space mapping utilities (getViewportPixel(...)). It keeps
the viewport scale and bounds synchronized with the current render dimensions.
CameraPanel
Lets you edit the active camera: the resolution (width and height), the
position and the rotation, through text fields and vector fields. Its
method rebuild(ICamera *camera) populates the fields from the current camera.
Reusable components
Reusable widgets are defined in srcs/plugins/user_interface/components/. They are assembled
by the panels and windows.
| Component | Role |
|---|---|
Button | Clickable button, fires an onClick callback. |
Checkbox | Boolean checkbox. |
ColorPicker | Color picker (material color, light color). |
Dropdown | Selection dropdown (e.g. material). |
Slider | Slider for a float value (material properties). |
SegmentedControl | Segmented selector (e.g. Phong / PBR model). |
TextField | Text / value input field. |
VectorField | Three-component vector input (position, rotation, scale). |
Separator | Visual separator between sections. |
| Menu system | Menu bar and menu items (components/menu/). |
Most widgets follow the same shared pattern:
- they store geometry in SFML shapes / text;
- they compute the
hoveredstate inupdate(...); - they mutate state in
handleEvent(...); - they expose callbacks (
onClick,onChange, etc.); - they return a cursor hint through
getCursor().
This uniform pattern (setFont / layout / update /
handleEvent / draw + callbacks) makes adding a new widget very mechanical:
just implement the Component contract.
Auxiliary windows
Pop-up windows are implemented in srcs/plugins/user_interface/windows/. They derive from
the abstract Window class, the base for threaded SFML sub-windows. Each derived window
implements handleEvent(...), drawUi() and updateUi();
loop() creates the window and runs the poll / update / draw cycle, while
destroy() stops the running flag and joins (or detaches) the thread safely.
| Window | Role |
|---|---|
ExploratorWindow | File/folder explorer for opening or saving a scene (OPEN / SAVE modes). |
LoadWindow | Merge / import decision window between the current scene and the loaded scene. |
JoinClusterWindow | Input of the cluster connection parameters (IP and port). |
MarketWindow | Material market (Local and Online tabs) to browse and add materials. |
- ExploratorWindow: directory listing with parent navigation (
..), hidden-file filtering, a filename field in save mode, and a confirmation button (Open / Save). - LoadWindow: presents paired checkbox groups for the camera fields and the environment-light coefficients; the user chooses, property by property, whether the value comes from the current scene or the loaded scene, then the selected values are written back to the active scene.
- JoinClusterWindow: IP and port fields (numeric validation); the callback
windowCallback(std::string ip, size_t port)is triggered on Enter validation or a JOIN click. - MarketWindow: browsing of a material library with search, under two tabs — local materials and online materials — with add-to-scene buttons.
Toast notifications
Toasts are transient, non-blocking notifications stacked in the bottom-right of the UI
(toast/Toast.hpp, toast/ToastManager.hpp).
| Type | Usage |
|---|---|
| INFO | Neutral information. |
| SUCCESS | Confirmation of a successful action. |
| WARNING | Non-blocking warning. |
| ERROR | Error (e.g. missing renderer plugin). |
The manager exposes push(title, content, type) to create an entry, update() to
apply fade-in / fade-out and remove expired items, and draw(window) to render the stack of
animated toasts.
Transform gizmos
When a single object is selected in the viewport, a gizmo lets you transform it directly with the mouse. Three tools are available:
| Tool | Shortcut | Action |
|---|---|---|
| Move | G | Translation through the gizmo arrows and planes. |
| Rotate | R | Rotation through the gizmo rings. |
| Scale | S | Scaling through the gizmo handles. |
The T key toggles the gizmo frame between World and Local. These shortcuts only act when the viewport has focus, an object is selected, you are not in vertex-edit mode, and movement (fly) mode is off — so that G / R / S don't fight the WASD/ZQSD fly keys.
The tool choice is also available in the ObjectPanel (callbacks
onGizmoModeChanged / onGizmoSpaceChanged), and a status badge (gizmo readout,
World/Local frame) is drawn in the viewport.
Vertex editing
Vertex editing lets you move the individual vertices of an editable primitive directly in the viewport, by dragging on-screen handles or typing coordinates. It is the interactive counterpart to the object transform (position / rotation / scale) exposed in the ObjectPanel.
What is editable
A primitive is editable when it implements IEditablePrimitive in addition to
IPrimitive. The UI discovers the capability at runtime with
dynamic_cast<IEditablePrimitive *>(primitive) — the same runtime-capability pattern
the renderer uses for selection awareness — so the IPrimitive plugin contract is left
untouched.
| Primitive | Vertices |
|---|---|
| Triangle | its 3 corners. |
| Mesh | the unique (welded) vertices of the loaded .obj; a shared vertex moves every incident face. |
The analytic primitives (cube, sphere, plane, cylinder, cone, torus, tanglecube, fractal) are defined by parameters rather than vertices and are not vertex-editable: move / scale them with the ObjectPanel transform fields.
Convert to Mesh
To edit an analytic primitive vertex by vertex, select it and press
Convert to Mesh in the ObjectPanel. Its surface is tessellated into a
triangle mesh (baked to generated_meshes/<name>_<n>.obj), the primitive is
replaced by that mesh, and the mesh is selected so you can immediately enter edit mode. Because it is a
normal file-backed mesh, it saves / reloads and accepts vertex_overrides like any other
mesh.
- Infinite primitives (a plane) cannot be converted.
- A cube is baked exactly to its 8 corners / 12 triangles (flat faces), so it stays a clean editable box.
- Everything else uses a generic surface sampling: smooth / convex shapes (sphere, cone, cylinder) come out clean, while non-convex ones (torus, tanglecube) are approximated.
Entering / leaving edit mode
- Enter: select a single editable primitive, then press Tab — or double-click it in the viewport.
- Leave: press Tab again or Escape. Selecting a different object (in the hierarchy, or by deleting the edited one) also leaves edit mode.
- While in edit mode a banner "Edit Mode — <name>" is shown over the viewport and a toast confirms the transition.
Classic object picking in the viewport is disabled in edit mode so a click never selects a different object by accident. Right-drag still orbits the camera; the WASD/ZQSD fly keys stay available except during an active vertex drag (so the axis-lock keys don't also move the camera).
Handles, hovering and picking
Every vertex is projected to screen and drawn as a small circle:
| Color | Meaning |
|---|---|
| Blue | idle vertex. |
| White | the vertex under the cursor. |
| Yellow | the selected vertex. |
Clicking picks the vertex whose handle is nearest the cursor within a small pixel radius; ties (overlapping handles) go to the vertex closest to the camera. Vertices behind the camera or off screen are skipped. For very dense meshes the number of drawn handles is capped (picking still considers them all).
Alternatively, use the < Point N / total > navigator in the ObjectPanel (just above the
Size row): the arrows step through the shape's vertices one by one without aiming at the
on-screen handles. Stepping enters edit mode if needed and highlights the current vertex in
bright red on the shape.
Dragging a vertex
Drag a handle to move its vertex. By default the vertex slides in the plane that passes through its starting position and is parallel to the screen (normal = camera forward) — the intuitive "move it where I point" behaviour.
- Axis lock: hold X, Y or Z during the drag to constrain the move to that world axis.
- Keyboard: the selected vertex's world coordinates appear in a Vertex field in the ObjectPanel; typing a value moves the vertex and stays in sync with dragging.
- Grow / shrink (- / + buttons): the - / + buttons in the Size row move the selected vertex away from / toward the shape's centroid, so a single point can be pushed out or pulled in. With no vertex selected the same buttons scale the whole object instead.
After every edit the scene BVH is flagged dirty and the viewport re-traces, so the change is visible immediately.
Mesh specifics
- Moving a shared vertex updates all incident faces.
- The affected face normals are recomputed, and smooth (interpolated) vertex normals are refreshed as the average of the incident face normals.
- The mesh's local BVH is rebuilt when the vertex is released
(
onGeometryChanged). For small meshes it is also rebuilt live during the drag; for large meshes (> ~4000 triangles) the surface renders slightly stale during the drag and snaps to the final shape on release, which keeps dragging responsive.
Saving edits (serialization)
- Triangle — its three corners are already part of the scene JSON, so edited positions are saved and reloaded verbatim.
- Mesh — the
.objfile is never modified. Edits are stored as object-space overrides in avertex_overrideslist on the mesh object and re-applied after the.objis loaded.
{
"name": "Cube",
"type": "mesh",
"file": "tests/obj/cube.obj",
"position": [26.0, 10.0, 15.0],
"rotation": [0.0, 0.0, 20.0],
"scale": [14.0, 14.0, 14.0],
"material": "Blue",
"vertex_overrides": [
{ "index": 0, "position": [1.0, -1.0, 0.857] }
]
}
Because overrides are object-space, they survive later changes to the mesh transform and round-trip
cleanly: load → edit → save → reload restores the edited shape. See
tests/configs/vertex_edit.json for a scene (a Triangle and a small cube mesh) set up to
try this out.
Limitations
- Editing changes geometry only; it does not add or remove vertices/faces.
- Smooth normals recomputed at edited vertices are geometric averages, so hard edges authored in the
.obj(vn) are softened locally where you edit. - The move plane is view-parallel; there is no separate depth-along-view drag (use the Vertex field or an axis lock for precise placement).
Keyboard shortcuts
| Key | Context | Action |
|---|---|---|
| G | Viewport, object selected | Move gizmo tool. |
| R | Viewport, object selected | Rotate gizmo tool. |
| S | Viewport, object selected | Scale gizmo tool. |
| T | Viewport, object selected | Toggle the World / Local frame. |
| Tab | Editable primitive selected | Enter / leave vertex-edit mode. |
| Double-click | Editable primitive (viewport) | Enter vertex-edit mode. |
| Escape | Edit mode | Leave vertex-edit mode. |
| X / Y / Z | During a vertex drag | Lock the move to the world X / Y / Z axis. |
| - / + | ObjectPanel Size row | Move the vertex away from / toward the centroid (or scale the object if no vertex is selected). |
The camera fly keys (WASD/ZQSD style, with the wheel for speed) stay active in the viewport outside edit mode; that is why the gizmo shortcuts are disabled while movement mode is on.