.ai import: hidden ruler guides open as ruler guides with their own state

A straight `(N) *` guide path that runs across the canvas or an artboard
opens as a ruler guide, but a hidden one (`1 Xw`) stayed a guide path.
It now opens as a ruler guide too, and every ruler guide made from such a
path keeps the path's hide, lock and name, so its Layers row shows them.
Before, a locked guide opened unlocked and a named one lost its name.

Hidden or locked guide paths that are short, slanted, grouped or `(S) *`
stay guide paths, and commented copies (`%_`) are still left out.

Docs: docs/target-app-parity.md, docs/file-format-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 17:55:31 -08:00
parent 71cef8ac8d
commit cfa76d674f
6 changed files with 43 additions and 7 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
//! next layers: a clipping group in each layer, read by [`masks`].
//! - Ruler guides: a straight `(N) *` guide path, directly on a layer, that runs across the whole
//! canvas (16,383 points around the template box's centre) or from edge to edge of one artboard
//! is a ruler guide on that layer ([`Document::guides`]); other guide paths stay paths.
//! is a ruler guide on that layer ([`Document::guides`]), shown or hidden, with the path's name,
//! lock and visibility; other guide paths stay paths.
//! - Groups `u` … `U`, compound paths `*u` … `*U`, clipping groups `q` … `Q` (the path painted
//! with `W` clips); a group's transparency and name follow its end. A wraparound group `*w` …
//! `*W` (version 7 specification §15.4.2) is a group whose objects other than its type wrap
+1 -2
View File
@@ -234,11 +234,10 @@ impl<'a> Reader<'a> {
}
let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide };
let mut node = self.new_node(kind, &gs);
// A ruler guide is written as a straight `(N) *` guide path on its layer.
// A ruler guide is written as a straight `(N) *` guide path on its layer, shown or hidden.
if guide
&& op == "N"
&& !hidden
&& !gs.hidden
&& !clip
&& self.pattern.is_none()
&& matches!(self.frames.last(), Some(Frame { kind: Kind::Layer(_), .. }))
+7 -2
View File
@@ -1443,7 +1443,8 @@ fn ruler_guide(a: Point, b: Point, centre: Option<(f64, f64)>, boards: &[Rect])
}
/// Makes the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
/// ruler guide nodes with the same ids, moved to the top of their layer in their order.
/// ruler guide nodes with the same ids, names, locks and visibility, moved to the top of their layer
/// in their order.
fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Guide>, depth: usize) {
if depth > MAX_DEPTH {
return;
@@ -1456,7 +1457,11 @@ fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Gu
let mut guides = vec![];
kids.retain(|k| match pick(k.id) {
Some(g) => {
guides.push(Arc::new(Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard })));
let mut n = Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard });
n.name = k.name.clone();
n.locked = k.locked;
n.visible = k.visible;
guides.push(Arc::new(n));
false
}
None => true,
+31 -1
View File
@@ -8786,7 +8786,8 @@ fn other_guide_paths_stay_guide_paths() {
guide_line(0.0, 30.0, 200.0, 30.0, "S"),
guide_line(-7885.0, -7796.0, 8498.0, 8587.0, "N"),
format!("u\n{}U\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
format!("1 Xw\n{}0 Xw\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
format!("1 Xw\n{}0 Xw\n", guide_line(20.0, 40.0, 80.0, 40.0, "N")),
format!("1 A\n{}0 A\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "S")),
"-7885 40 m\n306 40 L\n8498 40 L\n(N) *\n".to_string(),
];
for body in cases {
@@ -8796,6 +8797,35 @@ fn other_guide_paths_stay_guide_paths() {
}
}
/// A hidden (`1 Xw`), locked (`1 A`) or named ruler guide path is a ruler guide that keeps its own
/// visibility, lock and name, across the canvas or an artboard, on a shown layer.
#[test]
fn hidden_locked_and_named_guide_paths_are_ruler_guides() {
let body = format!(
"1 Xw\n{}0 Xw\n1 A\n{}0 A\n{}%_/ArtDictionary :\n%_(Mark) /String (AIArtName) ,\n%_;\n%_\n1 Xw\n{}0 Xw\n",
guide_line(-7885.0, 40.0, 8498.0, 40.0, "N"),
guide_line(25.0, 8587.0, 25.0, -7796.0, "N"),
guide_line(-7885.0, 90.0, 8498.0, 90.0, "N"),
guide_line(0.0, 30.0, 200.0, 30.0, "N"),
);
let doc = read(canvas_stream(&layer(&body)).as_bytes()).unwrap().doc;
let top = &doc.layers[0];
assert!(top.visible);
let kids = top.children().unwrap();
assert_eq!(kids.len(), 4);
assert!(kids.iter().all(|c| c.is_ruler_guide()), "no guide path is left");
let got: Vec<_> = doc.ruler_guides().into_iter().map(|g| (g.vertical, g.pos, g.artboard, g.layer, g.hidden, g.locked, g.name)).collect();
let want = [
(false, 60.0, None, Some(top.id), true, false, None),
(true, 25.0, None, Some(top.id), false, true, None),
(false, 10.0, None, Some(top.id), false, false, Some("Mark".to_string())),
(false, 70.0, Some(1), Some(top.id), true, false, None),
];
assert_eq!(got, want);
// The nodes themselves carry the state, so the Layers rows show it.
assert_eq!(kids.iter().map(|k| (k.visible, k.locked)).collect::<Vec<_>>(), [(false, false), (true, true), (true, false), (false, false)]);
}
/// The layer whose `Lb` has `ops` as its options (the first one in the file).
fn layer_with(ops: &str) -> Node {
let doc = read_ok(&format!("%AI5_BeginLayer\n{ops} Lb\n(L) Ln\nLB\n%AI5_EndLayer--\n")).doc;