mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
feat: change and reset the ruler origin (#1212)
This commit is contained in:
1 parent
932c127323
commit
29ecab8c48
25 files changed
+531
-40
No files matched your search
@@ -551,6 +551,9 @@ pub struct Document {
|
||||
pub template: bool,
|
||||
#[serde(default)]
|
||||
pub units: Unit,
|
||||
/// The rulers' zero point in document coordinates. Moving it changes measurements, never art.
|
||||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||||
pub ruler_origin: Point,
|
||||
#[serde(default)]
|
||||
pub color_mode: ColorMode,
|
||||
pub artboards: Vec<Artboard>,
|
||||
@@ -706,6 +709,7 @@ impl Document {
|
||||
title: "Untitled-1".into(),
|
||||
template: false,
|
||||
units: Unit::Points,
|
||||
ruler_origin: Point::ZERO,
|
||||
color_mode: mode,
|
||||
artboards: vec![Artboard {
|
||||
id: 1,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, Node, NodeId, Unit};
|
||||
use vectorcraft_geom::{Affine, Vec2};
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
|
||||
use super::clipboard::SwatchChoices;
|
||||
use super::*;
|
||||
@@ -37,6 +37,25 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
set_units
|
||||
),
|
||||
cmd!(query "document.rulerOrigin", "Ruler Origin", [], None, "{} → {x, y} the rulers' zero point, in absolute document points", has_doc, |s, _| Ok(json!(s.doc()?.doc.ruler_origin))),
|
||||
cmd!(
|
||||
"document.setRulerOrigin",
|
||||
"Ruler Origin",
|
||||
[],
|
||||
None,
|
||||
"{x, y} set the rulers' zero point in absolute document points; art, guides and command coordinates stay in place",
|
||||
has_doc,
|
||||
set_ruler_origin
|
||||
),
|
||||
cmd!(
|
||||
"document.resetRulerOrigin",
|
||||
"Reset Ruler Origin",
|
||||
[],
|
||||
None,
|
||||
"{artboard?: index=0} reset the rulers' zero point to that artboard's top-left corner",
|
||||
has_doc,
|
||||
reset_ruler_origin
|
||||
),
|
||||
cmd!("edit.undo", "Undo", ["Edit"], Some("Cmd+Z"), "{}", can_undo, undo),
|
||||
cmd!("edit.redo", "Redo", ["Edit"], Some("Cmd+Shift+Z"), "{}", can_redo, redo),
|
||||
cmd!(
|
||||
@@ -180,6 +199,35 @@ fn set_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
fn set_ruler_origin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "document.setRulerOrigin";
|
||||
let origin = Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?);
|
||||
change_ruler_origin(s, origin, "Ruler Origin")
|
||||
}
|
||||
|
||||
fn reset_ruler_origin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "document.resetRulerOrigin";
|
||||
let index = match p.get("artboard") {
|
||||
None => 0,
|
||||
Some(v) => v.as_u64().and_then(|n| usize::try_from(n).ok()).ok_or_else(|| bad(C, "index out of range"))?,
|
||||
};
|
||||
let ab = s.doc()?.doc.artboards.get(index).ok_or_else(|| bad(C, "index out of range"))?;
|
||||
change_ruler_origin(s, Point::new(ab.rect.x0, ab.rect.y0), "Reset Ruler Origin")
|
||||
}
|
||||
|
||||
fn change_ruler_origin(s: &mut Session, origin: Point, label: &str) -> Result<Value> {
|
||||
if !origin.is_finite() || origin.x.abs() > crate::MAX_COORD || origin.y.abs() > crate::MAX_COORD {
|
||||
return Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()));
|
||||
}
|
||||
if s.doc()?.doc.ruler_origin != origin {
|
||||
s.edit(label, |d, _| {
|
||||
d.ruler_origin = origin;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!(origin))
|
||||
}
|
||||
|
||||
/// A typing session in progress (the Type tool previews the whole session as one interaction).
|
||||
pub(crate) fn typing_in_progress(s: &Session) -> bool {
|
||||
s.active().and_then(|d| d.interaction.as_ref()).is_some_and(|it| it.label == "Typing" && it.preview.is_some())
|
||||
|
||||
@@ -556,10 +556,15 @@ fn strip_replay_only(id: &str, p: &mut Value) {
|
||||
/// have finite, in-range geometry, so saved files always reload and renderers never see NaN/∞.
|
||||
fn doc_sane(d: &Document, sel: &Selection) -> bool {
|
||||
let ok = |r: vectorcraft_geom::Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD);
|
||||
d.artboards.iter().all(|a| ok(a.rect)) && d.guides.iter().all(|g| g.pos.is_finite() && g.pos.abs() <= MAX_COORD) && {
|
||||
let paths = d.index_paths(sel.objects.iter().copied());
|
||||
sel.objects.iter().all(|id| paths.get(id).and_then(|p| d.node_at(p)).and_then(|n| n.geometric_bounds()).is_none_or(ok))
|
||||
}
|
||||
d.ruler_origin.is_finite()
|
||||
&& d.ruler_origin.x.abs() <= MAX_COORD
|
||||
&& d.ruler_origin.y.abs() <= MAX_COORD
|
||||
&& d.artboards.iter().all(|a| ok(a.rect))
|
||||
&& d.guides.iter().all(|g| g.pos.is_finite() && g.pos.abs() <= MAX_COORD)
|
||||
&& {
|
||||
let paths = d.index_paths(sel.objects.iter().copied());
|
||||
sel.objects.iter().all(|id| paths.get(id).and_then(|p| d.node_at(p)).and_then(|n| n.geometric_bounds()).is_none_or(ok))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where new art goes (Illustrator's drawing modes, Shift+D cycles).
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! #1212: ruler coordinates are saved metadata, independent of the artwork's coordinates.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_format::{SaveOptions, load, save, save_with};
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 100, "y": 50, "width": 80, "height": 40})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 125})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn origin(s: &mut Session) -> Value {
|
||||
s.execute("document.rulerOrigin", &json!({})).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_origin_preserves_artwork_and_selection_and_is_one_undo_step() {
|
||||
let mut s = session();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
assert_eq!(origin(&mut s), json!({"x": 0.0, "y": 0.0}));
|
||||
let p = json!({"x": 125.5, "y": -30.25});
|
||||
s.execute("document.setRulerOrigin", &p).unwrap();
|
||||
assert_eq!(origin(&mut s), p);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
let mut changed = s.execute("document.json", &json!({})).unwrap();
|
||||
changed.as_object_mut().unwrap().remove("ruler_origin");
|
||||
assert_eq!(changed, serde_json::to_value(&*before).unwrap(), "only the origin changed");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
let revision = s.doc().unwrap().revision;
|
||||
s.execute("document.setRulerOrigin", &p).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "same origin is a no-op");
|
||||
assert_eq!(s.doc().unwrap().revision, revision);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(&*s.doc().unwrap().doc, &*before);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(origin(&mut s), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_uses_the_requested_artboards_top_left_and_ignores_noops() {
|
||||
let mut s = session();
|
||||
s.execute("artboard.new", &json!({"x": 700, "y": -250, "width": 200, "height": 150})).unwrap();
|
||||
s.execute("document.setRulerOrigin", &json!({"x": 25, "y": 50})).unwrap();
|
||||
let boards = s.doc().unwrap().doc.artboards.clone();
|
||||
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
|
||||
assert_eq!(origin(&mut s), json!({"x": 700.0, "y": -250.0}));
|
||||
assert_eq!(s.doc().unwrap().doc.artboards, boards);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
for p in [json!({"artboard": 9}), json!({"artboard": -1}), json!({"artboard": "first"})] {
|
||||
assert!(s.execute("document.resetRulerOrigin", &p).is_err());
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
}
|
||||
s.execute("document.resetRulerOrigin", &json!({})).unwrap();
|
||||
assert_eq!(origin(&mut s), json!({"x": 0.0, "y": 0.0}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_versions_and_compression_keep_the_origin_and_old_files_default_to_zero() {
|
||||
let mut s = session();
|
||||
let legacy = String::from_utf8(save(&s.doc().unwrap().doc, false)).unwrap().replacen("\"format\":\"vectorcraft\"", "\"format\":\"drawcraft\"", 1);
|
||||
assert!(serde_json::to_value(load(legacy.as_bytes()).unwrap()).unwrap().get("ruler_origin").is_none());
|
||||
let p = json!({"x": -12.5, "y": 27.75});
|
||||
s.execute("document.setRulerOrigin", &p).unwrap();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
for (version, compress) in [(1, false), (2, false), (3, false), (4, false), (4, true)] {
|
||||
let bytes = save_with(doc, &SaveOptions { version, compress, ..Default::default() }).unwrap();
|
||||
let back = load(&bytes).unwrap();
|
||||
assert_eq!(serde_json::to_value(&back).unwrap()["ruler_origin"], p, "v{version}, compression {compress}");
|
||||
assert_eq!(back.layers, doc.layers);
|
||||
assert_eq!(back.guides, doc.guides);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_rejects_invalid_coordinates_without_changing_absolute_command_coordinates() {
|
||||
let mut s = session();
|
||||
s.execute("document.setRulerOrigin", &json!({"x": 200, "y": 150})).unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
for p in
|
||||
[json!({"x": 4_000_001, "y": 0}), json!({"x": 0, "y": -4_000_001}), json!({"x": "10", "y": 0}), json!({"x": 0}), json!({"x": null, "y": 0})]
|
||||
{
|
||||
assert!(s.execute("document.setRulerOrigin", &p).is_err(), "{p}");
|
||||
assert_eq!(&*s.doc().unwrap().doc, &*before);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
}
|
||||
s.execute("object.setBounds", &json!({"x": 80, "y": 40, "reference": 0})).unwrap();
|
||||
let bounds = s.doc().unwrap().doc.bounds_of(&s.doc().unwrap().selection.objects, false).unwrap();
|
||||
assert_eq!((bounds.x0, bounds.y0), (80.0, 40.0), "engine coordinates stay absolute");
|
||||
assert_eq!(origin(&mut s), json!({"x": 200.0, "y": 150.0}));
|
||||
}
|
||||
@@ -185,9 +185,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||||
ruler_guides(app, ui, full, rect, &xf);
|
||||
}
|
||||
let origin_preview = if app.ui.view.rulers && app.ui.screen_mode < 3 { ruler_input(app, ui, full, rect, &xf) } else { None };
|
||||
panel_drop(app, ui, &resp, &xf);
|
||||
context_menu(app, &resp, &xf);
|
||||
let painter = ui.painter_at(rect);
|
||||
@@ -435,7 +433,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
crate::place::paint_drop_highlight(app, ui.ctx(), &painter, rect);
|
||||
|
||||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||||
rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), &t);
|
||||
if let Some(origin) = origin_preview {
|
||||
let at = xf.to_screen(origin);
|
||||
painter.line_segment([pos2(at.x, rect.top()), pos2(at.x, rect.bottom())], Stroke::new(1.0, t.text_dim));
|
||||
painter.line_segment([pos2(rect.left(), at.y), pos2(rect.right(), at.y)], Stroke::new(1.0, t.text_dim));
|
||||
}
|
||||
rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), origin_preview.unwrap_or_else(|| app.ruler_origin()), &t);
|
||||
}
|
||||
if app.ui.task_bar && !revolve_edit && !app.session.tool_busy() && app.ui.screen_mode < 3 {
|
||||
task_bar(app, ui, &xf);
|
||||
@@ -1076,8 +1079,8 @@ fn ruler_rects(full: egui::Rect) -> [egui::Rect; 3] {
|
||||
/// A drag from a ruler onto the canvas makes a guide where the button is released (the engine's
|
||||
/// `Session::ruler_guide`): a horizontal one from the top ruler, a vertical one from the left
|
||||
/// ruler, snapped as a moved guide is (with Shift to the ruler's ticks). Released anywhere else,
|
||||
/// it makes none.
|
||||
fn ruler_guides(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egui::Rect, xf: &Xf) {
|
||||
/// it makes none. The corner drags the rulers' origin or resets it on a double-click.
|
||||
fn ruler_input(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egui::Rect, xf: &Xf) -> Option<Point> {
|
||||
let [top, left, corner] = ruler_rects(full);
|
||||
// Right-click on a ruler or the origin box: the document units, to swap between them as
|
||||
// Preferences ▸ Units ▸ General does ([`crate::menus::ruler_menu_body`]).
|
||||
@@ -1104,14 +1107,49 @@ fn ruler_guides(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egu
|
||||
app.status(e.to_string());
|
||||
}
|
||||
}
|
||||
let corner = ui.interact(corner, egui::Id::new("ruler-corner"), Sense::click());
|
||||
let corner = ui.interact(corner, egui::Id::new("ruler-corner"), Sense::click_and_drag());
|
||||
corner.context_menu(|ui| crate::menus::ruler_menu_body(app, ui, &mut unit_clicked));
|
||||
if let Some((id, p)) = unit_clicked {
|
||||
crate::menus::invoke(app, &id, p);
|
||||
}
|
||||
if corner.double_clicked_by(egui::PointerButton::Primary) {
|
||||
let artboard = app.view().map_or(0, |v| v.artboard);
|
||||
if let Err(e) = app.run("document.resetRulerOrigin", json!({"artboard": artboard})) {
|
||||
app.status(e.to_string());
|
||||
}
|
||||
}
|
||||
// Keep previews in the UI: only a release on the canvas edits the document, once. Cancelling
|
||||
// never adds an undo step or interferes with the drawing tool's interaction.
|
||||
let drag_id = egui::Id::new("ruler-origin-drag");
|
||||
let uid = app.session.active().map(|d| d.uid);
|
||||
if corner.drag_started_by(egui::PointerButton::Primary) {
|
||||
ui.data_mut(|d| d.insert_temp(drag_id, (uid, false)));
|
||||
}
|
||||
let mut preview = None;
|
||||
if let Some((started_in, mut cancelled)) = ui.data(|d| d.get_temp::<(Option<u64>, bool)>(drag_id)) {
|
||||
cancelled |= started_in != uid || ui.input(|i| !i.focused || i.key_pressed(egui::Key::Escape));
|
||||
let at = corner.interact_pointer_pos().or_else(|| ui.input(|i| i.pointer.latest_pos())).filter(|p| canvas.contains(*p)).map(|p| xf.to_doc(p));
|
||||
if corner.drag_stopped_by(egui::PointerButton::Primary) {
|
||||
ui.data_mut(|d| d.remove::<(Option<u64>, bool)>(drag_id));
|
||||
if !cancelled
|
||||
&& let Some(at) = at
|
||||
&& let Err(e) = app.run("document.setRulerOrigin", json!({"x": at.x, "y": at.y}))
|
||||
{
|
||||
app.status(e.to_string());
|
||||
}
|
||||
} else if !ui.input(|i| i.pointer.primary_down()) {
|
||||
ui.data_mut(|d| d.remove::<(Option<u64>, bool)>(drag_id));
|
||||
} else {
|
||||
ui.data_mut(|d| d.insert_temp(drag_id, (started_in, cancelled)));
|
||||
if !cancelled {
|
||||
preview = at;
|
||||
}
|
||||
}
|
||||
}
|
||||
preview
|
||||
}
|
||||
|
||||
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit, t: &Tokens) {
|
||||
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit, origin: Point, t: &Tokens) {
|
||||
let p = ui.painter();
|
||||
let [top, left, corner] = ruler_rects(full);
|
||||
for r in [top, left, corner] {
|
||||
@@ -1143,7 +1181,7 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
|
||||
// (#826): at 0° the top one reads x and the left one y. `draw` gets each tick's distance
|
||||
// (screen points) from `from` along the ruler, and its value in `unit`.
|
||||
let ticks = |from: Pos2, to: Pos2, axis: vectorcraft_geom::Vec2, draw: &mut dyn FnMut(f32, f64)| {
|
||||
let along = |s: Pos2| xf.to_doc(s).to_vec2().dot(axis);
|
||||
let along = |s: Pos2| (xf.to_doc(s) - origin).dot(axis);
|
||||
let (a, b) = (along(from), along(to));
|
||||
let mut v = (a / per / minor).floor() * minor;
|
||||
while v * per <= b {
|
||||
@@ -2168,7 +2206,7 @@ mod tests {
|
||||
let [top, left, _] = ruler_rects(full);
|
||||
for deg in [0.0_f64, 30.0, 90.0, 135.0, 200.0] {
|
||||
let xf = Xf { rect: full, zoom: 1.0, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, Unit::Points, &Tokens::get(ui.ctx())));
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, Unit::Points, Point::ZERO, &Tokens::get(ui.ctx())));
|
||||
out.textures_delta.clear();
|
||||
// The ticks' positions along each ruler.
|
||||
let along = |ruler: egui::Rect, x: bool| -> Vec<f32> {
|
||||
@@ -2199,6 +2237,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_zero_ticks_follow_the_origin_at_any_rotation_and_unit() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let full = egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
|
||||
let [top, left, _] = ruler_rects(full);
|
||||
let origin = Point::new(350.25, 250.5);
|
||||
for deg in [0.0_f64, 30.0, 90.0, 200.0] {
|
||||
for unit in [Unit::Points, Unit::Millimeters] {
|
||||
let xf = Xf { rect: full, zoom: 1.7, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, unit, origin, &Tokens::get(ui.ctx())));
|
||||
out.textures_delta.clear();
|
||||
let at = xf.to_screen(origin);
|
||||
for (r, horizontal) in [(top, true), (left, false)] {
|
||||
let zeros: Vec<_> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|c| c.clip_rect == r)
|
||||
.filter_map(|c| match &c.shape {
|
||||
Shape::Text(t) if t.galley.text() == "0" => Some(t.pos),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// Left-ruler labels are separate digits: a zero in "100" isn't the zero label.
|
||||
assert!(
|
||||
zeros.iter().any(|zero| {
|
||||
let delta = if horizontal { zero.x - 2.0 - at.x } else { zero.y - 2.0 - at.y };
|
||||
delta.abs() < 0.02
|
||||
}),
|
||||
"{deg}°, {unit:?}: zeros at {zeros:?}, origin {at:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shaper_selection_highlights_only_the_visible_result() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -313,15 +313,21 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
&& ui.available_width() >= INLINE_TRANSFORM_WIDTH
|
||||
{
|
||||
let c = b.center();
|
||||
let origin = app.ruler_origin();
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
for (k, lbl, v) in [("x", "X:", c.x), ("y", "Y:", c.y), ("width", "W:", b.rect.width()), ("height", "H:", b.rect.height())] {
|
||||
for (k, lbl, v, offset) in [
|
||||
("x", "X:", c.x, origin.x),
|
||||
("y", "Y:", c.y, origin.y),
|
||||
("width", "W:", b.rect.width(), 0.0),
|
||||
("height", "H:", b.rect.height(), 0.0),
|
||||
] {
|
||||
// The W/H link sits between W and H.
|
||||
if k == "height" {
|
||||
crate::panels::transform::constrain_link(app, ui);
|
||||
}
|
||||
widgets::field_label(ui, egui::RichText::new(tl!(lbl)).size(12.0).color(t.text_dim));
|
||||
if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v), units, 80.0) {
|
||||
app.run("object.setBounds", json!({k: nv, "reference": 4, "proportional": link})).ok();
|
||||
if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v - offset), units, 80.0) {
|
||||
app.run("object.setBounds", json!({k: nv + offset, "reference": 4, "proportional": link})).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4198,6 +4198,8 @@ effect Regular Pravidelný
|
||||
@msg missing panel layout chybí rozložení panelů
|
||||
@msg unsupported native docking operation nepodporovaná nativní operace ukotvení
|
||||
@msg this kind of brush has no options dialog yet tento druh štětce zatím nemá dialogové okno možností
|
||||
Ruler Origin Počátek pravítek
|
||||
Reset Ruler Origin Obnovit počátek pravítek
|
||||
Column Width: Šířka sloupce:
|
||||
Import Data… Importovat data…
|
||||
Number of Decimals: Počet desetinných míst:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4140,6 +4140,8 @@ theme Appearance Erscheinungsbild
|
||||
@msg unsupported native docking operation Nicht unterstützte native Docking-Operation
|
||||
Linux Display Server Linux-Anzeigeserver
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Linealursprung
|
||||
Reset Ruler Origin Linealursprung zurücksetzen
|
||||
Artistic Kunstfilter
|
||||
Film Grain… Filmkörnung…
|
||||
Grain Körnung
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4184,6 +4184,8 @@ lighting Light Luz
|
||||
@msg unsupported native docking operation Operación nativa de anclaje no admitida
|
||||
Linux Display Server Servidor de pantalla de Linux
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Origen de las reglas
|
||||
Reset Ruler Origin Restablecer el origen de las reglas
|
||||
Artistic Artístico
|
||||
Film Grain… Granulado de película…
|
||||
Highlight Area Área de iluminaciones
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4194,6 +4194,8 @@ lighting Light Lumière
|
||||
@msg unsupported native docking operation Opération native d’ancrage non prise en charge
|
||||
Linux Display Server Serveur d’affichage Linux
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Origine des règles
|
||||
Reset Ruler Origin Réinitialiser l’origine des règles
|
||||
Artistic Artistique
|
||||
Film Grain… Grain photographique…
|
||||
Highlight Area Zone des tons clairs
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4186,6 +4186,8 @@ lighting Light Luce
|
||||
@msg unsupported native docking operation Operazione nativa di ancoraggio non supportata
|
||||
Linux Display Server Server di visualizzazione Linux
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Origine dei righelli
|
||||
Reset Ruler Origin Ripristina origine dei righelli
|
||||
Artistic Artistico
|
||||
Film Grain… Grana della pellicola…
|
||||
Highlight Area Area luci
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4180,6 +4180,8 @@ lighting Light ライト
|
||||
@msg unsupported native docking operation ネイティブドッキング操作は未対応です
|
||||
Linux Display Server Linux ディスプレイサーバー
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin 定規の原点
|
||||
Reset Ruler Origin 定規の原点をリセット
|
||||
Artistic アーティスティック
|
||||
Film Grain… フィルム粒子…
|
||||
Highlight Area ハイライト領域
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3668,6 +3668,8 @@ axis Both Ambos
|
||||
@msg invalid native panel set conjunto de painéis nativos inválido
|
||||
@msg missing panel layout layout de painéis ausente
|
||||
@msg unsupported native docking operation operação de encaixe nativa não suportada
|
||||
Ruler Origin Origem das réguas
|
||||
Reset Ruler Origin Redefinir origem das réguas
|
||||
Artistic Artístico
|
||||
Film Grain… Granulação de filme…
|
||||
Grain Grão
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4182,6 +4182,8 @@ lighting Light Свет
|
||||
@msg unsupported native docking operation Неподдерживаемая собственная операция прикрепления
|
||||
Linux Display Server Дисплейный сервер Linux
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Начало координат линеек
|
||||
Reset Ruler Origin Сбросить начало координат линеек
|
||||
Artistic Художественные
|
||||
Film Grain… Зернистость плёнки…
|
||||
Highlight Area Область светов
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4166,6 +4166,8 @@ lighting Light Світло
|
||||
@msg unsupported native docking operation Непідтримувана власна операція прикріплення
|
||||
Linux Display Server Дисплейний сервер Linux
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin Початок координат лінійок
|
||||
Reset Ruler Origin Скинути початок координат лінійок
|
||||
Artistic Художні
|
||||
Film Grain… Зернистість плівки…
|
||||
Highlight Area Область світлих тонів
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 659 and column 40.
|
@@ -3781,6 +3781,8 @@ lighting Light 光源
|
||||
Move Guide to Layer 将参考线移至图层
|
||||
Linux Display Server Linux 显示服务器
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin 标尺原点
|
||||
Reset Ruler Origin 重置标尺原点
|
||||
Artistic 艺术效果
|
||||
Film Grain… 胶片颗粒…
|
||||
Highlight Area 高光区域
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3562,6 +3562,8 @@ lighting Light 光源
|
||||
Move Guide to Layer 將參考線移至圖層
|
||||
Linux Display Server Linux 顯示伺服器
|
||||
X11 (XWayland) X11 (XWayland)
|
||||
Ruler Origin 尺標原點
|
||||
Reset Ruler Origin 重設尺標原點
|
||||
Artistic 藝術效果
|
||||
Film Grain… 底片顆粒…
|
||||
Highlight Area 亮部範圍
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -122,6 +122,8 @@ mod tests_recovery;
|
||||
#[cfg(test)]
|
||||
mod tests_removeanchors;
|
||||
#[cfg(test)]
|
||||
mod tests_ruler_origin;
|
||||
#[cfg(test)]
|
||||
mod tests_save;
|
||||
#[cfg(test)]
|
||||
mod tests_saveext;
|
||||
@@ -551,6 +553,17 @@ impl VectorcraftApp {
|
||||
pub fn view(&self) -> Option<&View> {
|
||||
self.session.active_index().and_then(|i| self.views.get(i))
|
||||
}
|
||||
|
||||
/// The zero point shown by rulers and position fields; command coordinates stay absolute.
|
||||
pub(crate) fn ruler_origin(&self) -> vectorcraft_geom::Point {
|
||||
let origin = self.session.active().map_or(vectorcraft_geom::Point::ZERO, |d| d.doc.ruler_origin);
|
||||
// An untrusted file cannot make the ruler's tick loop or position fields overflow.
|
||||
if origin.is_finite() && origin.x.abs() <= vectorcraft_engine::MAX_COORD && origin.y.abs() <= vectorcraft_engine::MAX_COORD {
|
||||
origin
|
||||
} else {
|
||||
vectorcraft_geom::Point::ZERO
|
||||
}
|
||||
}
|
||||
pub fn view_mut(&mut self) -> Option<&mut View> {
|
||||
self.sync_views();
|
||||
let i = self.session.active_index()?;
|
||||
|
||||
@@ -199,6 +199,7 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
/// Exit back to the Selection tool (#530).
|
||||
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.general_unit();
|
||||
let origin = app.ruler_origin();
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
let i = super::artboards::selected(app, n);
|
||||
let scale_art = super::artboards::scale_art(app);
|
||||
@@ -214,8 +215,13 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
|
||||
dim_label(ui, label);
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v, "scaleArt": scale_art, "moveArt": move_art})).ok();
|
||||
let offset = match key {
|
||||
"x" => origin.x,
|
||||
"y" => origin.y,
|
||||
_ => 0.0,
|
||||
};
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v - offset), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v + offset, "scaleArt": scale_art, "moveArt": move_art})).ok();
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
@@ -393,6 +399,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
}
|
||||
let units = app.session.general_unit();
|
||||
let origin = app.ruler_origin();
|
||||
// The bounding box, rotated with rotated objects (as in the Transform panel).
|
||||
let Some(bx) = app.selection_box() else {
|
||||
dim_label(ui, tl!("No Selection"));
|
||||
@@ -411,8 +418,8 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
egui::Grid::new("xf-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
dim_label(ui, "X:");
|
||||
if let Some(v) = widgets::num_field(ui, "tx", Some(rp.x), units, fw) {
|
||||
app.run("object.setBounds", json!({"x": v, "reference": refi})).ok();
|
||||
if let Some(v) = widgets::num_field(ui, "tx", Some(rp.x - origin.x), units, fw) {
|
||||
app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi})).ok();
|
||||
}
|
||||
dim_label(ui, "W:");
|
||||
if let Some(v) = widgets::num_field(ui, "tw", Some(b.width()), units, fw) {
|
||||
@@ -420,8 +427,8 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.end_row();
|
||||
dim_label(ui, "Y:");
|
||||
if let Some(v) = widgets::num_field(ui, "ty", Some(rp.y), units, fw) {
|
||||
app.run("object.setBounds", json!({"y": v, "reference": refi})).ok();
|
||||
if let Some(v) = widgets::num_field(ui, "ty", Some(rp.y - origin.y), units, fw) {
|
||||
app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi})).ok();
|
||||
}
|
||||
dim_label(ui, "H:");
|
||||
if let Some(v) = widgets::num_field(ui, "th", Some(b.height()), units, fw) {
|
||||
|
||||
@@ -52,6 +52,7 @@ pub fn artboard_rect(r: Rect, refi: usize, x: Option<f64>, y: Option<f64>, w: Op
|
||||
/// The Artboard tool's view of the panel: the active artboard's position and size.
|
||||
fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect) {
|
||||
let units = app.session.general_unit();
|
||||
let origin = app.ruler_origin();
|
||||
let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4);
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
let rp = vectorcraft_geom::reference_point(r, refi);
|
||||
@@ -71,8 +72,8 @@ fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect)
|
||||
ui.add_space(4.0);
|
||||
egui::Grid::new("xfp-ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
widgets::dim_label(ui, "X:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-x", Some(rp.x), units, 80.0) {
|
||||
set(app, Some(v), None, None, None);
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-x", Some(rp.x - origin.x), units, 80.0) {
|
||||
set(app, Some(v + origin.x), None, None, None);
|
||||
}
|
||||
widgets::dim_label(ui, "W:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-w", Some(r.width()), units, 80.0) {
|
||||
@@ -80,8 +81,8 @@ fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect)
|
||||
}
|
||||
ui.end_row();
|
||||
widgets::dim_label(ui, "Y:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-y", Some(rp.y), units, 80.0) {
|
||||
set(app, None, Some(v), None, None);
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-y", Some(rp.y - origin.y), units, 80.0) {
|
||||
set(app, None, Some(v + origin.y), None, None);
|
||||
}
|
||||
widgets::dim_label(ui, "H:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-ab-h", Some(r.height()), units, 80.0) {
|
||||
@@ -104,6 +105,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
}
|
||||
let units = app.session.general_unit();
|
||||
let origin = app.ruler_origin();
|
||||
// The bounding box, rotated with rotated objects: W/H are its own sides, X/Y its reference
|
||||
// point on the page.
|
||||
let bx = app.selection_box();
|
||||
@@ -120,8 +122,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
egui::Grid::new("xfp-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
widgets::dim_label(ui, "X:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"x": v, "reference": refi})).ok();
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x - origin.x), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"x": v + origin.x, "reference": refi})).ok();
|
||||
}
|
||||
widgets::dim_label(ui, "W:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-w", bounds.map(|b| b.width()), units, 80.0) {
|
||||
@@ -129,8 +131,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.end_row();
|
||||
widgets::dim_label(ui, "Y:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"y": v, "reference": refi})).ok();
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y - origin.y), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"y": v + origin.y, "reference": refi})).ok();
|
||||
}
|
||||
widgets::dim_label(ui, "H:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-h", bounds.map(|b| b.height()), units, 80.0) {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! #1212: real ruler gestures and position fields share the document's zero point.
|
||||
|
||||
use egui::{Event, Key, PointerButton, Pos2, Rect, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, Unit};
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use crate::{VectorcraftApp, canvas, chrome, panels};
|
||||
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.ui.view.rulers = true;
|
||||
app.ui.control_bar = true;
|
||||
app
|
||||
}
|
||||
|
||||
fn context() -> egui::Context {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
ctx
|
||||
}
|
||||
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>, draw: fn(&mut VectorcraftApp, &mut Ui)) -> Vec<(String, Rect)> {
|
||||
let raw = egui::RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 900.0))),
|
||||
time: Some(ctx.input(|i| i.time) + 0.05),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = ctx.run_ui(raw, |ui| draw(app, ui));
|
||||
out.textures_delta.clear();
|
||||
fn walk(s: &egui::Shape, v: &mut Vec<(String, Rect)>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => v.push((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
egui::Shape::Vec(s) => s.iter().for_each(|s| walk(s, v)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut texts = vec![];
|
||||
out.shapes.iter().for_each(|c| walk(&c.shape, &mut texts));
|
||||
texts
|
||||
}
|
||||
|
||||
fn press(at: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
fn key(key: Key) -> Event {
|
||||
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
fn start_drag(app: &mut VectorcraftApp, ctx: &egui::Context, to: Pos2) {
|
||||
let at = pos2(8.0, 8.0);
|
||||
frame(app, ctx, vec![Event::PointerMoved(at)], canvas::show);
|
||||
frame(app, ctx, vec![press(at, true)], canvas::show);
|
||||
frame(app, ctx, vec![Event::PointerMoved(to)], canvas::show);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_drag_uses_the_rotated_zoomed_view_and_commits_only_on_release() {
|
||||
let mut app = app();
|
||||
app.select_tool("rectangle");
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], canvas::show);
|
||||
let v = app.view_mut().unwrap();
|
||||
v.center = Point::new(220.0, 130.0);
|
||||
v.zoom = 2.3;
|
||||
v.rotation = 35.0;
|
||||
frame(&mut app, &ctx, vec![], canvas::show);
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let goal = Point::new(125.25, 90.5);
|
||||
let to = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(goal);
|
||||
start_drag(&mut app, &ctx, to);
|
||||
assert_eq!(&*app.session.doc().unwrap().doc, &*before, "preview does not edit the document");
|
||||
frame(&mut app, &ctx, vec![press(to, false)], canvas::show);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert!(st.doc.ruler_origin.distance(goal) < 1e-4, "{:?}", st.doc.ruler_origin);
|
||||
assert_eq!(st.history.undo.len(), undo + 1);
|
||||
assert_eq!(st.doc.art_bounds(), None, "the drawing tool never receives the corner drag");
|
||||
assert!(st.doc.guides.is_empty());
|
||||
let origin = st.doc.ruler_origin;
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(&*app.session.doc().unwrap().doc, &*before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.ruler_origin(), origin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_double_click_resets_to_the_active_artboard() {
|
||||
let mut app = app();
|
||||
app.run("artboard.new", json!({"x": 700, "y": -250, "width": 200, "height": 150})).unwrap();
|
||||
app.run("document.setRulerOrigin", json!({"x": 125, "y": 90})).unwrap();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], canvas::show);
|
||||
app.view_mut().unwrap().artboard = 1;
|
||||
let at = pos2(8.0, 8.0);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
for _ in 0..2 {
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at)], canvas::show);
|
||||
frame(&mut app, &ctx, vec![press(at, true)], canvas::show);
|
||||
frame(&mut app, &ctx, vec![press(at, false)], canvas::show);
|
||||
}
|
||||
assert_eq!(app.ruler_origin(), Point::new(700.0, -250.0));
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.ruler_origin(), Point::new(125.0, 90.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_cancelled_drags_do_not_modify_either_document() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Cancel {
|
||||
Outside,
|
||||
Escape,
|
||||
Focus,
|
||||
Document,
|
||||
}
|
||||
for cancel in [Cancel::Outside, Cancel::Escape, Cancel::Focus, Cancel::Document] {
|
||||
let mut app = app();
|
||||
app.run("document.setRulerOrigin", json!({"x": 25, "y": 30})).unwrap();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], canvas::show);
|
||||
let to = app.canvas_rect.unwrap().center();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
start_drag(&mut app, &ctx, to);
|
||||
let release = match cancel {
|
||||
Cancel::Outside => pos2(4.0, 4.0),
|
||||
Cancel::Escape => {
|
||||
frame(&mut app, &ctx, vec![key(Key::Escape)], canvas::show);
|
||||
to
|
||||
}
|
||||
Cancel::Focus => {
|
||||
let mut out = ctx.run_ui(egui::RawInput { focused: false, ..Default::default() }, |ui| canvas::show(&mut app, ui));
|
||||
out.textures_delta.clear();
|
||||
to
|
||||
}
|
||||
Cancel::Document => {
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
to
|
||||
}
|
||||
};
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(release), press(release, false)], canvas::show);
|
||||
if cancel == Cancel::Document {
|
||||
assert_eq!(app.ruler_origin(), Point::ZERO);
|
||||
app.run("document.activate", json!({"index": 0})).unwrap();
|
||||
assert_eq!(&*app.session.doc().unwrap().doc, &*before);
|
||||
} else {
|
||||
assert_eq!(&*app.session.doc().unwrap().doc, &*before, "{cancel:?}");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "{cancel:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_position_fields_show_relative_values_and_edit_absolute_positions() {
|
||||
for draw in [panels::properties::show as fn(&mut VectorcraftApp, &mut Ui), panels::transform::show, chrome::control_bar] {
|
||||
for (label, arrow) in [("X:", (1.0, 0.0)), ("Y:", (0.0, 1.0))] {
|
||||
let mut app = app();
|
||||
let id = NodeId(app.run("shape.rectangle", json!({"x": 100, "y": 50, "width": 80, "height": 40})).unwrap()["id"].as_u64().unwrap());
|
||||
app.run("document.setRulerOrigin", json!({"x": 120, "y": 50})).unwrap();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], draw);
|
||||
let texts = frame(&mut app, &ctx, vec![], draw);
|
||||
assert!(texts.iter().filter(|(t, _)| t == &Unit::Points.format(20.0)).count() >= 2, "{texts:?}");
|
||||
let label = texts.iter().find(|(t, _)| t == label).unwrap().1;
|
||||
let at = pos2(label.right() + 40.0, label.center().y);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at), press(at, true)], draw);
|
||||
frame(&mut app, &ctx, vec![press(at, false)], draw);
|
||||
frame(&mut app, &ctx, vec![key(Key::ArrowUp)], draw);
|
||||
let bounds = app.session.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap();
|
||||
assert_eq!((bounds.x0, bounds.y0), (100.0 + arrow.0, 50.0 + arrow.1));
|
||||
assert_eq!((bounds.width(), bounds.height()), (80.0, 40.0));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap().x0, 100.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_origin_artboard_fields_preserve_the_absolute_reference_point() {
|
||||
for draw in [panels::properties::show as fn(&mut VectorcraftApp, &mut Ui), panels::transform::show] {
|
||||
for label in ["X:", "Y:"] {
|
||||
let mut app = app();
|
||||
app.run("document.setRulerOrigin", json!({"x": 120, "y": 50})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], draw);
|
||||
let texts = frame(&mut app, &ctx, vec![], draw);
|
||||
let label_rect = texts.iter().find(|(t, _)| t == label).unwrap().1;
|
||||
let at = pos2(label_rect.right() + 40.0, label_rect.center().y);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at), press(at, true)], draw);
|
||||
frame(&mut app, &ctx, vec![press(at, false)], draw);
|
||||
frame(&mut app, &ctx, vec![key(Key::ArrowUp)], draw);
|
||||
let ab = app.session.doc().unwrap().doc.artboards[0].rect;
|
||||
assert_eq!((ab.x0, ab.y0), if label == "X:" { (1.0, 0.0) } else { (0.0, 1.0) });
|
||||
assert_eq!((ab.width(), ab.height()), (400.0, 300.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
|
||||
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
|
||||
@@ -179,8 +179,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
## G7 Views and windows
|
||||
|
||||
- **Done:** custom document ruler origin (#1212): corner drag with preview, double-click reset to the active artboard, relative X/Y inputs and native save/reopen.
|
||||
- **Missing:** multiple windows and Window › Arrange (Cascade, Tile, Float in Window, Consolidate All Windows),
|
||||
panels on another monitor, New View/Edit Views, global and video rulers.
|
||||
panels on another monitor, New View/Edit Views, global/artboard ruler mode switch and video rulers.
|
||||
- **Estimate:** 15–25 h.
|
||||
|
||||
## G6 Automation and scripting
|
||||
@@ -233,6 +234,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | G7: custom document ruler origin (#1212) done; global/artboard mode switching and video rulers remain, estimate unchanged |
|
||||
| 2026-10-12 | minor | G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder single-result booleans preserve source names and SVG IDs (#1293); paint-order, grouped/compound sources, unchanged geometry/appearance and Undo/Redo regression tests; scores and estimates unchanged |
|
||||
| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged |
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5
|
||||
@@ -50,7 +50,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
||||
|---|---|---|---|
|
||||
| M0 | Skeleton + vertical slice | ✅ done | — |
|
||||
| M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending) | 4–6 |
|
||||
| M2 | Drawing tools + smart guides | ✅ mostly done (Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; construction widget/refinements pending) | 12–18 |
|
||||
| M2 | Drawing tools + smart guides | ✅ mostly done (Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending) | 12–18 |
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork; gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool, Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries. Left: freeform and mixed spot/process gradients export as stops or process colours to SVG/PDF; confirm the Scale Strokes & Effects default | 4–8 |
|
||||
| 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 |
|
||||
@@ -80,6 +80,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | M2: custom ruler origin (#1212), with drag preview, active-artboard reset, relative X/Y and native round-trip; remaining ruler modes and estimates unchanged |
|
||||
| 2026-10-12 | minor | M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder single-result booleans preserve source names and SVG IDs (#1293); paint-order, grouped/compound sources, unchanged geometry/appearance and Undo/Redo regression tests; scores and estimates unchanged |
|
||||
| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is
|
||||
the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md),
|
||||
@@ -82,7 +82,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
|
||||
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done) | 5–8 | estimated (raised from 72%) |
|
||||
| Layers, artboards, document setup | 5 | 78% | 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved) |
|
||||
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows and Arrange, Snap to Glyph | 9–14 | estimated |
|
||||
| Guides, grids, smart guides, snapping, rulers | 3 | 80% | 30.0's tangent and perpendicular snapping and its snapping panel, "intersect" smart guides, global/video rulers, distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved) |
|
||||
| Guides, grids, smart guides, snapping, rulers | 3 | 80% | 30.0's tangent and perpendicular snapping and its snapping panel, "intersect" smart guides, global/artboard ruler mode switch and video rulers, distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved) |
|
||||
| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), guides lost (#779), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC/JPEG 2000 import, 3D export; see [file-format-parity.md](file-format-parity.md) | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320) |
|
||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983), TIFF in Export for Screens (30.2) | 2–4 | estimated |
|
||||
| Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated |
|
||||
@@ -371,6 +371,8 @@ since 2026-10-01.
|
||||
|
||||
Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a feature lands.
|
||||
|
||||
- **Rulers (#1212):** drag the intersection to preview and set a document zero point, or double-click it to reset to the active artboard's top-left. Transform, Properties and Control X/Y fields measure from it and convert entered positions back to absolute command coordinates. The origin survives every native format version and compression; geometry, guides and patterns stay in place. Global/artboard mode switching and video rulers remain separate. Public reference: [Use rulers](https://helpx.adobe.com/ca/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html).
|
||||
|
||||
- **Document Setup modal handoff:** Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool. Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`.
|
||||
- **Artboard Escape routing:** Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests_distortkeys.rs`.
|
||||
- **Linux RPM packaging:** 64-bit runtime capabilities replace Fedora-specific required package names (#1275); glibc 2.35, libm and libgcc_s are declared alongside dynamically loaded xkbcommon and Vulkan/EGL. Generated x86_64/aarch64 metadata and isolated synthetic-provider resolution checked; real-distro install/start-up QA remains pending.
|
||||
@@ -514,6 +516,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Guides/rulers: custom document zero point (#1212) with preview, reset, relative position fields and native round-trip; global/artboard mode switch and video rulers remain; score 80% and 4–7 h unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder single-result booleans preserve source names and SVG IDs (#1293); paint-order, grouped/compound sources, unchanged geometry/appearance and Undo/Redo regression tests; scores and estimates unchanged |
|
||||
| 2026-10-12 | minor | Localization: Korean (`ko`) catalog added, complete under the tests; Localization 54% → 63% (measured), hours 70–110 → 60–100; full readiness unchanged (~62%) |
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's
|
||||
trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to,
|
||||
@@ -94,9 +94,10 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Snapping panel/popover consolidating the snap options (30.0) | missing | |
|
||||
| Customisable rotation snap angle (#973) | missing | Illustrator: Constrain Angle and construction angles only |
|
||||
| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451 |
|
||||
| Ruler origin: corner drag with preview, double-click reset to the active artboard, relative X/Y fields, native save/reopen | done | #1212; `document.setRulerOrigin`, `document.resetRulerOrigin`; geometry and command coordinates stay absolute |
|
||||
| Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer |
|
||||
| Alt swaps a ruler guide's orientation while dragging | missing | |
|
||||
| Global and video rulers | missing | menu stubs |
|
||||
| Global/artboard ruler mode switch and video rulers | missing | menu stubs; custom document origin is done (#1212) |
|
||||
|
||||
## Transform handles, bounding box, Free Transform
|
||||
|
||||
@@ -185,6 +186,7 @@ re-measure of the panels added since.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Custom ruler origin (#1212): corner drag previews and sets it; double-click resets to the active artboard; X/Y fields use it and native files keep it; scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder single-result booleans preserve source names and SVG IDs (#1293); paint-order, grouped/compound sources, unchanged geometry/appearance and Undo/Redo regression tests; scores and estimates unchanged |
|
||||
| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged |
|
||||
|
||||
Reference in new issue
Block a user