mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Merge pull request #7248 from dotCooCoo/fix/ai-twist-past-180-keeps-look
.ai import: straight paths twisted past 180° keep the drawn look
This commit is contained in:
@@ -7486,6 +7486,13 @@ fn a_link_to_a_twist_style_is_kept_where_the_reader_reads_it() {
|
||||
n.appearance = ap.into();
|
||||
n
|
||||
};
|
||||
let frame = |doc: &mut Document, ap: Appearance| {
|
||||
let outer = Node::path(doc.alloc_id(), square(), Appearance::default());
|
||||
let inner = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(30.0, 20.0, 70.0, 50.0)), Appearance::default());
|
||||
let mut n = Node::new(doc.alloc_id(), NodeKind::Compound { children: vec![Arc::new(outer), Arc::new(inner)], 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| {
|
||||
@@ -7510,7 +7517,12 @@ fn a_link_to_a_twist_style_is_kept_where_the_reader_reads_it() {
|
||||
for (what, doc, kept) in [
|
||||
("a twisted curve", twisted(45.0, filled(), path(oval())), true),
|
||||
("a curve twisted a quarter turn back", twisted(-90.0, filled(), path(oval())), true),
|
||||
("a square twisted three quarters of a turn", twisted(270.0, filled(), path(square())), true),
|
||||
("a square twisted half a turn", twisted(180.0, filled(), path(square())), true),
|
||||
("a square twisted half a turn back", twisted(-180.0, filled(), path(square())), true),
|
||||
("a square twisted three quarters of a turn", twisted(270.0, filled(), path(square())), false),
|
||||
("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 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),
|
||||
|
||||
@@ -1107,11 +1107,17 @@ fn no_guides(n: &Node, depth: usize) -> bool {
|
||||
/// Twist keeps the file's drawn look.
|
||||
const MAX_TWIST: f64 = 90.0;
|
||||
|
||||
/// The largest Twist angle, in degrees either way, read as a live effect on a path or compound path
|
||||
/// with straight segments: VectorCraft's own limit. One with a larger Twist keeps the file's drawn
|
||||
/// look.
|
||||
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: on a path or compound path with straight segments at any angle, 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).
|
||||
/// 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
|
||||
/// 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 {
|
||||
use crate::import::ai::style::StyleItem;
|
||||
let stroked = st.items.iter().any(|i| matches!(i, StyleItem::Stroke { paint, .. } if !paint.is_none()))
|
||||
@@ -1121,7 +1127,8 @@ fn moves_with(object: &Node, st: &crate::import::ai::style::ArtStyle) -> bool {
|
||||
&& object.children().into_iter().flatten().all(|c| moves_alike(c, 1))
|
||||
&& st.effects.iter().all(|e| match e.id.as_str() {
|
||||
"distort.twist" => {
|
||||
!stroked && (straight(object) || (no_guides(object, 0) && e.params["angle"].as_f64().is_some_and(|a| a.abs() <= MAX_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) }
|
||||
}
|
||||
"distort.transform" => {
|
||||
let p = &e.params;
|
||||
|
||||
@@ -7159,7 +7159,6 @@ fn twist_is_read_on_curves_compound_paths_and_groups_with_nothing_stroked_up_to_
|
||||
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%_"))
|
||||
};
|
||||
let square = object(&format!("{}f\n", rect_path(20.0, 20.0, 60.0, 50.0)));
|
||||
let oval = "20 35 m\n20 45 60 45 60 35 c\n60 25 20 25 20 35 c\n";
|
||||
let curved = object(&format!("{oval}f\n"));
|
||||
let ring = object(&format!("*u\n{oval}f\n30 35 m\n30 40 50 40 50 35 c\n50 30 30 30 30 35 c\nf\n*U\n"));
|
||||
@@ -7184,7 +7183,6 @@ 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 square twisted three quarters of a turn, as before", read(&twist(270.0, BLUE_FILL), &square), 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),
|
||||
] {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{what}: {:?}", r.warnings);
|
||||
@@ -7213,6 +7211,45 @@ fn twist_is_read_on_curves_compound_paths_and_groups_with_nothing_stroked_up_to_
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twist_is_read_on_straight_paths_and_compound_paths_up_to_half_a_turn() {
|
||||
// GUARDS: a path or compound path with straight segments was read with a live Twist at any angle.
|
||||
// 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%_"))
|
||||
};
|
||||
let square = object(&format!("{}f\n", rect_path(20.0, 20.0, 60.0, 50.0)));
|
||||
let frame = object(&format!("*u\n{}f\n{}f\n*U\n", rect_path(20.0, 20.0, 60.0, 50.0), rect_path(30.0, 30.0, 50.0, 40.0)));
|
||||
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, members, angle) in [
|
||||
("a square twisted half a turn", read(180.0, &square), 0, 180.0),
|
||||
("a square twisted half a turn back", read(-180.0, &square), 0, -180.0),
|
||||
("a straight compound path twisted half a turn", read(180.0, &frame), 2, 180.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()), members, "{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.as_str(), e.params["angle"].as_f64()), ("distort.twist", Some(angle)), "{what}");
|
||||
}
|
||||
for (what, r) in [
|
||||
("a square twisted just past half a turn", read(181.0, &square)),
|
||||
("a square twisted just past half a turn back", read(-181.0, &square)),
|
||||
("a square twisted three quarters of a turn", read(270.0, &square)),
|
||||
("a square twisted three quarters of a turn back", read(-270.0, &square)),
|
||||
("a square twisted two turns", read(720.0, &square)),
|
||||
("a square twisted ten turns", read(3600.0, &square)),
|
||||
("a straight compound path twisted a full turn", read(360.0, &frame)),
|
||||
] {
|
||||
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
@@ -1623,7 +1623,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 any angle, and another path, compound path or group with no guide in it at up to 90° either way (a limit VectorCraft sets). 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, 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