Merge pull request #1784 from sanyamsingh001/codex/r62-paragraph-alignment

G4: show mixed paragraph alignment without a selected button
This commit is contained in:
BFlat 2026-10-14 05:40:20 +07:00 committed by GitHub
commit e1c0d92a5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 279 additions and 8 deletions

View File

@ -252,7 +252,7 @@ pub(crate) fn text_editing(app: &VectorcraftApp) -> Option<(NodeId, usize, usize
let o = app.session.tool_options();
let id = NodeId(o.get("editing")?.as_u64()?);
let len = match &app.session.active()?.doc.node(id)?.kind {
NodeKind::Text(t) => t.plain_text().len(),
NodeKind::Text(t) => vectorcraft_text::edit::runs_len(&t.runs),
_ => return None,
};
let g = |k: &str| o.get(k).and_then(Value::as_u64).map_or(0, |v| (v as usize).min(len));

View File

@ -80,12 +80,92 @@ fn shown_alignment(justify: Justify, rtl: bool) -> Justify {
}
}
/// The seven alignment buttons, `size` points square, the one of `para`'s alignment on (the
/// Paragraph panel's and the Properties panel's).
/// Add the displayed alignments touched by the byte range (all paragraphs without a range).
/// Walk forward through run fragments so paragraph lookup never rescans an earlier prefix.
/// Only Auto needs paragraph text; borrow a fragment until style runs split it.
fn collect_text_alignment(text: &vectorcraft_doc::TextObject, range: Option<(usize, usize)>, shared: &mut Option<Justify>) -> bool {
use std::borrow::Cow;
let (a, b) = range.map_or((0, usize::MAX), |(a, b)| (a.min(b), a.max(b)));
let mut record = |index, plain: &str, start, end| {
if end < a || start > b {
return true;
}
let para = text.para_at(index);
let rtl = para.justify == Justify::Auto && vectorcraft_text::paragraph_is_rtl(plain, para.direction);
let value = shown_alignment(para.justify, rtl);
if shared.is_some_and(|old| old != value) {
return false;
}
*shared = Some(value);
true
};
let (mut index, mut start, mut end) = (0, 0, 0);
let mut plain = Cow::Borrowed("");
for run in &text.runs {
let mut rest = run.text.as_str();
while !rest.is_empty() {
let span = vectorcraft_text::edit::paragraph_at(rest, 0);
let part = rest.get(span.clone()).unwrap_or_default();
let para = text.para_at(index);
if para.justify == Justify::Auto && !part.is_empty() {
if plain.is_empty() {
plain = Cow::Borrowed(part);
} else {
plain.to_mut().push_str(part);
}
}
end += part.len();
if span.end == rest.len() {
break; // The same paragraph can continue in the next style run.
}
if !record(index, &plain, start, end) {
return false;
}
// Like TextObject::paragraphs_in, a range ending at the next paragraph's start
// includes that paragraph; the newline itself belongs to the preceding one.
end += 1;
start = end;
index += 1;
if start > b {
return true;
}
plain = Cow::Borrowed("");
rest = rest.get(span.end + 1..).unwrap_or_default();
}
}
record(index, &plain, start, end) // Includes empty text and the paragraph after a final newline.
}
/// None means differing displayed alignments, following the other mixed-value controls.
fn selected_alignment(app: &VectorcraftApp, fallback: &ParaStyle) -> Option<Justify> {
let default = Some(shown_alignment(fallback.justify, fallback.direction == Some(ParaDirection::RightToLeft)));
let Some(st) = app.session.active() else { return default };
let mut shared = None;
if let Some((id, a, b)) = super::character::text_editing(app)
&& let Some(NodeKind::Text(text)) = st.doc.node(id).map(|n| &n.kind)
{
return if collect_text_alignment(text, Some((a, b)), &mut shared) { shared.or(default) } else { None };
}
fn collect(node: &vectorcraft_doc::Node, shared: &mut Option<Justify>) -> bool {
if let NodeKind::Text(text) = &node.kind {
return collect_text_alignment(text, None, shared);
}
node.children().is_none_or(|children| children.iter().all(|child| collect(child, shared)))
}
for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
if !collect(node, &mut shared) {
return None;
}
}
shared.or(default)
}
/// The seven alignment buttons shared by Paragraph and Properties; no button is selected
/// when the paragraphs they act on have differing displayed alignments.
pub fn alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32) {
let shown = shown_alignment(para.justify, resolved_rtl(app, para));
let shown = selected_alignment(app, para);
for (j, icon, tip, id) in ALIGNMENTS {
if widgets::icon_button(ui, icon, tip, shown == j, size).clicked() {
if widgets::icon_button(ui, icon, tip, shown == Some(j), size).clicked() {
para_cmd(app, "text.setStyle", json!({"justify": id}));
}
}
@ -253,6 +333,191 @@ mod tests {
use super::*;
use vectorcraft_engine::Session;
fn alignment_row(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) -> egui::Rect {
let para = text_style(app).map_or_else(ParaStyle::default, |(_, p)| p);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 3.0;
alignment_buttons(app, ui, &para, size);
})
.response
.rect
}
// Inspect the actual selected wells, including the shared Properties-size buttons.
fn alignment_frame(ctx: &egui::Context, app: &mut VectorcraftApp, size: f32) -> (Vec<Justify>, egui::Pos2) {
let mut row = egui::Rect::NOTHING;
let mut frame = ctx.run_ui(egui::RawInput::default(), |ui| row = alignment_row(app, ui, size));
frame.textures_delta.clear();
fn wells(shape: &egui::Shape, color: egui::Color32, size: f32, out: &mut Vec<egui::Pos2>) {
match shape {
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| wells(s, color, size, out)),
egui::Shape::Rect(r) if r.fill == color && r.rect.size() == vec2(size, size) => out.push(r.rect.center()),
_ => {}
}
}
let centers: [egui::Pos2; 7] = std::array::from_fn(|i| row.min + vec2(size / 2.0 + i as f32 * (size + 3.0), size / 2.0));
let mut active = Vec::new();
for shape in &frame.shapes {
wells(&shape.shape, Tokens::get(ctx).tool_active, size, &mut active);
}
let selected = active
.iter()
.map(|pos| {
let index = centers.iter().position(|center| center.distance(*pos) < 0.1).expect("alignment well");
ALIGNMENTS[index].0
})
.collect();
(selected, centers[1])
}
fn editing_text(text: &str) -> (VectorcraftApp, Value) {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": text})).unwrap()["id"].clone();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
assert!(super::super::character::text_editing(&app).is_some());
(app, id)
}
fn select_range(app: &mut VectorcraftApp, start: usize, end: usize) {
app.session.set_tool_option("select", &json!({"start": start, "end": end}));
}
#[test]
fn paragraph_mixed_alignment_click_center_and_undo() {
let (mut app, id) = editing_text("é\nאב\n終");
app.run("text.setStyle", json!({"id": id, "justify": "left"})).unwrap();
app.run("text.setStyle", json!({"id": id, "start": 3, "end": 7, "justify": "right"})).unwrap();
app.run("text.setStyle", json!({"id": id, "start": 8, "end": 11, "justify": "justifyAll"})).unwrap();
app.run("text.setFormat", json!({"id": id, "start": 3, "end": 7, "leftIndent": 17, "spaceAfter": 9})).unwrap();
select_range(&mut app, 0, 7);
let before = app.session.active().unwrap().doc.clone();
let history = app.session.active().unwrap().history.undo.len();
let ctx = egui::Context::default();
let (selected, center) = alignment_frame(&ctx, &mut app, 28.0);
assert!(selected.is_empty(), "mixed paragraphs must not light one alignment: {selected:?}");
super::super::tests_appearance::click(&ctx, &mut app, center, |app, ui| {
alignment_row(app, ui, 28.0);
});
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Center]);
let node = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
let NodeKind::Text(text) = &node.kind else { panic!("text") };
assert_eq!(
(text.para_at(0).justify, text.para_at(1).justify, text.para_at(2).justify),
(Justify::Center, Justify::Center, Justify::JustifyAll)
);
let NodeKind::Text(old) = &before.node(node.id).unwrap().kind else { panic!("text") };
for i in 0..3 {
let mut expected = old.para_at(i).clone();
if i < 2 {
expected.justify = Justify::Center;
}
assert_eq!(text.para_at(i), &expected, "other paragraph fields stay unchanged");
}
assert_eq!(text.runs, old.runs);
assert_eq!(app.session.active().unwrap().history.undo.len(), history + 1);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc, before);
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty());
}
#[test]
fn paragraph_mixed_alignment_caret_unicode_boundaries_and_uniform_controls() {
let (mut app, id) = editing_text("é\nאב\n");
app.run("text.setStyle", json!({"id": id, "justify": "left"})).unwrap();
app.run("text.setStyle", json!({"id": id, "start": 3, "end": 7, "justify": "right"})).unwrap();
app.run("text.setStyle", json!({"id": id, "start": 8, "end": 8, "justify": "justifyAll"})).unwrap();
let ctx = egui::Context::default();
for (start, end, expected) in [
(0, 2, vec![Justify::Left]),
(2, 2, vec![Justify::Left]), // newline belongs to the preceding paragraph
(3, 3, vec![Justify::Right]),
(4, 6, vec![Justify::Right]), // byte offsets inside multi-byte characters
(0, 3, vec![]), // ending at the next paragraph's start touches both
(7, 0, vec![]), // reversed selection
(8, 8, vec![Justify::JustifyAll]), // empty trailing paragraph
(100, 100, vec![Justify::JustifyAll]), // existing selection clamping
] {
select_range(&mut app, start, end);
assert_eq!(alignment_frame(&ctx, &mut app, 24.0).0, expected, "range {start}..{end}");
}
for (expected, _, _, key) in ALIGNMENTS {
app.run("text.setStyle", json!({"id": id, "justify": key})).unwrap();
app.session.set_tool_option("selectAll", &json!(true));
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [expected]);
}
}
#[test]
fn paragraph_mixed_alignment_auto_direction_and_split_runs() {
let (mut app, id) = editing_text("é\nאב");
// Character formatting splits the RTL paragraph across runs.
app.run("text.setRangeStyle", json!({"id": id, "start": 3, "end": 5, "size": 24})).unwrap();
app.session.set_tool_option("selectAll", &json!(true));
let ctx = egui::Context::default();
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty(), "Auto resolves separately per paragraph");
select_range(&mut app, 3, 7);
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Right]);
app.run("text.setFormat", json!({"id": id, "start": 3, "end": 7, "direction": "leftToRight"})).unwrap();
app.session.set_tool_option("selectAll", &json!(true));
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Left]);
app.run("text.setStyle", json!({"id": id, "start": 0, "end": 2, "justify": "left"})).unwrap();
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Left], "explicit Left and resolved Auto agree");
app.run("text.setFormat", json!({"id": id, "start": 0, "end": 2, "direction": "rightToLeft"})).unwrap();
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Left], "explicit alignment ignores direction");
}
#[test]
fn paragraph_mixed_alignment_explicit_rtl_and_empty_controls() {
let (mut app, id) = editing_text("Latin\nאב\n\n");
app.run("text.setStyle", json!({"id": id, "justify": "auto"})).unwrap();
app.run("text.setFormat", json!({"id": id, "direction": "rightToLeft"})).unwrap();
// Auto with an explicit direction still needs the real, possibly split, paragraph text.
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 2, "size": 24})).unwrap();
let ctx = egui::Context::default();
for (start, end, expected) in [
(0, 5, Justify::Right), // nonempty Latin with explicit RTL
(6, 10, Justify::Right), // nonempty Hebrew with explicit RTL
(11, 11, Justify::Left), // existing resolver's empty paragraph behavior
(12, 12, Justify::Left), // empty trailing paragraph
] {
select_range(&mut app, start, end);
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [expected], "explicit RTL range {start}..{end}");
}
app.run("text.setStyle", json!({"id": id, "start": 0, "end": 5, "justify": "left"})).unwrap();
select_range(&mut app, 0, 10);
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty(), "explicit Left and Auto/RTL differ");
let (mut empty, id) = editing_text("");
empty.run("text.setStyle", json!({"id": id, "justify": "auto"})).unwrap();
empty.run("text.setFormat", json!({"id": id, "direction": "rightToLeft"})).unwrap();
assert_eq!(alignment_frame(&egui::Context::default(), &mut empty, 28.0).0, [Justify::Left]);
}
#[test]
fn paragraph_mixed_alignment_object_group_and_no_text_controls() {
let (mut app, id) = editing_text("A\nB");
app.run("text.setStyle", json!({"id": id, "justify": "left"})).unwrap();
app.run("text.setStyle", json!({"id": id, "start": 2, "end": 3, "justify": "right"})).unwrap();
app.session.set_tool_option("stopEditing", &json!(true));
let ctx = egui::Context::default();
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty(), "whole object covers every paragraph");
app.run("object.group", json!({})).unwrap();
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty(), "group descendant has mixed paragraphs");
app.run("text.setStyle", json!({"justify": "center"})).unwrap();
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Center]);
let group = app.session.active().unwrap().selection.objects.clone();
let other = app.run("text.create", json!({"x": 20, "y": 100, "text": "C"})).unwrap()["id"].clone();
let mut ids: Vec<Value> = group.iter().map(|id| json!(id.0)).collect();
ids.push(other);
app.run("select.set", json!({"ids": ids})).unwrap();
assert!(alignment_frame(&ctx, &mut app, 28.0).0.is_empty(), "different selected objects");
app.run("select.set", json!({"ids": []})).unwrap();
assert_eq!(alignment_frame(&ctx, &mut app, 28.0).0, [Justify::Left], "no-text default stays unchanged");
let (mut empty, _) = editing_text("");
assert_eq!(alignment_frame(&egui::Context::default(), &mut empty, 28.0).0, [Justify::Left]);
}
/// New Hebrew type aligns to the start of its direction (Align Right shows), an explicit
/// alignment stays as chosen, and Paragraph Direction (with the Indic options) sets the
/// direction: Auto then follows it.

View File

@ -126,6 +126,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
## G4 Advanced type
- **Improved:** mixed paragraph alignments leave the existing Paragraph/Properties alignment buttons unselected; choosing an alignment still uses the range-aware command. Headless regressions, full integration CI and visual review pass. Other mixed paragraph fields remain; estimate unchanged.
- **Improved (#1258):** full font names (name ID 4) resolve the face instead of falling back, and Missing Fonts searches recognize the same aliases. Synthetic regressions cover loaded/cataloged fonts, localized names, real-family precedence and variable default faces. Windows font-service activation still needs reporter QA; estimate unchanged.
- **Missing:** hyphenation and justification options, Optical Margin Alignment (type core); CJK vertical
composition (ruby, mojikumi sets and dialog, kinsoku settings #633, manual tate-chu-yoko), Middle Eastern
@ -246,6 +247,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | G4: mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; other mixed paragraph fields and estimates unchanged, no gap entry closed |
| 2026-10-13 | minor | G1: create width points through Width Point Edit with optional adjoining adjustment; command/pointer/history regressions and full CI passed; dialog layout reviewed; scores and hours unchanged |
| 2026-10-13 | minor | Control transport deadlines (#1726): idle and partial clients and blocked replies release connection slots; parity scores and estimates unchanged. |
| 2026-10-13 | minor | G1: Average and Join in one step with a corner or smooth choice is listed as missing (the ui-parity row said done; the command does not exist); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). G11: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). G9: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Polish catalog (#1498); estimates unchanged, no gap entry closed |

View File

@ -55,7 +55,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (closed SPLINE flags round-trip, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) |
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles, per-paragraph paragraph attributes, Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles, per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain done (29/57 Photoshop filters); initial Revolve, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 |
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
@ -81,6 +81,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | M7: mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; other mixed paragraph fields, scores and hours unchanged |
| 2026-10-13 | minor | Interaction fidelity: Alt swaps a new ruler guide's orientation during creation; preview, snapping, release, cancellation and Undo/Redo have event regressions. Scores and hours unchanged (estimated). |
| 2026-10-13 | minor | G1: create width points through Width Point Edit with optional adjoining adjustment; command/pointer/history regressions and full CI passed; dialog layout reviewed; scores and hours unchanged |
| 2026-10-13 | minor | Corner Radius: a link toggle behind the field in the Properties and Transform panels; off, a live rectangle shows a radius field for each corner (Top Left, Top Right, Bottom Right, Bottom Left), each setting its own corner (`singleCornerRadius` preference); split from #1594; score unchanged (estimated) |

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Area Type Options (rows, columns, inset, first baseline, Auto Size, Shrink Text to Fit) | done | the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) |
| Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444) |
| Type on a Path effects and options; brackets | done | #429 |
| Character and Paragraph panels, styles with overrides | done | mixed-value display for differing paragraphs missing |
| Character and Paragraph panels, styles with overrides | done | differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields missing |
| 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) |
@ -56,6 +56,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Paragraph/Properties alignment buttons show mixed alignment without changing range commands; headless regressions, full integration CI and visual review pass; other mixed paragraph fields, scores and hours unchanged |
| 2026-10-13 | minor | The font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); scores and hours unchanged |
| 2026-10-13 | minor | The commands that make or restyle type, and MCP `add_text`, report area type that does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471); a tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458); scores and hours unchanged |
| 2026-10-13 | minor | Fit Headline no longer pushes the tracking to its ceiling and loses glyphs when the frame holds one line (#1445, fixes #1444); text frames, wrap shapes and type paths with coordinates beyond a billion points hold no text, and layout limits its search for room, instead of hanging (#1451); scores and hours unchanged |