GH-251: document controller polling and edit interactions

Document polling of selection/settings/control ranges and shared edit
interactions. Clarify explicit-photo reads, empty selection and navigation,
and correct photo-tool ID descriptions and MCP initialization guidance.
Partial documentation work for issue 251;
hardware feedback remains outside this change.

Co-authored-by: Codex <[email protected]>
This commit is contained in:
sanyamsingh001andCodex committed 2026-10-12 20:33:21 +00:00
1 parent 594362d717
commit 6a82142d69
5 files changed
+89 -6

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ const INTERNAL_ERROR: i64 = -32603;
const RESOURCE_NOT_FOUND: i64 = -32002;
const INSTRUCTIONS: &str = "LightCraft is a non-destructive photo library and raw developer (a Lightroom alternative). \
Photos have integer ids; most editing acts on the *active* photo (tools that take `id` make that photo active first). \
Photos have integer ids; most editing acts on the *active* photo (`set_develop`, `crop` and `export` with an explicit photo id end the current edit interaction, replace the selection with that one photo and make it active; `get_develop` and `render_photo` do not change the selection). \
Typical loop: query_photos → select_photos (or pass id) → list_controls → set_develop {values: {\"light.exposure\": 0.5}} → \
render_preview to look at the result → export. Positions (crop rects, mask points, brush strokes) are normalized image \
coordinates 0..1 with the origin at the top-left. Every action is a command: command_list shows ids and parameter \
+2 -2
View File
@@ -182,7 +182,7 @@ pub fn helper_tools(has_ui: bool) -> Vec<Value> {
"get_develop",
"Get develop settings",
"The full develop settings JSON of a photo (white balance, light, color, curves, mixer, grading, effects, crop, masks, spots…).",
json!({"id": photo_id}),
json!({"id": {"type": "integer", "description": "Photo id (default: the active photo). Reads without changing the selection."}}),
&[],
),
tool(
@@ -216,7 +216,7 @@ pub fn helper_tools(has_ui: bool) -> Vec<Value> {
"Render photo",
"Render a photo with its current develop settings (cropped) and return it as an image so you can look at the result.",
json!({
"id": photo_id,
"id": {"type": "integer", "description": "Photo id (default: the active photo); selection is unchanged"},
"size": {"type": "integer", "description": "Long edge in pixels (default 1024, max 4096)"},
"format": {"type": "string", "enum": ["png", "jpeg"], "description": "Image encoding (default png; jpeg is smaller)"},
"path": {"type": "string", "description": "Also save the rendered file here"}
+72
View File
@@ -140,6 +140,78 @@ opens as one. With no library open (the library problem window) it is an error i
places (`EngineError::LightroomCatalog`, so `lightcraft-cli --library` and `mcp --library` too). `library.importLightroom
{status: true}` reports the catalog's `path` after the task ends as well as while it runs.
## External controllers
External apps can use this channel to send commands and poll the running desktop app for feedback
(e.g. knob values or LEDs). Start the app with `--control PORT`, then keep a JSON-lines connection to
`127.0.0.1:PORT`. Discover command ids, parameters and current enablement with `engine.commands`;
use control ids and ranges from `develop.controls` rather than translated labels or widget positions.
| Task | Command through `engine.execute` | Result / effect |
|---|---|---|
| Read selection and active photo | `library.state {}` | `selection: {ids, active}`; `active` is `null` when no photo is active |
| Read photo identity and settings together | `photo.inspect {}` | Active photo's `id`, `develop`, rating, flag and history in one reply |
| Read a specific photo without selecting it | `photo.inspect {id}` / `develop.get {id}` | Photo details / full develop JSON; omitting `id` reads the active photo |
| Read global slider metadata and values | `develop.controls {section?}` | Array of `{id, label, section, min, max, default, step, decimals, track, value}` for the active photo |
| Set absolute slider values | `develop.set {values: {"light.exposure": 0.7}}` | Undoable edit of the active photo; also accepts `{control, value}` |
| Move a relative control | `develop.adjust {control: "light.exposure", delta: 0.1}` | Adds to the current value, clamped to the control's range |
| Change photo | `library.select {ids: [photoId]}` / `library.next {}` / `library.previous {}` | Changes the active photo; next/previous follow the current view's order |
| Undo / redo | `edit.undo {}` / `edit.redo {}` | Restore settings; poll again for feedback |
With Auto Sync enabled, `develop.set` and `develop.adjust` can also change the other selected photos.
Only changed settings propagate; spot removal, red eye and the rendering process stay per photo.
During an interaction, propagation occurs when it ends. Re-read affected photos after the edit.
`develop.autoSync {}` toggles the mode; it is not a state query.
For example, send these requests one at a time and read each reply before sending the next:
```json
{"id":1,"method":"engine.commands"}
{"id":2,"method":"engine.execute","params":{"command":"photo.inspect"}}
{"id":3,"method":"engine.execute","params":{"command":"develop.controls","params":{"section":"light"}}}
{"id":4,"method":"engine.execute","params":{"command":"develop.set","params":{"values":{"light.exposure":0.7}}}}
{"id":5,"method":"engine.execute","params":{"command":"photo.inspect"}}
```
Replies use `{id, ok, result}` or `{id, ok: false, error}`. In the photo snapshot, exposure is
`result.develop.light.exposure`; the controls array's matching `light.exposure` entry has `value`,
`min: -5`, `max: 5` and `step: 0.01`. `wb.temp` is Kelvin on raws with colour matrices and relative
(6500 = as shot) elsewhere, as described above. Re-read values after edits to reflect the stored result.
Local mask adjustments are in the photo snapshot's `develop.masks`; use the mask commands from the
registry (e.g. `mask.adjust`) for those, rather than treating global control values as mask values.
Feedback uses polling: the channel sends replies to requests, with no unsolicited change events.
Compare successive `photo.inspect` snapshots for photo-id or develop-setting changes, including GUI
edits, photo navigation, presets and undo/redo. GUI slider updates are visible while a drag is in
progress; ending a changed interaction commits it as one undo step (unchanged drags add none).
A controller can group `develop.set` / `develop.adjust` updates between
`develop.beginInteraction {label}` and `develop.endInteraction {}`. The GUI and controllers share
one interaction: another `develop.beginInteraction` or `library.select` ends the current one.
Update controller feedback from reads without sending the same values back as new edits.
Each request is a separate snapshot. `photo.inspect` keeps identity and settings together; a
`develop.controls` reply has no photo id, so do not associate it with an earlier cached selection
when the user is changing photos. Refresh the selection and retry those reads after navigation.
With no active photo, `develop.controls` returns default values and `photo.inspect` / `develop.get`
without `id` return an error; check `library.state` before treating defaults as photo feedback.
Photo ids are local to the current library/session. After reconnecting or opening another library,
discard cached photo mappings and re-read state; matching photo ids alone do not identify the library.
For persistent libraries, poll `library.info` for the current `path` as well. Polling observes the state
at each read, not every intermediate change.
The same queries and edits are available through the connected CLI:
```sh
lightcraft-cli run --connect 127.0.0.1:7980 library.state photo.inspect develop.controls section=light
lightcraft-cli run --connect 127.0.0.1:7980 develop.adjust control=light.exposure delta=0.1 photo.inspect
```
For MCP, use `lightcraft-cli mcp --connect 127.0.0.1:7980`: `run_command` reaches the commands above,
`list_controls` / `set_develop` provide slider helpers, and `resources/read` polls
`lightcraft://library`, `lightcraft://photo/active`, `lightcraft://develop/active` and
`lightcraft://controls` ([mcp.md](mcp.md#resources)). Headless CLI/MCP mode owns a separate engine
session; use connect mode for feedback from edits in the desktop app.
## Headless rendering (no window, no GPU)
The egui UI can be rasterized on the CPU (`crates/ui-egui/src/softpaint.rs`, driven by
+12 -1
View File
@@ -170,7 +170,11 @@ per-request `_meta` receive `resultType: complete` and list/read cache hints; re
| `render_photo {id?, size?, format?, path?}` | Render with current settings → **image content** (PNG, or JPEG with `format: "jpeg"`), long edge `size` (default 1024) |
| `export {path \| dir, id? \| ids?, format?, longEdge? \| shortEdge? \| width?/height? \| megapixels? \| percent?, dontEnlarge?, ppi?, quality?, colorSpace?, bitDepth?, …}` | Full-quality render to `.png` / `.jpg` / `.tif` / `.webp` / `.avif` / `.jxl` (`jxlLossless`, `jxlEffort`, `jxlProgressive`); `format: "original"` copies the file + an XMP sidecar with the edits, `format: "dng"` writes raw photos as DNG with the edits embedded. No size param = 3000 px long edge; `longEdge: 0` = full size (cropped, native resolution); `width` + `height` fit either orientation; `dontEnlarge` defaults to true |
Tools taking `id` make that photo active first; without it they act on the active photo.
`get_develop {id}` and `render_photo {id}` use that photo without changing the selection.
`set_develop`, `crop` and `export` with a supplied photo `id` end the current edit interaction,
replace the selection with that one photo and make it active. Without `id`,
`get_develop`, `set_develop` and `crop` use the active photo; `render_photo` also falls back to the first
selected photo, and `export` uses the selection for batch export, as above.
### Connect mode only
@@ -208,6 +212,13 @@ still reaches every command.
| `lightcraft://develop/active` | The active photo's develop settings (`develop.get`) |
| `lightcraft://controls` | Every develop control with its current value (`develop.controls`) |
Read these resources again to poll current state; they are snapshots, and `initialize` advertises
`resources.subscribe: false`. `lightcraft://photo/active` includes the photo's `id` and `develop`
settings together; `lightcraft://controls` contains slider values for the active photo at that read.
Separate reads can observe different active photos. For controller feedback from GUI edits, use
connect mode; headless mode owns a separate session. See [External controllers](control-protocol.md#external-controllers)
for command mapping, live slider values and polling semantics.
## Example session
```text
+2 -2
View File
@@ -53,9 +53,9 @@ cited here still exists. Formerly docs/parity.md.
| Y. Menus | 84 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) |
| Z. Keyboard shortcuts (desktop) | 76 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) |
| Lightroom Classic extras | 34 | 16 | 35 | 8 | — | 24/26 (92%) |
| **Total** | 407 | 50 | 73 | 35 | 192/200 (96%) | 145/157 (92%) |
| **Total** | 408 | 51 | 72 | 35 | 193/201 (96%) | 145/157 (92%) |
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.5%** of 530 in-scope rows — P0 98.0% of 200 · P1 95.2% of 157 · P2 49.7% of 172.
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.6%** of 531 in-scope rows — P0 98.0% of 201 · P1 95.2% of 157 · P2 50.0% of 172.
<!-- /parity:summary -->
## Top gaps and shortcut notes