mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 05:55:32 +00:00
Merge pull request #7345 from dotCooCoo/fix/ai-twist-straight-guide-reach
.ai import: straight compound paths whose guides reach past them keep the drawn look under Twist
This commit is contained in:
@@ -7582,9 +7582,10 @@ fn a_linked_object_past_the_object_limit_warns_that_its_link_is_left_out() {
|
||||
}
|
||||
|
||||
/// A link to a graphic style with Twist is kept where the reader reads Twist: on a curve, a curved
|
||||
/// compound path or a group with nothing stroked up to a quarter turn, and on a path with straight
|
||||
/// segments at any angle. A stroked curve, a group with a stroked member or a guide, or a curve,
|
||||
/// compound path or group twisted further, is written without the link and warns.
|
||||
/// compound path or a group with nothing stroked up to a quarter turn, and on a path or compound path
|
||||
/// with straight segments up to half a turn. A stroked curve, a group with a stroked member or a
|
||||
/// guide, a straight compound path with a guide member that reaches past its other members, or an
|
||||
/// object twisted further, is written without the link and warns.
|
||||
#[test]
|
||||
fn a_link_to_a_twist_style_is_kept_where_the_reader_reads_it() {
|
||||
// A document with the object `make` builds from its appearance, `ap` with Twist at `angle`,
|
||||
@@ -7619,6 +7620,17 @@ fn a_link_to_a_twist_style_is_kept_where_the_reader_reads_it() {
|
||||
n.appearance = ap.into();
|
||||
n
|
||||
};
|
||||
// A compound path of a square and a guide, a rectangle with corners `a` and `b`.
|
||||
let guided = |a: (f64, f64), b: (f64, f64)| {
|
||||
move |doc: &mut Document, ap: Appearance| {
|
||||
let outer = Node::path(doc.alloc_id(), square(), Appearance::default());
|
||||
let path = shapes::rectangle(Rect::new(a.0, a.1, b.0, b.1));
|
||||
let guide = Node::new(doc.alloc_id(), NodeKind::Path { path, rule: FillRule::NonZero, live: None, clipping: false, guide: true });
|
||||
let mut n = Node::new(doc.alloc_id(), NodeKind::Compound { children: vec![Arc::new(outer), Arc::new(guide)], rule: FillRule::NonZero });
|
||||
n.appearance = ap.into();
|
||||
n
|
||||
}
|
||||
};
|
||||
// A group of a filled square and `second`.
|
||||
let group = |second: fn(&mut Document) -> Node| {
|
||||
move |doc: &mut Document, ap: Appearance| {
|
||||
@@ -7649,6 +7661,8 @@ fn a_link_to_a_twist_style_is_kept_where_the_reader_reads_it() {
|
||||
("a square twisted three quarters of a turn back", twisted(-270.0, filled(), path(square())), false),
|
||||
("a straight compound path twisted half a turn", twisted(180.0, filled(), frame), true),
|
||||
("a straight compound path twisted a full turn", twisted(360.0, filled(), frame), false),
|
||||
("a straight compound path with a guide inside it", twisted(45.0, filled(), guided((30.0, 20.0), (70.0, 50.0))), true),
|
||||
("a straight compound path with a guide reaching past it", twisted(45.0, filled(), guided((30.0, 20.0), (120.0, 50.0))), false),
|
||||
("a curve twisted past a quarter turn", twisted(120.0, filled(), path(oval())), false),
|
||||
("a stroked curve", twisted(45.0, Appearance::basic(black_fill(), red.clone(), 2.0), path(oval())), false),
|
||||
("a curved compound path", twisted(-60.0, filled(), ring), true),
|
||||
|
||||
@@ -1076,6 +1076,15 @@ fn no_guides(n: &Node, depth: usize) -> bool {
|
||||
depth <= MAX_DEPTH && !n.is_guide() && n.children().into_iter().flatten().all(|c| no_guides(c, depth + 1))
|
||||
}
|
||||
|
||||
/// Is `n` a compound path with a guide member whose bounds reach past the bounds of its members that
|
||||
/// aren't guides, or with guide members only? Any other object counts as no.
|
||||
fn guides_reach_past(n: &Node) -> bool {
|
||||
let NodeKind::Compound { children, .. } = &n.kind else { return false };
|
||||
let bounds =
|
||||
|guides: bool| children.iter().filter(|c| c.is_guide() == guides).fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds()));
|
||||
bounds(true).is_some_and(|g| bounds(false).is_none_or(|o| o.union(g) != o))
|
||||
}
|
||||
|
||||
/// The largest Twist angle, in degrees either way, read as a live effect on an object that isn't a
|
||||
/// path or compound path with straight segments: VectorCraft's own limit. An object with a larger
|
||||
/// Twist keeps the file's drawn look.
|
||||
@@ -1089,7 +1098,8 @@ const MAX_STRAIGHT_TWIST: f64 = 180.0;
|
||||
/// Is `object` one that style `st`, whose effect moves the outline, is read on? It and everything
|
||||
/// in it [`moves_alike`], and the style paints no gradient. Twist is read only when nothing is
|
||||
/// stroked and its angle is a number: up to [`MAX_STRAIGHT_TWIST`] degrees either way on a path or
|
||||
/// compound path with straight segments, or up to [`MAX_TWIST`] degrees either way on another object
|
||||
/// compound path with straight segments (only at 0° when its guide members reach past its other
|
||||
/// members, see [`guides_reach_past`]), or up to [`MAX_TWIST`] degrees either way on another object
|
||||
/// with no guide in it. Transform that scales, turns or reflects is read only when nothing is stroked
|
||||
/// (its reference point lies on the bounds without strokes).
|
||||
fn moves_with(object: &Node, st: &crate::import::ai::style::ArtStyle) -> bool {
|
||||
@@ -1102,7 +1112,12 @@ fn moves_with(object: &Node, st: &crate::import::ai::style::ArtStyle) -> bool {
|
||||
&& st.effects.iter().all(|e| match e.id.as_str() {
|
||||
"distort.twist" => {
|
||||
let within = |limit: f64| e.params["angle"].as_f64().is_some_and(|a| a.abs() <= limit);
|
||||
!stroked && if straight(object) { within(MAX_STRAIGHT_TWIST) } else { no_guides(object, 0) && within(MAX_TWIST) }
|
||||
!stroked
|
||||
&& if straight(object) {
|
||||
within(MAX_STRAIGHT_TWIST) && (e.params["angle"].as_f64() == Some(0.0) || !guides_reach_past(object))
|
||||
} else {
|
||||
no_guides(object, 0) && within(MAX_TWIST)
|
||||
}
|
||||
}
|
||||
"distort.transform" => {
|
||||
let p = &e.params;
|
||||
|
||||
@@ -7296,7 +7296,11 @@ fn twist_is_read_on_curves_compound_paths_and_groups_with_nothing_stroked_up_to_
|
||||
("a twisted group", read(&twist(90.0, None), &distort_group()), 2),
|
||||
("a group with a hidden member", read(&twist(45.0, None), &hidden_member), 2),
|
||||
("a curve twisted a quarter turn back", read(&twist(-90.0, BLUE_FILL), &curved), 0),
|
||||
("a straight compound path with a guide, as before", read(&twist(45.0, BLUE_FILL), &with_guide(&rect_path(20.0, 20.0, 60.0, 50.0))), 2),
|
||||
(
|
||||
"a straight compound path with a guide inside it, as before",
|
||||
read(&twist(45.0, BLUE_FILL), &with_guide(&rect_path(0.0, 0.0, 60.0, 50.0))),
|
||||
2,
|
||||
),
|
||||
] {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{what}: {:?}", r.warnings);
|
||||
let a = art(&r.doc);
|
||||
@@ -7363,6 +7367,48 @@ fn twist_is_read_on_straight_paths_and_compound_paths_up_to_half_a_turn() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twist_on_a_straight_compound_path_keeps_the_drawn_look_where_a_guide_reaches_past_its_other_members() {
|
||||
// GUARDS: a straight compound path whose guide members reach past its other members was read
|
||||
// with a live Twist measured from bounds that take in those guides, which draws differently from
|
||||
// the art saved with it.
|
||||
// The object, `hidden` painted with its style, under its drawn look.
|
||||
let object = |hidden: &str| {
|
||||
format!("0 Ae\nu\n{RED}{}f\nU\n%_Bb\n%_{}0 BB\n1 (Bent) XW\n", rect_path(16.0, 16.0, 64.0, 54.0), hidden.replace('\n', "\n%_"))
|
||||
};
|
||||
// A compound path of a square and the guide from `from` to `to`.
|
||||
let with_guide = |from: (f64, f64), to: (f64, f64)| {
|
||||
object(&format!("*u\n{}f\n{} {} m\n{} {} L\n(N) *\n*U\n", rect_path(20.0, 20.0, 60.0, 50.0), from.0, from.1, to.0, to.1))
|
||||
};
|
||||
let twist = |angle: f64| distort_styles("Adobe Twirl", &format!("{angle} /Real (angle) ,\n"), BLUE_FILL);
|
||||
let read = |angle: f64, art_: &str| read_ok(&format!("{}{}", twist(angle), layer(art_)));
|
||||
for (what, r) in [
|
||||
("a guide inside the square", read(45.0, &with_guide((25.0, 25.0), (35.0, 35.0)))),
|
||||
("a guide on the square's corners", read(180.0, &with_guide((20.0, 20.0), (60.0, 50.0)))),
|
||||
("a guide inside the square, half a turn back", read(-180.0, &with_guide((30.0, 40.0), (50.0, 30.0)))),
|
||||
("a guide outside the square, not turned", read(0.0, &with_guide((5.0, 0.0), (15.0, 10.0)))),
|
||||
] {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{what}: {:?}", r.warnings);
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{what}: {a:#?}");
|
||||
assert_eq!(a[0].children().map_or(0, |c| c.len()), 2, "{what}: the object, not its look: {a:#?}");
|
||||
let [e] = a[0].appearance.effects.as_slice() else { panic!("{what}: {:?}", a[0].appearance) };
|
||||
assert_eq!(e.id, "distort.twist", "{what}");
|
||||
}
|
||||
for (what, r) in [
|
||||
("a guide outside the square", read(45.0, &with_guide((5.0, 0.0), (15.0, 10.0)))),
|
||||
("a guide reaching past the square's right side", read(45.0, &with_guide((30.0, 30.0), (70.0, 40.0)))),
|
||||
("a guide reaching past the square's top, half a turn", read(180.0, &with_guide((30.0, 30.0), (40.0, 51.0)))),
|
||||
("a guide around the square, a quarter turn back", read(-90.0, &with_guide((0.0, 0.0), (80.0, 70.0)))),
|
||||
("a compound path of guides only", read(45.0, &object("*u\n25 25 m\n35 35 L\n(N) *\n40 25 m\n50 35 L\n(N) *\n*U\n"))),
|
||||
] {
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{what}: {a:#?}");
|
||||
assert!(a[0].appearance.effects.is_empty(), "{what}: the drawn look: {a:#?}");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("drawn look")), "{what}: {:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_comment_line_in_a_prolog_leaves_its_end_alone() {
|
||||
// GUARDS: a bare `%` line's empty marker matched the empty begin marker of a section that has none.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1666,7 +1666,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` or EPS opened from its editing data, a Scatter brush whose last 15 numbers start with 1 comes into the Brushes panel in Tints, and each brush art section is read without its frame, an unnamed, unpainted rectangle with no settings of its own at the art's bounds (within 1/1000 pt). A wider frame stays in the art, and an art group with a state or look of its own (hidden, locked, isolated, knockout, an opacity, a blend mode, a name, a mask, a note or an appearance) stays a group (`crates/eps/src/import/ai/tests.rs`).
|
||||
In a `.ai` opened from its editing data, type with Warp on the object or on one of its fills or strokes opens as type with the effect live when the live warp draws as the art saved with it. When it draws differently, or when the file's limit of checks has no check left for it, it opens as that art, and one note says how many did. Type stays live without a comparison when the saved art paints nothing or draws type of its own, in frames of threaded type, when a text variable binds it, and when its text object doesn't come in as one text object (`crates/eps/src/import/ai/envelope.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, an object with a Tweak that moves nothing or Pucker & Bloat at 0 opens as itself with the effect live. A path, compound path or group whose only effect is Transform without copies, or a Free Distort whose corners make a parallelogram, opens as itself with it live, and so does an object with nothing stroked whose only effect is Twist: a path or compound path with straight segments at up to 180° either way, and another path, compound path or group with no guide in it at up to 90° either way (limits VectorCraft sets); one twisted further keeps the drawn art. A gradient or pattern, an image, type, a clipping group, an opacity mask or effects inside the object, or a stroke with a Transform that scales, rotates or reflects, keep the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/ai/read/ops.rs`).
|
||||
In a `.ai` opened from its editing data, an object with a Tweak that moves nothing or Pucker & Bloat at 0 opens as itself with the effect live. A path, compound path or group whose only effect is Transform without copies, or a Free Distort whose corners make a parallelogram, opens as itself with it live, and so does an object with nothing stroked whose only effect is Twist: a path or compound path with straight segments at up to 180° either way, unless it is a compound path with a guide member that reaches past its other members and the angle is not 0°, and another path, compound path or group with no guide in it at up to 90° either way (limits VectorCraft sets); one twisted further keeps the drawn art. A gradient or pattern, an image, type, a clipping group, an opacity mask or effects inside the object, or a stroke with a Transform that scales, rotates or reflects, keep the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/ai/read/ops.rs`).
|
||||
A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482).
|
||||
`document.open`, `file.place` and `document.pdfInfo` reject a `cropTo`, `crop`, `password`, `colorMode` or `textAs` that is not a string, such as `"textAs": true`, and `document.pdfInfo` rejects a `thumbnailSize` that is not a number; the error names the param, and nothing opens or is placed. A param left out or given as null keeps its default (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`).
|
||||
When the PDF content of a `.ai` has a page for each artboard, the layers are compared with every page on its artboard (until the pages compared take 8,388,608 pixels at the comparison's scale, about 67 letter-size pages, or number 128): art that only the layers or only a later page draws is handled as art that differs on the first page is, and a text object that a later page doesn't draw is left out unless its layer doesn't print (`crates/eps/src/import/tests_native.rs`).
|
||||
|
||||
Reference in New Issue
Block a user