summaryrefslogtreecommitdiff
path: root/AGENTS.md
blob: 12f25fa32261d4ec169f13eac574abd8f51a12f0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# AGENTS.md — gipfel-rs

Guidance for AI coding agents working in this repository.

## What this is

`gipfel-rs` is a Rust port of **gipfel** (a mountain-photo annotator: you load a
photo, mark known peaks, and it fits the camera viewpoint + lens distortion from
the hill database). The original is C++/FLTK (`https://git.flpsed.org/gipfel`,
cloned at `~/gipfel`). This port targets **Cosmic DE** (libcosmic) on Linux.

The port is organized as milestones (M1 core → M5 polish); see `~/gipfel/plan.md`
for the full plan and `~/gipfel-spikes/MEMO.md` for early UI-spike decisions.

## Workspace layout

```
crates/gipfel-core/   headless computational core (no GUI, no native C deps)
  hill.rs             hill DB: CSV load (Latin-1), duplicate marking
  panorama.rs         viewpoint, geodesy, refraction, occlusion, visibility
  projection/         rectilinear + cylindrical projections
    generated/        maxima-generated symbolic functions (DO NOT hand-edit)
  lsq.rs              pure-Rust Levenberg-Marquardt fit (was GSL; GSL removed)
  metadata.rs         `gipfel:` JPEG comment + EXIF read/write
  profile.rs          distortion profiles (FLTK prefs format, XDG data dir)
  stitch.rs           cylindrical panorama resampling (ScanImage/Stitch port) + TIFF/JPEG out
  track.rs            GPX track parser (hardened: no XXE, size cap)
crates/gipfel-cli/    CLI: export-position, export-hills, info, stitch, profiles
crates/gipfel-app/    libcosmic GUI app (binary name: `gipfel`)
  main.rs             app state + Elm-style update/view + sidebar
  viewport.rs         custom canvas Widget (image + markers + labels)
  compass.rs          compass-strip orientation widget
  dump.rs             off-screen image renderer (screen dump)
codegen/              maxima scripts + regen.sh (regenerates projection/generated/)
tests/                C++ reference harnesses (.cxx) + golden files
```

## Build & test

```sh
cargo build --release                 # app binary: target/release/gipfel
cargo test --lib --test golden        # unit + golden tests (the main gate)
cargo test --workspace                # everything incl. app-crate tests
cargo clippy --workspace --all-targets
```

- **Rust edition 2021.** No pinned toolchain file.
- **No native C dependencies** since GSL was replaced by a pure-Rust LM. The only
  system libs are what libcosmic/wgpu need (Vulkan/GL, fontconfig).
- The dev environment has **no reachable display server** — the GUI app compiles
  but cannot be run here. Runtime UI verification is done by the user on a real
  Wayland desktop. Do not assume you can launch `gipfel` to test it.

## Golden tests — the critical invariant

`crates/gipfel-core/tests/golden.rs` compares output against files in
`tests/golden/` produced by the **original C++ build**. This is the contract that
keeps the math faithful.

- Fit tests (`fit_rectilinear_matches_c`, `fit_cylindrical_matches_c`): fitted
  params within **1e-6 relative**, final projected coords within **1e-9 relative**.
- Function-grid tests: ~1e-15 agreement on the maxima-generated derivatives.
- Several golden tests **skip silently** if the hill DB is missing. They look for
  the data file via `$GIPFEL_DATA`, then `$HOME/gipfel/gipfel.dat`. Set
  `GIPFEL_DATA=/path/to/gipfel.dat` so they actually run (a skipped golden test
  is a green test that verified nothing).
- **Never edit `tests/golden/*` to make a failing test pass.** They are ground
  truth from C++. If a golden test fails, your change altered behavior — fix the
  code or understand the deviation.

Regenerating goldens (only when the C++ reference changes):
`tests/gen_golden.sh` — needs the built C++ tree (`$GIPFEL_SRC`, default
`~/gipfel/src`), `$GIPFEL_DATA`, g++, GSL, maxima. The `.cxx` harnesses still use
C++ GSL; that is intentional (they are the reference, not part of the Rust build).

## Code generation (maxima)

`crates/gipfel-core/src/projection/generated/*_funcs.rs` are **generated** from
`codegen/lsq_{rectilinear,cylindrical}.mac` via `codegen/regen.sh` (needs maxima;
optional rustfmt). The mathematical models (refraction, cone-visibility,
distortion) and their symbolic derivatives are **frozen** — do not change the
models. If you regenerate, re-run the golden tests immediately.

## Hard compatibility constraints

- **`gipfel.dat` CSV format** and the **`gipfel:` JPEG comment parameter string**
  must stay byte-compatible with the C++ version (keys `nick`/`tilt` are kept in
  the file even though the code renamed them to pitch/roll).
- **Hill indices are stable handles.** `Panorama::hills` keeps insertion order;
  `mark_duplicates` sorts an index permutation, never the vec. Track lists and
  known-hill sets index into `hills`. Do not reorder `hills` in place.
- **Terminology:** UI says "Pitch"/"Roll"; internal fields are `a_pitch`/`a_roll`;
  hill elevation is `a_elev` (distinct from camera pitch). JPEG keys stay
  `nick`/`tilt`.
- Distortion profiles live in `$XDG_DATA_HOME/gipfel/distortion-profiles.prefs`
  (FLTK prefs format, legacy auto-import). Keep the `%g`-style float formatting
  (`profile::fmt_g`) matching C.

## Code conventions

- **Elm architecture** in the app: `update(&mut self, Message) -> Task` is one
  flat, **exhaustive** `match` (no catch-all) — keep it exhaustive so new
  `Message` variants are forced to be handled. Prefer extracting small helper
  methods over a catch-all arm.
- Shared helpers to reuse (don't duplicate): `slider_item()`, `pick_open`/
  `pick_save` (file dialogs), `compass::newton_inverse` (scalar inverse
  projection), `viewport::layout_label_y` (label layout shared by screen + dump).
- Sidebar rows use `settings::item_row` with fixed-width labels when fields must
  align to equal widths (see the Distortion section).
- libcosmic is **pinned to git rev `430f3d4`** (see `crates/gipfel-app/Cargo.toml`)
  and vendors iced 0.14.2. The correct nav-bar return type is
  `cosmic::Action<Message>` (crate root), not `cosmic::app::Action`.
- Rendering gotchas: the wgpu renderer draws in fixed layer order (use
  `start_layer`/`end_layer` to force overlays above the image); keep strong
  `Plain<P>` paragraph refs alive across the frame in widget `Tree` state.

## Platform notes

- **Linux / Cosmic first.** No Windows/macOS support yet.
- A standalone Windows build is an open experiment. The main blocker (GSL) is
  already removed. Remaining work: swap libcosmic's `xdg-portal` feature for
  `rfd` (file dialogs), port the hill-DB search path (`/usr/share`, `$HOME` →
  `dirs` + exe-relative) and font fallback, then get libcosmic itself to compile
  for `x86_64-pc-windows-gnu` (upstream does not test Windows).

## Releasing a new version

The version has a **single source of truth**: `[workspace.package] version` in
the root `Cargo.toml`. All three crates inherit it (`version.workspace = true`),
and the app's **About dialog** reads it at compile time via
`env!("CARGO_PKG_VERSION")`. Do **not** hardcode a version anywhere else.

To cut a release `X.Y.Z`:

1. Bump `version` in `[workspace.package]` (root `Cargo.toml`). That updates all
   three crates and the About dialog at once.
2. The **ChangeLog entry is human-authored** (see the Do / Don't rule above) —
   only touch it when explicitly asked. Its top `gipfel-X.Y.Z` header must equal
   the crate version; the test `gipfel-app: changelog_top_matches_version` fails
   otherwise, so the manifest and the ChangeLog cannot silently drift apart.
3. `cargo test --lib --test golden` (with `GIPFEL_DATA` set) — must be green.
4. Commit, then tag: `git tag vX.Y.Z`.
5. Build the **source** tarball (tracked files only, no build artifacts):
   ```sh
   git archive --format=tar.gz --prefix=gipfel-X.Y.Z/ \
       -o ~/gipfel-X.Y.Z.tar.gz vX.Y.Z
   ```
   Recipients build with `cargo build --release`. For a fully offline tarball,
   run `cargo vendor --versioned-dirs vendor/` and commit `vendor/` first.

## Do / Don't

- ❌ **Never write to `ChangeLog` or `README.md` unless explicitly asked.** These
  files are human-authored. Do not draft release notes, add ChangeLog entries,
  or edit README prose on your own initiative. The only exception is a purely
  mechanical version bump (e.g. renaming the top `gipfel-X.Y.Z` header during a
  release) when the user has asked you to cut that release.
- ✅ Run `cargo test --lib --test golden` (with `GIPFEL_DATA` set) after any
  change to the core math, projection, fit, metadata, or stitch code.
- ✅ Keep the build warning-free (rustc `dead_code`/`unused_*` lints are the
  signal; clippy may not be installed in every environment).
- ❌ Don't hand-edit `projection/generated/` or `tests/golden/`.
- ❌ Don't change the mathematical models or file formats.
- ❌ Don't assume the GUI can be launched in this environment.