-
Notifications
You must be signed in to change notification settings - Fork 0
Review System
← Home
New in v0.6.0.
The Review System lets you place spatially-anchored text annotations directly on
a 3D model, organize them in a side panel, and save them to a
.solarxy-review.json sidecar that travels with the asset. It is built for
asset feedback rounds - an art director marks up a model, the artist receives
the sidecar, fixes the flagged spots, and marks them complete.
Annotations anchor to the geometry, not to a fixed point in space: a marker follows the surface as you orbit the camera, and survives small edits to the model. When the model changes enough that an anchor can no longer be trusted, Solarxy flags it rather than silently moving it.
- Entering review mode
- Annotation categories
- Placing a marker
- Selecting and editing markers
- The Review Panel
- Saving and the sidecar file
- Author attribution
- Anchor stability across re-exports
- Sidecar file format
- What the Review System does not do
Press Shift+R, or use Review → Review Mode in the menu bar. While
review mode is active the Review menu label turns amber (● Review) and the
Review Panel opens if it wasn't already visible.
Press Shift+R again - or Esc from a non-modal state - to leave review mode.

Every annotation has one of four categories. Each has a fixed glyph and colour that match between the in-viewport marker and its row in the Review Panel:
| Category | Glyph | Use it for |
|---|---|---|
| Info | i |
A general note or observation. |
| Warning | ! |
A concern or potential issue. |
| Question | ? |
An open question for the author. This is the default category for a new annotation - review conversations are most often questions. |
| Change | ✎ |
A requested change. |
Category colours are theme-aware - they re-contrast on the light theme so they stay legible either way.
In review mode, click any point on the model surface. A popup opens at the cursor:
| Control | Effect |
|---|---|
| Category dropdown | Pick Info / Warning / Question / Change (defaults to Question). |
| Multiline text field | The annotation body. Newlines are preserved. |
Cmd/⌘+Enter |
Save the annotation and close the popup. The marker appears in the panel. |
Esc |
Cancel - no marker is created. |
Clicking empty space (a miss) toasts "Click on the model surface to annotate" - the click has to land on geometry.
The marker stays attached to the exact triangle and sub-triangle position you clicked. Markers render on top of post-processing (bloom, SSAO, tone mapping) so they stay readable in every render mode.
flowchart TD
classDef start fill:#33415E,stroke:#FFC44C,color:#FFC44C
classDef chain fill:#1F2430,stroke:#5C6773,color:#CCCAC2
classDef select fill:#1F2430,stroke:#7FD962,color:#7FD962
classDef create fill:#1F2430,stroke:#78A0EE,color:#78A0EE
classDef miss fill:#1F2430,stroke:#FFC44C,color:#FFC44C
Click[Left-click in review mode]:::start
Click --> Q1{Re-anchor<br/>pending?}
Q1 -->|yes| A1[Commit re-anchor<br/>at click point]:::select
Q1 -->|no| Q2{Within ~20 px<br/>of an existing marker?}
Q2 -->|yes| A2[Select marker<br/>panel scrolls to row]:::select
Q2 -->|no| Q3{Geometry hit?}
Q3 -->|yes| A3[Open new-annotation popup]:::create
Q3 -->|no| A4[Toast: click on<br/>the model surface]:::miss
The three-rung click ladder. Each rung consumes the click - a re-anchor commit does not also create a new annotation; a marker selection does not also fall through to the new-annotation popup.
A left-click in review mode walks a priority chain:
- Re-anchor pending - if you're in the re-anchor sub-mode (see below), the click re-places the targeted marker.
- Marker hit-test - a click within ~20 px of an existing marker selects it. A cyan ring surrounds the marker and the Review Panel scrolls to its row. Completed markers stay selectable (they render dimmed, not hidden).
- New annotation - otherwise the click raycasts the geometry and opens the new-annotation popup.
Selecting a marker reveals the inline editor at the bottom of the Review Panel. From there you can change the category, edit the text, mark the annotation Complete, Reply to it, Re-place it (when stale), or Delete it.
Deleting a marker with no replies removes it immediately. Deleting one that has replies opens a confirmation modal showing the cascade count first.
The Review Panel is a dockable tab - Review (N), where N is the annotation
count. It opens automatically when you enter review mode; you can also toggle it
from Window → Review Panel. Like every panel it can be docked, floated, or
stacked - see Interface → Dockable layout.

| Region | Content |
|---|---|
| Category filter chips | One chip per category - filled = shown, outline = hidden. |
| Search box | Case-insensitive substring match across annotation text. |
| Markers toggle | Show / hide all markers in the viewport without leaving review mode. |
| Sections | Open, Needs re-anchor (shown only when non-empty), Complete. |
| Reply rows | Indented under their parent annotation. |
| Inline editor | Appears when an annotation is selected. |
"Complete" is a display label. On disk the field is named
resolved- the panel section and the editor button read "Complete" because that reads better for an asset-review workflow.
Annotations are saved to a .solarxy-review.json sidecar next to the model.
For dragon.glb the sidecar is dragon.solarxy-review.json.
Save it with:
-
Cmd/⌘+Swhile review mode is active, or - Review → Save Review Notes in the menu bar (enabled only when there are unsaved changes).
As a safety net, Solarxy also flushes unsaved annotations to the sidecar automatically when you quit the app - so closing the window mid-review never silently drops work. In-session saving stays manual.
To keep sidecars out of your binary-asset directories, override the location
with sidecar_dir in solarxy.toml:
[review]
sidecar_dir = "reviews" # -> <model_dir>/reviews/<stem>.solarxy-review.jsonA relative path resolves against the model's parent directory; an absolute path is used as-is. See Configuration.
Every annotation records an author field. By default it is unset and the
annotation renders as anonymous.
Attribution is opt-in. Set your display name either way:
-
GUI -
Edit → Preferences… → Interfacetab, the Reviewer name field. -
Config file -
[review].authorin your userconfig.toml:[review] author = "Your Name"
Solarxy deliberately does not auto-derive the author from git config or
your operating-system username - attaching an identity is always an explicit
choice.
flowchart TD
classDef ok fill:#1F2430,stroke:#7FD962,color:#7FD962
classDef stale fill:#1F2430,stroke:#FFC44C,color:#FFC44C
classDef action fill:#33415E,stroke:#FFC44C,color:#FFC44C
classDef neutral fill:#1F2430,stroke:#5C6773,color:#CCCAC2
Load[Open model + sidecar]:::neutral
Load --> Cmp{Per-mesh hash<br/>matches recorded?}
Cmp -->|yes| Open[Open<br/>marker visible]:::ok
Cmp -->|no| Stale[Needs re-anchor<br/>marker dimmed at fallback]:::stale
Stale --> ReplaceBtn[User clicks Re-place]:::action
ReplaceBtn --> Sub[Re-anchor sub-mode<br/>amber pulse on row]:::action
Sub -->|click geometry| Open
Sub -->|Esc| Stale
Open -->|user marks Complete| Complete[Complete<br/>dimmed but visible]:::ok
The stale-anchor lifecycle. Solarxy never auto-re-anchors; re-placement is always a deliberate human click against the current geometry.
This is the question the Review System rests on: when the model changes between feedback rounds, do the annotations stay on the geometry they were meant to comment on?
An anchor is the tuple (mesh_index, face_index, barycentric) - a mesh, a
triangle within that mesh, and a barycentric position inside that triangle - plus
a world-space fallback position captured at creation time.
A re-export is anchor-safe when all three of these hold:
| Invariant | Why it matters |
|---|---|
| Mesh order is preserved - the Nth mesh stays the same logical part. |
mesh_index is the primary key. |
| Face indexing is preserved - triangle indices into a mesh don't shift. |
face_index identifies the triangle. A single inserted triangle shifts every later index. |
| Vertex order within a face is preserved - triangle ABC stays (A,B,C). |
barycentric is relative to that order. |
In practice: re-exporting the same source file with the same exporter - the common "tweak material, re-export" / "fix a shader, re-export" loop - is almost always anchor-safe. What breaks the contract is anything that rebuilds the index buffer: a remesh, a decimate pass, "optimize / weld vertices", or a recompute-normals pass that re-winds triangles.
The sidecar stores a per-mesh SHA-256 hash of each mesh's geometry. On reload,
Solarxy re-hashes the model and compares. If a mesh's hash changed - or an
anchor's (mesh_index, face_index) is now out of bounds - every annotation on
that mesh is marked stale:
- It moves from Open to the Needs re-anchor section of the panel.
- Its viewport marker renders dimmed (50% alpha) at the
world_pos_fallbackposition, so you can still see roughly what was meant. - A stale annotation is never deleted.
To fix one, click Re-place on its panel row. Solarxy enters re-anchor
sub-mode (the targeted row pulses amber); your next click on the model commits a
fresh anchor against the current geometry and clears the stale flag. Esc
cancels.
Solarxy does not auto-re-anchor: the nearest face on a re-topologized mesh is not necessarily the artist's intended reference, so re-placement is always a deliberate human action.
The .solarxy-review.json sidecar is plain JSON - version it in Git alongside
the model. The on-disk shape (format_version 1):
{
"format_version": 1,
"model_hash": "<sha-256 hex>",
"mesh_hashes": ["<sha-256 hex>", "<sha-256 hex>"],
"annotations": [
{
"id": "01HF7Z3N9K...",
"created_at": "2026-05-19T14:30:00Z",
"updated_at": "2026-05-19T14:45:12Z",
"author": "Alice",
"anchor": {
"mesh_index": 0,
"face_index": 1234,
"barycentric": [0.5, 0.3, 0.2],
"world_pos_fallback": [1.2, 0.8, -0.3]
},
"category": "question",
"text": "Feels too sharp - soften by ~30%?",
"reply_to": null,
"resolved": false
}
]
}| Field | Meaning |
|---|---|
format_version |
Schema version. Always 1 in v0.6.0. Additive optional fields will not bump it; a field removal or shape change will. |
model_hash |
SHA-256 of the model file's bytes when the sidecar was first written. A coarse "same model?" guard. |
mesh_hashes |
Per-mesh SHA-256 of positions + indices, positionally indexed. Drives the stale-anchor check. |
annotations[].id |
A ULID - sortable, ~26 chars. Referenced by reply_to. |
annotations[].created_at / updated_at
|
RFC 3339 UTC timestamps. |
annotations[].author |
Free-form string, or null for anonymous. Opt-in. |
annotations[].category |
info, warning, question, or change (lowercase on disk). |
annotations[].reply_to |
The parent annotation's id for a threaded reply, or null for a top-level note. Threading is one level deep; replies have no 3D marker of their own. |
annotations[].resolved |
true once the annotation is marked Complete. |
The runtime "stale" state is not written to disk - it is recomputed from the
mesh hashes every time the model is loaded. Consumers reading the JSON directly
should tolerate unknown fields, pin to a known format_version, and re-evaluate
on a version bump.
Out of scope for v0.6.0, stated here so expectations are clear:
- Cross-tool round-tripping. The anchor contract holds when the same exporter is used on the same source file. Export from one DCC tool and re-export from another and the mesh is almost always rebuilt en route.
- UV-space anchoring. Annotations anchor in 3D space, not on UV islands.
- Multi-user concurrent editing. Two people editing the same sidecar at once produce a Git merge conflict, like any other text file. Version the sidecar in Git - that is the intended workflow.
- Annotation history / undo. Each save replaces the file. Git history is the audit trail.
See also: User Guide · Keyboard Shortcuts · Configuration · Troubleshooting
Solarxy - A lightweight, cross-platform 3D model viewer and validator built with Rust and wgpu.
GitHub Repository · Releases & Downloads
© 2026 Marko Koljancic · MIT License
Getting Started
Tutorials
Using Solarxy
Reference
Help
Project