Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08ca07c69 | ||
|
|
d17c07f343 |
No files matched your search
+1
-1
@@ -127,7 +127,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). Spot colours defined in Lab open with their Lab values from `.ai` and PDF files, instead of as wrong RGB colours or their CMYK equivalents (#1032). Build scripts leave a generated source they wouldn't change untouched, so the next build doesn't compile most of the workspace again where file times lose their sub-second part (#1051). A fill colour recolours the gradient mesh point last clicked with the Mesh tool or Direct Selection, and a Mesh tool click on a mesh's point selects that mesh (#1052). The All Tools drawer stays two tools wide instead of filling a wide window (#1054). Text Wrap's Make, Release and Options refuse a list of object ids with a malformed or unknown id instead of skipping it (#1055). A freeform gradient line with a malformed point index is refused instead of being shortened to the indices that are valid (#1056). The Corners dialog (`ui.corners`) refuses a malformed object id or a listed anchor that isn't a corner instead of editing the selection or the other corners (#1057). New View refuses a centre that isn't exactly [x, y] and a rotation that isn't a number, instead of trimming the one and saving the other as 0° (#1058). The command sweeps try junk scale, spacing and artboard values too, which they had skipped since before those bugs were fixed (#1059). EPS and PostScript `arc` and `arcn` with huge angles no longer hang the import, and an arc whose angles are whole turns apart is empty, as the PostScript reference defines it (#1062). Align and DisLine truncated
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). Spot colours defined in Lab open with their Lab values from `.ai` and PDF files, instead of as wrong RGB colours or their CMYK equivalents (#1032). Build scripts leave a generated source they wouldn't change untouched, so the next build doesn't compile most of the workspace again where file times lose their sub-second part (#1051). A fill colour recolours the gradient mesh point last clicked with the Mesh tool or Direct Selection, and a Mesh tool click on a mesh's point selects that mesh (#1052). The All Tools drawer stays two tools wide instead of filling a wide window (#1054). Text Wrap's Make, Release and Options refuse a list of object ids with a malformed or unknown id instead of skipping it (#1055). A freeform gradient line with a malformed point index is refused instead of being shortened to the indices that are valid (#1056). The Corners dialog (`ui.corners`) refuses a malformed object id or a listed anchor that isn't a corner instead of editing the selection or the other corners (#1057). New View refuses a centre that isn't exactly [x, y] and a rotation that isn't a number, instead of trimming the one and saving the other as 0° (#1058). The command sweeps try junk scale, spacing and artboard values too, which they had skipped since before those bugs were fixed (#1059). EPS and PostScript `arc` and `arcn` with huge angles no longer hang the import, and an arc whose angles are whole turns apart is empty, as the PostScript reference defines it (#1062). Align and DisLine truncated
|
||||
| 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) |
|
||||
| 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day |
|
||||
| 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections |
|
||||
|
||||
+26
-7
@@ -205,32 +205,49 @@ pub fn next_cluster(s: &str, i: usize) -> usize {
|
||||
GraphemeCursor::new(i, s.len(), true).next_boundary(s, 0).ok().flatten().unwrap_or(s.len())
|
||||
}
|
||||
|
||||
/// Part of a word: a letter, digit or mark (Thai vowel and tone marks, combining accents), an
|
||||
/// underscore or an apostrophe.
|
||||
fn is_word_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '\'' || c == '’'
|
||||
use unicode_general_category::{GeneralCategory as G, get_general_category};
|
||||
c.is_alphanumeric() || matches!(c, '_' | '\'' | '’') || matches!(get_general_category(c), G::NonspacingMark | G::SpacingMark | G::EnclosingMark)
|
||||
}
|
||||
|
||||
/// Start of the previous word (Cmd/Alt+Left).
|
||||
/// Is byte `b` of `s` (with `breaks`, its [`crate::shape::unspaced_word_breaks`]) a boundary
|
||||
/// between two words of Thai, Lao, Khmer or Myanmar, which no space separates?
|
||||
fn inner_word_break(s: &str, breaks: &[usize], b: usize) -> bool {
|
||||
use crate::shape::is_unspaced_script;
|
||||
breaks.binary_search(&b).is_ok()
|
||||
&& s.get(b..).and_then(|t| t.chars().next()).is_some_and(is_unspaced_script)
|
||||
&& s.get(..b).and_then(|t| t.chars().next_back()).is_some_and(is_unspaced_script)
|
||||
}
|
||||
|
||||
/// Start of the previous word (Cmd/Alt+Left). Thai (Lao, Khmer, Myanmar) words end where their
|
||||
/// script's word boundaries are, spaces or not.
|
||||
pub fn prev_word(s: &str, i: usize) -> usize {
|
||||
let i = floor_char(s, i);
|
||||
let breaks = crate::shape::unspaced_word_breaks(s);
|
||||
let chars: Vec<(usize, char)> = s[..i].char_indices().collect();
|
||||
let mut k = chars.len();
|
||||
while k > 0 && !is_word_char(chars[k - 1].1) {
|
||||
k -= 1;
|
||||
}
|
||||
while k > 0 && is_word_char(chars[k - 1].1) {
|
||||
let end = k;
|
||||
while k > 0 && is_word_char(chars[k - 1].1) && !(k < end && inner_word_break(s, &breaks, chars[k].0)) {
|
||||
k -= 1;
|
||||
}
|
||||
chars.get(k).map(|c| c.0).unwrap_or(i)
|
||||
}
|
||||
|
||||
/// End of the next word (Cmd/Alt+Right).
|
||||
/// End of the next word (Cmd/Alt+Right): see [`prev_word`].
|
||||
pub fn next_word(s: &str, i: usize) -> usize {
|
||||
let i = floor_char(s, i);
|
||||
let breaks = crate::shape::unspaced_word_breaks(s);
|
||||
let mut it = s[i..].char_indices().peekable();
|
||||
while it.peek().is_some_and(|(_, c)| !is_word_char(*c)) {
|
||||
it.next();
|
||||
}
|
||||
while it.peek().is_some_and(|(_, c)| is_word_char(*c)) {
|
||||
let start = it.peek().map(|(k, _)| i + k);
|
||||
while it.peek().is_some_and(|(k, c)| is_word_char(*c) && !(Some(i + k) != start && inner_word_break(s, &breaks, i + k))) {
|
||||
it.next();
|
||||
}
|
||||
it.peek().map(|(k, _)| i + k).unwrap_or(s.len())
|
||||
@@ -260,16 +277,18 @@ pub fn word_at(s: &str, i: usize) -> Range<usize> {
|
||||
if k == 2 {
|
||||
return at..at + c.len_utf8();
|
||||
}
|
||||
// Thai (Lao, Khmer, Myanmar) words also end where their script's word boundaries are.
|
||||
let breaks = if k == 0 { crate::shape::unspaced_word_breaks(s) } else { vec![] };
|
||||
let mut a = at;
|
||||
for (p, d) in s[..at].char_indices().rev() {
|
||||
if class(d) != k {
|
||||
if class(d) != k || inner_word_break(s, &breaks, a) {
|
||||
break;
|
||||
}
|
||||
a = p;
|
||||
}
|
||||
let mut b = at;
|
||||
for (p, d) in s[at..].char_indices() {
|
||||
if class(d) != k {
|
||||
if class(d) != k || (b > at && inner_word_break(s, &breaks, b)) {
|
||||
break;
|
||||
}
|
||||
b = at + p + d.len_utf8();
|
||||
|
||||
@@ -241,13 +241,12 @@ pub(crate) fn is_unspaced_script(c: char) -> bool {
|
||||
matches!(c.script(), Script::Thai | Script::Lao | Script::Khmer | Script::Myanmar)
|
||||
}
|
||||
|
||||
/// Mark the glyphs of `sg` (shaped from `text`, which starts at byte `start` of the text the
|
||||
/// glyphs index) after which a word of Thai, Lao, Khmer or Myanmar ends: where a line may break in
|
||||
/// text without spaces between words. The words come from ICU4X's line segmenter (its LSTM models
|
||||
/// for those scripts); other text keeps its own breaks.
|
||||
pub(crate) fn mark_word_ends(text: &str, start: usize, sg: &mut [SGlyph]) {
|
||||
// The dictionaries on the desktop (the most accurate, a few megabytes); the far smaller LSTM
|
||||
// models on the web.
|
||||
/// The byte offsets of `text` where a word of Thai, Lao, Khmer or Myanmar starts or ends: where a
|
||||
/// line may break, and where the caret stops word by word, in text without spaces between words.
|
||||
/// They come from ICU4X's line segmenter (its dictionaries on the desktop, its far smaller LSTM
|
||||
/// models on the web); empty when `text` has none of those scripts. Offsets elsewhere (around
|
||||
/// other text) are UAX #14's breaks.
|
||||
pub(crate) fn unspaced_word_breaks(text: &str) -> Vec<usize> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static SEGMENTER: std::sync::LazyLock<icu_segmenter::LineSegmenterBorrowed<'static>> =
|
||||
std::sync::LazyLock::new(|| icu_segmenter::LineSegmenter::new_dictionary(Default::default()));
|
||||
@@ -255,9 +254,19 @@ pub(crate) fn mark_word_ends(text: &str, start: usize, sg: &mut [SGlyph]) {
|
||||
static SEGMENTER: std::sync::LazyLock<icu_segmenter::LineSegmenterBorrowed<'static>> =
|
||||
std::sync::LazyLock::new(|| icu_segmenter::LineSegmenter::new_auto(Default::default()));
|
||||
if !text.chars().any(is_unspaced_script) {
|
||||
return vec![];
|
||||
}
|
||||
SEGMENTER.segment_str(text).collect()
|
||||
}
|
||||
|
||||
/// Mark the glyphs of `sg` (shaped from `text`, which starts at byte `start` of the text the
|
||||
/// glyphs index) after which a word of Thai, Lao, Khmer or Myanmar ends ([`unspaced_word_breaks`]);
|
||||
/// other text keeps its own breaks.
|
||||
pub(crate) fn mark_word_ends(text: &str, start: usize, sg: &mut [SGlyph]) {
|
||||
let breaks: Vec<usize> = unspaced_word_breaks(text).into_iter().map(|b| b + start).collect();
|
||||
if breaks.is_empty() {
|
||||
return;
|
||||
}
|
||||
let breaks: Vec<usize> = SEGMENTER.segment_str(text).map(|b| b + start).collect();
|
||||
for g in sg.iter_mut().filter(|g| g.inline.is_none() && is_unspaced_script(g.ch)) {
|
||||
g.word_end = breaks.binary_search(&(g.byte + g.len)).is_ok();
|
||||
}
|
||||
|
||||
@@ -805,3 +805,22 @@ fn caret_moves_over_whole_clusters() {
|
||||
assert_eq!((edit::prev_cluster(s, 6), edit::next_cluster(s, 6)), (3, 12));
|
||||
assert_eq!((edit::next_cluster("", 0), edit::prev_cluster("ab", 99)), (0, 1));
|
||||
}
|
||||
|
||||
/// Word steps and double-click stop between Thai words, which no space separates: ภาษา|ไทย|ง่าย.
|
||||
#[test]
|
||||
fn thai_words_are_words_to_the_caret() {
|
||||
let s = "ภาษาไทยง่าย ok";
|
||||
let (thai, easy) = ("ภาษา".len(), "ภาษาไทย".len());
|
||||
let end = "ภาษาไทยง่าย".len();
|
||||
assert_eq!(edit::next_word(s, 0), thai);
|
||||
assert_eq!(edit::next_word(s, thai), easy);
|
||||
assert_eq!(edit::next_word(s, easy), end);
|
||||
assert_eq!(edit::prev_word(s, end), easy);
|
||||
assert_eq!(edit::prev_word(s, easy), thai);
|
||||
assert_eq!(edit::prev_word(s, thai), 0);
|
||||
assert_eq!(edit::word_at(s, thai + 3), thai..easy, "double-click inside ไทย");
|
||||
assert_eq!(edit::word_at(s, 0), 0..thai);
|
||||
// Latin keeps its words.
|
||||
assert_eq!(edit::word_at(s, s.len() - 1), end + 1..s.len());
|
||||
assert_eq!(edit::next_word("hello world", 0), 5);
|
||||
}
|
||||
+2
-1
@@ -42,7 +42,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Touch Type tool | missing | listed in the toolbar, not implemented |
|
||||
| Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model |
|
||||
| Snap to Glyph | missing | menu stub |
|
||||
| Thai, Lao, Khmer, Myanmar: shaping (marks, reordering), lines broken between words | done | words found by ICU4X's line segmenter: dictionaries on the desktop, LSTM models on the web |
|
||||
| Thai, Lao, Khmer, Myanmar: shaping (marks, reordering), lines broken between words, word-by-word caret and double-click | done | words found by ICU4X's line segmenter: dictionaries on the desktop, LSTM models on the web |
|
||||
| Middle Eastern: bidirectional layout, Paragraph Direction | done | |
|
||||
| Middle Eastern: Character Direction, digit types, kashidas, Middle Eastern composers, split caret | missing | |
|
||||
| CJK: input methods (marked text, candidate window) | done | |
|
||||
@@ -55,6 +55,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Thai, Lao, Khmer and Myanmar words for the caret (Ctrl/Alt+arrows) and double-click; combining marks count as part of a word; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Font menu: a Thai script filter beside Japanese and Latin; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Thai, Lao, Khmer and Myanmar text breaks its lines between words (ICU4X line segmenter) instead of only at spaces; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | The Type tool's arrows and Delete move by grapheme cluster, never between a Thai consonant and its vowel or tone marks; score unchanged (estimated) |
|
||||
|
||||
Reference in new issue
Block a user