mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
M7: keep Windows windowed UI and canvas text sharp
This commit is contained in:
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; 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 ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (DirectComposition presentation for sharp Windows windowed UI; canvas texture placement aligned to device pixels to avoid interpolation blur; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; 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 ~80%; ready ~49%) · **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
|
||||
@@ -312,6 +312,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Windows UI sharpness: use DirectComposition for Direct3D 12 presentation, preserving explicit overrides. Native-density canvas images align to device pixels at fractional zoom, pan and display scaling. Readiness scores unchanged. |
|
||||
| 2026-10-14 | minor | Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. |
|
||||
| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged |
|
||||
| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `<image>` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged |
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
//! an access violation inside the vendor's driver, which no guard of ours can catch (seen on a laptop
|
||||
//! with two AMD GPUs whose drivers were years apart). Direct3D 12 is part of Windows, so on Windows
|
||||
//! wgpu is offered Direct3D 12, and OpenGL for hardware without a Direct3D 12 driver.
|
||||
//! Windows Direct3D 12 uses DirectComposition by default so window resizing keeps the UI sharp.
|
||||
//! `WGPU_DX12_PRESENTATION_SYSTEM` can select `Visual` or roll back to `Hwnd`.
|
||||
//!
|
||||
//! `WGPU_BACKEND` (`dx12`, `vulkan`, `gl`, `metal`) still names the backend on every system, and
|
||||
//! systems other than Windows keep eframe's defaults.
|
||||
|
||||
use eframe::egui_wgpu::{WgpuConfiguration, WgpuSetup};
|
||||
use eframe::wgpu::Backends;
|
||||
use eframe::wgpu::{Backends, Dx12SwapchainKind};
|
||||
|
||||
/// Direct3D 12, then OpenGL: wgpu ranks a hardware GPU on either ahead of Direct3D's software one.
|
||||
const WINDOWS: Backends = Backends::DX12.union(Backends::GL);
|
||||
@@ -25,10 +27,32 @@ pub fn backends(requested: Option<Backends>, windows: bool, defaults: Backends)
|
||||
}
|
||||
}
|
||||
|
||||
/// The DX12 presentation system: explicit setting wins, else Windows uses DirectComposition.
|
||||
pub(super) fn presentation_system(requested: Option<Dx12SwapchainKind>, windows: bool, defaults: Dx12SwapchainKind) -> Dx12SwapchainKind {
|
||||
match requested {
|
||||
Some(requested) => requested,
|
||||
None if windows => Dx12SwapchainKind::DxgiFromVisual,
|
||||
None => defaults,
|
||||
}
|
||||
}
|
||||
|
||||
/// Offer wgpu [`backends`] instead of eframe's defaults.
|
||||
pub fn configure(options: &mut WgpuConfiguration) {
|
||||
configure_for_platform(options, Backends::from_env(), Dx12SwapchainKind::from_env(), cfg!(windows));
|
||||
}
|
||||
|
||||
pub(super) fn configure_for_platform(
|
||||
options: &mut WgpuConfiguration,
|
||||
requested_backends: Option<Backends>,
|
||||
requested_presentation: Option<Dx12SwapchainKind>,
|
||||
windows: bool,
|
||||
) {
|
||||
if let WgpuSetup::CreateNew(create) = &mut options.wgpu_setup {
|
||||
let offered = &mut create.instance_descriptor.backends;
|
||||
*offered = backends(Backends::from_env(), cfg!(windows), *offered);
|
||||
*offered = backends(requested_backends, windows, *offered);
|
||||
if windows {
|
||||
let presentation = &mut create.instance_descriptor.backend_options.dx12.presentation_system;
|
||||
*presentation = presentation_system(requested_presentation, windows, *presentation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! The backends offered to wgpu: no Vulkan on Windows unless it is asked for (a faulting Vulkan
|
||||
//! driver crashed the app as its window opened), and nothing changed anywhere else.
|
||||
|
||||
use eframe::wgpu::Backends;
|
||||
use eframe::egui_wgpu::{WgpuConfiguration, WgpuSetup};
|
||||
use eframe::wgpu::{Backends, Dx12SwapchainKind};
|
||||
|
||||
use crate::graphics::backends;
|
||||
use crate::graphics::{backends, configure_for_platform, presentation_system};
|
||||
|
||||
/// What eframe offers when `WGPU_BACKEND` isn't set.
|
||||
const DEFAULTS: Backends = Backends::PRIMARY.union(Backends::GL);
|
||||
@@ -30,3 +31,65 @@ fn other_systems_keep_eframes_defaults() {
|
||||
assert_eq!(backends(None, false, DEFAULTS), DEFAULTS);
|
||||
assert_eq!(backends(None, false, Backends::METAL), Backends::METAL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_defaults_to_direct_composition_visual() {
|
||||
assert_eq!(presentation_system(None, true, Dx12SwapchainKind::DxgiFromHwnd), Dx12SwapchainKind::DxgiFromVisual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_presentation_override_wins_on_all_platforms() {
|
||||
for requested in [Dx12SwapchainKind::DxgiFromHwnd, Dx12SwapchainKind::DxgiFromVisual] {
|
||||
for windows in [true, false] {
|
||||
assert_eq!(presentation_system(Some(requested), windows, Dx12SwapchainKind::DxgiFromVisual), requested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_platforms_keep_existing_presentation_system() {
|
||||
assert_eq!(presentation_system(None, false, Dx12SwapchainKind::DxgiFromHwnd), Dx12SwapchainKind::DxgiFromHwnd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_new_configuration_uses_windows_default_and_explicit_override() {
|
||||
let mut options = WgpuConfiguration::default();
|
||||
set_presentation_system(&mut options, Dx12SwapchainKind::DxgiFromHwnd);
|
||||
configure_for_platform(&mut options, None, None, true);
|
||||
assert_eq!(configured_presentation_system(&options), Some(Dx12SwapchainKind::DxgiFromVisual));
|
||||
|
||||
for requested in [Dx12SwapchainKind::DxgiFromHwnd, Dx12SwapchainKind::DxgiFromVisual] {
|
||||
let mut options = WgpuConfiguration::default();
|
||||
set_presentation_system(&mut options, Dx12SwapchainKind::DxgiFromHwnd);
|
||||
configure_for_platform(&mut options, None, Some(requested), true);
|
||||
assert_eq!(configured_presentation_system(&options), Some(requested));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_windows_configuration_keeps_existing_presentation_system() {
|
||||
let mut options = WgpuConfiguration::default();
|
||||
set_presentation_system(&mut options, Dx12SwapchainKind::DxgiFromHwnd);
|
||||
let backends_before = configured_backends(&options);
|
||||
configure_for_platform(&mut options, None, Some(Dx12SwapchainKind::DxgiFromVisual), false);
|
||||
assert_eq!(configured_presentation_system(&options), Some(Dx12SwapchainKind::DxgiFromHwnd));
|
||||
assert_eq!(configured_backends(&options), backends_before);
|
||||
}
|
||||
|
||||
fn set_presentation_system(options: &mut WgpuConfiguration, presentation: Dx12SwapchainKind) {
|
||||
if let WgpuSetup::CreateNew(create) = &mut options.wgpu_setup {
|
||||
create.instance_descriptor.backend_options.dx12.presentation_system = presentation;
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_presentation_system(options: &WgpuConfiguration) -> Option<Dx12SwapchainKind> {
|
||||
if let WgpuSetup::CreateNew(create) = &options.wgpu_setup {
|
||||
Some(create.instance_descriptor.backend_options.dx12.presentation_system)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_backends(options: &WgpuConfiguration) -> Option<Backends> {
|
||||
if let WgpuSetup::CreateNew(create) = &options.wgpu_setup { Some(create.instance_descriptor.backends) } else { None }
|
||||
}
|
||||
|
||||
@@ -204,9 +204,9 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
|
||||
// Re-project the last render onto the current view.
|
||||
let tl = app.to_screen(v.origin.0, v.origin.1);
|
||||
let zoom = app.session.ds().map(|d| d.view.zoom).unwrap_or(1.0);
|
||||
let w = (f64::from(v.width) / v.scale * zoom) as f32;
|
||||
let h = (f64::from(v.height) / v.scale * zoom) as f32;
|
||||
painter.image(tex.id(), Rect::from_min_size(tl, vec2(w, h)), Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
let pixel_preview = app.session.ds().ok().and_then(|ds| ds.view.pixel_preview_scale()) == Some(v.scale);
|
||||
let image_rect = presented_image_rect(tl, v, zoom, ppp, pixel_preview);
|
||||
painter.image(tex.id(), image_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
app.session.tool.labels = overlays(app, &painter, &t);
|
||||
// The zoom tool's area.
|
||||
@@ -238,6 +238,22 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The image's screen rect; align native-density renders to device pixels to avoid linear filtering
|
||||
/// blending neighbouring texels. Stale and pixel-preview renders retain their original projection.
|
||||
fn presented_image_rect(tl: Pos2, view: &figcraft_render::View, zoom: f64, ppp: f32, pixel_preview: bool) -> Rect {
|
||||
let w = (f64::from(view.width) / view.scale * zoom) as f32;
|
||||
let h = (f64::from(view.height) / view.scale * zoom) as f32;
|
||||
let native_density = !pixel_preview
|
||||
&& ppp.is_finite()
|
||||
&& ppp > 0.0
|
||||
&& zoom.is_finite()
|
||||
&& view.scale.is_finite()
|
||||
&& view.scale > 0.0
|
||||
&& (view.scale - zoom * f64::from(ppp)).abs() <= view.scale.abs().max(1.0) * 1e-9;
|
||||
let min = if native_density { pos2((tl.x * ppp).round() / ppp, (tl.y * ppp).round() / ppp) } else { tl };
|
||||
Rect::from_min_size(min, vec2(w, h))
|
||||
}
|
||||
|
||||
/// What the canvas says to screen readers: the selected layer ("Rectangle 1, Rectangle, 120 by 80,
|
||||
/// selected"), how many are selected, or the page with nothing selected.
|
||||
pub fn describe(s: &figcraft_engine::Session) -> String {
|
||||
@@ -1342,3 +1358,121 @@ pub fn fmt(v: f64) -> String {
|
||||
let r = (v * 100.0).round() / 100.0;
|
||||
if (r - r.round()).abs() < 1e-9 { format!("{}", r.round() as i64) } else { format!("{r}") }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod canvas_image_rect_tests {
|
||||
use super::presented_image_rect;
|
||||
use egui::{Pos2, RawInput, Rect, pos2, vec2};
|
||||
use figcraft_render::View;
|
||||
|
||||
fn resized_frame(ui: &mut crate::layers_drag_tests::Ui, worker: &mut crate::render_worker::Worker, size: [f32; 2], ppp: f32) -> egui::FullOutput {
|
||||
let ctx = ui.ctx.clone();
|
||||
let time = ctx.input(|i| i.time) + 1.0 / 60.0;
|
||||
let mut input =
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(size[0], size[1]))), time: Some(time), ..Default::default() };
|
||||
input.viewports.entry(egui::ViewportId::ROOT).or_default().native_pixels_per_point = Some(ppp);
|
||||
let mut output = ctx.run_ui(input, |egui_ui| super::show(&mut ui.app, egui_ui, worker));
|
||||
output.textures_delta.clear();
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_density_image_edges_align_to_device_pixels_without_changing_extent() {
|
||||
for ppp in [1.0_f32, 1.25, 1.5, 2.0] {
|
||||
for zoom in [0.37_f64, 0.67, 1.0, 1.3, 2.0] {
|
||||
let scale = zoom * f64::from(ppp);
|
||||
let view = View { origin: (-31.25, 18.75), scale, width: 375, height: 240 };
|
||||
for (cx, cy, canvas_x, canvas_y) in [(-200.5, 74.25, 100.25, 50.5), (10.0, -20.5, 3.25, 9.75), (913.75, 481.125, 212.5, 77.125)] {
|
||||
let tl = pos2(canvas_x + ((view.origin.0 - cx) * zoom) as f32, canvas_y + ((view.origin.1 - cy) * zoom) as f32);
|
||||
let rect = presented_image_rect(tl, &view, zoom, ppp, false);
|
||||
assert!((rect.min.x - tl.x).abs() * ppp <= 0.5001, "x snap bound: ppp={ppp}, zoom={zoom}, tl={tl:?}, rect={rect:?}");
|
||||
assert!((rect.min.y - tl.y).abs() * ppp <= 0.5001, "y snap bound: ppp={ppp}, zoom={zoom}, tl={tl:?}, rect={rect:?}");
|
||||
assert!(((rect.min.x * ppp).round() - rect.min.x * ppp).abs() < 1e-4, "x edge: ppp={ppp}, zoom={zoom}, tl={tl:?}, rect={rect:?}");
|
||||
assert!(((rect.min.y * ppp).round() - rect.min.y * ppp).abs() < 1e-4, "y edge: ppp={ppp}, zoom={zoom}, tl={tl:?}, rect={rect:?}");
|
||||
assert!((rect.width() * ppp - view.width as f32).abs() < 1e-3, "width: ppp={ppp}, zoom={zoom}, rect={rect:?}");
|
||||
assert!((rect.height() * ppp - view.height as f32).abs() < 1e-3, "height: ppp={ppp}, zoom={zoom}, rect={rect:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_pixel_preview_and_invalid_density_keep_original_projection() {
|
||||
let tl = Pos2::new(149.5, 91.25);
|
||||
let view = View { origin: (0.0, 0.0), scale: 1.95, width: 300, height: 180 };
|
||||
let original = egui::Rect::from_min_size(tl, egui::vec2(200.0, 120.0));
|
||||
|
||||
let stale = presented_image_rect(tl, &view, 1.2, 1.5, false);
|
||||
assert_eq!(stale, egui::Rect::from_min_size(tl, egui::vec2(300.0 / 1.95 * 1.2, 180.0 / 1.95 * 1.2)));
|
||||
let pixel_preview = presented_image_rect(tl, &view, 1.3, 1.5, true);
|
||||
assert_eq!(pixel_preview, original);
|
||||
for ppp in [0.0, -1.0, f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(presented_image_rect(tl, &view, 1.3, ppp, false), original);
|
||||
}
|
||||
assert_eq!(tl, pos2(149.5, 91.25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_mesh_stays_sharp_through_window_resizes() {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::live_tests::flat;
|
||||
use figcraft_engine::Session;
|
||||
|
||||
for ppp in [1.0_f32, 1.25, 1.5] {
|
||||
let mut ui = crate::layers_drag_tests::Ui::new(Session::new());
|
||||
let mut worker = ui.app.worker.take().expect("worker from headless UI");
|
||||
let (zoom, center) = {
|
||||
let ds = ui.app.session.ds_mut().expect("document state");
|
||||
ds.view.zoom = 1.3;
|
||||
ds.view.cx = 123.5;
|
||||
ds.view.cy = -87.25;
|
||||
(ds.view.zoom, (ds.view.cx, ds.view.cy))
|
||||
};
|
||||
for size in [[1440.0, 900.0], [1000.0, 700.0], [800.0, 500.0], [1440.0, 900.0]] {
|
||||
let start = Instant::now();
|
||||
let mut output;
|
||||
loop {
|
||||
output = resized_frame(&mut ui, &mut worker, size, ppp);
|
||||
let has_current_texture = ui.app.canvas.tex_view.as_ref().is_some_and(|v| {
|
||||
(v.scale - zoom * f64::from(ppp)).abs() < 1e-9
|
||||
&& v.width == (ui.app.canvas.rect.width() * ppp).ceil() as u32 + 1
|
||||
&& v.height == (ui.app.canvas.rect.height() * ppp).ceil() as u32 + 1
|
||||
&& !worker.in_flight()
|
||||
});
|
||||
if has_current_texture {
|
||||
break;
|
||||
}
|
||||
assert!(start.elapsed() < Duration::from_secs(10), "render did not finish for size={size:?}, ppp={ppp}");
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
}
|
||||
|
||||
let (ds_zoom, ds_center) = {
|
||||
let ds = ui.app.session.ds().expect("document state");
|
||||
(ds.view.zoom, (ds.view.cx, ds.view.cy))
|
||||
};
|
||||
assert_eq!(ds_zoom, zoom, "resize preserves zoom");
|
||||
assert_eq!(ds_center, center, "resize preserves page centre");
|
||||
|
||||
let tex = ui.app.canvas.texture.as_ref().expect("canvas texture");
|
||||
let mesh = flat(&output.shapes)
|
||||
.into_iter()
|
||||
.find_map(|shape| match shape {
|
||||
egui::Shape::Mesh(mesh) if mesh.texture_id == tex.id() => Some(mesh),
|
||||
_ => None,
|
||||
})
|
||||
.expect("canvas image mesh");
|
||||
let min_x = mesh.vertices.iter().map(|v| v.pos.x).fold(f32::INFINITY, f32::min);
|
||||
let min_y = mesh.vertices.iter().map(|v| v.pos.y).fold(f32::INFINITY, f32::min);
|
||||
let max_x = mesh.vertices.iter().map(|v| v.pos.x).fold(f32::NEG_INFINITY, f32::max);
|
||||
let max_y = mesh.vertices.iter().map(|v| v.pos.y).fold(f32::NEG_INFINITY, f32::max);
|
||||
let view = ui.app.canvas.tex_view.as_ref().expect("canvas render view");
|
||||
assert!((min_x * ppp - (min_x * ppp).round()).abs() < 1e-4, "mesh left edge: {min_x}, ppp={ppp}");
|
||||
assert!((min_y * ppp - (min_y * ppp).round()).abs() < 1e-4, "mesh top edge: {min_y}, ppp={ppp}");
|
||||
assert!(((max_x - min_x) * ppp - view.width as f32).abs() < 1e-3, "mesh width matches texture");
|
||||
assert!(((max_y - min_y) * ppp - view.height as f32).abs() < 1e-3, "mesh height matches texture");
|
||||
}
|
||||
assert!(!worker.in_flight(), "last resize render finished");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -1,6 +1,6 @@
|
||||
# Hardware and platform parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Windows QA pass; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (DirectComposition presentation for Windows Direct3D 12; Windows QA pass; 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
|
||||
@@ -46,10 +46,26 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
|
||||
| Web | primary (figma.com) | `figcraft-web` (WASM, WebGPU/WebGL2), web zip | no accessibility tree; file access through the browser |
|
||||
| iOS / Android | apps: view, comment, mirror prototypes | none (the web build is not touch-first) | 20–40 h for a viewer |
|
||||
|
||||
## Windows: presentation and text sharpness
|
||||
|
||||
The desktop app offers Direct3D 12 and OpenGL on Windows. Direct3D 12 uses DirectComposition presentation (`DxgiFromVisual`) by default: a Windows 11 comparison found the entire interface sharp in smaller and snapped windows with this path, while the previous HWND presentation softened menus, the layers panel and canvas text. Backend selection and non-Windows presentation are unchanged.
|
||||
|
||||
`WGPU_DX12_PRESENTATION_SYSTEM` still overrides presentation. To compare the previous HWND path, or use RenderDoc (which wgpu does not support with Visual presentation), start the executable from a temporary PowerShell session:
|
||||
|
||||
```powershell
|
||||
$env:WGPU_DX12_PRESENTATION_SYSTEM = 'Hwnd'
|
||||
.\figcraft.exe
|
||||
```
|
||||
|
||||
`Visual` explicitly selects DirectComposition; `WGPU_BACKEND=gl` selects OpenGL as another diagnostic comparison. Environment overrides belong to that shell and its child processes; closing the shell discards them. These comparisons do not change Windows display or driver settings.
|
||||
|
||||
Canvas-only interpolation blur is addressed separately: native-density canvas textures start on device-pixel boundaries. The presentation change concerns the entire interface and does not change document geometry, text shaping or exported images.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Use DirectComposition for Windows Direct3D 12 presentation after a same-binary HWND/Visual/OpenGL comparison; preserve explicit presentation and backend overrides. Document the HWND override for diagnostics and RenderDoc. |
|
||||
| 2026-10-11 | minor | Windows QA pass (gap 8): everyday workflows on Windows 11 through the desktop app and MCP; fixed: unsaved changes lost when the app was killed or crashed (recovery copies every few seconds, reopened at the next start), Ctrl+arrows/Backspace/Delete acting on the whole line in text (now by word; Ctrl+Home/End to the text's ends), scrubbed fields snapping back on release, typing on a field Tab reached, Escape not closing dialogs, skewed or vanishing linear gradients, hints cut off at the panel edge, the window-close question in English in the Spanish interface. Percentages and hours unchanged |
|
||||
| 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 |
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (native-density canvas textures aligned to device pixels; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; 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:
|
||||
@@ -92,6 +92,7 @@ interaction feel not yet compared.
|
||||
| Item | Status | Notes |
|
||||
|---|---|---|
|
||||
| Pan/zoom: wheel, pinch, Space-drag, Shift+0/1/2, Cmd+0, `+`/`-` | done | Shift-wheel pans sideways, pinch and the zoom tool keep the point under the pointer; zoom steps, limits (2%–25,600%) and frame order pinned where the documentation is silent (`view_behaviour_tests`) |
|
||||
| Canvas texture sharpness | done (ours) | At native render density the canvas image starts on whole device pixels, avoiding extra linear-filter blending after fractional zoom or pan and at fractional display scales. Placement changes by at most half a device pixel; document geometry and overlays keep their coordinates. Stale zoom renders and pixel preview retain their existing projection (`canvas_image_rect_tests`). This does not establish text-rendering parity with Figma. |
|
||||
| Rulers and guides (drag out, move, remove; rotated frames; frame guides) | done | guides belong to the page or a top-level frame (a frame moved into another keeps its guides but they stop applying); a click selects one for Delete, the arrows or the context menu's Remove guide; Alt-drag drops a copy; Alt while dragging measures to the layers in the frame it crosses; ruler numbering checked by eye only |
|
||||
| Frame labels (rotated, right-to-left), size badge | done | |
|
||||
| Outline mode | done | Outlines, outlines for the selection, mask outlines, frame outlines; shapes are clicked on their outlines, reaching layers behind; View ▸ Outlines ▸ Include hidden layers (`view.outlinesHiddenLayers`) outlines hidden layers too and Include object bounds (`view.outlinesObjectBounds`) draws the box of every layer whose outline isn't its box (groups, text, ellipses, vectors); what either draws can be clicked there (a hidden layer on its outline, a group on its box) and nothing else can; both are view settings like the other outline flags (per file, off in a new one, checked in the menu); the lines are light on a dark page (`outline_options_tests`). The main menu now shows a check mark beside every View toggle that is on |
|
||||
@@ -164,6 +165,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Align native-density canvas image placement to the device-pixel grid, avoiding interpolation blur at fractional zoom, pan and display scaling. No readiness score change. |
|
||||
| 2026-10-14 | minor | Home screen recent files as cards with thumbnails on their page colour, a sortable table view, selection, keyboard and a context menu |
|
||||
| 2026-10-13 | minor | Vector edit mode regions: with the Move tool the filled area under the pointer is hatched with "/" lines in the selection colour (about 1.4 px), clipped by the area's own fill rule so holes stay clear, and the hatch goes when the pointer is 3.5 px from a point or 2 px from a segment (screen pixels at any zoom); the spacing follows octave folding, fitted to measurements at several zoom levels (8 px at every power-of-two zoom, the gap growing with the zoom and stepping back at √2 · 2ⁿ), anchored to the vector so it doesn't slide when panning, one continuous pattern across selected areas; a click selects an area with its points (they move with it but keep the unselected look), Shift adds an area or takes a selected one out, a press on a selected area drags everything selected, drags show no hover hatch, a click on an empty spot (such as a hole) clears the areas and stays in edit mode, the last area listed wins where areas overlap, only filled vectors have areas; `vector.edit.select {regions}` and `vector.edit.state` report them (`regions_tests`, `vector_regions_tests`) |
|
||||
| 2026-10-13 | minor | Home screen: a Templates tab (eight starter templates as cards with thumbnails, search and a kind filter; click or Enter opens a new document) replaces the Community placeholder, the Drafts placeholder is removed, Recents offers four templates while there are no recent files and shows a Getting started card (closable; Help ▸ Getting started brings it back), File ▸ New from template…, and a first run opens on the home screen |
|
||||
|
||||
Reference in New Issue
Block a user