mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-12 08:51:11 +00:00
M10: Export: a background blur or glass exported on its own keeps the content behind it (#2213)
An export of a layer draws only that layer, so a background blur or glass in it read an empty backdrop. The export now renders the page's layers below the first exported layer, with the margin the effects reach, as the backdrop (render_area_over), clipped to the export area; only the exported layers are drawn on top. Layers without those effects, and every other export path, are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
1 parent
a2e87433d8
commit
020388cf1f
5 files changed
+126
-12
No files matched your search
@@ -26,8 +26,8 @@ pub(crate) use export::FORBIDDEN_FILE_NAME_CHARS;
|
||||
use export::add_member;
|
||||
pub(super) use export::export_setting;
|
||||
pub use export::{
|
||||
ExportJob, copy_png, drawn_layers, export_area, export_bytes, export_bytes_with, export_jobs, export_scale, exportable_layers, fig_bytes,
|
||||
frames_pdf, jobs_for, penpot_bytes, safe_file_name,
|
||||
ExportJob, backdrop_layers, copy_png, drawn_layers, export_area, export_bytes, export_bytes_with, export_jobs, export_scale, exportable_layers,
|
||||
fig_bytes, frames_pdf, jobs_for, penpot_bytes, safe_file_name,
|
||||
};
|
||||
use export::{export, export_frames_pdf, export_layers, pdf_bytes};
|
||||
use load::font_report;
|
||||
@@ -408,7 +408,10 @@ pub fn export_file(doc: &Document, ids: &[NodeId], setting: &ExportSetting, scal
|
||||
Ok((pdf_bytes(doc, &groups, &setting.print, (!own_area).then_some(area))?, Vec::new()))
|
||||
}
|
||||
_ => {
|
||||
let mut pix = figcraft_render::render_area(doc, &drawn, area, scale)
|
||||
// Layers exported on their own still read the page beneath them for a background blur
|
||||
// or glass (#2213); a "contents only" off export already draws the whole page.
|
||||
let page = if setting.contents_only { backdrop_layers(doc, ids) } else { Vec::new() };
|
||||
let mut pix = figcraft_render::render_area_over(doc, &drawn, &page, area, scale)
|
||||
.ok_or_else(|| EngineError::Other("render failed (empty or too large)".into()))?;
|
||||
// The render is in the document's profile; the setting's profile converts the pixels
|
||||
// (out-of-gamut colors clip) and tags the file.
|
||||
|
||||
@@ -55,6 +55,12 @@ pub fn drawn_layers(doc: &Document, ids: &[NodeId], setting: &ExportSetting) ->
|
||||
crate::cmd::edit::z_sorted(doc, &crate::cmd::edit::roots(doc, &own))
|
||||
}
|
||||
|
||||
/// The top-level layers of the page of `ids`, bottom to top: what a background blur or glass in an
|
||||
/// export of `ids` reads from (empty when `ids` are on no page).
|
||||
pub fn backdrop_layers(doc: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
ids.first().and_then(|i| doc.page_of(*i)).and_then(|p| doc.node(p)).map(|p| p.children.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The frame or group a slice sits in: None on the page or in a section.
|
||||
fn slice_container(doc: &Document, slice: NodeId) -> Option<NodeId> {
|
||||
let parent = doc.node(slice)?.parent?;
|
||||
|
||||
@@ -384,3 +384,75 @@ fn copy_as_png_svg_and_code() {
|
||||
let css = crate::codegen::code_for(doc, a, crate::codegen::CodeLang::Css);
|
||||
assert!(css.contains("background: #FF0000;"), "Copy as CSS is the layer's CSS from Inspect");
|
||||
}
|
||||
|
||||
/// A red square with a fill-less pane over its right part, holding `effect` (#2213).
|
||||
fn pane_scene(effect: EffectKind) -> (figcraft_doc::Document, NodeId, NodeId) {
|
||||
use figcraft_doc::{Document, NodeKind};
|
||||
let mut d = Document::new("blur-export");
|
||||
let page = d.first_page().unwrap();
|
||||
let under = d.create(page, NodeKind::Rectangle, "Under", 0.0, 0.0, 120.0, 120.0).unwrap();
|
||||
d.get_mut(under).unwrap().fills = vec![figcraft_color::Paint::solid(figcraft_color::Color::from_hex("E03030").unwrap())];
|
||||
let pane = d.create(page, NodeKind::Rectangle, "Glass", 60.0, 20.0, 120.0, 80.0).unwrap();
|
||||
d.get_mut(pane).unwrap().fills = vec![];
|
||||
d.get_mut(pane).unwrap().effects = vec![Effect { kind: effect, visible: true }];
|
||||
(d, under, pane)
|
||||
}
|
||||
|
||||
/// The PNG export of `ids` as the Export command makes it, and what the canvas draws there.
|
||||
fn export_vs_canvas(d: &figcraft_doc::Document, ids: &[NodeId]) -> (image::RgbaImage, image::RgbaImage) {
|
||||
let setting = figcraft_doc::ExportSetting::default();
|
||||
let page = d.first_page().unwrap();
|
||||
let area = crate::cmd::file::export_area(d, ids, &setting).unwrap();
|
||||
let canvas = figcraft_render::render_area(d, &d.get(page).unwrap().children.clone(), area, 1.0).unwrap();
|
||||
let canvas = image::RgbaImage::from_raw(canvas.width(), canvas.height(), figcraft_render::to_rgba8(&canvas)).unwrap();
|
||||
let (bytes, _) = crate::cmd::file::export_file(d, ids, &setting, 1.0).unwrap();
|
||||
(pixels(&bytes), canvas)
|
||||
}
|
||||
|
||||
/// #2213: a layer with a background blur exported on its own shows the same pixels as the canvas
|
||||
/// shows for that area: the blur reads what lies under it, so the export must draw the layers
|
||||
/// beneath it as the backdrop (clipped to the layer) rather than blurring nothing.
|
||||
#[test]
|
||||
fn exporting_a_background_blur_layer_keeps_the_blurred_backdrop() {
|
||||
let (d, _, pane) = pane_scene(EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(12.0)));
|
||||
let (exported, canvas) = export_vs_canvas(&d, &[pane]);
|
||||
// The pane's left half sits over the red square, which ends at x = 120 (60 px into the pane).
|
||||
assert_ne!(canvas.get_pixel(60, 40).0, [0, 0, 0, 0], "the canvas draws the blurred backdrop");
|
||||
assert_eq!(exported.get_pixel(60, 40), canvas.get_pixel(60, 40), "export lost the backdrop the blur reads");
|
||||
assert_eq!(exported.dimensions(), (120, 80), "the pane's own bounds");
|
||||
}
|
||||
|
||||
/// #2213: the same for Glass.
|
||||
#[test]
|
||||
fn exporting_a_glass_layer_keeps_the_backdrop_it_bends() {
|
||||
let (d, _, pane) = pane_scene(EffectKind::Glass(figcraft_doc::Glass::default()));
|
||||
let (exported, canvas) = export_vs_canvas(&d, &[pane]);
|
||||
assert_ne!(canvas.get_pixel(30, 40).0, [0, 0, 0, 0]);
|
||||
assert_eq!(exported.get_pixel(30, 40), canvas.get_pixel(30, 40), "export lost the backdrop the glass reads");
|
||||
assert_eq!(exported.get_pixel(60, 40), canvas.get_pixel(60, 40));
|
||||
}
|
||||
|
||||
/// Measured in Figma 2026-11-10: inside the pane's bounds the export is opaque where the backdrop
|
||||
/// is (the blurred red), and the layers over the pane stay out.
|
||||
#[test]
|
||||
fn a_blur_export_is_opaque_inside_the_pane_and_leaves_layers_above_out() {
|
||||
let (mut d, _, pane) = pane_scene(EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(12.0)));
|
||||
let page = d.first_page().unwrap();
|
||||
let above = d.create(page, figcraft_doc::NodeKind::Rectangle, "Above", 0.0, 0.0, 200.0, 200.0).unwrap();
|
||||
d.get_mut(above).unwrap().fills = vec![figcraft_color::Paint::solid(figcraft_color::Color::from_hex("0000FF").unwrap())];
|
||||
let (exported, _) = export_vs_canvas(&d, &[pane]);
|
||||
let left = exported.get_pixel(10, 40).0;
|
||||
assert_eq!(left[3], 255, "red under the pane: opaque");
|
||||
assert!(left[0] > 150 && left[2] < 60, "red, not the blue layer above: {left:?}");
|
||||
// The far right of the pane has no square under it: nothing to read, nothing drawn.
|
||||
assert_eq!(exported.get_pixel(110, 40).0[3], 0);
|
||||
}
|
||||
|
||||
/// Guard: a layer without backdrop effects over another still exports alone ("contents only").
|
||||
#[test]
|
||||
fn a_layer_without_backdrop_effects_still_leaves_the_layers_below_out() {
|
||||
let (mut d, _, pane) = pane_scene(EffectKind::BackgroundBlur(figcraft_doc::Blur::uniform(0.0)));
|
||||
d.get_mut(pane).unwrap().effects.clear();
|
||||
let (exported, _) = export_vs_canvas(&d, &[pane]);
|
||||
assert!(exported.pixels().all(|p| p.0 == [0, 0, 0, 0]), "a fill-less pane alone exports clear");
|
||||
}
|
||||
@@ -266,11 +266,11 @@ fn render_one(doc: &Document, page: NodeId, view: &View, opts: &Options) -> Opti
|
||||
|
||||
/// Render several nodes (in z-order) into an existing pixmap through `view`.
|
||||
pub fn render_nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap) {
|
||||
nodes_into(doc, ids, view, pix, 0.0);
|
||||
nodes_into(doc, ids, view, pix, 0.0, None);
|
||||
}
|
||||
|
||||
/// [`render_nodes_into`] `time` seconds into animated GIFs.
|
||||
fn nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap, time: f64) {
|
||||
fn nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap, time: f64, under: Option<&backdrop::Under>) {
|
||||
let bounds = draw::Bounds::default();
|
||||
let ctx = draw::Ctx {
|
||||
doc,
|
||||
@@ -282,7 +282,7 @@ fn nodes_into(doc: &Document, ids: &[NodeId], view: &View, pix: &mut Pixmap, tim
|
||||
lod: false,
|
||||
preview: false,
|
||||
magnified_image_pixels: false,
|
||||
under: None,
|
||||
under,
|
||||
time,
|
||||
bounds: &bounds,
|
||||
};
|
||||
@@ -353,16 +353,24 @@ pub fn render_node(doc: &Document, id: NodeId, scale: f64) -> Option<Pixmap> {
|
||||
|
||||
/// [`render_node`] `time` seconds into animated GIFs (the presenter plays them).
|
||||
pub fn render_node_at(doc: &Document, id: NodeId, scale: f64, time: f64) -> Option<Pixmap> {
|
||||
area_at(doc, &[id], export_bounds(doc, id)?, scale, time)
|
||||
area_at(doc, &[id], None, export_bounds(doc, id)?, scale, time)
|
||||
}
|
||||
|
||||
/// Rasterise `ids` (bottom to top) over page area `b` at `scale` (export): the image starts at the
|
||||
/// area's top-left and its sides round up to whole pixels. `None` past 16384 pixels a side.
|
||||
pub fn render_area(doc: &Document, ids: &[NodeId], b: Rect, scale: f64) -> Option<Pixmap> {
|
||||
area_at(doc, ids, b, scale, 0.0)
|
||||
area_at(doc, ids, None, b, scale, 0.0)
|
||||
}
|
||||
|
||||
fn area_at(doc: &Document, ids: &[NodeId], b: Rect, scale: f64, time: f64) -> Option<Pixmap> {
|
||||
/// [`render_area`] for an export of layers that sit among `page_layers` (the page's top-level
|
||||
/// layers, in z-order): a background blur or glass in `ids` reads what lies under the first of
|
||||
/// them on that page, clipped to the area (#2213). Only `ids` are drawn; the layers above stay
|
||||
/// out. Without such an effect it is [`render_area`].
|
||||
pub fn render_area_over(doc: &Document, ids: &[NodeId], page_layers: &[NodeId], b: Rect, scale: f64) -> Option<Pixmap> {
|
||||
area_at(doc, ids, Some(page_layers), b, scale, 0.0)
|
||||
}
|
||||
|
||||
fn area_at(doc: &Document, ids: &[NodeId], page_layers: Option<&[NodeId]>, b: Rect, scale: f64, time: f64) -> Option<Pixmap> {
|
||||
let scale = if scale.is_finite() && scale > 0.0 { scale.min(64.0) } else { 1.0 };
|
||||
let w = (b.width() * scale - 1e-6).ceil().max(1.0);
|
||||
let h = (b.height() * scale - 1e-6).ceil().max(1.0);
|
||||
@@ -371,7 +379,31 @@ fn area_at(doc: &Document, ids: &[NodeId], b: Rect, scale: f64, time: f64) -> Op
|
||||
}
|
||||
let view = View { origin: (b.x0, b.y0), scale, width: w as u32, height: h as u32 };
|
||||
let mut pix = new_pixmap(view.width, view.height)?;
|
||||
nodes_into(doc, ids, &view, &mut pix, time);
|
||||
// What the backdrop effects read: the page under the first layer, with the margin they reach.
|
||||
let reach = page_layers.and_then(|_| {
|
||||
ids.iter()
|
||||
.flat_map(|i| doc.subtree(*i))
|
||||
.filter_map(|i| doc.node(i))
|
||||
.filter(|n| n.visible && backdrop::has_effects(n))
|
||||
.map(|n| backdrop::pad(n, scale))
|
||||
.max()
|
||||
});
|
||||
let beneath = match (page_layers, ids.first(), reach) {
|
||||
(Some(layers), Some(first), Some(m)) => {
|
||||
let m = i32::try_from(m).unwrap_or(0);
|
||||
let p = f64::from(m) / scale;
|
||||
let around = View {
|
||||
origin: (b.x0 - p, b.y0 - p),
|
||||
scale,
|
||||
width: view.width.saturating_add(m.unsigned_abs() * 2),
|
||||
height: view.height.saturating_add(m.unsigned_abs() * 2),
|
||||
};
|
||||
backdrop::render_beneath(doc, layers, *first, &around).map(|back| (back, m))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let under = beneath.as_ref().map(|(back, m)| backdrop::Under::new(back, (*m, *m), None));
|
||||
nodes_into(doc, ids, &view, &mut pix, time, under.as_ref());
|
||||
Some(pix)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# FigCraft parity with Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (justified Chinese, Japanese and Korean text, written without spaces, spreads each line's room between its characters so its right edge aligns (#1970); joining the two ends of a path broken open by a deleted point closes it again, so its fill and selection outline follow the new segment and its corner radius instead of a straight line across the gap (#1951); glass's docs say which centre splay bends away from and where a shape gets no core; glass bends towards its core, rounded rectangles in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps, measured against the reference tool; bulleted lists use the same bullet at every level of indentation, as the help center says, instead of a white bullet and a square at the deeper levels; typing `1)` and a space at the start of a paragraph starts a numbered list, as `1.` does, both being the help center's creation characters; a link on text is underlined by default, as the help center says, and Underline takes the underline off while the link stays; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); plugin widgets have hover styles: a layer's `hover` fill, stroke, opacity or text colour shows while the pointer is over it (gap 6); Thai, Lao, Khmer and Burmese lines break between words (#1219); MCP and control calls name their document (#998); shadows of images with transparent pixels follow their alpha (#1289); Arabic draft catalog; truncated text fills its last line before the ellipsis and keeps truncating when resized; auto-height text with max lines (#1375, #1376, #1377), numbers unchanged; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); 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; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; 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 effectLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (an exported layer with a background blur or glass keeps the blurred content behind it within its bounds (#2213); justified Chinese, Japanese and Korean text, written without spaces, spreads each line's room between its characters so its right edge aligns (#1970); joining the two ends of a path broken open by a deleted point closes it again, so its fill and selection outline follow the new segment and its corner radius instead of a straight line across the gap (#1951); glass's docs say which centre splay bends away from and where a shape gets no core; glass bends towards its core, rounded rectangles in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps, measured against the reference tool; bulleted lists use the same bullet at every level of indentation, as the help center says, instead of a white bullet and a square at the deeper levels; typing `1)` and a space at the start of a paragraph starts a numbered list, as `1.` does, both being the help center's creation characters; a link on text is underlined by default, as the help center says, and Underline takes the underline off while the link stays; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; gap 6: widgets draw lines and SVG pictures (plugin API 6); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); plugin widgets have hover styles: a layer's `hover` fill, stroke, opacity or text colour shows while the pointer is over it (gap 6); Thai, Lao, Khmer and Burmese lines break between words (#1219); MCP and control calls name their document (#998); shadows of images with transparent pixels follow their alpha (#1289); Arabic draft catalog; truncated text fills its last line before the ellipsis and keeps truncating when resized; auto-height text with max lines (#1375, #1376, #1377), numbers unchanged; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; `.sketch` import, best effort: import/export breadth 86% → 87%, file formats 60% → 61%, headline numbers unchanged; Canvas & navigation's remaining work no longer lists outline mode's Include hidden layers and Include object bounds; boards: area 22 (weight 0, outside the weighted sum); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, iteration marks 〱–〵 and the halfwidth parentheses; Turkish reviewed, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); 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; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; 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 impLine truncated
|
||||
|
||||
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
|
||||
@@ -102,7 +102,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|
||||
| 4 | Shapes & vector | 8 | 99% | 73% | Corner radius and smoothing on boolean groups that contain rectangles; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (behaviour suites from the documentation, corner radius and star ratio handles, corner smoothing on polygons, stars and closed vectors, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 |
|
||||
| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 |
|
||||
| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 |
|
||||
| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 |
|
||||
| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed; a background blur or glass exported on its own keeps the content behind it within its bounds, measured 2026-11-10, #2213) | 2–5 |
|
||||
| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 |
|
||||
| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 |
|
||||
| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, Thai, Lao, Khmer and Burmese breaks between words, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing (typing `-`, `*`, `1.` or `1)` and a space starts one), hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 |
|
||||
@@ -656,6 +656,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||
| 2026-11-04 | patch | M4, #1970: justified Chinese, Japanese and Korean text, written without spaces between words, aligns its right edge: the room on each line but a paragraph's last goes between its characters, each a word of its own as for line breaking ("Justify aligns the first word in each line of text to the left-most edge, and the last to the right-most edge"), wherever a line could break there, so none opens before a full stop or closing bracket, nor after an opening one. Latin text widens its spaces as before; mixed text widens both. A gap is reordered with the line's runs as a space of no width would be, so one beside a Hebrew word in Chinese text, or beside a Chinese word within that Hebrew, opens beside the word, not inside it. `figcraft_text::breaks::justify_between`, beside `break_between`, says where. Evidence: `tests::justified_ideographs_spread_the_room_between_characters` (fails on main). |
|
||||
| 2026-11-04 | patch | M8, #1951 (#1506 again): a path broken open by deleting a point (Delete, not Shift+Delete) is filled closed by a straight line across the gap, as an open path's fill is; joining its two ends again (the pen onto the other end, Connect, ⌘J) left the loop without the new segment, so once the points were rounded the fill and the selection outline ran straight across while the stroke followed the segment and its arcs. Joining now closes the loop through the new segment ("Click to close the path."; "You can use the Paint tool to add fills to any closed regions of a vector network."): `ops::connect` puts it into the loop whose segments are the chain between the ends, at the gap and in the loop's own order, so a hole keeps its winding whichever end was clicked first; a loop listed out of walking order is left as it was. Evidence: `vectoredit::tests::a_rounded_chamfer_made_other_ways_fills_along_its_arcs` (fails on main: the loop kept 4 of 5 segments), `a_rounded_chamfer_cut_in_the_editor_fills_along_its_arcs` (Shift+Delete, with and without a region fill), `doc::tests::a_chamfered_rectangles_rounded_fill_is_cut_at_every_point` (fill, region path and stroke agree for every loop order and segment direction of a chamfered rectangle). |
|
||||
| 2026-11-06 | patch | M3: glass's docs say which centre `splay` bends away from: the centre of the layer's bounds, as the reference tool's, checked side by side on a ring and an L at Splay 0% and 100% (an inner edge bends towards the hole in both). And where a glass shape gets no core: past 4M px on the desktop and 1M on the web (`MAX_SHAPE_PX`) it bends along its edge normals, so a large glass text or vector can bend differently once zoomed into, in a 2× export, or on the web. Docs only. Grade unchanged. |
|
||||
| 2026-11-10 | patch | M10, #2213: a layer with a background blur or glass exported on its own includes the blurred content behind it within its bounds, as the reference tool's web app does (measured 2026-11-10: the pane's own bounds, opaque where the backdrop is, nothing outside them; layers above it stay out, "contents only" unchanged for layers without those effects); the export reads the page's layers below the exported one as its backdrop |
|
||||
| 2026-11-02 | minor | M3, #1885: glass bends towards its core, the shape shrunk by `depth` and the bevel, as the reference tool's: round a sharp corner the bend turns smoothly, and a thin part (a capsule, a letter's stroke) bends across itself, checked side by side on a capsule 44 units tall. Rounded rectangles work it out in closed form. A shape too big to keep whole at the zoom bends along its edge normals instead, so no render works out a core over more pixels than a kept shape holds. Tests: `glass::tests::a_sharp_corner_bends_round_towards_the_core`, `glass::tests::a_thin_part_bends_across_itself`, `glass::tests::a_shape_too_big_to_keep_bends_along_its_normals`, `glass_cut_tests::a_cut_through_the_pane_matches_the_whole_render`. Grade unchanged. |
|
||||
| 2026-11-02 | minor | M3, #1885: glass's light is a line on its 1-unit bevel added to the backdrop, as the reference tool's: fully where the edge faces the light, none below 0.35 of facing it, 0.6 as much on the opposite side, with a faint gradient across the curve. `splay` turns the bend from along the edge's normal towards the way away from the layer's centre and no longer widens the light, as in the reference tool. Checked side by side on synthetic files: a splay of 1 bends the backdrop towards the centre in both. Tests: `glass::tests::the_light_lies_on_the_bevel_and_the_gradient_across_the_curve`, `backdrop_tests::splay_turns_the_bend_towards_the_centre`, `glass_cut_tests::the_light_is_a_line_on_the_bevel`. Grade unchanged. |
|
||||
| 2026-11-02 | minor | M3, #1885: glass bends the backdrop with the reference tool's thick-lens profile (most a 1-unit bevel in from the edge, falling off as the 3.5th power of the way out of the curve to nothing `depth` further in) and splits its colours over 8 steps, red and blue 0.16 of the bend apart at 100% dispersion. Measured against the reference tool on synthetic files at 2×: the black band a pane pulls to its edge ends within 3 px of the reference tool's, and red and blue part 2.27 px at 50% dispersion and 4.66 px at 100% (2.38 and 4.69 there). Tests: `glass::tests::the_bend_follows_the_thick_lens_profile`, `glass::tests::dispersion_splits_the_channels_along_the_bend`. Grade unchanged. |
|
||||
|
||||
Reference in new issue
Block a user