mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-13 23:17:43 +00:00
M5: Display P3: gamut warning and soft-proof sRGB (#127)
True wide-gamut display is blocked upstream (egui-wgpu 0.36 always picks an 8-bit sRGB surface and draws sRGB-encoded UI), so the colour picker warns when a Display P3 colour is outside sRGB and View > Soft-proof sRGB stripes the canvas pixels sRGB can't show. Exports keep wide-gamut colours as before.
This commit is contained in:
1 parent
f49142dad9
commit
24e457240e
21 files changed
+133
-15
No files matched your search
+5
-1
@@ -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 |
|
||||
|
||||
@@ -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<bool> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -199,6 +199,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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)),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -104,6 +104,7 @@ struct JobKey {
|
||||
solo: Option<NodeId>,
|
||||
/// 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);
|
||||
|
||||
@@ -60,6 +60,7 @@ fn big_job(doc: &Arc<figcraft_doc::Document>, page: figcraft_doc::NodeId, scale:
|
||||
outlined: vec![],
|
||||
skip: vec![],
|
||||
solo: None,
|
||||
proof: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct Job {
|
||||
pub skip: Vec<NodeId>,
|
||||
/// Only this layer (the focus view).
|
||||
pub solo: Option<NodeId>,
|
||||
/// 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<Done> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let t0 = std::time::Instant::now();
|
||||
@@ -56,7 +69,10 @@ fn run(job: &Job, retained: &mut Retained) -> Option<Done> {
|
||||
..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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-2
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
+4
-1
@@ -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`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+2
-1
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+3
-1
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user