mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Path: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J (G1)
This commit is contained in:
+114
-38
@@ -98,6 +98,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
average
|
||||
),
|
||||
cmd!(
|
||||
"path.averageJoin",
|
||||
"Average & Join…",
|
||||
["Object", "Path"],
|
||||
Some("Cmd+Alt+Shift+J"),
|
||||
"{to: \"corner\"|\"smooth\" (the joined anchor: corner retracts both handles, smooth pulls handles in line with the neighbours), ids?: [path, ...] (default: the selection; a compound path ID expands to its open member paths)} move two open path ends to their midpoint and join them there, as one undo step: the two direct-selected anchors when they are open path ends, else the nearest ends of the two open paths in ids or the selection (or both ends of one open path, closing it) → the joined path stays selected",
|
||||
has_selection,
|
||||
average_join
|
||||
),
|
||||
cmd!(
|
||||
"path.convertAnchors",
|
||||
"Convert Anchor Points",
|
||||
@@ -802,29 +811,11 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
};
|
||||
if ida == idb {
|
||||
if sa == sb {
|
||||
s.edit("Join", |d, sel| {
|
||||
close_subpath_in_node(d, ida, sa, join_kind)?;
|
||||
sel.set([compound_or_self(d, ida)]);
|
||||
Ok(())
|
||||
})?;
|
||||
} else {
|
||||
s.edit("Join", |d, sel| {
|
||||
join_subpaths_in_node(d, ida, (sa, sb), (rx, ry), join_kind)?;
|
||||
sel.set([compound_or_self(d, ida)]);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
return ok();
|
||||
} else {
|
||||
s.edit("Join", |d, sel| {
|
||||
join_two(d, ida, idb, (sa, sb), (rx, ry), join_kind)?;
|
||||
sel.set([compound_or_self(d, ida)]);
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
s.edit("Join", |d, sel| {
|
||||
sel.set([join_pair(d, (ida, sa), (idb, sb), (rx, ry), join_kind)?]);
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
|
||||
let ids = selected_paths(s)?;
|
||||
@@ -865,21 +856,11 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some((rx, ry)) = ends
|
||||
&& open_subs.len() == 2
|
||||
{
|
||||
let (a, sa) = open_subs[0];
|
||||
let (b, sb) = open_subs[1];
|
||||
if a == b {
|
||||
s.edit("Join", |d, sel| {
|
||||
join_subpaths_in_node(d, a, (sa, sb), (rx, ry), join_kind)?;
|
||||
sel.set([compound_or_self(d, a)]);
|
||||
Ok(())
|
||||
})?;
|
||||
} else {
|
||||
s.edit("Join", |d, sel| {
|
||||
join_two(d, a, b, (sa, sb), (rx, ry), join_kind)?;
|
||||
sel.set([compound_or_self(d, a)]);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
let (a, b) = (open_subs[0], open_subs[1]);
|
||||
s.edit("Join", |d, sel| {
|
||||
sel.set([join_pair(d, a, b, (rx, ry), join_kind)?]);
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
|
||||
@@ -992,6 +973,17 @@ fn join_two(d: &mut Document, a: NodeId, b: NodeId, (sa, sb): (usize, usize), (r
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Join subpath `sa` of path `a` and subpath `sb` of path `b` oriented as [`join_two`] takes them
|
||||
/// (one subpath: close it). Returns what to select: the joined path, or its compound path.
|
||||
fn join_pair(d: &mut Document, (a, sa): (NodeId, usize), (b, sb): (NodeId, usize), r: (bool, bool), join_kind: Option<bool>) -> Result<NodeId> {
|
||||
if a == b {
|
||||
join_subpaths_in_node(d, a, (sa, sb), r, join_kind)?;
|
||||
} else {
|
||||
join_two(d, a, b, (sa, sb), r, join_kind)?;
|
||||
}
|
||||
Ok(compound_or_self(d, a))
|
||||
}
|
||||
|
||||
pub(super) type AnchorTargets = Vec<(NodeId, Vec<(usize, usize)>)>;
|
||||
|
||||
pub(super) fn anchor_targets(s: &Session) -> Result<AnchorTargets> {
|
||||
@@ -1039,6 +1031,90 @@ fn average(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// An open path end: (path, subpath, anchor).
|
||||
type End = (NodeId, usize, usize);
|
||||
|
||||
/// Average & Join: the two ends Average moves to their midpoint, joined there as Join joins them.
|
||||
fn average_join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "path.averageJoin";
|
||||
let smooth = match str_param(p, "to") {
|
||||
Some("smooth") => true,
|
||||
Some("corner") => false,
|
||||
_ => return Err(bad(C, "`to` must be \"corner\" or \"smooth\"")),
|
||||
};
|
||||
let ends = average_join_ends(s, p, C)?;
|
||||
let [a, b] = ends.ok_or_else(|| bad(C, "select two open path ends, or two open paths, to average and join"))?;
|
||||
let doc = &s.doc()?.doc;
|
||||
let point = |(id, si, ai): End| doc.node(id)?.path_data()?.subpaths.get(si)?.anchors.get(ai).map(|a| a.p);
|
||||
let (Some(pa), Some(pb)) = (point(a), point(b)) else { return Err(bad(C, "join needs open paths")) };
|
||||
let mid = pa.midpoint(pb);
|
||||
// The first end must come last in its subpath, the second first (as `ends` takes them in Join).
|
||||
let r = (a.2 == 0, b.2 != 0);
|
||||
s.edit("Average & Join", |d, sel| {
|
||||
for (id, si, ai) in [a, b] {
|
||||
if let Some(anchor) = path_mut(d, id)?.anchor_mut(si, ai) {
|
||||
anchor.translate(mid - anchor.p);
|
||||
}
|
||||
}
|
||||
sel.set([join_pair(d, (a.0, a.1), (b.0, b.1), r, Some(smooth))?]);
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
}
|
||||
|
||||
/// The two ends Average & Join brings together: the two direct-selected anchors when they are
|
||||
/// open path ends; else, of the paths in `ids` or the selected ones, the nearest ends of their two
|
||||
/// open subpaths, or both ends of their one. `None` when there are no such two ends, or when they
|
||||
/// are the two ends of a line, which the midpoint would collapse.
|
||||
fn average_join_ends(s: &Session, p: &Value, c: &str) -> Result<Option<[End; 2]>> {
|
||||
let st = s.doc()?;
|
||||
let doc = &st.doc;
|
||||
let subpath = |id: NodeId, si: usize| doc.node(id)?.path_data()?.subpaths.get(si).filter(|sp| !sp.closed && !sp.anchors.is_empty());
|
||||
let last = |sp: &SubPath| sp.anchors.len().saturating_sub(1);
|
||||
let ids = ids_param(p, "ids");
|
||||
let ends = if ids.is_none() && !st.selection.anchors.is_empty() {
|
||||
let picked: Vec<End> = st.selection.anchors.iter().flat_map(|(&id, set)| set.iter().map(move |&(si, ai)| (id, si, ai))).collect();
|
||||
match picked.as_slice() {
|
||||
&[a, b] if [a, b].iter().all(|&(id, si, ai)| subpath(id, si).is_some_and(|sp| ai == 0 || ai == last(sp))) => [a, b],
|
||||
_ => return Ok(None),
|
||||
}
|
||||
} else {
|
||||
let paths = match ids {
|
||||
None => selected_paths(s)?,
|
||||
Some(ids) => {
|
||||
let mut out = vec![];
|
||||
for id in ids {
|
||||
match doc.node(id).map(|n| (n, &n.kind)) {
|
||||
Some((_, NodeKind::Path { .. })) => out.push(id),
|
||||
Some((n, NodeKind::Compound { .. })) => out.extend(open_members(n)),
|
||||
_ => return Err(bad(c, format!("{} isn't a path", id.0))),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
};
|
||||
let open: Vec<(NodeId, usize, &SubPath)> = paths
|
||||
.iter()
|
||||
.filter_map(|&id| Some((id, doc.node(id)?.path_data()?)))
|
||||
.flat_map(|(id, pd)| {
|
||||
pd.subpaths.iter().enumerate().filter(|(_, sp)| !sp.closed && !sp.anchors.is_empty()).map(move |(si, sp)| (id, si, sp))
|
||||
})
|
||||
.collect();
|
||||
match *open.as_slice() {
|
||||
[(id, si, sp)] => [(id, si, 0), (id, si, last(sp))],
|
||||
[(ia, sa, x), (ib, sb, y)] => {
|
||||
let Some((rx, ry)) = nearest_ends(x, y) else { return Ok(None) };
|
||||
[(ia, sa, if rx { 0 } else { last(x) }), (ib, sb, if ry { last(y) } else { 0 })]
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
};
|
||||
// A subpath's two ends: it needs an anchor between them to close around.
|
||||
let [a, b] = ends;
|
||||
let line = (a.0, a.1) == (b.0, b.1) && subpath(a.0, a.1).is_none_or(|sp| sp.anchors.len() <= 2);
|
||||
Ok((!line).then_some(ends))
|
||||
}
|
||||
|
||||
fn convert_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "path.convertAnchors";
|
||||
let smooth = match str_param(p, "to") {
|
||||
|
||||
@@ -2723,6 +2723,88 @@ fn join_takes_the_paths_and_the_ends_to_join() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Average & Join (Cmd+Alt+Shift+J): the two ends meet at their midpoint and become one anchor of
|
||||
/// one path, a corner or a smooth point as asked, in one undo step; from two selected paths (their
|
||||
/// nearest ends) or from two direct-selected ends (the ends picked, not the nearest).
|
||||
#[test]
|
||||
fn average_join_meets_at_the_midpoint_as_a_corner_or_smooth_point() {
|
||||
let pts = |s: &Session, id: NodeId| path(s, id).subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect::<Vec<_>>();
|
||||
for (to, picks) in [("corner", false), ("smooth", false), ("corner", true), ("smooth", true)] {
|
||||
let mut s = session();
|
||||
let a = line(&mut s, 0.0, 0.0, 100.0, 0.0);
|
||||
let b = line(&mut s, 110.0, 40.0, 200.0, 40.0);
|
||||
let want = if picks {
|
||||
// a's first and b's last, the far ends.
|
||||
s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0]], "mode": "set"})).unwrap();
|
||||
s.execute("select.anchors", &json!({"id": b.0, "anchors": [[0, 1]], "mode": "add"})).unwrap();
|
||||
[(100.0, 0.0), (100.0, 20.0), (110.0, 40.0)]
|
||||
} else {
|
||||
s.select(|_, sel| sel.set([a, b])).unwrap();
|
||||
[(0.0, 0.0), (105.0, 20.0), (200.0, 40.0)]
|
||||
};
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("path.averageJoin", &json!({ "to": to })).unwrap();
|
||||
assert_eq!(pts(&s, a), want, "{to} picks:{picks}");
|
||||
assert!(s.doc().unwrap().doc.node(b).is_none(), "b was joined into a");
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [a]);
|
||||
let joined = path(&s, a).subpaths[0].anchors[1];
|
||||
let (din, dout) = (joined.h_in - joined.p, joined.h_out - joined.p);
|
||||
if to == "corner" {
|
||||
assert!(din.hypot() < 1e-9 && dout.hypot() < 1e-9, "a corner has no handles: {joined:?}");
|
||||
} else {
|
||||
assert!(din.hypot() > 1.0 && dout.hypot() > 1.0 && din.cross(dout).abs() < 1e-6 && din.dot(dout) < 0.0, "smooth: {joined:?}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(pts(&s, a), [(0.0, 0.0), (100.0, 0.0)]);
|
||||
assert_eq!(pts(&s, b), [(110.0, 40.0), (200.0, 40.0)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Average & Join closes one open path at the midpoint of its ends, and refuses a bad `to` and a
|
||||
/// selection without two open ends to join (a closed path, three paths, one picked end, a line's
|
||||
/// two ends), leaving the document as it was; `ids` names the paths over the selection.
|
||||
#[test]
|
||||
fn average_join_closes_one_path_and_refuses_what_it_cannot_join() {
|
||||
let mut s = session();
|
||||
let v = NodeId(s.execute("path.create", &json!({"d": "M0 0 L50 100 L100 10"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select(|_, sel| sel.set([v])).unwrap();
|
||||
s.execute("path.averageJoin", &json!({"to": "corner"})).unwrap();
|
||||
let pd = path(&s, v);
|
||||
assert!(pd.subpaths[0].closed);
|
||||
assert_eq!(pd.subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect::<Vec<_>>(), [(50.0, 5.0), (50.0, 100.0)]);
|
||||
|
||||
let mut s = session();
|
||||
let a = line(&mut s, 0.0, 0.0, 100.0, 0.0);
|
||||
let b = line(&mut s, 110.0, 0.0, 200.0, 0.0);
|
||||
let c = line(&mut s, 210.0, 0.0, 300.0, 0.0);
|
||||
let r = rect(&mut s, 0.0, 50.0, 10.0, 10.0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let corner = json!({"to": "corner"});
|
||||
let cases = [
|
||||
(vec![a, b], None, json!({})),
|
||||
(vec![a, b], None, json!({"to": "round"})),
|
||||
(vec![r], None, corner.clone()),
|
||||
(vec![a, b, c], None, corner.clone()),
|
||||
(vec![a], None, corner.clone()),
|
||||
(vec![r], None, json!({"to": "corner", "ids": [a.0, 999]})),
|
||||
(vec![r], None, json!({"to": "corner", "ids": [r.0, a.0]})),
|
||||
(vec![a, b], Some(json!({"id": a.0, "anchors": [[0, 0]], "mode": "set"})), corner.clone()),
|
||||
(vec![a, b], Some(json!({"id": a.0, "anchors": [[0, 0], [0, 1]], "mode": "set"})), corner.clone()),
|
||||
];
|
||||
for (picked, anchors, p) in cases {
|
||||
s.select(|_, sel| sel.set(picked.clone())).unwrap();
|
||||
if let Some(anchors) = anchors {
|
||||
s.execute("select.anchors", &anchors).unwrap();
|
||||
}
|
||||
assert!(s.execute("path.averageJoin", &p).is_err(), "{picked:?} {p}");
|
||||
assert_eq!(s.doc().unwrap().doc, before, "{picked:?} {p}: nothing changed");
|
||||
}
|
||||
s.select(|_, sel| sel.set([r])).unwrap();
|
||||
s.execute("path.averageJoin", &json!({"to": "corner", "ids": [b.0, c.0]})).unwrap();
|
||||
assert_eq!(path(&s, b).subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect::<Vec<_>>(), [(110.0, 0.0), (205.0, 0.0), (300.0, 0.0)]);
|
||||
}
|
||||
|
||||
/// `path.join {ends}` on selected paths honors the requested ends, not the nearest pair.
|
||||
#[test]
|
||||
fn join_selected_paths_honors_ends_parameter() {
|
||||
|
||||
@@ -273,7 +273,7 @@ registry! {
|
||||
NewDocument: [new_document::KIND] => new_document::SPEC,
|
||||
Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "arc", "spiral", "rectangularGrid", "polarGrid"] => shapes::SPEC,
|
||||
Transform: ["move", "rotate", "scale", "reflect", "shear"] => transform::SPEC,
|
||||
PathOp: ["average", "offsetPath", "simplify", "splitIntoGrid"] => path_ops::SPEC,
|
||||
PathOp: ["average", "averageJoin", "offsetPath", "simplify", "splitIntoGrid"] => path_ops::SPEC,
|
||||
DocumentSetup: ["documentSetup"] => document_setup::SPEC,
|
||||
ArtboardOptions: [artboard_options::KIND] => artboard_options::SPEC,
|
||||
ExportForScreens: ["exportForScreens"] => export_for_screens::SPEC,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Path dialogs: Average, and Offset Path, Simplify and Split Into Grid with Preview.
|
||||
//! Path dialogs: Average, Average & Join, and Offset Path, Simplify and Split Into Grid with Preview.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind, Unit};
|
||||
@@ -23,6 +23,7 @@ const ORIGINAL_IDS: &str = "__ids";
|
||||
fn title(kind: &str) -> &'static str {
|
||||
match kind {
|
||||
"average" => tl!("Average"),
|
||||
"averageJoin" => tl!("Average & Join"),
|
||||
"offsetPath" => tl!("Offset Path"),
|
||||
"simplify" => tl!("Simplify"),
|
||||
_ => tl!("Split Into Grid"),
|
||||
@@ -33,6 +34,7 @@ fn title(kind: &str) -> &'static str {
|
||||
fn command(d: &Dialog) -> (&'static str, Value) {
|
||||
match d.kind.as_str() {
|
||||
"average" => ("path.average", json!({"axis": d.str("axis")})),
|
||||
"averageJoin" => ("path.averageJoin", json!({"to": d.str("to")})),
|
||||
"offsetPath" => {
|
||||
("object.path.offsetPath", json!({"offset": d.f64("offset", 10.0), "joins": d.str("joins"), "miterLimit": d.f64("miterLimit", 4.0)}))
|
||||
}
|
||||
@@ -44,7 +46,7 @@ fn command(d: &Dialog) -> (&'static str, Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The undo label of the dialogs that preview on the canvas (all but Average).
|
||||
/// The undo label of the dialogs that preview on the canvas (all but Average and Average & Join).
|
||||
fn previewed(kind: &str) -> Option<&'static str> {
|
||||
match kind {
|
||||
"offsetPath" => Some("Offset Path"),
|
||||
@@ -108,6 +110,20 @@ fn split_grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
form::check(ui, d, "addGuides", tl!("Add Guides"));
|
||||
}
|
||||
|
||||
/// Average & Join's Points: Corner or Smooth, the joined anchor. "Points:" in the "anchor"
|
||||
/// context: the plain row is a star's number of points.
|
||||
fn join_points(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let to = d.str("to");
|
||||
ui.horizontal(|ui| {
|
||||
form::row_label(ui, crate::i18n::tr_ctx(crate::i18n::current(), "anchor", "Points:"));
|
||||
for (value, label) in [("corner", "Corner"), ("smooth", "Smooth")] {
|
||||
if crate::widgets::radio(ui, label, to == value, true) {
|
||||
d.fields.insert("to".into(), json!(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The fields the Simplify dialog opens with at `tolerance` (points): the slider's tolerance when
|
||||
/// its More Options opens the dialog.
|
||||
pub(crate) fn simplify_dialog(tolerance: f64) -> Value {
|
||||
@@ -160,6 +176,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
split_grid(ui, d, unit);
|
||||
} else if d.kind == "simplify" {
|
||||
simplify_fields(ui, d, unit);
|
||||
} else if d.kind == "averageJoin" {
|
||||
join_points(ui, d);
|
||||
} else {
|
||||
form::grid(ui, d, unit);
|
||||
}
|
||||
@@ -189,7 +207,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let (id, params) = command(d);
|
||||
if previewed(&d.kind).is_none() {
|
||||
return run_and_close(app, id, params);
|
||||
let r = run_and_close(app, id, params)?;
|
||||
if d.kind == "averageJoin" {
|
||||
app.ui.average_join = json!({"to": d.str("to")});
|
||||
}
|
||||
return Ok(r);
|
||||
}
|
||||
let r = form::commit_preview(app, id, params)?;
|
||||
// Simplify: with retain ticked, the menu item opens this dialog on these fields next time; with it
|
||||
|
||||
@@ -105,6 +105,7 @@ fn every_dialog_draws() {
|
||||
for (kind, fields) in [
|
||||
("move", json!({"dx": "0 pt", "dy": "0 pt"})),
|
||||
("average", json!({"axis": "both"})),
|
||||
("averageJoin", json!({"to": "corner"})),
|
||||
(crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"})),
|
||||
("newWorkspace", json!({"name": "Mine"})),
|
||||
("shortcuts", json!({})),
|
||||
|
||||
@@ -5584,3 +5584,8 @@ axis Y Axis Y ос
|
||||
Glyph Bounds Граници на глифа
|
||||
Proximity Guides Водачи за близост
|
||||
Angular Guides Ъглови водачи
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Усредняване и свързване…
|
||||
Average & Join Усредняване и свързване
|
||||
anchor Points: Точки:
|
||||
@msg select two open path ends, or two open paths, to average and join изберете два края на отворени пътища или два отворени пътя за усредняване и свързване
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5685,3 +5685,8 @@ theme Appearance Aparença
|
||||
Glyph Bounds Límits del glif
|
||||
Proximity Guides Guies de proximitat
|
||||
Angular Guides Guies angulars
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Mitjana i unió…
|
||||
Average & Join Mitjana i unió
|
||||
anchor Points: Punts:
|
||||
@msg select two open path ends, or two open paths, to average and join selecciona dos extrems de traçats oberts, o dos traçats oberts, per fer-ne la mitjana i unir-los
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5694,3 +5694,8 @@ axis Y Axis osa Y
|
||||
Glyph Bounds Hranice glyfů
|
||||
Proximity Guides Vodítka blízkosti
|
||||
Angular Guides Úhlová vodítka
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Zprůměrovat a spojit…
|
||||
Average & Join Zprůměrovat a spojit
|
||||
anchor Points: Body:
|
||||
@msg select two open path ends, or two open paths, to average and join vyberte dva konce otevřených cest nebo dvě otevřené cesty ke zprůměrování a spojení
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5599,3 +5599,8 @@ axis Y Axis Y-akse
|
||||
Glyph Bounds Glyfafgrænsning
|
||||
Proximity Guides Nærhedshjælpelinjer
|
||||
Angular Guides Vinkelhjælpelinjer
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Gennemsnit og forbind…
|
||||
Average & Join Gennemsnit og forbind
|
||||
anchor Points: Punkter:
|
||||
@msg select two open path ends, or two open paths, to average and join markér to ender af åbne stier eller to åbne stier, der skal udlignes til gennemsnit og forbindes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5276,3 +5276,8 @@ axis Y Axis Y-Achse
|
||||
Glyph Bounds Glyphenbegrenzung
|
||||
Proximity Guides Näherungshilfslinien
|
||||
Angular Guides Winkelhilfslinien
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Durchschnitt berechnen und zusammenfügen…
|
||||
Average & Join Durchschnitt berechnen und zusammenfügen
|
||||
anchor Points: Punkte:
|
||||
@msg select two open path ends, or two open paths, to average and join wählen Sie zwei Enden offener Pfade oder zwei offene Pfade aus, um sie zu mitteln und zusammenzufügen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5646,3 +5646,8 @@ axis Y Axis Άξονας Y
|
||||
Glyph Bounds Όρια γλυφής
|
||||
Proximity Guides Οδηγοί εγγύτητας
|
||||
Angular Guides Γωνιακοί οδηγοί
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Μέσος όρος και ένωση…
|
||||
Average & Join Μέσος όρος και ένωση
|
||||
anchor Points: Σημεία:
|
||||
@msg select two open path ends, or two open paths, to average and join επιλέξτε δύο άκρα ανοιχτών διαδρομών ή δύο ανοιχτές διαδρομές για μέσο όρο και ένωση
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -5636,3 +5636,8 @@ axis Y Axis Eje Y
|
||||
Glyph Bounds Límites del glifo
|
||||
Proximity Guides Guías de proximidad
|
||||
Angular Guides Guías angulares
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Promediar y unir…
|
||||
Average & Join Promediar y unir
|
||||
anchor Points: Puntos:
|
||||
@msg select two open path ends, or two open paths, to average and join selecciona dos extremos de trazados abiertos, o dos trazados abiertos, para promediarlos y unirlos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5725,3 +5725,8 @@ axis Y Axis Y-akseli
|
||||
Glyph Bounds Merkin rajat
|
||||
Proximity Guides Läheisyysapuviivat
|
||||
Angular Guides Kulma-apuviivat
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Keskiarvota ja liitä…
|
||||
Average & Join Keskiarvota ja liitä
|
||||
anchor Points: Pisteet:
|
||||
@msg select two open path ends, or two open paths, to average and join valitse kaksi avoimen polun päätä tai kaksi avointa polkua keskiarvotettaviksi ja liitettäviksi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5693,3 +5693,8 @@ axis Y Axis Axe Y
|
||||
Glyph Bounds Limites du glyphe
|
||||
Proximity Guides Repères de proximité
|
||||
Angular Guides Repères angulaires
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Alignement et jonction…
|
||||
Average & Join Alignement et jonction
|
||||
anchor Points: Points :
|
||||
@msg select two open path ends, or two open paths, to average and join sélectionnez deux extrémités de tracés ouverts, ou deux tracés ouverts, à aligner et joindre
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5680,3 +5680,8 @@ axis Y Axis Y tengely
|
||||
Glyph Bounds Glifahatárok
|
||||
Proximity Guides Közelségi segédvonalak
|
||||
Angular Guides Szög-segédvonalak
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Átlagolás és összekapcsolás…
|
||||
Average & Join Átlagolás és összekapcsolás
|
||||
anchor Points: Pontok:
|
||||
@msg select two open path ends, or two open paths, to average and join jelölj ki két nyitott útvonalvéget vagy két nyitott útvonalat az átlagoláshoz és összekapcsoláshoz
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5599,3 +5599,8 @@ axis Y Axis Sumbu Y
|
||||
Glyph Bounds Batas glif
|
||||
Proximity Guides Panduan kedekatan
|
||||
Angular Guides Panduan sudut
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Rata-rata dan sambungkan…
|
||||
Average & Join Rata-rata dan sambungkan
|
||||
anchor Points: Titik:
|
||||
@msg select two open path ends, or two open paths, to average and join pilih dua ujung jalur terbuka, atau dua jalur terbuka, untuk dirata-ratakan dan disambungkan
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2169 and column 5.
|
@@ -5310,3 +5310,8 @@ axis Y Axis Asse Y
|
||||
Glyph Bounds Limiti del glifo
|
||||
Proximity Guides Guide di prossimità
|
||||
Angular Guides Guide angolari
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Media e unisci…
|
||||
Average & Join Media e unisci
|
||||
anchor Points: Punti:
|
||||
@msg select two open path ends, or two open paths, to average and join seleziona due estremità di tracciati aperti, o due tracciati aperti, da mediare e unire
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5372,3 +5372,8 @@ axis Y Axis Y 軸
|
||||
Glyph Bounds 字形の境界
|
||||
Proximity Guides 近接ガイド
|
||||
Angular Guides 角度ガイド
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… 平均と連結…
|
||||
Average & Join 平均と連結
|
||||
anchor Points: ポイント:
|
||||
@msg select two open path ends, or two open paths, to average and join 平均と連結には、オープンパスの端点を 2 つ、またはオープンパスを 2 つ選択してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5683,3 +5683,8 @@ swatch Bright Violet კაშკაშა იისფერი
|
||||
Glyph Bounds გლიფის საზღვრები
|
||||
Proximity Guides სიახლოვის დამხმარე ხაზები
|
||||
Angular Guides კუთხური დამხმარე ხაზები
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… საშუალო და შეერთება…
|
||||
Average & Join საშუალო და შეერთება
|
||||
anchor Points: წერტილები:
|
||||
@msg select two open path ends, or two open paths, to average and join მონიშნეთ ღია ტრაექტორიების ორი ბოლო ან ორი ღია ტრაექტორია საშუალოსა და შეერთებისთვის
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5273,3 +5273,8 @@ axis Y Axis Y축
|
||||
Glyph Bounds 글리프 경계
|
||||
Proximity Guides 근접 안내선
|
||||
Angular Guides 각도 안내선
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… 평균 및 결합…
|
||||
Average & Join 평균 및 결합
|
||||
anchor Points: 포인트:
|
||||
@msg select two open path ends, or two open paths, to average and join 평균 및 결합할 열린 패스의 끝점 두 개 또는 열린 패스 두 개를 선택하세요
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5643,3 +5643,8 @@ axis Y Axis Y-as
|
||||
Glyph Bounds Glyphbegrenzing
|
||||
Proximity Guides Nabijheidshulplijnen
|
||||
Angular Guides Hoekhulplijnen
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Gemiddelde en verbinden…
|
||||
Average & Join Gemiddelde en verbinden
|
||||
anchor Points: Punten:
|
||||
@msg select two open path ends, or two open paths, to average and join selecteer twee uiteinden van open paden, of twee open paden, om te middelen en te verbinden
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5548,3 +5548,8 @@ axis Y Axis Oś Y
|
||||
Glyph Bounds Granice glifów
|
||||
Proximity Guides Linie bliskości
|
||||
Angular Guides Linie kątowe
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Uśrednij i połącz…
|
||||
Average & Join Uśrednij i połącz
|
||||
anchor Points: Punkty:
|
||||
@msg select two open path ends, or two open paths, to average and join zaznacz dwa końce otwartych ścieżek lub dwie otwarte ścieżki do uśrednienia i połączenia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5611,3 +5611,8 @@ axis Y Axis Eixo Y
|
||||
Glyph Bounds Limites do Glifo
|
||||
Proximity Guides Guias de Proximidade
|
||||
Angular Guides Guias Angulares
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Média e união…
|
||||
Average & Join Média e união
|
||||
anchor Points: Pontos:
|
||||
@msg select two open path ends, or two open paths, to average and join selecione duas extremidades de traçados abertos, ou dois traçados abertos, para calcular a média e unir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5672,3 +5672,8 @@ theme Appearance Оформление
|
||||
Glyph Bounds Границы глифа
|
||||
Proximity Guides Направляющие близости
|
||||
Angular Guides Угловые направляющие
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Усреднить и соединить…
|
||||
Average & Join Усреднить и соединить
|
||||
anchor Points: Точки:
|
||||
@msg select two open path ends, or two open paths, to average and join выделите два конца открытых контуров или два открытых контура для усреднения и соединения
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5718,3 +5718,8 @@ axis Y Axis Y-axel
|
||||
Glyph Bounds Glyfgränser
|
||||
Proximity Guides Närhetsstödlinjer
|
||||
Angular Guides Vinkelstödlinjer
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Medelvärde och foga samman…
|
||||
Average & Join Medelvärde och foga samman
|
||||
anchor Points: Punkter:
|
||||
@msg select two open path ends, or two open paths, to average and join markera två ändar av öppna banor, eller två öppna banor, att medelvärdesberäkna och foga samman
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1925,6 +1925,7 @@ const RENDER_SITE_ROWS: &[(&str, &str)] = &[
|
||||
("file-filter", "PDF-compatible .ai"),
|
||||
("file-filter", "VectorCraft Template"),
|
||||
("file-filter", "Templates"),
|
||||
("anchor", "Points:"),
|
||||
("diagnostics", "render"),
|
||||
("diagnostics", "ui"),
|
||||
("", "Preset {number}"),
|
||||
|
||||
@@ -5652,3 +5652,8 @@ axis Y Axis Y ekseni
|
||||
Glyph Bounds Glif Sınırları
|
||||
Proximity Guides Yakınlık Kılavuzları
|
||||
Angular Guides Açısal Kılavuzlar
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Ortalama ve katıl…
|
||||
Average & Join Ortalama ve katıl
|
||||
anchor Points: Noktalar:
|
||||
@msg select two open path ends, or two open paths, to average and join ortalaması alınıp katılmak için iki açık yol ucu veya iki açık yol seçin
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5290,3 +5290,8 @@ axis Y Axis Вісь Y
|
||||
Glyph Bounds Межі гліфа
|
||||
Proximity Guides Напрямні близькості
|
||||
Angular Guides Кутові напрямні
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Усереднити і з’єднати…
|
||||
Average & Join Усереднити і з’єднати
|
||||
anchor Points: Точки:
|
||||
@msg select two open path ends, or two open paths, to average and join виділіть два кінці відкритих контурів або два відкриті контури для усереднення і з’єднання
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -5292,3 +5292,8 @@ axis Y Axis Trục Y
|
||||
Glyph Bounds Giới hạn glyph
|
||||
Proximity Guides Đường căn lân cận
|
||||
Angular Guides Đường căn góc
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… Trung bình và nối…
|
||||
Average & Join Trung bình và nối
|
||||
anchor Points: Điểm:
|
||||
@msg select two open path ends, or two open paths, to average and join hãy chọn hai đầu của đường dẫn mở, hoặc hai đường dẫn mở, để lấy trung bình và nối
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -5533,3 +5533,8 @@ axis Y Axis Y 轴
|
||||
Glyph Bounds 字形边界
|
||||
Proximity Guides 邻近参考线
|
||||
Angular Guides 角度参考线
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… 平均并连接…
|
||||
Average & Join 平均并连接
|
||||
anchor Points: 点:
|
||||
@msg select two open path ends, or two open paths, to average and join 请选择两个开放路径的端点或两条开放路径以进行平均并连接
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5329,3 +5329,8 @@ axis Y Axis Y 軸
|
||||
Glyph Bounds 字符邊界
|
||||
Proximity Guides 鄰近參考線
|
||||
Angular Guides 角度參考線
|
||||
# Average & Join (Object › Path, Cmd+Alt+Shift+J).
|
||||
Average & Join… 平均並連接…
|
||||
Average & Join 平均並連接
|
||||
anchor Points: 點:
|
||||
@msg select two open path ends, or two open paths, to average and join 請選取兩個開放路徑的端點或兩條開放路徑以進行平均並連接
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -47,6 +47,9 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st
|
||||
"object.reflect" => ("reflect", json!({"axis": "vertical", "angle": 90})),
|
||||
"object.shear" => ("shear", json!({"angle": 0, "axis": "horizontal"})),
|
||||
"path.average" => ("average", json!({"axis": "both"})),
|
||||
// Average & Join opens on the joined anchor chosen last (Corner the first time).
|
||||
"path.averageJoin" if app.ui.average_join.is_object() => ("averageJoin", app.ui.average_join.clone()),
|
||||
"path.averageJoin" => ("averageJoin", json!({"to": "corner"})),
|
||||
// Offset Path opens on the values it was last confirmed with.
|
||||
"object.path.offsetPath" if app.ui.offset_path.is_object() => ("offsetPath", app.ui.offset_path.clone()),
|
||||
"object.path.offsetPath" => ("offsetPath", json!({"offset": "10 pt", "joins": "miter", "miterLimit": 4})),
|
||||
|
||||
@@ -218,6 +218,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
vec![
|
||||
c("Join", "path.join"),
|
||||
c("Average…", "path.average"),
|
||||
c("Average & Join…", "path.averageJoin"),
|
||||
Sep,
|
||||
c("Outline Stroke", "object.path.outlineStroke"),
|
||||
c("Offset Path…", "object.path.offsetPath"),
|
||||
|
||||
@@ -904,7 +904,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"ui.menuDialog",
|
||||
"Menu Dialog",
|
||||
"",
|
||||
"{command: object.move|object.rotate|object.scale|object.reflect|object.shear|object.transformEach|path.average|object.path.offsetPath|object.path.simplify|object.path.smooth|object.path.splitIntoGrid|object.vectorHalftone|artboard.rearrange|object.envelope.makeWithWarp|object.envelope.resetWithWarp|object.envelope.makeWithMesh|object.envelope.resetWithMesh|object.envelope.options} open the dialog that command's menu item opens (dialog kind: move, rotate, scale, reflect, shear, transformEach, …, vectorHalftone, rearrangeArtboards, envelopeWarp, envelopeMesh, envelopeOptions; Scale and Transform Each have `corners` and `strokes`, from the preferences, which OK updates; Transform Each opens on the values it was last confirmed with (not `copy`), and its Copy button confirms with `copy: true`; the envelope dialogs start from the selected envelope, Make opens as Reset (`reset: true`) while one is selected)",
|
||||
"{command: object.move|object.rotate|object.scale|object.reflect|object.shear|object.transformEach|path.average|path.averageJoin|object.path.offsetPath|object.path.simplify|object.path.smooth|object.path.splitIntoGrid|object.vectorHalftone|artboard.rearrange|object.envelope.makeWithWarp|object.envelope.resetWithWarp|object.envelope.makeWithMesh|object.envelope.resetWithMesh|object.envelope.options} open the dialog that command's menu item opens (dialog kind: move, rotate, scale, reflect, shear, transformEach, …, vectorHalftone, rearrangeArtboards, envelopeWarp, envelopeMesh, envelopeOptions; Scale and Transform Each have `corners` and `strokes`, from the preferences, which OK updates; Transform Each opens on the values it was last confirmed with (not `copy`), and its Copy button confirms with `copy: true`; the envelope dialogs start from the selected envelope, Make opens as Reset (`reset: true`) while one is selected)",
|
||||
),
|
||||
(
|
||||
"ui.widthPointEdit",
|
||||
|
||||
@@ -234,6 +234,46 @@ fn cmd_alt_shift_3_hides_unselected_artwork() {
|
||||
assert_eq!(shown(&app), [true, true], "nothing is hidden in isolation mode");
|
||||
}
|
||||
|
||||
/// Cmd+Alt+Shift+J opens Average & Join's dialog, Points: Corner the first time. Cancel changes
|
||||
/// nothing; OK averages the selected lines' nearest ends and joins them there; the dialog opens
|
||||
/// again on the choice confirmed last.
|
||||
#[test]
|
||||
fn cmd_alt_shift_j_opens_average_and_join() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
assert_eq!(crate::menus::shortcut_of("path.averageJoin"), Some("Cmd+Alt+Shift+J"));
|
||||
let line = |app: &mut VectorcraftApp, x1: f64, x2: f64| {
|
||||
app.session.execute("shape.line", &json!({"x1": x1, "y1": 10, "x2": x2, "y2": 10})).unwrap()["id"].as_u64().unwrap()
|
||||
};
|
||||
let (a, b) = (line(&mut app, 10.0, 50.0), line(&mut app, 60.0, 100.0));
|
||||
app.session.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let j = || egui::Event::Key {
|
||||
key: Key::J,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: Modifiers::COMMAND | Modifiers::ALT | Modifiers::SHIFT,
|
||||
};
|
||||
frame(&mut app, vec![j()]);
|
||||
let d = app.ui.dialog.as_ref().expect("the dialog opened");
|
||||
assert_eq!((d.kind.as_str(), d.str("to").as_str()), ("averageJoin", "corner"));
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "Cancel changes nothing");
|
||||
frame(&mut app, vec![j()]);
|
||||
app.ui.dialog.as_mut().expect("the dialog opened again").fields.insert("to".into(), json!("smooth"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
assert!(doc.node(vectorcraft_doc::NodeId(b)).is_none(), "joined into one path");
|
||||
let path = doc.node(vectorcraft_doc::NodeId(a)).unwrap().path_data().unwrap();
|
||||
let pts: Vec<(f64, f64)> = path.subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect();
|
||||
assert_eq!(pts, [(10.0, 10.0), (55.0, 10.0), (100.0, 10.0)]);
|
||||
frame(&mut app, vec![j()]);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.str("to")).as_deref(), Some("smooth"), "the choice confirmed last");
|
||||
}
|
||||
|
||||
/// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724).
|
||||
#[test]
|
||||
fn cmd_shift_b_while_typing_is_bold_not_the_bounding_box() {
|
||||
|
||||
@@ -541,6 +541,9 @@ pub struct UiState {
|
||||
/// dialog opens on them (#1152).
|
||||
#[serde(default)]
|
||||
pub offset_path: Value,
|
||||
/// The Average & Join fields confirmed last (`to`; null: never used): its dialog opens on them.
|
||||
#[serde(default)]
|
||||
pub average_join: Value,
|
||||
/// The Simplify dialog's fields confirmed last with "Open this dialog with these settings next
|
||||
/// time" ticked (null: not ticked): Object › Path › Simplify opens the dialog on them instead
|
||||
/// of the Simplify slider.
|
||||
@@ -878,6 +881,7 @@ impl Default for UiState {
|
||||
dxf_options: Value::Null,
|
||||
eps_options: Value::Null,
|
||||
offset_path: Value::Null,
|
||||
average_join: Value::Null,
|
||||
simplify: Value::Null,
|
||||
move_options: Value::Null,
|
||||
transform_each: Value::Null,
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -131,7 +131,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
- **Done (#1251):** Eyedropper source reads are independent of locks and isolation, including Shift/Shift+Alt and gradient stops. Source and selection preservation, unchanged Alt reverse/selection restrictions, hidden art and Undo/Redo have regression coverage. This addresses the reported behavior, with no new full-parity measurement.
|
||||
- **Done (#1253):** adding paint to an unpainted zero-width stroke restores the existing 1 pt default, including new-art defaults and Swap Fill/Stroke. The real Eyedropper inheritance sequence, targeted appearance items, preserved widths and Undo/Redo have regression coverage.
|
||||
- **Done:** a busy cursor while a file opens off the UI thread or a labelled background save or export runs (`Cursor::Busy` in `crates/tools/src/cursors.toml`). Heavy synchronous effect previews remain open.
|
||||
- **Missing:** Average and Join in one step, with a corner or smooth choice for the joined anchor. `path.average` and `path.join` are separate commands and Join has no such choice (coinciding anchors merge into one since #1533). [ui-parity.md](ui-parity.md) listed "Join with corner/smooth choice" as done until 2026-10-13. About 1–2 h (estimated), inside the estimate below.
|
||||
- **Done:** Average and Join in one step, with a corner or smooth choice for the joined anchor: Object › Path › Average & Join… (`path.averageJoin {to: "corner"|"smooth"}`, Cmd+Alt+Shift+J, Ctrl+Alt+Shift+J on Windows) moves the two direct-selected open path ends, or the nearest ends of two selected open paths (both ends of one, closing it), to their midpoint and joins them there in one undo step. The public [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists the shortcut as "Create corner or smooth join" ("Select the paths" on Windows, "Select the anchor point" on macOS). The dialog (Points: Corner / Smooth) opens on Corner and then on the choice confirmed last in the session: VectorCraft's reading, as the page doesn't say. Inside the estimate below.
|
||||
- **Implemented (#1760):** Effect › Warp has a shared style selector with original previews, orientation buttons and percent sliders. Existing effects retain their stack slot and target. Focused checks passed and original captures were reviewed; optional Properties integration and remembered defaults remain outside this change.
|
||||
- **Done (#2120):** Smart Guides line extension: dragging the end of a straight segment with Direct Selection or Curvature, the point snaps to the segment's own direction at any angle and the guide runs along it from the other end. Inside the estimate below.
|
||||
- **Done (#1692):** Tab in the Color Picker moves through one color model's fields before the next. Completed upstream before the layout change (#2008).
|
||||
@@ -653,6 +653,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | G1: Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; the "Average and Join in one step" entry is done. Estimate unchanged |
|
||||
| 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act. Estimate unchanged. |
|
||||
| 2026-10-29 | minor | G10: the Linux Flatpak on GNOME stops the clipboard retry storm (retried with a growing wait, logged once). Estimate unchanged. |
|
||||
| 2026-10-29 | minor | G12: Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act. No entry closed, estimates unchanged. |
|
||||
|
||||
+4
-1
@@ -2582,7 +2582,10 @@ or pulls handles out in line with the neighbouring anchors (a smooth anchor keep
|
||||
on top of each other, and an open path becomes one path per piece. Each cut leaves one of its two anchors selected,
|
||||
so `path.moveAnchors {dx, dy}` (or a Direct Selection drag) pulls the path apart there; `path.join {}` (Connect
|
||||
Selected End Points) joins the ends again; `path.join {ids: [a, b], ends: ["last", "first"]}` joins the ends asked for
|
||||
(the Pen's join, #776: drawing on from one open path, a click on another's end makes them one path). `path.convertAnchor {id, subpath?, anchor, to, x?, y?}` and
|
||||
(the Pen's join, #776: drawing on from one open path, a click on another's end makes them one path). `path.averageJoin {to: "corner"|"smooth", ids?}`
|
||||
(Average & Join, Cmd+Alt+Shift+J) moves two ends to their midpoint and joins them there, the joined anchor a corner or a smooth point, in one
|
||||
undo step: the two direct-selected anchors when they are open path ends, else the nearest ends of the two open paths in `ids` or the
|
||||
selection (both ends of one, closing it); anything else is refused. `path.convertAnchor {id, subpath?, anchor, to, x?, y?}` and
|
||||
`path.split {id, subpath?, anchor}` do the same to one anchor (the Anchor Point and Scissors tools); the Pen with Alt
|
||||
held over a selected path's handle, anchor or segment works as the Anchor Point tool. `press_key` Escape between two
|
||||
`pointer_gesture` calls of an Anchor Point drag cancels it: the path goes back and no undo step is added. `path.reshapeSegment {id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (UI chrome: Preferences audit (#394), 5 options stored but not acted on; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Tools: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -699,7 +699,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). A link toggle behind Corner Radius (the `singleCornerRadius` preference) switches the panels to a field for each corner of a live rectangle: Top Left, Top Right, Bottom Right and Bottom Left. Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two sides meet at an angle at an anchor that isn't smooth: curved sides too, trimmed to what the cut leaves of them (#1144). Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply. With Direct Selection a click on a widget (no drag) selects its corner, as a click on the corner's anchor does, so the next drag on it rounds that corner alone; Shift-click toggles it (#1239). While a corner widget is dragged with the Selection tool the bounding box hides, so the rounding shows against the path alone (#1229). A round corner next to a curved side curves smoothly instead of sticking out of the shape (#1547, fixes #1512).
|
||||
View › Hide Edges hides the Live Corners widgets, the bounding box and the type widget, and the selection tools then do not answer a press on them or change the cursor over them.
|
||||
- **Blend spine Anchor Point tool (#2544):** on a selected blend, dragging a spine corner makes symmetric Smooth handles; the tool displays and picks existing spine handles when switched to, drags one independently or retracts it on click, and clicking a point with handles retracts both. The spine is edited live without moving its key objects. Windows native canvas checks and screenshots reviewed: symmetric and independent handles, corner/retraction, Shift constraints and perpendicular reshaping, tool switching, key preservation, Undo/Redo, save/reopen, hidden handles and overlapping Path/Blend priority (PR #2643). Behaviour reference: [public path-editing documentation](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Average and Join in one step, with a corner or smooth choice for the joined anchor, is missing ([gap 1](gaps.md)).
|
||||
- **Path editing (#494):** the handles shown are the handles that drag: Direct Selection drags those of a path selected as a whole (as the Pen leaves it), the Anchor Point tool and Pen+Alt no longer grab handles that Show handles when multiple anchors are selected hides, a handle shorter than the tolerance leaves its anchor to be picked, and the anchor tools pick within Selection & Anchor Display › Tolerance. A dragged segment keeps a smooth anchor at either end smooth. With Direct Selection, the Pen, the Curvature and the anchor tools, a path selected as a whole shows all its anchors selected, and the Control bar and the Properties panel offer Convert (corner, smooth) for them too; Remove and Cut wait for anchors direct-selected (#504). A handle drag gathers its snap targets once per drag (it took 21 ms per pointer move on a 20,000-path document, now 1 ms; `vectorcraft-cli perf` measures handle and segment drags on a 5,000-anchor path). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J, the "Create corner or smooth join" shortcut of [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)) moves two open path ends to their midpoint and joins them there, a corner or smooth point as its dialog's Points choice asks, in one undo step.
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. A series starts in greyscale; Group Selection's fill and stroke are kept separately on the graph and reapplied when its data, type or size changes, and through save, open and undo. Graph Type's Override Calculated Values (both `axisMin` and `axisMax`) uses exactly that value-axis range, split into the tick count (5 when automatic). Line graphs take Edge-to-Edge Lines (off by default: the points sit at the centres of their categories; graphs saved before the option keep their edge-to-edge lines), and radar graphs follow Mark Data Points and Connect Data Points. Blank cells (an empty CSV cell or `null`) are values left out, not zeros: columns, bars and stacked segments skip them, lines and scatter lines break around them and the value axis ignores them, while area, pie and radar draw them as 0; a number in straight quotes is a label, and the data window writes it back quoted. Graph types combine: Graph Type on series picked with Group Selection (or `series` indexes) draws them as their own type, with column, stacked column, line and area mixing on the vertical value axis and bar and stacked bar on the horizontal one; stacked columns take one slot of the cluster, lines draw in front of columns and areas behind. The value axis goes on the left, the right or both sides (column, stacked column, line, area and scatter graphs; bar graphs keep theirs along the bottom), and Separate Scales gives the series assigned to the right axis a scale of their own with its own tick values. Each value axis takes tick marks of no, short or full (across the plot) length and a count per division, and a prefix and suffix for its numbers; the category axis takes tick marks too (none by default), at or between its labels. Graph designs: Object › Graph › Design lists the designs with a preview of the selected one, and New Design (the selected art), Rename, Delete Design, Paste Design (a copy back to edit) and Select Unused, and Marker draws the data points and legend swatches of line, scatter and radar series with a design, scaled so its backmost object fills the default marker's square. Graph Data transposes rows and columns and switches a scatter graph's x and y, and a `|` in a category or series label starts a new line. Pie graphs take Legends in Wedges, Position (Even, Ratio sized by area, Stacked on one centre) and Sort (None, All, First). Graph Type takes column and cluster widths up to 1000% (overlapping past 100%) with First Row and First Column in Front (First Row orders each series' categories; across series the series order decides, so series stay one group each for Group Selection), draws filled lines, adds drop shadows behind columns, bars, lines and wedges, and puts the legend in rows across the top. Text formatted with the Character or Appearance controls, on the whole graph or on labels picked with Group Selection, is kept per kind of label (value axis numbers, category labels, legend labels) through the graph being drawn again. Object › Graph › Data opens a worksheet: cells picked with a click and edited above the sheet (Tab moves right, Enter down, the arrows between cells), Import Data from a tab-separated text file, Transpose, Switch x/y, Cell Style (decimals shown, column width), Revert and Apply. `graph.setData` returns and takes the data as CSV with each value written exactly, not rounded to two decimals, so applying the returned CSV changes nothing (#1485).
|
||||
- **Graph column designs:** Object › Graph › Column (`graph.column`) draws the columns, bars or stacked segments and the legend swatch of the series picked with Group Selection with a graph design: Vertically Scaled stretches it to each column's length, Uniformly Scaled scales it both ways and centers it, Repeating stacks a copy for each Each Design Represents value and chops or scales the last one (For Fractions), and Sliding keeps the part above the design's guide line in proportion and stretches the part below. The dialog lists the designs with a preview; the choice is saved with the graph and follows the design's rename and delete.
|
||||
Type in a column design that holds a `%` followed by two digits shows each column's value in place of the `%` and the digits: the first digit is the least number of places before the decimal point (zeros are added in front; 0 gives as many as the value needs), and the second digit sets the places after it, rounded. Unless it is inside a group that clips or has its own opacity or appearance, or inside a blend, compound shape, envelope, repeat or intertwined objects, the type keeps its size and is drawn once per column in a repeating design. The legend swatch leaves it out.
|
||||
@@ -1206,6 +1206,7 @@ applies to fills). Tests: `color::gradient::tests` (perceptual midpoint, expansi
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Tools: Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; evidence added; scores and hours unchanged (estimated). |
|
||||
| 2026-10-30 | minor | UI chrome: G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; evidence added; score and hours unchanged (estimated). |
|
||||
| 2026-10-30 | minor | Print, colour management, separations: Separations Preview's plates apply to placed and embedded raster images, a CMYK image's own inks included (#4153); evidence added; score and hours unchanged (estimated). |
|
||||
| 2026-10-30 | minor | Colour, gradients & mesh: several gradient mesh points can be picked (Shift-clicks with the Mesh tool and Direct Selection, Direct Selection's marquee, its click on a mesh line or inside a patch); a drag or the arrow keys move them all (`object.mesh.movePoints`), and a solid fill colour or the Transparency panel's opacity goes to each of them (#4207, #4208, #4170); scores unchanged. |
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J; one row done; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -95,7 +95,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity goes down to 0.01 pt in the dialog, the tool and `path.freehand` (the Smooth tool keeps 0.05); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 pt, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 pt, tenths up to 1 pt and whole points above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path |
|
||||
| Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533) |
|
||||
| Average and Join in one step, with a corner or smooth choice for the joined anchor | partial | `path.join` accepts `to: corner|smooth` on the merged anchor; Average and Join are still separate commands; [gap 1](gaps.md) |
|
||||
| Average and Join in one step, with a corner or smooth choice for the joined anchor | done | Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J, "Create corner or smooth join" in [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): two open path ends meet at their midpoint and join, a corner or smooth point as the dialog's Points choice asks (Corner first, then the last choice), in one undo step |
|
||||
| Smooth tool smooths only the stretch of path under the drag | done | #1512 |
|
||||
| Drawing modes apply to pastes: Draw Inside takes Paste, Paste in Place and Paste on All Artboards inside the object; Draw Behind puts Paste in Place and Paste on All Artboards behind the selection; Shift+D cycles past Draw Behind with any selection ([drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) | done | #3705; `pasting_in_draw_inside_puts_the_copies_inside_the_object`, `paste_in_place_in_draw_behind_goes_behind_the_selection`, `shift_d_goes_past_draw_behind_whatever_is_selected` |
|
||||
| Simplify (slider, auto, corner angle threshold), of a whole path or of the part of it selected with Direct Selection ([manually simplify paths](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html)) | done | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; the slider under the selection with Auto-Simplify and More Options, opening at the Auto-Simplify level for the selection with that result previewed, and the dialog's option to open it directly with its last settings. With anchors or segments direct-selected, the slider, Auto-Simplify and the dialog refit only each run of selected segments between its end anchors, a closed path's run across its start point included; the rest of the path stays exactly as it was (`tests/simplify_part.rs`, `simplify_slider_and_dialog_simplify_only_the_direct_selected_part_of_a_path`) |
|
||||
@@ -361,6 +361,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; one row done; scores and estimates unchanged. |
|
||||
| 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Preferences: the workspace, panels, toolbars and other UI settings are saved within 2 seconds of a change instead of only on quitting, and a window saves only what it changed, merged into the file as another VectorCraft window left it, so a second window closing last no longer puts back the settings it started with (#4625). Tests `prefs::tests` in `apps/vectorcraft`; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user