Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4049494d | ||
|
|
29bad213f8 | ||
|
|
7f35522b15 | ||
|
|
3b60d0ceb6 | ||
|
|
947b294f87 | ||
|
|
434b47d9c4 | ||
|
|
a08ca07c69 | ||
|
|
d17c07f343 | ||
|
|
70e99987e9 | ||
|
|
7a85f6050d | ||
|
|
cc2f45b129 | ||
|
|
fe8af48fa7 | ||
|
|
5a855971aa | ||
|
|
36494d0385 | ||
|
|
2cfee9b99b | ||
|
|
90bd91986a | ||
|
|
3fb51869ac | ||
|
|
436d7ecdd3 | ||
|
|
fba3702103 | ||
|
|
7c7a7dcdc9 | ||
|
|
339b01f48b | ||
|
|
6d07135456 |
No files matched your search
@@ -35,7 +35,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- name: Build and test in a FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1
|
||||
@@ -68,12 +68,12 @@ jobs:
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR in the VM).
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR in the VM).
|
||||
# Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- name: Package in a FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc
|
||||
|
||||
@@ -72,12 +72,12 @@ jobs:
|
||||
VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# step). Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -131,12 +131,12 @@ jobs:
|
||||
VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# step). Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
NFPM_VERSION: 2.47.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# step). Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- name: Install build and packaging tools
|
||||
run: |
|
||||
@@ -293,12 +293,12 @@ jobs:
|
||||
VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR in the VM).
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR in the VM).
|
||||
# Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: inputs.version != ''
|
||||
@@ -345,12 +345,12 @@ jobs:
|
||||
TRUNK_VERSION: 0.21.14
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# step). Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -391,12 +391,12 @@ jobs:
|
||||
VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# Japanese, Chinese, Arabic and Thai fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build
|
||||
# step). Bump the commit deliberately; see craftrules standards/fonts.md.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: storytold/craft-fonts
|
||||
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
|
||||
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
|
||||
path: craft-fonts
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
||||
Generated
+53
@@ -1860,11 +1860,32 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
|
||||
dependencies = [
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
@@ -1914,6 +1935,8 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -1921,6 +1944,29 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_segmenter"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"icu_collections",
|
||||
"icu_locale_fallback",
|
||||
"icu_provider",
|
||||
"icu_segmenter_data",
|
||||
"potential_utf",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_segmenter_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -3152,6 +3198,8 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -4133,6 +4181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -4806,6 +4855,7 @@ name = "vectorcraft-text"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"harfrust 0.13.3",
|
||||
"icu_segmenter",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"skrifa 0.47.0",
|
||||
@@ -4813,6 +4863,7 @@ dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-general-category",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-geom",
|
||||
"windows-registry",
|
||||
@@ -6183,6 +6234,7 @@ dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6191,6 +6243,7 @@ version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
+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 |
|
||||
|
||||
@@ -87,6 +87,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
load::open
|
||||
),
|
||||
cmd!(
|
||||
query "file.list",
|
||||
"List Folder",
|
||||
[],
|
||||
None,
|
||||
"{path} the files and folders in folder `path`, by name: what an agent may open there (held to the automation read root when one is in force) → {path, entries: [{name, kind: file|folder|link, size? (files, bytes)}], truncated (more than 5000 entries)} (#1163)",
|
||||
always,
|
||||
file_list
|
||||
),
|
||||
cmd!(
|
||||
query "document.serialize",
|
||||
"Serialize Document",
|
||||
@@ -783,6 +792,48 @@ pub(crate) fn write_new_file(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The most entries `file.list` returns.
|
||||
const LIST_MAX: usize = 5000;
|
||||
|
||||
fn file_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p.get("path").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or_else(|| bad("file.list", "missing `path`"))?;
|
||||
let (entries, truncated) = list_dir(path, LIST_MAX)?;
|
||||
Ok(json!({ "path": absolute_path(path), "entries": entries, "truncated": truncated }))
|
||||
}
|
||||
|
||||
/// The entries of folder `path`, sorted by name: at most `max`, and whether there were more. A
|
||||
/// link isn't followed: its target may lie outside the automation roots.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn list_dir(path: &str, max: usize) -> Result<(Vec<Value>, bool)> {
|
||||
may_read(path)?;
|
||||
let dir = std::fs::read_dir(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let mut entries: Vec<(String, Value)> = vec![];
|
||||
let mut truncated = false;
|
||||
for e in dir.flatten() {
|
||||
if entries.len() == max {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let Ok(t) = e.file_type() else { continue };
|
||||
let entry = if t.is_symlink() {
|
||||
json!({ "name": name, "kind": "link" })
|
||||
} else if t.is_dir() {
|
||||
json!({ "name": name, "kind": "folder" })
|
||||
} else {
|
||||
json!({ "name": name, "kind": "file", "size": e.metadata().map(|m| m.len()).ok() })
|
||||
};
|
||||
entries.push((name, entry));
|
||||
}
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok((entries.into_iter().map(|(_, v)| v).collect(), truncated))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn list_dir(_: &str, _: usize) -> Result<(Vec<Value>, bool)> {
|
||||
Err(EngineError::Other("no file system here".into()))
|
||||
}
|
||||
|
||||
/// A file's size (bytes) and modification time (ms since the Unix epoch, when the file system
|
||||
/// keeps one); `None` when there is no file at `path` (or automation may not read it).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -652,11 +652,19 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
let ch = d.node_mut(cid).and_then(|n| n.children_mut()).ok_or(EngineError::NoNode(cid))?;
|
||||
for mut p in paths {
|
||||
// The paths in front run against the backmost one, so where they overlap it they cut holes
|
||||
// under the non-zero fill rule (#1197).
|
||||
let direction = |n: &Node| n.path_data().and_then(|pd| pd.subpaths.first()).map_or(0.0, |sp| sp.signed_area().signum());
|
||||
let back = paths.first().map_or(0.0, |p| direction(p));
|
||||
for (i, mut p) in paths.into_iter().enumerate() {
|
||||
let pn = Arc::make_mut(&mut p);
|
||||
pn.appearance = Appearance::default();
|
||||
if let NodeKind::Path { live, .. } = &mut pn.kind {
|
||||
let flip = i > 0 && back != 0.0 && direction(pn) == back;
|
||||
if let NodeKind::Path { path, live, .. } = &mut pn.kind {
|
||||
*live = None;
|
||||
if flip {
|
||||
path.reverse();
|
||||
}
|
||||
}
|
||||
ch.push(p);
|
||||
}
|
||||
|
||||
@@ -1670,3 +1670,48 @@ fn path_commands_reject_bad_anchors_before_modifying_geometry() {
|
||||
s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": [{"x": 0, "y": 0}, {"x": 30, "y": 40}], "closed": false}]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 2);
|
||||
}
|
||||
|
||||
/// #1163: `file.list` lists a folder's files and folders by name, held to the automation read root
|
||||
/// when one is in force.
|
||||
#[test]
|
||||
fn file_list_lists_a_folder_within_the_read_root() {
|
||||
let base = std::env::temp_dir().join(format!("vc-file-list-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
for d in ["in/sub", "outside"] {
|
||||
std::fs::create_dir_all(base.join(d)).unwrap();
|
||||
}
|
||||
std::fs::write(base.join("in/b.svg"), "<svg/>").unwrap();
|
||||
std::fs::write(base.join("in/a.pdf"), "1234").unwrap();
|
||||
let path = |d: &str| base.join(d).to_string_lossy().into_owned();
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.list", &json!({"path": path("in")})).unwrap();
|
||||
assert_eq!(
|
||||
r["entries"],
|
||||
json!([{"name": "a.pdf", "kind": "file", "size": 4}, {"name": "b.svg", "kind": "file", "size": 6}, {"name": "sub", "kind": "folder"}])
|
||||
);
|
||||
assert_eq!(r["truncated"], false);
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&base.join("in")), None).unwrap().unwrap();
|
||||
crate::file_access::confine(Some(&roots), || {
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/sub")})).is_ok());
|
||||
assert!(s.execute("file.list", &json!({"path": path("outside")})).is_err(), "outside the read root");
|
||||
});
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/b.svg")})).is_err(), "not a folder");
|
||||
assert!(s.execute("file.list", &json!({})).is_err());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// #1197: Make Compound Path cuts holes where the paths in front overlap the backmost one, also
|
||||
/// when they were drawn running the same way.
|
||||
#[test]
|
||||
fn a_compound_path_has_holes_where_its_paths_overlap() {
|
||||
let mut s = session();
|
||||
let square = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let diamond = NodeId(s.execute("path.create", &json!({"d": "M50 -20 L120 50 L50 120 L-20 50 Z"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.set", &json!({"ids": [square.0, diamond.0]})).unwrap();
|
||||
let c = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let hit =
|
||||
|x: f64, y: f64| vectorcraft_doc::hit::hit_test(&s.doc().unwrap().doc, Point::new(x, y), Default::default()).map(|h| h.top_object(None));
|
||||
assert_eq!(hit(50.0, 50.0), None, "the overlap is a hole");
|
||||
assert_eq!(hit(3.0, 3.0), Some(c), "the square's corner is filled");
|
||||
assert_eq!(hit(110.0, 50.0), Some(c), "the diamond's point outside the square is filled");
|
||||
}
|
||||
@@ -21,6 +21,13 @@ subsetter = { version = "0.2", default-features = false, features = ["variable-f
|
||||
unicode-bidi = "0.3.18"
|
||||
unicode-general-category = "1"
|
||||
unicode-script = "0.5"
|
||||
# Word boundaries in scripts written without spaces between words (Thai, Lao, Khmer, Myanmar), where
|
||||
# lines may break: ICU4X's line segmenter (Unicode-3.0, as icu_properties already in the build),
|
||||
# with its dictionaries on the desktop and its far smaller LSTM models on the web.
|
||||
icu_segmenter = "2.3"
|
||||
# Grapheme clusters (a Thai consonant with its vowel and tone marks, an accented letter, an emoji
|
||||
# sequence) for caret movement; MIT OR Apache-2.0, already in the build.
|
||||
unicode-segmentation = "1.13"
|
||||
# Builds a small variable font for tests (`test_fonts`; no font file is committed). MIT OR
|
||||
# Apache-2.0, already in the build through subsetter.
|
||||
write-fonts = { version = "0.48", optional = true }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Fonts from the optional craft-fonts build input (<https://github.com/storytold/craft-fonts>).
|
||||
//!
|
||||
//! Built with `CRAFT_FONTS_DIR=<craft-fonts checkout>`, [`CRAFT_FONTS`] holds every font in its
|
||||
//! manifest (Japanese UI and document fonts, and Arabic fonts); built without it, it is empty and Japanese
|
||||
//! text falls back to the installed system fonts. Release builds always set it. See `build.rs`
|
||||
//! manifest (Japanese UI and document fonts, Arabic fonts and Noto Sans Thai); built without it,
|
||||
//! it is empty and that text falls back to the installed system fonts. Release builds always set it. See `build.rs`
|
||||
//! and craftrules `standards/fonts.md`.
|
||||
|
||||
/// A font from the optional craft-fonts build input (empty unless built with `CRAFT_FONTS_DIR`).
|
||||
@@ -39,6 +39,10 @@ impl CraftFont {
|
||||
pub fn is_arabic(&self) -> bool {
|
||||
self.scripts.contains(&"Arab")
|
||||
}
|
||||
/// Is the font for Thai text?
|
||||
pub fn is_thai(&self) -> bool {
|
||||
self.scripts.contains(&"Thai")
|
||||
}
|
||||
/// A serif (Mincho) family, for document text.
|
||||
pub fn is_mincho(&self) -> bool {
|
||||
self.family.contains("Mincho")
|
||||
@@ -63,11 +67,13 @@ pub fn japanese_document_fonts() -> Vec<&'static CraftFont> {
|
||||
|
||||
/// The craft-fonts faces the document font database loads: the Japanese ones in
|
||||
/// [`japanese_document_fonts`] order, then the Arabic ones in manifest order (Noto Sans Arabic
|
||||
/// first: the fallback for Arabic text; the other families are picked by name). Empty when built
|
||||
/// without craft-fonts.
|
||||
/// first: the fallback for Arabic text; the other families are picked by name), then the Thai ones
|
||||
/// (Noto Sans Thai: the fallback for Thai text where no Thai font is installed, and on the web).
|
||||
/// Empty when built without craft-fonts.
|
||||
pub fn document_fonts() -> Vec<&'static CraftFont> {
|
||||
let mut v = japanese_document_fonts();
|
||||
v.extend(CRAFT_FONTS.iter().filter(|f| f.is_arabic() && !f.is_japanese()));
|
||||
v.extend(CRAFT_FONTS.iter().filter(|f| f.is_thai() && !f.is_japanese() && !f.is_arabic()));
|
||||
v
|
||||
}
|
||||
|
||||
@@ -104,8 +110,10 @@ mod tests {
|
||||
assert!(name.ends_with(".ttf") || name.ends_with(".otf"), "{}", w.url);
|
||||
assert!(w.integrity.starts_with("sha256-") && w.integrity.len() == "sha256-".len() + 44, "{}", w.integrity);
|
||||
}
|
||||
// The fallbacks the first frame may need: Noto Sans Arabic and Noto Sans Thai, where the
|
||||
// checkout has them.
|
||||
let startup: Vec<_> = WEB_FONTS.iter().filter(|w| w.startup).map(|w| w.family).collect();
|
||||
let noto = CRAFT_FONTS.iter().any(|f| f.family == "Noto Sans Arabic");
|
||||
assert_eq!(startup, if noto { vec!["Noto Sans Arabic"] } else { vec![] });
|
||||
let expected: Vec<_> = ["Noto Sans Arabic", "Noto Sans Thai"].into_iter().filter(|n| CRAFT_FONTS.iter().any(|f| f.family == *n)).collect();
|
||||
assert_eq!(startup, expected);
|
||||
}
|
||||
}
|
||||
+42
-8
@@ -190,32 +190,64 @@ pub fn next_char(s: &str, i: usize) -> usize {
|
||||
s[i..].chars().next().map(|c| i + c.len_utf8()).unwrap_or(s.len())
|
||||
}
|
||||
|
||||
fn is_word_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '\'' || c == '’'
|
||||
/// The start of the grapheme cluster before `i` (Left arrow): a Thai consonant with its vowel and
|
||||
/// tone marks, an accented letter or an emoji sequence is passed over whole.
|
||||
pub fn prev_cluster(s: &str, i: usize) -> usize {
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
let i = floor_char(s, i);
|
||||
GraphemeCursor::new(i, s.len(), true).prev_boundary(s, 0).ok().flatten().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Start of the previous word (Cmd/Alt+Left).
|
||||
/// The end of the grapheme cluster at `i` (Right arrow, Delete): see [`prev_cluster`].
|
||||
pub fn next_cluster(s: &str, i: usize) -> usize {
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
let i = floor_char(s, i);
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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())
|
||||
@@ -245,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();
|
||||
|
||||
@@ -391,6 +391,36 @@ struct SysFallback {
|
||||
misses: std::collections::HashSet<char>,
|
||||
}
|
||||
|
||||
/// Fonts made for Thai, tried first (when installed) for Thai the font in use lacks: Windows
|
||||
/// (Leelawadee UI; Tahoma before Windows 8), macOS (Thonburi, Sukhumvit Set) and Linux (Noto,
|
||||
/// TLWG).
|
||||
pub const THAI_FAMILIES: &[&str] = &[
|
||||
"Leelawadee UI",
|
||||
"Leelawadee",
|
||||
"Tahoma",
|
||||
"Thonburi",
|
||||
"Sukhumvit Set",
|
||||
"Noto Sans Thai",
|
||||
"Noto Sans Thai UI",
|
||||
"Noto Sans Thai Looped",
|
||||
"Waree",
|
||||
"Loma",
|
||||
"Garuda",
|
||||
];
|
||||
|
||||
/// The families tried first for character `c`, in a script whose text only fonts made for it set
|
||||
/// well (Thai, Lao, Khmer, Myanmar); none for other characters.
|
||||
fn script_families(c: char) -> &'static [&'static str] {
|
||||
use unicode_script::{Script, UnicodeScript};
|
||||
match c.script() {
|
||||
Script::Thai => THAI_FAMILIES,
|
||||
Script::Lao => &["Lao UI", "Leelawadee UI", "Lao Sangam MN", "Noto Sans Lao", "Noto Sans Lao UI", "Phetsarath OT"],
|
||||
Script::Khmer => &["Khmer UI", "Khmer Sangam MN", "Noto Sans Khmer", "Noto Sans Khmer UI", "Khmer OS"],
|
||||
Script::Myanmar => &["Myanmar Text", "Myanmar Sangam MN", "Noto Sans Myanmar", "Noto Sans Myanmar UI", "Padauk"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Families tried (when installed) for characters the loaded fonts lack: CJK, symbols, emoji.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const SYSTEM_FALLBACKS: &[&str] = &[
|
||||
@@ -544,10 +574,12 @@ impl FontClass {
|
||||
[FontClass::Serif, FontClass::Sans, FontClass::Rounded, FontClass::Script, FontClass::Monospaced, FontClass::Decorative];
|
||||
}
|
||||
|
||||
/// What a font menu filters on: whether a family sets Japanese, and its kind.
|
||||
/// What a font menu filters on: whether a family sets Japanese or Thai, and its kind.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct FontTraits {
|
||||
pub japanese: bool,
|
||||
/// Its character map has Thai (ก and า).
|
||||
pub thai: bool,
|
||||
pub class: FontClass,
|
||||
}
|
||||
|
||||
@@ -565,6 +597,7 @@ impl FontTraits {
|
||||
};
|
||||
// Hiragana (bit 49) or Katakana (bit 50) in the Unicode ranges, or the JIS code page.
|
||||
let japanese = ranges2 & (0b11 << 17) != 0 || codepages & (1 << 17) != 0 || has_japanese_name;
|
||||
let thai = ['\u{0E01}', '\u{0E32}'].iter().all(|c| font.charmap().map(*c).is_some());
|
||||
let family_type = panose.first().copied().unwrap_or(0);
|
||||
let serif_style = panose.get(1).copied().unwrap_or(0);
|
||||
let proportion = panose.get(3).copied().unwrap_or(0);
|
||||
@@ -662,7 +695,7 @@ impl FontTraits {
|
||||
} else {
|
||||
FontClass::Other
|
||||
};
|
||||
FontTraits { japanese, class }
|
||||
FontTraits { japanese, thai, class }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1917,6 +1950,18 @@ impl FontDb {
|
||||
/// First face (fallback family first, then load order) that covers `c`; on native, system
|
||||
/// fonts are loaded lazily the first time no loaded face covers a character.
|
||||
pub(crate) fn fallback_for(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
||||
// Thai (Lao, Khmer, Myanmar) takes a font made for it, when one is installed, rather than
|
||||
// whichever face covering it happened to load first (a display face, say).
|
||||
let prefer = script_families(c);
|
||||
if let Some(f) = self.family_fallback(c, exclude, prefer) {
|
||||
return Some(f);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.load_families(c, prefer)
|
||||
&& let Some(f) = self.family_fallback(c, exclude, prefer)
|
||||
{
|
||||
return Some(f);
|
||||
}
|
||||
if let Some(f) = self.loaded_fallback(c, exclude) {
|
||||
return Some(f);
|
||||
}
|
||||
@@ -1934,6 +1979,28 @@ impl FontDb {
|
||||
self.fallback_for(c, 0)
|
||||
}
|
||||
|
||||
/// The loaded face of the first of `families` that covers `c` (upright, nearest Regular).
|
||||
fn family_fallback(&self, c: char, exclude: u32, families: &[&str]) -> Option<Arc<FontFace>> {
|
||||
let faces = self.read_faces();
|
||||
families.iter().find_map(|fam| {
|
||||
faces
|
||||
.iter()
|
||||
.filter(|f| f.id != exclude && f.family.eq_ignore_ascii_case(fam) && f.covers(c))
|
||||
.min_by_key(|f| (f.italic, (f.weight - 400.0).abs() as i32))
|
||||
.cloned()
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the first of the installed `families` not loaded yet that covers `c` (with system
|
||||
/// fallback on). Returns whether one was loaded.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn load_families(&self, c: char, families: &[&str]) -> bool {
|
||||
if families.is_empty() || !self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled {
|
||||
return false;
|
||||
}
|
||||
families.iter().any(|fam| !self.is_loaded(fam) && self.load_cataloged(fam) && self.family_fallback(c, 0, &[fam]).is_some())
|
||||
}
|
||||
|
||||
fn loaded_fallback(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
||||
let faces = self.read_faces();
|
||||
let mut order: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.id != exclude).collect();
|
||||
|
||||
@@ -13,7 +13,8 @@ use crate::composer::{Breakpoint, Params, compose};
|
||||
use crate::fontdb::FontDb;
|
||||
use crate::hyphen::hyphen_points;
|
||||
use crate::shape::{
|
||||
Punct, SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, no_line_end, no_line_start, punct, shape_range, soft_line_start, style_metrics,
|
||||
Punct, SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, mark_word_ends, no_line_end, no_line_start, punct, shape_range, soft_line_start,
|
||||
style_metrics,
|
||||
};
|
||||
use crate::{Composer, FirstBaseline, InlineGlyph, LayoutOptions, LineInfo, OtFeatures, PositionedGlyph, TextLayout, VerticalAlign};
|
||||
|
||||
@@ -1184,6 +1185,7 @@ mod wrapping_tests {
|
||||
ch,
|
||||
tcy: None,
|
||||
inline: None,
|
||||
word_end: false,
|
||||
lead: 0.0,
|
||||
proportional: false,
|
||||
vpal: None,
|
||||
@@ -1442,6 +1444,8 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
||||
let bidi = para_bidi(text.get(pr.clone()).unwrap_or_default(), para.direction);
|
||||
let rtl = is_rtl(bidi.as_ref());
|
||||
let mut sg = cx.shape_para(pr.clone(), bidi.as_ref());
|
||||
// Thai, Lao, Khmer and Myanmar break lines between words, not at spaces.
|
||||
mark_word_ends(text.get(pr.clone()).unwrap_or_default(), pr.start, &mut sg);
|
||||
// Japanese composition: consecutive punctuation shares one half-em space.
|
||||
let compressed = if para.mojikumi == Mojikumi::LineEndHalf { compress_punctuation(&mut sg) } else { vec![] };
|
||||
// …and a quarter em between Japanese and Latin text.
|
||||
|
||||
@@ -59,6 +59,9 @@ pub(crate) struct SGlyph {
|
||||
pub level: Level,
|
||||
/// An inline graphic ([`vectorcraft_doc::TextRun::inline`]) standing in for a glyph.
|
||||
pub inline: Option<InlineBox>,
|
||||
/// A word of a script written without spaces (Thai, Lao, Khmer, Myanmar) ends after the
|
||||
/// glyph's cluster, so a line may break there ([`mark_word_ends`]).
|
||||
pub word_end: bool,
|
||||
}
|
||||
|
||||
/// How `vpal` moves an upright glyph in vertical type and changes its length down the column, in
|
||||
@@ -102,7 +105,7 @@ impl SGlyph {
|
||||
}
|
||||
/// A line may break after this glyph.
|
||||
pub fn break_after(&self) -> bool {
|
||||
self.is_space() || matches!(self.ch, '-' | '\u{2010}' | '\u{2013}' | '\u{2014}' | '/' | SOFT_HYPHEN) || is_cjk(self.ch)
|
||||
self.is_space() || self.word_end || matches!(self.ch, '-' | '\u{2010}' | '\u{2013}' | '\u{2014}' | '/' | SOFT_HYPHEN) || is_cjk(self.ch)
|
||||
}
|
||||
/// A soft (discretionary) hyphen: invisible unless a line breaks after it.
|
||||
pub fn is_soft_hyphen(&self) -> bool {
|
||||
@@ -232,6 +235,43 @@ pub(crate) fn punct(c: char) -> Option<Punct> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `c` of a script written without spaces between words, whose line breaks need a model of its
|
||||
/// words: Thai, Lao, Khmer or Myanmar (UAX #14's class SA)?
|
||||
pub(crate) fn is_unspaced_script(c: char) -> bool {
|
||||
matches!(c.script(), Script::Thai | Script::Lao | Script::Khmer | Script::Myanmar)
|
||||
}
|
||||
|
||||
/// 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()));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_cjk(c: char) -> bool {
|
||||
matches!(c as u32, 0x2E80..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF | 0x20000..=0x2FFFF)
|
||||
}
|
||||
@@ -446,6 +486,7 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
|
||||
// RTL text like any other glyph once the line is reordered.
|
||||
level,
|
||||
inline: Some(InlineBox { art: drawn, xf }),
|
||||
word_end: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,6 +655,7 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
vpal: vpal.get(gi).copied().flatten(),
|
||||
level: *level,
|
||||
inline: None,
|
||||
word_end: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -728,3 +728,58 @@ fn burasagari_leaves_latin_commas_and_full_stops_inside_the_line() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// With craft-fonts and no installed fonts (the web), Thai is set in Noto Sans Thai, not as empty
|
||||
/// boxes. Skipped when built without craft-fonts or with a checkout that has no Thai font.
|
||||
#[test]
|
||||
fn thai_without_installed_fonts_takes_the_craft_fonts_thai_font() {
|
||||
if !crate::CRAFT_FONTS.iter().any(|f| f.is_thai()) {
|
||||
eprintln!("skipping: no Thai font in craft-fonts (set CRAFT_FONTS_DIR to a checkout that has one)");
|
||||
return;
|
||||
}
|
||||
let l = layout(db(), &point("ภาษาไทย", style(20.0)));
|
||||
assert!(!l.glyphs.is_empty() && l.glyphs.iter().all(|g| g.gid != 0), "no .notdef");
|
||||
let families: Vec<String> = l.glyphs.iter().filter_map(|g| db().face_by_id(g.font_id)).map(|f| f.family.clone()).collect();
|
||||
assert!(families.iter().all(|f| f == "Noto Sans Thai"), "{families:?}");
|
||||
}
|
||||
|
||||
/// Thai is written without spaces between words: area type breaks its lines between words (as a
|
||||
/// model of Thai words finds them), never inside a word nor before a vowel or tone mark.
|
||||
#[test]
|
||||
fn thai_lines_break_between_words() {
|
||||
let text = "สวัสดีครับ ภาษาไทยง่ายนิดเดียว";
|
||||
// Narrower than the shortest words: each line holds one word.
|
||||
let l = layout(db(), &area(text, style(12.0), Rect::new(0.0, 0.0, 70.0, 2000.0), Justify::Left));
|
||||
let lines: Vec<&str> = l.lines.iter().map(|ln| &text[ln.start..ln.end]).collect();
|
||||
assert!(lines.len() >= 3, "{lines:?}");
|
||||
// Its words: สวัสดี ครับ, ภาษา ไทย ง่าย นิดเดียว ("hello", "the Thai language is very easy").
|
||||
let words = ["สวัสดี", "ครับ ", "ภาษา", "ไทย", "ง่าย", "นิดเดียว"];
|
||||
let ends: Vec<usize> = words
|
||||
.iter()
|
||||
.scan(0, |at, w| {
|
||||
*at += w.len();
|
||||
Some(*at)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(ends.last(), Some(&text.len()));
|
||||
for ln in &l.lines {
|
||||
assert!(ends.contains(&ln.end), "a line ends inside a word: {lines:?}");
|
||||
}
|
||||
for w in l.lines.windows(2) {
|
||||
assert_eq!(w[0].end, w[1].start, "lines tile the text");
|
||||
}
|
||||
let mark = |c: char| matches!(c, '\u{0E31}' | '\u{0E34}'..='\u{0E3A}' | '\u{0E47}'..='\u{0E4E}');
|
||||
assert!(lines.iter().all(|s| s.chars().next().is_some_and(|c| !mark(c))), "a line starts with a mark: {lines:?}");
|
||||
}
|
||||
|
||||
/// The desktop's dictionaries keep loanwords whole that a smaller model splits: โปรแกรม
|
||||
/// ("program").
|
||||
#[test]
|
||||
fn thai_loanwords_stay_whole() {
|
||||
let text = "ยินดีต้อนรับสู่โปรแกรมวาดภาพเวกเตอร์";
|
||||
let l = layout(db(), &area(text, style(12.0), Rect::new(0.0, 0.0, 70.0, 2000.0), Justify::Left));
|
||||
let lines: Vec<&str> = l.lines.iter().map(|ln| &text[ln.start..ln.end]).collect();
|
||||
// No line ends inside the word, whatever the Thai font's widths put beside it.
|
||||
let word = text.find("โปรแกรม").unwrap();
|
||||
assert!(l.lines.iter().all(|ln| ln.end <= word || ln.end >= word + "โปรแกรม".len()), "{lines:?}");
|
||||
}
|
||||
@@ -569,3 +569,23 @@ fn family_display_name_follows_show_font_names_in_english() {
|
||||
let face = db.face("ヒラギノ角ゴシック", "Regular").unwrap();
|
||||
assert_eq!(face.family, "Hiragino Sans");
|
||||
}
|
||||
|
||||
/// Thai typed in a font without Thai takes an installed font made for Thai (Leelawadee UI,
|
||||
/// Thonburi, Noto Sans Thai…), whatever other face covering Thai loaded first; Latin beside it keeps
|
||||
/// the font chosen. Skipped where none of those families is installed.
|
||||
#[test]
|
||||
fn thai_falls_back_to_a_font_made_for_thai() {
|
||||
let db = FontDb::global();
|
||||
let Some(thai) = crate::fontdb::THAI_FAMILIES.iter().find(|f| db.has_family(f)) else {
|
||||
eprintln!("skipped: no Thai font installed");
|
||||
return;
|
||||
};
|
||||
let st = vectorcraft_doc::CharStyle { size: 20.0, ..Default::default() };
|
||||
let l = layout(db, &TextObject::point(kurbo::Point::ZERO, "Thai ภาษาไทย", st));
|
||||
let family = |g: &PositionedGlyph| db.face_by_id(g.font_id).map(|f| f.family.clone()).unwrap_or_default();
|
||||
let (latin, thai_glyphs) = l.glyphs.split_at(5);
|
||||
assert!(latin.iter().all(|g| family(g) == FALLBACK_FAMILY), "{:?}", latin.iter().map(family).collect::<Vec<_>>());
|
||||
let first = crate::fontdb::THAI_FAMILIES.iter().find(|f| thai_glyphs.iter().all(|g| family(g).eq_ignore_ascii_case(f)));
|
||||
assert!(first.is_some(), "Thai drawn in {:?}; {thai} is installed", thai_glyphs.iter().map(family).collect::<Vec<_>>());
|
||||
assert!(thai_glyphs.iter().all(|g| g.gid != 0), "no .notdef");
|
||||
}
|
||||
@@ -791,3 +791,36 @@ fn proportional_metrics_set_full_width_glyphs_on_their_proportional_widths() {
|
||||
let moved = x0(&half)[1] - x0(&none)[1];
|
||||
assert!((moved - 10.0).abs() < 0.01, "〙's empty half goes past the line's end: あ moved {moved}");
|
||||
}
|
||||
|
||||
/// The arrows pass a Thai consonant with its vowel and tone marks whole (น้ำ: no, mai tho, sara am),
|
||||
/// as an accented letter written with a combining mark; plain text moves a character at a time.
|
||||
#[test]
|
||||
fn caret_moves_over_whole_clusters() {
|
||||
let s = "กน\u{0E49}\u{0E33}e\u{0301}x";
|
||||
let stops: Vec<usize> = std::iter::successors(Some(0), |&i| (i < s.len()).then(|| edit::next_cluster(s, i))).collect();
|
||||
assert_eq!(stops, [0, 3, 12, 15, 16]);
|
||||
let back: Vec<usize> = std::iter::successors(Some(s.len()), |&i| (i > 0).then(|| edit::prev_cluster(s, i))).collect();
|
||||
assert_eq!(back, [16, 15, 12, 3, 0]);
|
||||
// Inside a cluster (a caret left there by a click), the arrows go to its edges.
|
||||
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);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
# background = fetched after the app starts (fonts users pick by name)
|
||||
# Read by build.rs (the WEB_FONTS table) and `cargo xtask web-fonts` (copies the files, a Trunk hook).
|
||||
startup | Noto Sans Arabic | Regular
|
||||
startup | Noto Sans Thai | Regular
|
||||
background | Cairo | Regular
|
||||
background | Tajawal | ExtraLight
|
||||
background | Tajawal | Light
|
||||
|
||||
@@ -530,8 +530,10 @@ impl Tool for TypeTool {
|
||||
self.anchor = if word { edit::prev_word(&text, self.caret) } else { edit::prev_char(&text, self.caret) };
|
||||
self.replace(cx, "", None)
|
||||
}
|
||||
// Backspace takes back one character (a mistyped Thai tone mark, say); Delete, the
|
||||
// Left and Right arrows a whole cluster.
|
||||
ToolKey::Delete if self.caret < len => {
|
||||
self.anchor = if word { edit::next_word(&text, self.caret) } else { edit::next_char(&text, self.caret) };
|
||||
self.anchor = if word { edit::next_word(&text, self.caret) } else { edit::next_cluster(&text, self.caret) };
|
||||
self.replace(cx, "", None)
|
||||
}
|
||||
ToolKey::Left => {
|
||||
@@ -549,7 +551,7 @@ impl Tool for TypeTool {
|
||||
if lay.glyphs.iter().any(|g| g.rtl) {
|
||||
vectorcraft_text::caret_horizontal(&lay, self.caret, false)
|
||||
} else {
|
||||
edit::prev_char(&text, self.caret)
|
||||
edit::prev_cluster(&text, self.caret)
|
||||
}
|
||||
};
|
||||
self.move_to(to, mods.shift)
|
||||
@@ -569,7 +571,7 @@ impl Tool for TypeTool {
|
||||
if lay.glyphs.iter().any(|g| g.rtl) {
|
||||
vectorcraft_text::caret_horizontal(&lay, self.caret, true)
|
||||
} else {
|
||||
edit::next_char(&text, self.caret)
|
||||
edit::next_cluster(&text, self.caret)
|
||||
}
|
||||
};
|
||||
self.move_to(to, mods.shift)
|
||||
|
||||
@@ -84,17 +84,29 @@ const SAMPLES_PER_FRAME: usize = 4;
|
||||
const MAX_PENDING: usize = 8;
|
||||
/// What a sample shows when the font lacks the selected text's characters (or there is none).
|
||||
const FALLBACK_SAMPLE: &str = "Sample";
|
||||
/// The script filter's choices: all, Japanese, Latin.
|
||||
const SCRIPTS: usize = 3;
|
||||
/// The script filter's choices: all, Japanese, Latin, Thai.
|
||||
const SCRIPTS: usize = 4;
|
||||
|
||||
fn script_label(i: usize) -> &'static str {
|
||||
match i {
|
||||
1 => tl!("Japanese"),
|
||||
2 => tl!("Latin"),
|
||||
3 => tl!("Thai"),
|
||||
_ => tl!("All Scripts"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does a family of `traits` pass script filter `script` ([`SCRIPTS`])? Latin: neither Japanese
|
||||
/// nor Thai.
|
||||
fn script_matches(script: usize, traits: vectorcraft_text::FontTraits) -> bool {
|
||||
match script {
|
||||
1 => traits.japanese,
|
||||
2 => !traits.japanese && !traits.thai,
|
||||
3 => traits.thai,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_label(c: FontClass) -> &'static str {
|
||||
match c {
|
||||
FontClass::Serif => tl!("Serif / Mincho"),
|
||||
@@ -283,7 +295,7 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp
|
||||
return true;
|
||||
}
|
||||
let traits = db.family_traits(f);
|
||||
(st.script == 0 || (st.script == 1) == traits.japanese) && st.class.is_none_or(|c| c == traits.class)
|
||||
script_matches(st.script, traits) && st.class.is_none_or(|c| c == traits.class)
|
||||
};
|
||||
let mut rows: Vec<Row> = vec![];
|
||||
for f in db.menu_family_list().iter().filter(|f| wanted(f)) {
|
||||
@@ -748,6 +760,21 @@ pub(crate) fn sample_text(app: &VectorcraftApp) -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
/// The script filter: Thai lists families with Thai; Latin, those with neither Thai nor
|
||||
/// Japanese.
|
||||
#[test]
|
||||
fn the_thai_filter_lists_fonts_with_thai() {
|
||||
use vectorcraft_text::FontTraits;
|
||||
let (thai, japanese, latin) =
|
||||
(FontTraits { thai: true, ..Default::default() }, FontTraits { japanese: true, ..Default::default() }, FontTraits::default());
|
||||
let passing = |t: FontTraits| (0..super::SCRIPTS).filter(|s| super::script_matches(*s, t)).collect::<Vec<_>>();
|
||||
assert_eq!(passing(thai), [0, 3]);
|
||||
assert_eq!(passing(japanese), [0, 1]);
|
||||
assert_eq!(passing(latin), [0, 2]);
|
||||
assert_eq!(super::script_label(3), "Thai");
|
||||
}
|
||||
|
||||
use super::{Row, remap_highlight};
|
||||
|
||||
fn family(name: &str) -> Row {
|
||||
|
||||
@@ -1323,6 +1323,7 @@
|
||||
Last Effect… Letzter Effekt…
|
||||
Last Page Letzte Seite
|
||||
Latin Lateinisch
|
||||
Thai Thai
|
||||
Latin Extended Lateinisch, erweitert
|
||||
Latin-1 Supplement Lateinisch-1, Ergänzung
|
||||
Layer Ebene
|
||||
@@ -2101,6 +2102,7 @@
|
||||
Redefine Symbol Symbol neu definieren
|
||||
Redefine {kind} Style {kind}format neu definieren
|
||||
Redo Wiederholen
|
||||
List Folder Ordner auflisten
|
||||
Begin Undo Group Rückgängig-Gruppe beginnen
|
||||
End Undo Group Rückgängig-Gruppe beenden
|
||||
Redo {name} Wiederholen: {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1341,6 +1341,7 @@
|
||||
Last Effect… Último efecto…
|
||||
Last Page Última página
|
||||
Latin Latín
|
||||
Thai Tailandés
|
||||
Latin Extended Latín extendido
|
||||
Latin-1 Supplement Suplemento Latín-1
|
||||
Layer Capa
|
||||
@@ -2152,6 +2153,7 @@
|
||||
Redefine Symbol Redefinir símbolo
|
||||
Redefine {kind} Style Redefinir estilo de {kind}
|
||||
Redo Rehacer
|
||||
List Folder Listar carpeta
|
||||
Begin Undo Group Iniciar grupo de deshacer
|
||||
End Undo Group Finalizar grupo de deshacer
|
||||
Redo {name} Rehacer {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1340,6 +1340,7 @@
|
||||
Last Effect… Dernier effet…
|
||||
Last Page Dernière page
|
||||
Latin Latin
|
||||
Thai Thaï
|
||||
Latin Extended Latin étendu
|
||||
Latin-1 Supplement Supplément Latin-1
|
||||
Layer Calque
|
||||
@@ -2149,6 +2150,7 @@
|
||||
Redefine Symbol Redéfinir le symbole
|
||||
Redefine {kind} Style Redéfinir le style de {kind}
|
||||
Redo Rétablir
|
||||
List Folder Lister le dossier
|
||||
Begin Undo Group Commencer un groupe d’annulation
|
||||
End Undo Group Terminer le groupe d’annulation
|
||||
Redo {name} Rétablir {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1341,6 +1341,7 @@
|
||||
Last Effect… Ultimo effetto…
|
||||
Last Page Ultima pagina
|
||||
Latin Latino
|
||||
Thai Thailandese
|
||||
Latin Extended Latino esteso
|
||||
Latin-1 Supplement Supplemento Latino-1
|
||||
Layer Livello
|
||||
@@ -2152,6 +2153,7 @@
|
||||
Redefine Symbol Ridefinisci simbolo
|
||||
Redefine {kind} Style Ridefinisci stile {kind}
|
||||
Redo Ripeti
|
||||
List Folder Elenca cartella
|
||||
Begin Undo Group Inizia gruppo di annullamento
|
||||
End Undo Group Termina gruppo di annullamento
|
||||
Redo {name} Ripeti {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -48,6 +48,7 @@
|
||||
Hard 強い禁則
|
||||
Soft 弱い禁則
|
||||
Latin 欧文
|
||||
Thai タイ文字
|
||||
Monospaced 等幅
|
||||
Roman Baseline 欧文ベースライン
|
||||
Rounded 丸ゴシック
|
||||
@@ -101,6 +102,7 @@
|
||||
Edit 編集
|
||||
Undo 取り消し
|
||||
Redo やり直し
|
||||
List Folder フォルダーの内容を一覧表示
|
||||
Begin Undo Group 取り消しグループを開始
|
||||
End Undo Group 取り消しグループを終了
|
||||
Cut 切り取り
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1339,6 +1339,7 @@
|
||||
Last Effect… Последний эффект…
|
||||
Last Page Последняя страница
|
||||
Latin Латиница
|
||||
Thai Тайская
|
||||
Latin Extended Расширенная латиница
|
||||
Latin-1 Supplement Дополнение Latin-1
|
||||
Layer Слой
|
||||
@@ -2148,6 +2149,7 @@
|
||||
Redefine Symbol Переопределить символ
|
||||
Redefine {kind} Style Переопределить стиль {kind}
|
||||
Redo Повторить
|
||||
List Folder Показать содержимое папки
|
||||
Begin Undo Group Начать группу отмены
|
||||
End Undo Group Завершить группу отмены
|
||||
Redo {name} Повторить {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1335,6 +1335,7 @@
|
||||
Last Effect… Останній ефект…
|
||||
Last Page Остання сторінка
|
||||
Latin Латиниця
|
||||
Thai Тайська
|
||||
Latin Extended Розширена латиниця
|
||||
Latin-1 Supplement Додаток Latin-1
|
||||
Layer Шар
|
||||
@@ -2144,6 +2145,7 @@
|
||||
Redefine Symbol Перевизначити символ
|
||||
Redefine {kind} Style Перевизначити стиль {kind}
|
||||
Redo Повторити
|
||||
List Folder Показати вміст теки
|
||||
Begin Undo Group Почати групу скасування
|
||||
End Undo Group Завершити групу скасування
|
||||
Redo {name} Повторити {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 659 and column 40.
|
@@ -2077,6 +2077,7 @@
|
||||
Redefine Symbol 重新定义符号
|
||||
Redefine {kind} Style 重新定义{kind}样式
|
||||
Redo 重做
|
||||
List Folder 列出文件夹
|
||||
Begin Undo Group 开始还原组
|
||||
End Undo Group 结束还原组
|
||||
Redo {name} 重做 {name}
|
||||
@@ -3416,6 +3417,7 @@
|
||||
Graphics: {adapter} 显卡:{adapter}
|
||||
Japanese 日文
|
||||
Latin 拉丁文
|
||||
Thai 泰文
|
||||
Layer Options 图层选项
|
||||
Layers Panel Options 图层面板选项
|
||||
Monospaced 等宽
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1316,6 +1316,7 @@
|
||||
Last Effect… 最後一次效果…
|
||||
Last Page 最後一頁
|
||||
Latin 拉丁文
|
||||
Thai 泰文
|
||||
Latin Extended 拉丁文擴充
|
||||
Latin-1 Supplement 拉丁文-1 補充
|
||||
Layer 圖層
|
||||
@@ -2130,6 +2131,7 @@
|
||||
Redefine Symbol 重新定義符號
|
||||
Redefine {kind} Style 重新定義{kind}樣式
|
||||
Redo 重做
|
||||
List Folder 列出資料夾
|
||||
Begin Undo Group 開始還原群組
|
||||
End Undo Group 結束還原群組
|
||||
Redo {name} 重做 {name}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -614,6 +614,7 @@ fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64) -> std::ti
|
||||
/// for) or until `done` → whether it was.
|
||||
fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64, done: &dyn Fn(&VectorcraftApp) -> bool) -> bool {
|
||||
let end = *time + secs;
|
||||
let real_end = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
loop {
|
||||
let wait = frame_at(app, ctx, *time);
|
||||
if done(app) {
|
||||
@@ -624,9 +625,11 @@ fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64
|
||||
*time = end;
|
||||
return false;
|
||||
}
|
||||
if app.session.link_scan.is_some() {
|
||||
// The look runs on a worker thread, in real time.
|
||||
// The look runs on a worker thread, in real time: the clock waits for it, else a busy
|
||||
// machine's slow look outlasts the simulated seconds and the test fails at random.
|
||||
if app.session.link_scan.is_some() && std::time::Instant::now() < real_end {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
continue;
|
||||
}
|
||||
*time = next;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,8 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese-
|
||||
embed everywhere, was 8.7 MB, so the web build is still smaller than before.
|
||||
- Document text: the Japanese faces join the font database after the bundled fonts (Mincho first),
|
||||
so they are the fallback for Japanese after the requested font; installed system fonts come after.
|
||||
Noto Sans Thai (when the checkout has it) is the fallback for Thai where no Thai font is
|
||||
installed, and the web build fetches it before the app starts (`crates/text/web-fonts.txt`).
|
||||
- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first), after
|
||||
the UI fonts and before the installed fonts `ui_fonts.rs` discovers for other scripts.
|
||||
- Tests that need Japanese glyphs skip with a message when built without craft-fonts. The FreeBSD
|
||||
|
||||
@@ -69,6 +69,7 @@ the suite) keep the agent to the folders you give it:
|
||||
- Every file a command writes must lie inside the **write root**: `save_file`, `export`, the save and
|
||||
export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`,
|
||||
library and preset saves to a path.
|
||||
- `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read root like any read, so an agent can see what it was given (#1163).
|
||||
- Read and write access are separate. Give the same folder twice for both; a root left out grants none
|
||||
of its access (with only `--automation-read-root`, nothing can be written to disk, though `export` and
|
||||
the save commands without a path still return the bytes). Without either flag nothing changes.
|
||||
|
||||
+10
-1
@@ -1,6 +1,6 @@
|
||||
# Type parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Proportional Metrics in vertical type, `vpal`, #966; document-point character controls after object/group scaling, #1034) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
|
||||
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
|
||||
@@ -26,7 +26,9 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
|
||||
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
|
||||
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference unused (#394) |
|
||||
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339; script filter: Japanese, Latin, Thai |
|
||||
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339 |
|
||||
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
|
||||
| Variable fonts | partial | named instances as styles; axis sliders missing |
|
||||
| Colour fonts (OpenType SVG / COLR) | partial | |
|
||||
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
|
||||
@@ -36,9 +38,11 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` |
|
||||
| Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary |
|
||||
| Smart Punctuation, Change Case, Find & Replace | done | |
|
||||
| Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | |
|
||||
| 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, 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 | |
|
||||
@@ -51,6 +55,11 @@ 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) |
|
||||
| 2026-10-11 | minor | Thai (Lao, Khmer, Myanmar) missing from the font in use falls back to an installed font made for the script instead of whichever face covering it loaded first; score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Proportional Metrics done: `palt` in horizontal type, `vpal` in vertical type (#966, #1082); ruby and manual tate-chu-yoko still missing |
|
||||
| 2026-10-10 | minor | #1034: transformed character controls and regression coverage; preserving group font sizes remains a separate enhancement. Scores/hours unchanged at this rounding. |
|
||||
| 2026-10-10 | major | First checklist, split out of the parity table's Type core and Type advanced rows |
|
||||
Reference in new issue
Block a user