diff --git a/ROADMAP.md b/ROADMAP.md index c53de6ce..ee4d077f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~28 points and ~195–355 h away (one Opus 5.5 agent; ~40–80 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -128,6 +128,9 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-11** · Gamut warnings (gap 18): the colour picker of a Display P3 file flags colours + outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas. Showing P3 itself waits on + eframe/egui-wgpu exposing the surface colour space wgpu 30 already supports. - **2026-10-11** · Several windows (gap 18): File ▸ New window, Open in new window on a tab or a tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it needs to and asks about unsaved changes when it closes (the main window too); the control channel @@ -187,6 +190,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | | 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | | 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/crates/color/src/profile.rs b/crates/color/src/profile.rs index 058a797f..c810e372 100644 --- a/crates/color/src/profile.rs +++ b/crates/color/src/profile.rs @@ -116,6 +116,23 @@ impl ProfileConversion { Color::new(encode(r), encode(g), encode(b), c.a) } + /// Whether `c` falls outside the target space: a channel clips by more than two 8-bit steps + /// (the colour picker's gamut warning). + pub fn clips(&self, c: Color) -> bool { + let Some(m) = &self.m else { return false }; + apply(m, [decode(c.r), decode(c.g), decode(c.b)]).into_iter().any(outside) + } + + /// Which premultiplied 8-bit RGBA pixels fall outside the target space (see [`Self::clips`]): + /// the canvas's soft proof. + pub fn clipped_pixels(&self, rgba: &[u8]) -> Vec { + let pixels = rgba.as_chunks::<4>().0; + let Some(m) = &self.m else { return vec![false; pixels.len()] }; + let to_linear: [f64; 256] = std::array::from_fn(|i| decode(i as f32 / 255.0)); + let lin = |v: u8| to_linear.get(usize::from(v)).copied().unwrap_or(0.0); + pixels.iter().map(|&[r, g, b, a]| a != 0 && apply(m, [r, g, b].map(|c| lin(straight(c, a)))).into_iter().any(outside)).collect() + } + /// Convert premultiplied 8-bit RGBA pixels in place (table-driven; a no-op for the identity). pub fn apply_pixels(&self, rgba: &mut [u8]) { let Some(m) = &self.m else { return }; @@ -129,8 +146,7 @@ impl ProfileConversion { continue; } let alpha = u32::from(*a); - let straight = |c: u8| if alpha == 255 { c } else { ((u32::from(c) * 255 + alpha / 2) / alpha).min(255) as u8 }; - let v = [lin(straight(*r)), lin(straight(*g)), lin(straight(*b))]; + let v = [lin(straight(*r, *a)), lin(straight(*g, *a)), lin(straight(*b, *a))]; let out = m.map(|row| enc(row[0] * v[0] + row[1] * v[1] + row[2] * v[2])); let premul = |c: u8| if alpha == 255 { c } else { ((u32::from(c) * alpha + 127) / 255) as u8 }; (*r, *g, *b) = (premul(out[0]), premul(out[1]), premul(out[2])); @@ -138,6 +154,20 @@ impl ProfileConversion { } } +/// A premultiplied 8-bit channel without its alpha (`a` > 0). +fn straight(c: u8, a: u8) -> u8 { + let alpha = u32::from(a.max(1)); + if alpha == 255 { c } else { ((u32::from(c) * 255 + alpha / 2) / alpha).min(255) as u8 } +} + +/// A linear channel the target space can't hold: below 0 or above 1 by more than two 8-bit steps +/// once encoded (smaller excesses are the rounding of 8-bit values in the other space). +fn outside(l: f64) -> bool { + const SLACK: f64 = 2.0 / 255.0; + let enc = |x: f64| if x <= 0.003_130_8 { x * 12.92 } else { 1.055 * x.powf(1.0 / 2.4) - 0.055 }; + (l < 0.0 && enc(-l) > SLACK) || (l > 1.0 && enc(l) > 1.0 + SLACK) +} + /// Entries of the linear → encoded table [`ProfileConversion::apply_pixels`] uses. const ENCODE_STEPS: usize = 4096; diff --git a/crates/color/src/profile_tests.rs b/crates/color/src/profile_tests.rs index 1a228d4b..13876f04 100644 --- a/crates/color/src/profile_tests.rs +++ b/crates/color/src/profile_tests.rs @@ -163,3 +163,18 @@ fn paint_colors_visit_solids_and_stops() { let mut img = Paint::image("h", ImageScaleMode::Fill); img.for_each_color_mut(&mut |_| panic!("images have no colours")); } + +#[test] +fn gamut_checks_find_p3_colours_outside_srgb() { + let to_srgb = ColorProfile::DisplayP3.conversion_to(ColorProfile::Srgb); + assert!(to_srgb.clips(Color::new(0.0, 1.0, 0.0, 1.0)), "P3 green is outside sRGB"); + assert!(!to_srgb.clips(Color::new(0.5, 0.5, 0.5, 1.0)), "greys fit"); + // sRGB red written as 8-bit P3 values still fits (rounding isn't clipping). + let red = ColorProfile::Srgb.conversion_to(ColorProfile::DisplayP3).apply(Color::new(1.0, 0.0, 0.0, 1.0)); + let [r, g, b, _] = red.to_rgba8(); + assert!(!to_srgb.clips(Color::from_rgba8(r, g, b, 255))); + assert!(!ColorProfile::Srgb.conversion_to(ColorProfile::Srgb).clips(Color::new(0.0, 1.0, 0.0, 1.0))); + // Pixels: premultiplied, transparent ones never clip. + let px = [0, 255, 0, 255, r, g, b, 255, 0, 128, 0, 128, 0, 255, 0, 0]; + assert_eq!(to_srgb.clipped_pixels(&px), [true, false, true, false]); +} diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index cd3eb7d1..efdb3229 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -93,6 +93,8 @@ pub static MENU: &[MenuItem] = &[ m!("View", "Pixel preview", "view.pixelPreview"), m!("View", "Mask outlines", "view.maskOutlines"), m!("View", "Frame outlines", "view.frameOutlines"), + // FigCraft's own: the canvas shows sRGB, so Display P3 files can mark what it can't show. + m!("View", "Soft-proof sRGB", "view.proofSrgb"), m!("View", SEP, ""), m!("View/Panels", "Show/Hide UI", "view.toggleUi"), m!("View/Panels", "Layers", "view.layers"), diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index 54a02544..d184e2c0 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -199,6 +199,13 @@ pub fn specs() -> Vec { spec!("view.maskOutlines", "Mask outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.mask_outlines)), spec!("view.pixelPreview", "Pixel preview", "{on?, scale? (1|2: rendered pixels per page pixel; turns it on)}", false, pixel_preview), spec!("view.frameOutlines", "Frame outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.frame_outlines)), + spec!( + "view.proofSrgb", + "Soft-proof sRGB", + "{on?} mark the colours of a Display P3 file that sRGB screens and files can't show", + false, + |s, p| { flag(s, p, |v| &mut v.proof_srgb) } + ), spec!("view.snapGeometry", "Snap to geometry", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_geometry)), spec!("view.snapObjects", "Snap to objects", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_objects)), spec!("view.snapPixel", "Snap to pixel grid", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_pixel)), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 9849f2b2..1a214df3 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -134,6 +134,8 @@ pub struct ViewState { pub show_slices: bool, pub comments: bool, pub frame_outlines: bool, + /// Soft-proof sRGB: the canvas marks the colours of a Display P3 file that sRGB can't show. + pub proof_srgb: bool, pub snap_geometry: bool, pub snap_objects: bool, pub snap_pixel: bool, @@ -159,6 +161,7 @@ impl Default for ViewState { show_slices: false, comments: true, frame_outlines: false, + proof_srgb: false, snap_geometry: true, snap_objects: true, snap_pixel: true, diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index cb7a829b..74e798fd 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -1773,6 +1773,7 @@ status = "draft — needs native review" "view.prevFrame" = "Zoom al marco anterior" "view.prevPage" = "Página anterior" "view.previousRegion" = "Enfocar la región anterior" +"view.proofSrgb" = "Prueba en pantalla sRGB" "view.prototype" = "Panel de prototipo" "view.rulers" = "Reglas" "view.set" = "Establecer vista" diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 53bc7099..e51e5b6d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -104,6 +104,7 @@ struct JobKey { solo: Option, /// Text lays out anew when fonts arrive (system fonts load in the background). fonts: u64, + proof: bool, } impl FigApp { @@ -291,6 +292,7 @@ fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) { // The edited text is drawn live (an IME composition included); the caret is an overlay. let doc = app.preedit_view().map(|p| p.doc.clone()).unwrap_or_else(|| ds.doc.clone()); let outlined = if ds.view.outlines_selection { ds.selection.clone() } else { vec![] }; + let proof = ds.view.proof_srgb && !doc.color_profile.is_srgb(); let key = JobKey { doc: Arc::as_ptr(&doc) as usize, page: ds.page, @@ -302,6 +304,7 @@ fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) { skip: vec![], solo: ds.focused(), fonts: figcraft_text::layout_generation(), + proof, }; if app.canvas.submitted.as_ref() == Some(&key) { return; @@ -314,6 +317,7 @@ fn request_render(app: &mut FigApp, worker: &mut Worker, ppp: f32) { outlined, skip: vec![], solo: key.solo, + proof, }; worker.submit(job); app.canvas.submitted = Some(key); diff --git a/crates/ui-egui/src/canvas_fixes_tests.rs b/crates/ui-egui/src/canvas_fixes_tests.rs index 0e62a5d6..556031a3 100644 --- a/crates/ui-egui/src/canvas_fixes_tests.rs +++ b/crates/ui-egui/src/canvas_fixes_tests.rs @@ -60,6 +60,7 @@ fn big_job(doc: &Arc, page: figcraft_doc::NodeId, scale: outlined: vec![], skip: vec![], solo: None, + proof: false, } } diff --git a/crates/ui-egui/src/colorpicker.rs b/crates/ui-egui/src/colorpicker.rs index 06ed67ca..1e8ad6b1 100644 --- a/crates/ui-egui/src/colorpicker.rs +++ b/crates/ui-egui/src/colorpicker.rs @@ -683,6 +683,10 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) { done = true; } ui.data_mut(|d| d.insert_temp(Id::new("cp_hsb"), (hsb.h, hsb.s, hsb.b, nc.to_rgba8()))); + // A Display P3 colour sRGB can't show: the canvas (sRGB) shows the nearest one. + if app.session.doc().is_ok_and(|d| d.color_profile.conversion_to(figcraft_color::ColorProfile::Srgb).clips(nc)) { + gamut_warning(ui, &t); + } if changed { match &mut paint.kind { PaintKind::Solid { color } => { @@ -744,6 +748,19 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) { let _ = Pos2::ZERO; } +/// The colour picker's note under the fields for a colour outside sRGB. +fn gamut_warning(ui: &mut Ui, t: &Tokens) { + use crate::a11y::Tip; + ui.horizontal(|ui| { + ui.add_space(16.0); + let (r, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover()); + icons::paint(ui.painter(), r, Icon::GamutWarning, t.icon_secondary); + widgets::label(ui, "Outside sRGB", t.text_secondary); + }) + .response + .tip("sRGB screens and files can't show this Display P3 color: the canvas shows the nearest sRGB color, P3 exports keep it"); +} + /// Layers a pattern can tile: the file's components and the current page's top-level layers, but /// the painted layers, what they contain and what contains them (at most 64). fn pattern_sources(app: &FigApp, target: &PickerTarget) -> Vec<(NodeId, String)> { diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index cc5511e1..5f8ef93a 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -172,6 +172,8 @@ icons! { ArcSweep, /// Arc ratio: a ring. ArcRatio, + /// A colour outside the screen's gamut: a triangle with an exclamation mark. + GamutWarning, } /// Points along a circular arc from angle `a0` to `a1` (radians, y down: increasing is clockwise). @@ -990,6 +992,11 @@ fn draw(p: &P, icon: Icon) { p.circle(8.0, 8.0, 5.5); p.circle(8.0, 8.0, 2.5); } + GamutWarning => { + p.poly(&[(8.0, 2.0), (14.5, 13.5), (1.5, 13.5)], true); + p.line(8.0, 6.0, 8.0, 9.5); + p.dot(8.0, 11.5, 0.9); + } Logo => { // FigCraft's own mark: three stacked rounded tiles forming an "F" shape. p.fill_rect(3.0, 2.0, 13.0, 5.0, 1.0); diff --git a/crates/ui-egui/src/idle_tests.rs b/crates/ui-egui/src/idle_tests.rs index 18e4a2c9..a7697d9d 100644 --- a/crates/ui-egui/src/idle_tests.rs +++ b/crates/ui-egui/src/idle_tests.rs @@ -210,6 +210,7 @@ fn a_finished_background_render_wakes_the_ui() { outlined: vec![], skip: vec![], solo: None, + proof: false, }); let start = Instant::now(); while wakes.load(Ordering::SeqCst) == 0 { diff --git a/crates/ui-egui/src/render_worker.rs b/crates/ui-egui/src/render_worker.rs index 71bb0a1f..5357eb11 100644 --- a/crates/ui-egui/src/render_worker.rs +++ b/crates/ui-egui/src/render_worker.rs @@ -21,6 +21,8 @@ pub struct Job { pub skip: Vec, /// Only this layer (the focus view). pub solo: Option, + /// Soft-proof sRGB: stripe the colours sRGB can't show. + pub proof: bool, } pub struct Done { @@ -43,6 +45,17 @@ pub fn screen_image(pix: &Pixmap, profile: ColorProfile) -> egui::ColorImage { egui::ColorImage::from_rgba_premultiplied(size, &data) } +/// Soft proof: grey diagonal stripes over the pixels whose colours sRGB can't show. +fn mark_outside_srgb(image: &mut egui::ColorImage, pix: &Pixmap, profile: ColorProfile) { + let w = image.size[0].max(1); + let clipped = profile.conversion_to(ColorProfile::Srgb).clipped_pixels(pix.data()); + for (i, (px, out)) in image.pixels.iter_mut().zip(clipped).enumerate() { + if out && ((i % w) + (i / w)) % 8 < 4 { + *px = egui::Color32::from_gray(128); + } + } +} + fn run(job: &Job, retained: &mut Retained) -> Option { #[cfg(not(target_arch = "wasm32"))] let t0 = std::time::Instant::now(); @@ -56,7 +69,10 @@ fn run(job: &Job, retained: &mut Retained) -> Option { ..Default::default() }; let (view, pix, work) = retained.render(&job.doc, job.page, &job.view, &opts)?; - let image = screen_image(pix, job.doc.color_profile); + let mut image = screen_image(pix, job.doc.color_profile); + if job.proof { + mark_outside_srgb(&mut image, pix, job.doc.color_profile); + } #[cfg(not(target_arch = "wasm32"))] let ms = t0.elapsed().as_secs_f64() * 1000.0; #[cfg(target_arch = "wasm32")] diff --git a/docs/control-protocol.md b/docs/control-protocol.md index f5347841..146134a0 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -169,7 +169,8 @@ its parameters form when it has parameters; `null` closes the form), `focusRegio (`toolbar|layers|canvas|properties`: give that region the keyboard as F6 would, on the next frame), `iconGallery` (bool: every UI icon at 12, 16, 24 and 32 px over the window, for reviewing the icon set), `colorPicker` -(`{fills?, index?}`: open the colour picker on the selection's paint; `null` closes it), `variables` (bool, +(`{fills?, index?}`: open the colour picker on the selection's paint, which notes a Display P3 colour +outside sRGB; `null` closes it), `variables` (bool, or a collection id or name to show, or `{library, collection?}` for an enabled library's collection: the Variables editor), `layerFilter` (string: open the layers search with that filter; `null` closes it), `collapseLayers` (true: collapse the diff --git a/docs/gaps.md b/docs/gaps.md index 6ef1af86..ea244268 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where FigCraft falls short of Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The @@ -31,7 +31,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | 15 | Prototyping: media triggers and actions, gamepad | Feature | 3–6 | [prototyping-parity](prototyping-parity.md) | | 16 | Dev Mode: dev resources links, editor extension (code connect and Dev Mode plugins landed) | Feature | 1–3 | [dev-mode-parity](dev-mode-parity.md) | | 17 | Branching: per-branch version history, finer merges of list properties (branches landed 2026-10-11) | Feature | 1–3 | [collaboration-parity](collaboration-parity.md) | -| 18 | Display P3 on screen (several windows landed) | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | +| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | | 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) | | 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) | | 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) | @@ -284,6 +284,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | | 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | | 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index fcbf326c..fbdfcf0b 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware and platform parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7) How FigCraft uses hardware and which platforms it runs on, against Figma. Figma's desktop app is Electron with a GPU helper process; its canvas renders on the GPU (WebGL, with WebGPU rolling out @@ -15,7 +15,7 @@ since 2025). FigCraft renders the canvas on the CPU and presents it through wgpu | Canvas rendering | GPU | CPU (tiny-skia), banded across cores, retained (re-renders only what an edit or pan touches), levels of detail below 50% zoom; shown through wgpu (WebGPU or WebGL2 in the browser) | 30–60 for a GPU path | | Effects (blurs, glass, noise) | GPU | CPU | in the above | | HiDPI / Retina | yes | yes (all) | — | -| Wide-gamut display (Display P3) | yes, on capable displays | colours stored and exported in P3; the screen shows sRGB (egui) | 3–6 | +| Wide-gamut display (Display P3) | yes, on capable displays | colours stored and exported in P3; the screen shows sRGB: eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no colour space option, although wgpu 30 can present Display P3 or extended sRGB on Metal, DX12, Vulkan and WebGPU (see gaps #18). The colour picker warns about colours outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas | 2–5 | | Trackpad pinch zoom and two-finger pan | yes | yes (egui zoom delta, smooth scroll) | — | | Pen tablet pressure (pencil, Figma Draw brushes) | yes (forum reports of regressions in 2025) | Windows pens (winit pointer input) and browser styluses that send touch events with a force (an Apple Pencil in Safari); the Pencil records it as the stroke's width profile. A mouse, macOS and Linux tablets and pens sending only pointer events in a browser report none: constant width | 1–3 | | Several windows / multi-monitor | yes (a window per file; presenter in its own window) | desktop: File ▸ New window, a tab's Open in new window or a tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it needs to and asks about unsaved changes when it closes. Only the main window has a screen-reader tree (eframe 0.36), so files stay tabs while a screen reader runs; positions aren't restored at the next start; the presenter stays inside its window; one window in the browser | 1–2 | @@ -50,6 +50,7 @@ Nothing has been timed against Figma on the same document (needs `.fig` import). | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | | 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | | 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | | 2026-10-10 | major | Created from the 2026-10-10 re-measure | diff --git a/docs/mcp.md b/docs/mcp.md index 1c6d120c..e4709f5a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -88,7 +88,10 @@ Or in a project's `.mcp.json`: 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). + converted to sRGB (out-of-gamut colours clip). The app's canvas shows sRGB (P3 colours outside sRGB clip on screen): + `view.proofSrgb {on?}` (View ▸ Soft-proof sRGB) stripes those pixels so `screenshot {window: + true}` shows where they are (after the next canvas render), and the colour picker says Outside + sRGB for such a colour. - **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`, diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index b8e3a84e..4bbe9a0b 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -2,9 +2,9 @@ > **Generated:** 2026-10-11 by `cargo xtask parity`. Do not edit by hand. -FigCraft commands registered for the incumbent's main menu and toolbar (497 commands in the engine). +FigCraft commands registered for the incumbent's main menu and toolbar (499 commands in the engine). -**205 / 205 menu items (100.0%)** +**206 / 206 menu items (100.0%)** Every catalog item has a command. diff --git a/docs/roadmap.md b/docs/roadmap.md index bee2939d..bffbb7aa 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -72,6 +72,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | | 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | | 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ffb86044..3aa39de9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # FigCraft parity with Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is @@ -580,6 +580,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | | 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | | 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a33e9920..f6e77e7c 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -76,6 +76,7 @@ interaction feel not yet compared. | Slices shown on the canvas (View ▸ Slices) | done | dashed rectangles | | Dragging a layer inside a group keeps it in the group | done | frames and auto layout take other drops | | Layout grids (Ctrl+G) | done | | +| Gamut warnings for Display P3 files | done | colour picker note; View ▸ Soft-proof sRGB stripes what sRGB can't show (the canvas itself is sRGB) | | Gradient and padding/gap handles on the canvas | done | | | Hover outline, selection colours, handle sizes | done | measured from UI3 screenshots | | Cursors per tool and modifier | partial | not compared | @@ -95,6 +96,7 @@ interaction feel not yet compared. | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Gamut warnings: the colour picker flags Display P3 colours outside sRGB; View ▸ Soft-proof sRGB stripes them on the canvas | | 2026-10-11 | minor | Several windows: File ▸ New window, Open in new window on a tab, a tab dragged off the strip; each window has its own tabs and panels and asks about unsaved changes when it closes | | 2026-10-11 | minor | Canvas fixes: boolean groups clicked where their result is, a layer dragged inside a group stays in it, View ▸ Frame outlines and View ▸ Slices draw | | 2026-10-11 | minor | Outline mode done: outlines for the selection (the selected layers draw as outlines, the rest normally) and mask outlines |