Skip to content

Review System

Marko Koljancic edited this page May 28, 2026 · 5 revisions

Home

Review System

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

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.

Review mode with categorized markers

Annotation categories

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.

Placing a marker

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.

Selecting and editing markers

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
Loading

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:

  1. Re-anchor pending - if you're in the re-anchor sub-mode (see below), the click re-places the targeted marker.
  2. 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).
  3. 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

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.

The Review Panel

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.

Saving and the sidecar file

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/⌘+S while 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.json

A relative path resolves against the model's parent directory; an absolute path is used as-is. See Configuration.

Author attribution

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… → Interface tab, the Reviewer name field.
  • Config file - [review].author in your user config.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.

Anchor stability across re-exports

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
Loading

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?

The anchor

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.

What stays stable

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 "Needs re-anchor" flow

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_fallback position, 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.

Sidecar file format

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.

What the Review System does not do

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

Clone this wiki locally