git.lucas.co / cce-model
3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git

CLAUDE.md (8K)

  1 # cce-model
  2 
  3 A viewer for 3D model files. Read the workspace guide
  4 (`../cce-compositor/WORKSPACE.md`) first; this file covers only what is
  5 particular to this crate. The plan it is built to — formats, milestones,
  6 what each one must pass — is the design doc "cce-model: a general-purpose
  7 3D viewer" (claude.ai/code/artifact/1ebec744-8990-4884-ad92-73c72e0ea265).
  8 Milestones 1–5 are here: STL, OBJ, glTF/GLB and PLY read by the shared
  9 `cce-mesh-io` crate, a grid floor, wireframe and normals overlays, sizes in
 10 the file's units, ←/→ through the folder, drag-and-drop, a path-traced
 11 view (`r`), and the model drawn through cce-ui's lit pipeline (per-fragment
 12 light, metallic-roughness, base-colour textures).
 13 
 14 ## Shape
 15 
 16 - `src/main.rs` — the `Application`. Portable hooks only: `create`,
 17   `init_3d` / `stage_3d` (never the native `renderer_init` /
 18   `stage_renderer`). A file is read on a worker thread and comes back through
 19   the `AppSender` as an `Arc<Model>` (the runner clones messages); a
 20   generation drops a load the user has moved past, and a reader's panic is
 21   caught there and shown as an error. The model's baked vertices stay on
 22   the CPU after upload, because `init_3d` runs again for a replacement
 23   renderer after a reconnect and every mesh must go back up (660 MB of lit
 24   vertices for a 5M-triangle model; slimming it is open). Textures go up as
 25   mipmapped image ids when a model arrives, are freed when it is replaced,
 26   and are uploaded again for a replacement renderer (`seen_renderer`).
 27 - Reading is `cce_mesh_io::load` (see that crate's CLAUDE.md): a `Scene` of
 28   parts in the file's own coordinates plus its up axis. This app merges the
 29   parts, turns a Z-up scene upright, fits it to the unit sphere and bakes.
 30 - `src/light.rs` — the lit meshes and their light: `lit_parts` turns the
 31   mesh into one `LitVertex` list per material (a draw binds one texture),
 32   with the file's normals or crease-aware ones; `rig()` is the studio
 33   light (key, fill, sky/ground) the lit pipeline shades by.
 34 - `src/camera.rs` — orbit camera: yaw, pitch, distance about a pivot.
 35 - `src/overlay.rs` — the line meshes: grid floor (laid out in FILE units on
 36   1-2-5 steps through the file's origin, then fitted), edges, normals.
 37   Edges and normals are built on a worker the first time `w` or `n` asks
 38   (`Message::Overlays`, tied to the load's generation). No wireframe past
 39   `MAX_WIRE_TRIANGLES` (2M): it paints the model solid and costs 360 MB;
 40   normals are thinned to `MAX_NORMALS` and drawn as long as their spacing.
 41 - `src/trace.rs` — the traced view's scene (a material per distinct
 42   triangle colour, quantized; a wide ground plane at the grid's height), its
 43   grey sky with the sun on the raster key light, the sample caps (256 on
 44   mains, 32 on battery) and the `/sys/class/power_supply` battery check.
 45 - `src/units.rs` — sizes in mm/m only when the format says what its unit
 46   is (`cce_mesh_io::Unit`); OBJ and PLY sizes are bare numbers.
 47 - `src/files.rs` — the folder's models for ←/→ (any file opened from
 48   outside the list makes its folder the list) and `text/uri-list` drops.
 49 
 50 ## Things that are not obvious
 51 
 52 - **The model is drawn lit, not baked** (since milestone 5): `LitDraw`s
 53   staged after `stage_scene`, each `before` the first wire draw so the
 54   background and grid go under and the overlays over. Until then the light
 55   was baked into `Vertex3D` colours on the CPU, which could show neither a
 56   texture nor a highlight that moves. `Stage3D::lit()` is `None` on a
 57   renderer without the pipeline; the app then says it cannot draw.
 58 - **Every model is moved into the unit sphere** (`Mesh::fit_to_unit`)
 59   before it is baked, so the camera frames every file alike. (Until
 60   cce-ui 38bcad1 this was also a workaround: any vertex near z = 9.99 was
 61   drawn as the background quad. The background is a `screen_space` draw now.)
 62 - **A Z-up scene (STL) is turned upright here, not by the reader.** So a
 63   cce-designer STL export, which is its Y-up world written as is, shows on
 64   its side — as it would in a slicer.
 65 - **A staged scene persists** in the backdrop until the next one, so
 66   `stage_3d` stages only when `scene_dirty` (camera, resize, upload); a HUD
 67   change repaints the 2D pass alone.
 68 - **Every GPU slot is created once and updated in place** (`upload`), so
 69   stepping through a folder does not leak meshes; `init_3d` clears the
 70   slots and marks every CPU-side list pending for a replacement renderer.
 71 - **The clip planes keep 3 radii** around the pivot, for the grid's
 72   corners, not just the model's 1.
 73 - **The traced view is a state machine in `stage_3d`.** Any change of
 74   camera or pane (`view_key`) resets the samples and the wait; only after
 75   `STILL` (150 ms) does a frame stage `stage_rt` instead of the raster
 76   scene, one sample a frame; at the cap it stages nothing and stops asking
 77   for frames, so the GPU and the process go idle (0 CPU ticks measured)
 78   with the traced image left in the backdrop. One frame past each edge is
 79   deliberate: the HUD paints BEFORE `stage_3d`, so the cap and the
 80   "preparing" notice each need a frame of their own to be seen.
 81 - **The tracer's BVH is built on the worker**, with the rest of the scene
 82   (`PreparedRtScene::new` in `want_trace`, cce-ui ≥ e5fbe9a); `stage_3d`
 83   only uploads it (`set_rt_scene_prepared`). Whether to build one is the
 84   stage's `rt_needs_bvh()`, asked in `init_3d`: the hardware ray-query tier
 85   builds its own structure on the GPU and skips it. Measured 2026-10-07,
 86   torus5m.stl (5,001,600 triangles), scale-2 shadow: Intel iGPU (compute
 87   tier) worker 2032 ms, UI thread 61 ms (it was 2141 ms on the UI thread);
 88   RTX 4080 (ray-query, `CCE_VK_DEVICE=discrete` plus the live `DISPLAY=:0`
 89   in the shadow) worker 321 ms, upload with the BLAS build 146 ms. The
 90   prepared scene stays in memory for a reconnect's re-upload (~300 MB at
 91   5M with its BVH, on top of the baked vertices).
 92 - **The tracer has no smooth normals**: a coarse sphere shows its facets
 93   when traced. Milestone 5's territory (cce-ui's materials and vertices).
 94 - No root plate, on purpose (`style-audit: opt-out` in `display_list`): the
 95   scene is the window's content, and a root plate would frost over it.
 96 
 97 ## Verify
 98 
 99 Headless, in a scale-2 shadow, always through `cce-shadow run`:
100 
101 ```sh
102 cce-shadow start --new --scale 2          # note the agent-N it prints
103 CCE_SHADOW_INSTANCE=agent-N cce-shadow ctl idle timeouts 0 0
104 CCE_SHADOW_INSTANCE=agent-N cce-shadow run env CCE_FONTS_DIR=$HOME/Dropbox/Fonts \
105     /home/lsgalante/projects/cce/target/release/cce-model /path/to/model.stl &
106 ```
107 
108 then `ctl windows` for the id and `shot-window <id>`. The window is
109 1000×750 logical, larger than the 640×360 logical headless output at scale
110 2, so use `shot-window`, not `shot`, and keep pointer targets inside
111 640×360. `pointer-press` / `pointer-move-by` / `pointer-release` drive an
112 orbit, `pointer-pinch 1.6` a zoom, `pointer-scroll 0 -12 finger` then
113 `pointer-scroll finger-stop` a scroll orbit and its coast, `keypress 11` the
114 `0` key, 17/49/34/19 `w`/`n`/`g`/`r`, 105/106 ←/→. A traced run logs
115 `traced N samples in M ms; idle` at its cap. A new viewer window can map
116 WITHOUT focus (keys then reach nothing, or a stale window): `ctl
117 focus-window cce-model` after each launch, and close windows by pid
118 (checking `/proc/<pid>/environ` for the shadow's display) — one launched by
119 cce-files has argv0 `cce-model`, so a `release/cce-model` match misses it.
120 cce-files in a shadow finds the real desktop entries with
121 `XDG_DATA_HOME=$HOME/.local/share` and a tree build via a PATH symlink.
122 Drops are untested end to end: cce-ui has no drag SOURCE, so no cce app
123 can drag a file out. The window's display is the shadow's
124 `WAYLAND_DISPLAY` from `cce-shadow env`, which changes when the instance
125 restarts — match it when picking processes to stop. The log carries
126 `read and lit in N ms` (worker) and `uploaded N vertices in N ms` (the UI
127 thread's only share of a load). Real test models: the slicers' resource STLs under
128 `~/.local/share/Steam/steamapps/compatdata/*/pfx/drive_c/Program Files/`
129 (ChiTuBox's `high_precision_sphere.stl`, Bambu Studio's calibration towers).