M3: paint: colour profiles (Display P3), image adjustment presets, style picker (#54)

* M3: colour profiles: sRGB or Display P3 per file, convert or keep values, tagged exports

- color: ColorProfile (srgb, displayP3) with conversions derived from the primaries (shared D65
  white and sRGB curve; out-of-gamut channels clip), table-driven pixel conversion, a generated
  ICC v4 matrix/TRC profile and PNG cICP code points.
- doc: Document.color_profile (serde default sRGB, written only when set); every colour value
  (paints, text runs, effects, grids, page and prototype backgrounds, overlay backdrops, styles,
  colour variables, document colours) can be mapped; set_color_profile converts or reinterprets.
- engine: file.colorProfile {profile?, convert?} (File > Color profile...; without a profile it
  opens the dialog); document.inspect reports colorProfile.
- render: images are taken as sRGB and managed into the file's space; encode_export tags PNG
  (sRGB chunk, or iCCP + cICP for Display P3) and embeds the ICC profile in Display P3 JPG/WebP;
  SVG and PDF export Display P3 files converted to sRGB. The CLI exports the same way.
- ui: Color profile dialog (keep appearance / keep values) and a Color profile row on the page
  panel; the canvas, presenter, thumbnails, swatches and colour picker show a Display P3 file's
  colours converted to the screen's sRGB, and Copy as PNG converts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: image adjustment presets: built-in, saved in the file, reset

- color: ImageFilters::PRESETS (Vivid, Muted, Warm, Cool, Black & white, Fade, Reset), lookup by
  id or label, and sanitized values.
- doc: Document.image_presets (name + adjustments; written only when there are some).
- engine: image.adjust {ids?, target?, index?, preset?, filters?} sets the adjustments of image
  paints from a preset (built-in or saved) with values on top, one undo step; image.savePreset,
  image.deletePreset and image.presets.
- ui: the colour picker's image section gets a presets menu named after the preset in effect
  ("Custom" otherwise), saved presets with delete, "Save as preset...", a Reset button, and
  aligned adjustment sliders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: style picker: search, folders, list or swatch grid, edit, create, detach

The Fill, Stroke, Effects and Layout grid sections' Styles button opens a popover laid out like
the variable picker (crates/ui-egui/src/style_picker.rs):

- search over style names; styles grouped by the folders of their `/`-separated names, which
  collapse (searching opens them); colour styles as a list or a grid of swatches;
- click applies, a check marks the applied style; the row's edit button (or a right-click) edits
  the name, description and values: colour style paints open the colour picker on the style,
  effect and grid styles update from the selection; delete;
- `+` creates a style from the selection's current values and opens it for naming; detach;
- colour slots still list colour variables to bind;
- the applied style chip opens the picker; new icons Adjust, ViewList, ViewGrid (drawn in code);
- control channel: ui.set {stylePicker, stylePickerEdit, stylePickerView}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 07:38:17 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f2c4fb21e1
commit d3dbc6f495
46 changed files with 2424 additions and 232 deletions
+8 -3
View File
@@ -46,7 +46,8 @@ Every command: `[{id, label, params, menu, shortcut, undoable}]`.
The active document as JSON: name, path, dirty, currentPage, selection, pages (the current page's
layer tree to `depth`, default 8; others summarised unless `allPages`), styles, variable count,
images, the text being edited and the undo history.
images, the colour profile (`colorProfile`: `srgb` or `displayP3`), the text being edited and the
undo history.
### `node.get {id, depth?}`
@@ -93,7 +94,7 @@ text tool).
Invoke a menu item exactly like clicking it (this may open a dialog). Returns `{ok, lastError}`.
### `ui.set {rightTab?, leftTab?, theme?, home?, palette?, mainMenu?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?}`
### `ui.set {rightTab?, leftTab?, theme?, home?, palette?, mainMenu?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?}`
Switch panels and modes: `rightTab` `design|prototype|inspect`, `leftTab` `file|assets`, `theme`,
`home` (bool), `palette` (bool: the Actions palette), `mainMenu` (bool), `colorPicker`
@@ -103,7 +104,11 @@ open the layers search with that filter; `null` closes it), `collapseLayers` (tr
layers tree), `exportPreview` (bool: the Design panel Export section's preview), `preview` (true or a
frame id: open the prototype preview pane like `proto.preview`; false or `null` closes it),
`presentScroll` (`[x, y]`: scroll the presented screen, the full window's else the pane's, clamped
to what it can scroll). Returns `ui.inspect`.
to what it can scroll), `colorProfileDialog` (`srgb|displayP3`: open File ▸ Color profile with that
profile chosen; `null` closes it), `stylePicker` (`fill|stroke|effect|grid`: open the style picker
for the selection; `null` closes it) with `stylePickerEdit` (a style id or name: open its editor),
`stylePickerView` (`list|grid`). Returns `ui.inspect` (which reports `stylePicker` and
`stylePickerView`).
### `ui.screenshot {path}`
+13
View File
@@ -82,6 +82,19 @@ Or in a project's `.mcp.json`:
tools, `color` may also be a hex string and gradients can be written
`{"type": "linearGradient", "stops": ["#7048E8", "#1098AD"]}`; the server converts them to the
engine's paint JSON.
- **Colour profile**: colour values are in the file's profile, sRGB (default) or Display P3
(`inspect_document` reports `colorProfile`). `file.colorProfile {profile: srgb|displayP3,
convert?}` changes it: `convert` (default true) converts every colour value so colours keep their
appearance; `false` keeps the values and reinterprets them. Images are treated as sRGB and managed
into the file's space. PNG exports are tagged (`sRGB` chunk, or a Display P3 ICC profile plus
`cICP`), Display P3 JPG and WebP embed the ICC profile, and SVG and PDF export Display P3 files
converted to sRGB (out-of-gamut colours clip).
- **Image adjustments**: image paints carry `filters` (exposure, contrast, saturation, temperature,
tint, highlights, shadows; each -1..1). `image.adjust {ids?, target?: fill|stroke, index?,
preset?, filters?}` applies a built-in preset (`vivid`, `muted`, `warm`, `cool`, `blackAndWhite`,
`fade`, `reset`) or one saved in the file, with any `filters` values on top.
`image.savePreset {name, filters?}` saves the current (or given) adjustments in the file,
`image.deletePreset {name}` removes one and `image.presets` lists both kinds.
- Text props (`fontFamily`, `fontStyle`, `fontSize`, `lineHeight`, `letterSpacing`, `textDecoration`,
`textCase`, `textFills`, `fontVariations`…) apply to the whole layer, or to chars `start`..`end`
when given (`text.style {id, start, end, …}` does the same). Styled ranges come back as
+2 -2
View File
@@ -1,8 +1,8 @@
# Parity report
Generated by `cargo xtask parity`: FigCraft commands registered for the incumbent's main menu and toolbar (376 commands in the engine).
Generated by `cargo xtask parity`: FigCraft commands registered for the incumbent's main menu and toolbar (381 commands in the engine).
**196 / 201 menu items (97.5%)**
**197 / 202 menu items (97.5%)**
## Missing