mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #370 from imachus/pr/ja-wakan
Type: mojikumi — a quarter em between Japanese and Latin text # Conflicts: # ROADMAP.md
This commit is contained in:
commit
e6da1ca3c3
3 files changed
+93
-5
No files matched your search
+2
-2
@@ -125,7 +125,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected.
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline. Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done.
|
||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional Chinese and Japanese cover the whole interface, Czech every menu label (tested); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese, and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph › Mojikumi: Line-end Punctuation Half Width (new type's default) shares one half-em space between consecutive punctuation and sets a closing mark ending a line half width; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional Chinese and Japanese cover the whole interface, Czech every menu label (tested); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech ones from the bundled UI fonts. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese, and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph › Mojikumi: Line-end Punctuation Half Width (new type's default) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width, and puts a quarter em between Japanese and Latin letters or digits; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder, Live Paint (every path an edge, open lines included), Image Trace (12 presets), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror).
|
||||
- **Paint and appearance (M3):**
|
||||
@@ -196,7 +196,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 20% | ~55 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video) and the Effect Gallery; Document Raster Effects Settings and raster effects in PDF are done | 28–42 |
|
||||
| 3D and Materials | 4 | 0% | Extrude & Bevel, Revolve, Inflate, Rotate, lighting, materials (software renderer) | 50–80 |
|
||||
| Type core | 9 | 78% | composer/hyphenation options, Optical Margin Alignment, hidden characters, Transform Again after a type-area resize (it scales the type), partial selection of type-area anchors by marquee | 15–20 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — Japanese–Latin spacing, hanging punctuation, kinsoku sets and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support | 20–28 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — hanging punctuation, line-start brackets, kinsoku sets and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support | 20–28 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | symbol libraries (original), dynamic symbols, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done) | 5–8 |
|
||||
| Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish (Vector Halftone is done) | 6–10 |
|
||||
| Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers, drag-scrolling a long panel while dragging rows; artboard presets/rearrange polish (Document Setup, New Document and the rest of the Layers panel are done) | 4–7 |
|
||||
|
||||
@@ -264,6 +264,53 @@ fn compress_punctuation(sg: &mut [SGlyph]) -> Vec<bool> {
|
||||
lost_after
|
||||
}
|
||||
|
||||
/// The space between Japanese and Latin letters or digits (JLREQ 3.2.2: a quarter em of the
|
||||
/// Japanese characters' size), with Line-end Punctuation Half Width.
|
||||
const WAKAN_AKI: f64 = 0.25;
|
||||
|
||||
/// A Japanese character for the space next to Latin text: kana and kanji (and full-width
|
||||
/// letters), not punctuation or symbols.
|
||||
fn is_japanese_letter(c: char) -> bool {
|
||||
is_cjk(c)
|
||||
&& !matches!(c as u32, 0x3000..=0x303F | 0xFF01..=0xFF0F | 0xFF1A..=0xFF20 | 0xFF3B..=0xFF40 | 0xFF5B..=0xFF65)
|
||||
&& c != '・'
|
||||
&& punct(c).is_none()
|
||||
}
|
||||
|
||||
/// A Latin letter or digit (or another script's letter), set proportionally.
|
||||
fn is_latin_letter(c: char) -> bool {
|
||||
c.is_alphanumeric() && !is_cjk(c)
|
||||
}
|
||||
|
||||
/// Mojikumi (JLREQ 3.2.2): a quarter em between a Japanese character and a Latin letter or digit,
|
||||
/// either way round, added after the first of the two. Returns what each glyph got after it (taken
|
||||
/// off again when the line ends there).
|
||||
fn space_japanese_and_latin(sg: &mut [SGlyph]) -> Vec<f64> {
|
||||
let mut added = vec![0.0; sg.len()];
|
||||
for j in 1..sg.len() {
|
||||
let (before, after) = sg.split_at_mut(j);
|
||||
let (Some(a), Some(b)) = (before.last_mut(), after.first()) else { continue };
|
||||
if a.tcy.is_some() || b.tcy.is_some() {
|
||||
continue;
|
||||
}
|
||||
let japanese = if is_japanese_letter(a.ch) && is_latin_letter(b.ch) {
|
||||
Some(&*a)
|
||||
} else if is_latin_letter(a.ch) && is_japanese_letter(b.ch) {
|
||||
Some(b)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(g) = japanese {
|
||||
let aki = WAKAN_AKI * g.face.units_per_em() * g.sx;
|
||||
a.adv += aki;
|
||||
if let Some(x) = added.get_mut(j - 1) {
|
||||
*x = aki;
|
||||
}
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
/// The length an upright glyph takes down the column before tracking and justification: its
|
||||
/// vertical advance (the font's vertical metrics), else its advance, at least one em.
|
||||
fn upright_cell(g: &SGlyph) -> f64 {
|
||||
@@ -825,6 +872,8 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
let mut sg = cx.shape_para(pr.clone());
|
||||
// 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.
|
||||
let wakan = if para.mojikumi == Mojikumi::LineEndHalf { space_japanese_and_latin(&mut sg) } else { vec![] };
|
||||
let pm = {
|
||||
let (asc, desc, lead) = style_metrics(cx.db, cx.style_at(pr.start));
|
||||
let (cap, xh) = cap_x_heights(cx.db, cx.style_at(pr.start));
|
||||
@@ -882,10 +931,14 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
trimmed -= 1;
|
||||
}
|
||||
let hyphen = (hyph && end > i).then(|| hyphen_glyph(&sg[end - 1]));
|
||||
// Mojikumi: a closing bracket, comma or full stop ending the line is set half width.
|
||||
// Mojikumi: a closing bracket, comma or full stop ending the line is set half width, and
|
||||
// no Japanese–Latin space is left at the end of a line.
|
||||
let end_trim = match trimmed.checked_sub(1).filter(|&k| k >= i && para.mojikumi == Mojikumi::LineEndHalf) {
|
||||
Some(k) if !compressed.get(k).copied().unwrap_or(false) => sg.get(k).and_then(|g| punct_half(g, Punct::Closing)).unwrap_or(0.0),
|
||||
_ => 0.0,
|
||||
Some(k) => {
|
||||
let half = if compressed.get(k).copied().unwrap_or(false) { None } else { sg.get(k).and_then(|g| punct_half(g, Punct::Closing)) };
|
||||
half.unwrap_or(0.0) + wakan.get(k).copied().unwrap_or(0.0)
|
||||
}
|
||||
None => 0.0,
|
||||
};
|
||||
let w: f64 = sg[i..trimmed].iter().map(|g| g.adv).sum::<f64>() + hyphen.as_ref().map_or(0.0, |h| h.adv) - end_trim;
|
||||
let (align, justify) = match para.justify {
|
||||
|
||||
@@ -329,3 +329,38 @@ fn mojikumi_halves_line_end_and_consecutive_punctuation() {
|
||||
assert_eq!(adv("一二", Mojikumi::LineEndHalf), adv("一二", Mojikumi::None));
|
||||
}
|
||||
}
|
||||
|
||||
/// Mojikumi (JLREQ 3.2.2): a quarter em between Japanese and Latin letters or digits, either way
|
||||
/// round, horizontal and vertical; none inside a tate-chu-yoko block, none left at a line's end, and
|
||||
/// none with Mojikumi None. Needs a font with full-width Japanese (skipped without one).
|
||||
#[test]
|
||||
fn mojikumi_spaces_japanese_from_latin_by_a_quarter_em() {
|
||||
use vectorcraft_doc::Mojikumi;
|
||||
let lay = |text: &str, m: Mojikumi, vertical_type: bool| {
|
||||
let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() });
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.vertical = vertical_type;
|
||||
t.para.mojikumi = m;
|
||||
layout(FontDb::global(), &t)
|
||||
};
|
||||
if (lay("雅", Mojikumi::None, false).glyphs[0].advance - 20.0).abs() > 2.0 {
|
||||
return; // no font with full-width Japanese here
|
||||
}
|
||||
for vertical_type in [false, true] {
|
||||
let extra = |text: &str| -> Vec<f64> {
|
||||
let (on, off) = (lay(text, Mojikumi::LineEndHalf, vertical_type), lay(text, Mojikumi::None, vertical_type));
|
||||
on.glyphs.iter().zip(&off.glyphs).map(|(a, b)| a.advance - b.advance).collect()
|
||||
};
|
||||
// 雅楽 2026 年: after 楽 and after 6.
|
||||
assert_eq!(extra("雅楽2026年").iter().map(|x| (x * 100.0).round() / 100.0).collect::<Vec<_>>(), [0.0, 5.0, 0.0, 0.0, 0.0, 5.0, 0.0]);
|
||||
// Latin first, and nothing after the last character of the line.
|
||||
assert_eq!(extra("AB雅").iter().map(|x| x.round()).collect::<Vec<_>>(), [0.0, 5.0, 0.0]);
|
||||
assert_eq!(extra("雅A").iter().map(|x| x.round()).collect::<Vec<_>>(), [5.0, 0.0]);
|
||||
// Punctuation takes no Japanese–Latin space (the closing bracket isn't at the line's end).
|
||||
assert!(extra("「A」です").iter().all(|x| x.abs() < 0.01), "{:?}", extra("「A」です"));
|
||||
}
|
||||
// A tate-chu-yoko block is set as a Japanese character, with no space inside or around it.
|
||||
let on = lay("第10回", Mojikumi::LineEndHalf, true);
|
||||
let off = lay("第10回", Mojikumi::None, true);
|
||||
assert!(on.glyphs.iter().zip(&off.glyphs).all(|(a, b)| (a.advance - b.advance).abs() < 0.01));
|
||||
}
|
||||
Reference in new issue
Block a user