diff --git a/ROADMAP.md b/ROADMAP.md index df1d625c..c4d98d90 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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-29 · **Change:** minor (comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; 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; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); 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, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; 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; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); 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, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **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 @@ -355,6 +355,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-29 | patch | M4, #1478: a letter and its combining marks are shaped with one font that has them all. Text typed decomposed (NFD, `E` + U+0302 + U+0309 for Ể) in a font with the letters but not the marks drew missing-glyph boxes beside and above the letter, while the precomposed form fell back to a font that has it; each grapheme cluster now takes the first face (the style's own, then the fallbacks) that covers every char in it (joiners and variation selectors need no glyph), else the letter's own face as before. Fonts that cover Vietnamese were already right: mark positioning (GPOS `mark`/`mkmk`) stacks the marks over the letter on the canvas, in PNG, SVG and PDF. Tests in `crates/text/src/vietnamese_tests.rs`. Numbers unchanged | | 2026-10-29 | minor | M9, #1415: `comment.list` (CLI, MCP `run_command`, the `figcraft://comments` resource) gives every message, the first and each reply, its `mentions`: `{name, known, you}` for each name it @mentions, by the rule the thread view highlights them with (`figcraft_doc::mentions`, which the highlighting now shares). A name resolves against the file's people and you, as the file spells it. An unknown name is reported with `known: false`. `a@b.com` mentions nobody. Filtering threads by a mentioned name and listing mentioned names in `comment.people` stay open. Numbers unchanged | | 2026-10-29 | minor | M8, #1061: a mask group's pixmaps (the mask, its own layer, the content and the pixels it goes onto) cover only where the mask or its content can paint (effects included) within the view, rounded out to whole pixels, instead of the whole view: a 40 × 40 mask on a 3000 × 2000 view held 96 MB, now 61.5 kB, drawing the same pixels. Isolated layers (effects, opacity, masks, clipping frames) also share one budget of 2 GiB held at once per render thread (`figcraft_render::layer_budget`); a layer past it is left out with a warning, so 64 nested full-view layers no longer ask for tens of GB. Tests: `mask_bounds_tests`. Numbers unchanged | | 2026-10-29 | minor | M2: components can't contain their own instances (#1084). Any edit that would make a component hold an instance of itself, directly or through other components (an instance-swap property's current value included), is refused with the loop named: creating, placing, swapping, switching variants, moving, pasting and instance-swap values. Each variant counts as a component of its own, so a variant holding a sibling variant's instance is fine unless that one leads back to it (`components::check_component_cycles`, beside the instance structure check); a dropped instance lands on the page instead. A file holding such a loop (opened or imported) has one instance per loop detached, with a note; a loop merged in a live session is reported, and a rebuild never expands an instance inside a copy of itself, so its copies stop growing (12 rebuilds of a two-component loop: 25 → 113 layers before, 25 throughout now). Tests: `component_cycle_tests`. Numbers unchanged | diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index cf0d5512..3ff78cfa 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -383,33 +383,24 @@ fn shape_text(text: &TextData) -> ShapedText { let Some(Some(primary)) = faces.get(st_idx) else { continue }; let size = sanitize_size(style.font_size); let spacing = style.letter_spacing.resolve(size); + let span_fallbacks = style_fallbacks.get(st_idx).unwrap_or(&fallbacks); + // The face for the grapheme cluster starting at `i` (clipped to the span). + let face_at = |i: usize| { + let to = (i + 1..*end).find(|&j| cluster_start.get(j).copied().unwrap_or(true)).unwrap_or(*end); + cluster_face(chars.get(i..to).unwrap_or(&[]), primary, &emoji, span_fallbacks) + }; // Segment by face. let mut seg_start = *start; while seg_start < *end { - let face_for = |c: char| -> Arc { - // Emoji take a colour face before a face without colour glyphs (the style's own or a - // symbol font), which would draw them in one colour. - if is_emoji(c) - && !primary.has_color() - && let Some(f) = emoji.iter().find(|f| f.has_char(c)) - { - return f.clone(); - } - if is_line_break(c) || primary.has_char(c) { - return primary.clone(); - } - let fallbacks = style_fallbacks.get(st_idx).unwrap_or(&fallbacks); - fallbacks.iter().find(|f| f.has_char(c)).cloned().unwrap_or_else(|| primary.clone()) - }; let first = chars.get(seg_start).copied().unwrap_or(' '); - let face = face_for(first); + let face = face_at(seg_start); let level = level_at(seg_start); let mut seg_end = seg_start + 1; while seg_end < *end && chars.get(seg_end).is_some_and(|c| { !is_line_break(*c) && !is_line_break(first) - && (!cluster_start.get(seg_end).copied().unwrap_or(true) || face_for(*c).id == face.id) + && (!cluster_start.get(seg_end).copied().unwrap_or(true) || face_at(seg_end).id == face.id) }) && level_at(seg_end) == level { @@ -442,6 +433,44 @@ fn shape_text(text: &TextData) -> ShapedText { ShapedText { chars, styles, faces, fallbacks, items, bidi, cluster_start } } +/// The face a grapheme cluster `cl` is shaped with. A letter and its marks come from one face that +/// has them all: a mark the letter's face lacks would draw as a missing glyph beside it, and a mark +/// from another face would sit apart from it (#1478). +fn cluster_face(cl: &[char], primary: &Arc, emoji: &[Arc], fallbacks: &[Arc]) -> Arc { + pick_cluster_face(cl, primary, emoji, fallbacks, |f, c| f.has_char(c), |f| f.has_color()).clone() +} + +/// [`cluster_face`]'s rule over any kind of face (`has`: whether a face has a char's glyph, +/// `color`: whether it has colour glyphs), so it can be checked without fonts. +pub(crate) fn pick_cluster_face<'a, F>( + cl: &[char], + primary: &'a F, + emoji: &'a [F], + fallbacks: &'a [F], + has: impl Fn(&F, char) -> bool, + color: impl Fn(&F) -> bool, +) -> &'a F { + let c = cl.first().copied().unwrap_or(' '); + // Emoji take a colour face before a face without colour glyphs (the style's own or a symbol + // font), which would draw them in one colour. + if is_emoji(c) + && !color(primary) + && let Some(f) = emoji.iter().find(|f| has(f, c)) + { + return f; + } + // Joiners and variation selectors need no glyph: shaping hides them. + let covers = |f: &F| { + cl.iter().all(|&m| has(f, m) || matches!(m, '\u{034F}' | '\u{200C}' | '\u{200D}' | '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}')) + }; + if is_line_break(c) || covers(primary) { + return primary; + } + // No face has the whole cluster: the letter's own face, as for a lone letter. + let own = || if has(primary, c) { Some(primary) } else { fallbacks.iter().find(|f| has(f, c)) }; + fallbacks.iter().find(|f| covers(f)).or_else(own).unwrap_or(primary) +} + fn line_break_after(items: &[Item], end: usize) -> bool { end.checked_sub(1).and_then(|i| items.get(i)).is_some_and(|item| is_line_break(item.ch)) } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 2e99b64e..9f5e0cd1 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -166,3 +166,5 @@ mod saved_breaks_tests; mod tail_tests; #[cfg(test)] mod tests; +#[cfg(test)] +mod vietnamese_tests; diff --git a/crates/text/src/vietnamese_tests.rs b/crates/text/src/vietnamese_tests.rs new file mode 100644 index 00000000..661573dc --- /dev/null +++ b/crates/text/src/vietnamese_tests.rs @@ -0,0 +1,49 @@ +//! Vietnamese stacked diacritics (#1478): a letter and its marks are shaped with one face that has +//! them all, whether written precomposed (NFC) or decomposed (NFD). The face rule is checked with +//! stand-in faces, so it runs on any machine whatever fonts it has. + +use crate::layout::pick_cluster_face; + +/// A stand-in face: the chars it has glyphs for, and whether those are colour glyphs. +struct Fake { + chars: &'static str, + color: bool, +} + +const fn fake(chars: &'static str) -> Fake { + Fake { chars, color: false } +} + +fn pick<'a>(cluster: &str, primary: &'a Fake, emoji: &'a [Fake], fallbacks: &'a [Fake]) -> &'a str { + let cl: Vec = cluster.chars().collect(); + pick_cluster_face(&cl, primary, emoji, fallbacks, |f, c| f.chars.contains(c), |f| f.color).chars +} + +#[test] +fn a_cluster_takes_one_face_that_has_all_its_chars() { + let latin = fake("EU"); + let full = [fake("EU\u{0302}\u{0309}\u{031B}\u{0301}")]; + let marks_only = [fake("\u{0302}\u{0309}"), fake("EU\u{0302}\u{0309}")]; + // The style's own face has the whole cluster: it keeps it. + assert_eq!(pick("E\u{0302}\u{0309}", &full[0], &[], &[]), full[0].chars); + // The style's face lacks a mark (Ể typed decomposed in a Latin-1 font): the first fallback with + // the letter and every mark, not one with the marks alone. + assert_eq!(pick("E\u{0302}\u{0309}", &latin, &[], &marks_only), "EU\u{0302}\u{0309}"); + // Joiners and variation selectors need no glyph of their own. + assert_eq!(pick("E\u{200D}\u{FE0F}", &latin, &[], &full), "EU"); + // No face has the whole cluster: the letter's own face, then the first fallback with the letter. + assert_eq!(pick("E\u{031B}\u{0303}", &latin, &[], &marks_only), "EU"); + assert_eq!(pick("Q\u{0302}", &fake("U"), &[], &[fake("\u{0302}"), fake("Q")]), "Q"); + assert_eq!(pick("Z\u{0302}", &latin, &[], &marks_only), "EU"); + // A line break stays with the style's face. + assert_eq!(pick("\n", &latin, &[], &full), "EU"); +} + +#[test] +fn an_emoji_takes_a_colour_face_unless_the_style_has_colour_glyphs() { + let emoji = [Fake { chars: "\u{1F600}\u{1F601}", color: true }]; + let mono = fake("\u{1F600}"); + assert_eq!(pick("\u{1F600}", &mono, &emoji, &[]), emoji[0].chars); + let colour = Fake { chars: "\u{1F600}", color: true }; + assert!(std::ptr::eq(pick_cluster_face(&['\u{1F600}'], &colour, &emoji, &[], |f, c| f.chars.contains(c), |f| f.color), &colour)); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index 86bc1c40..64736092 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **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 @@ -75,6 +75,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-29 | patch | M4, #1478: a letter and its combining marks are shaped with one font that has them all. Text typed decomposed (NFD, `E` + U+0302 + U+0309 for Ể) in a font with the letters but not the marks drew missing-glyph boxes beside and above the letter, while the precomposed form fell back to a font that has it; each grapheme cluster now takes the first face (the style's own, then the fallbacks) that covers every char in it (joiners and variation selectors need no glyph), else the letter's own face as before. Fonts that cover Vietnamese were already right: mark positioning (GPOS `mark`/`mkmk`) stacks the marks over the letter on the canvas, in PNG, SVG and PDF. Tests in `crates/text/src/vietnamese_tests.rs`. Numbers unchanged | | 2026-10-29 | minor | M9, #1415: `comment.list` (CLI, MCP `run_command`, the `figcraft://comments` resource) gives every message, the first and each reply, its `mentions`: `{name, known, you}` for each name it @mentions, by the rule the thread view highlights them with (`figcraft_doc::mentions`, which the highlighting now shares). A name resolves against the file's people and you, as the file spells it. An unknown name is reported with `known: false`. `a@b.com` mentions nobody. Filtering threads by a mentioned name and listing mentioned names in `comment.people` stay open. Numbers unchanged | | 2026-10-29 | minor | M8, #1061: a mask group's pixmaps cover only where its mask or content can paint within the view (a 40 × 40 mask on a 3000 × 2000 view: 96 MB → 61.5 kB, same pixels), and isolated layers share a 2 GiB budget held at once per render thread; a layer past it is left out with a warning. Tests: `mask_bounds_tests` | | 2026-10-29 | minor | M2, #1084: a component can't contain its own instance, directly or through other components (each variant counts as its own component, so a variant may hold a sibling variant's instance unless that one leads back to it). Edits that would close such a loop are refused with it named, a file holding one opens with an instance detached and a note, and a rebuild never expands an instance inside a copy of itself (a loop merged in a live session stops growing). Tests: `component_cycle_tests` | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f5b3b550..49429d31 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -296,7 +296,8 @@ Hours in this narrative are the builders' estimates; the tables above supersede decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no break inside Latin words, kinsoku to JIS X 4051 for closing and opening punctuation, small kana (halfwidth too), the prolonged sound mark, the iteration marks, the (semi-)voiced sound marks - and the halfwidth parentheses); a style's metrics from its own font, not a fallback; carets, Delete and + and the halfwidth parentheses); a style's metrics from its own font, not a fallback; a letter and + its combining marks (Vietnamese typed decomposed, say) from one font that has them all; carets, Delete and Backspace by grapheme cluster; double-click and word moves by script run; IME composition with CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`, `crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and