mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
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:
1 parent
594362d717
commit
6a82142d69
5 files changed
+89
-6
No files matched your search
@@ -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 \
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user