mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 22:22:25 +00:00
.ai import: warped type bound to a text variable stays type
Warped type that a text variable binds stays type with the effect live, so it keeps its binding. Before, a failed comparison replaced it with its drawn art and the binding was skipped. When drawn art replaces warped type with a wrap setting, the wrap shapes are checked against the file's limit on wrap work again, so a file whose drawn art would go past it opens from its page, as one with too many text objects does.
This commit is contained in:
@@ -426,6 +426,14 @@ impl WarpedType {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl WarpedType {
|
||||
/// The text objects of `looks` (slot and saved look) with no check left.
|
||||
pub(crate) fn unchecked(looks: Vec<(NodeId, Node)>) -> Self {
|
||||
Self { looks: looks.into_iter().map(|(slot, look)| WarpedLook { slot, look, left_out: None }).collect(), ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to `out` the ids of `n` and what is in it, unless hidden.
|
||||
fn collect_shown(n: &Node, out: &mut BTreeSet<NodeId>) {
|
||||
if n.visible {
|
||||
|
||||
@@ -771,7 +771,8 @@ impl<'a> Reader<'a> {
|
||||
// draws type of its own.
|
||||
if slot_of(object.name.as_deref()).is_some() && crate::import::ai::envelope::warps(&appearance) && self.warped_type.len() < MAX_NODES {
|
||||
let mut saved = look.clone();
|
||||
// Inside a copy on `%_` lines everything is, so nothing tells the look's own copies apart.
|
||||
// When the object itself is on `%_` lines, every child of its look is too, so the
|
||||
// look's own copies can't be told apart from the rest.
|
||||
if !hidden {
|
||||
strip(&mut saved, &self.hidden_ids);
|
||||
}
|
||||
|
||||
@@ -204,12 +204,12 @@ pub(super) fn build_from(
|
||||
placeholders,
|
||||
placed_elsewhere,
|
||||
envelopes_drawn,
|
||||
warped_type,
|
||||
styles_read,
|
||||
canvas_scale,
|
||||
xml_ids,
|
||||
bindings,
|
||||
bindings_skipped,
|
||||
warped_type,
|
||||
warnings,
|
||||
} = structure;
|
||||
let mut restyled = left_out_of_comparisons(page, restyled, placeholders);
|
||||
@@ -417,7 +417,8 @@ pub(super) fn build_from(
|
||||
}
|
||||
// Area type flows around the wrap objects above it before it is compared with the page.
|
||||
resolve_text_wrap(&mut doc)?;
|
||||
settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &mut restyled, &mut notes);
|
||||
let bound_text = text_bound_slots(&doc, &bindings, &xml_ids);
|
||||
settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &bound_text, &mut restyled, &mut notes)?;
|
||||
if empty > 0 {
|
||||
notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT }));
|
||||
}
|
||||
@@ -497,28 +498,6 @@ pub(super) fn build_from(
|
||||
Ok((doc, notes))
|
||||
}
|
||||
|
||||
/// Warped type stays type where its live warp draws as its look. Elsewhere the look takes its place,
|
||||
/// and one note says how many did. `filled` says what each slot became, and the comparisons with the
|
||||
/// page leave out the boxes in `restyled`.
|
||||
fn settle_warped_type(
|
||||
warped: ai::WarpedType,
|
||||
doc: &mut Document,
|
||||
filled: &std::collections::HashMap<NodeId, Filled>,
|
||||
threaded: &BTreeSet<NodeId>,
|
||||
restyled: &mut Vec<Rect>,
|
||||
notes: &mut Vec<String>,
|
||||
) {
|
||||
let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()) {
|
||||
Some(&[one]) => Some(one),
|
||||
_ => None,
|
||||
};
|
||||
let replaced = warped.settle(doc, made, threaded, restyled);
|
||||
if replaced > 0 {
|
||||
notes.push(format!("{}: {replaced}", ai::WARPED_TYPE_DRAWN));
|
||||
relayout_wrapped(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind the objects of `doc` to the variables of its library that `bindings` binds them to, each
|
||||
/// object named by its XML id (`ids`: the object read with that id, or what `filled` made of a text
|
||||
/// object's slot: for a visibility variable every object that took the slot's place, for a text
|
||||
@@ -579,6 +558,42 @@ fn bind_variables(
|
||||
notes.extend(ai::bindings_note(skipped));
|
||||
}
|
||||
|
||||
/// The text object slots of `doc` that `bindings` binds to a text variable, each named by its XML id
|
||||
/// (`ids`).
|
||||
fn text_bound_slots(doc: &Document, bindings: &[ai::BindingRef], ids: &std::collections::HashMap<String, Option<NodeId>>) -> BTreeSet<NodeId> {
|
||||
bindings
|
||||
.iter()
|
||||
.filter(|b| doc.variables.variable(&b.variable).is_some_and(|v| v.kind == VariableKind::Text))
|
||||
.filter_map(|b| b.target.as_ref().and_then(|t| ids.get(t).copied().flatten()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Warped type stays type where its live warp draws as its look, and where a text variable binds it
|
||||
/// (`bound_text`, by slot). Elsewhere the look takes its place, and one note says how many did.
|
||||
/// `filled` says what each slot became, and the comparisons with the page leave out the boxes in
|
||||
/// `restyled`. Fails as [`resolve_text_wrap`] does when the looks give the wrap shapes too many
|
||||
/// anchors.
|
||||
fn settle_warped_type(
|
||||
warped: ai::WarpedType,
|
||||
doc: &mut Document,
|
||||
filled: &std::collections::HashMap<NodeId, Filled>,
|
||||
threaded: &BTreeSet<NodeId>,
|
||||
bound_text: &BTreeSet<NodeId>,
|
||||
restyled: &mut Vec<Rect>,
|
||||
notes: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()).filter(|_| !bound_text.contains(&slot)) {
|
||||
Some(&[one]) => Some(one),
|
||||
_ => None,
|
||||
};
|
||||
let replaced = warped.settle(doc, made, threaded, restyled);
|
||||
if replaced > 0 {
|
||||
notes.push(format!("{}: {replaced}", ai::WARPED_TYPE_DRAWN));
|
||||
resolve_text_wrap(doc)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Most anchors copied into the wrap shapes of a file's area type ([`vectorcraft_doc::text::wrap_work`]).
|
||||
const MAX_WRAP_WORK: usize = 2_000_000;
|
||||
|
||||
@@ -1905,6 +1920,34 @@ mod candidate_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Warped type whose look takes its place, with the type's wrap setting, is held to the same
|
||||
/// limit on wrap shapes: a file whose looks would copy more than `MAX_WRAP_WORK` anchors isn't
|
||||
/// read from its layers.
|
||||
#[test]
|
||||
fn warped_type_replaced_by_its_look_is_held_to_the_wrap_limit() {
|
||||
for (texts, ok) in [(150, true), (250, false)] {
|
||||
let mut doc = Document::new(100.0, 100.0);
|
||||
(0..texts).for_each(|_| _ = insert_area(&mut doc, 0, Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "TILT", Default::default());
|
||||
let mut text = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let mut appearance = vectorcraft_doc::Appearance::default();
|
||||
appearance.effects.push(vectorcraft_doc::Effect { id: "warp.arc".into(), params: serde_json::json!({"bend": 40.0}), visible: true });
|
||||
text.appearance = appearance.into();
|
||||
text.extras_mut().wrap = Some(vectorcraft_doc::TextWrap::default());
|
||||
let top = doc.layers[0].children().map_or(0, Vec::len);
|
||||
let id = doc.insert(Some(doc.layers[0].id), top, text).unwrap();
|
||||
let look = insert_wrap(&mut doc, Rect::new(40.0, 0.0, 60.0, 100.0), 10_000);
|
||||
let look = doc.remove(look).unwrap();
|
||||
assert!(resolve_text_wrap(&mut doc).is_ok());
|
||||
let warped = ai::WarpedType::unchecked(vec![(id, (*look).clone())]);
|
||||
let filled = std::collections::HashMap::from([(id, Filled { text: vec![id], objects: vec![id] })]);
|
||||
let mut notes = vec![];
|
||||
let settled = settle_warped_type(warped, &mut doc, &filled, &BTreeSet::new(), &BTreeSet::new(), &mut vec![], &mut notes);
|
||||
assert_eq!(settled.is_ok(), ok, "{texts} text objects");
|
||||
assert_eq!(notes, ["warped text objects that came in as drawn art, not as editable type: 1"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Area type that wrapped around an object taken out of the layers flows without it.
|
||||
#[test]
|
||||
fn wrap_shapes_go_with_their_wrap_object() {
|
||||
|
||||
@@ -4956,6 +4956,33 @@ fn warped_type_that_draws_apart_from_its_look_comes_in_as_the_look() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Warped type that a text variable binds stays type with the effect live and keeps its binding,
|
||||
/// though its live warp draws apart from its look.
|
||||
#[test]
|
||||
fn warped_type_bound_to_a_text_variable_stays_bound_type() {
|
||||
use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node};
|
||||
let attributes =
|
||||
[("trait", XmlValue::Str("textcontent")), ("varName", XmlValue::Unicode("v_text")), ("docRef", XmlValue::Raw("/XMLUIDREF : (type_x5F_1) ;"))];
|
||||
let declared = [xml_node(1, "variable", XmlValue::Str(""), &attributes, &[])];
|
||||
let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]);
|
||||
let library = variable_library_data(&xml_element("variableSets", &[], &[set]));
|
||||
let drawing = "0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let art = format!("u\n{drawing}\nU\n9 () XW\n{own}{}1 (Warped) XW\n", named("type_x5F_1"));
|
||||
let doc = text_document(&[("TILT\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let file = native_with_text(&(library + &warped_style(40.0, WarpOn::Object) + &layer("Art", true, &art)), &doc);
|
||||
let (d, notes) = crate::ai_alone(&private(&file)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [("TILT".to_string(), true)], "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
let mut text = None;
|
||||
d.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
text = Some(n.id);
|
||||
}
|
||||
});
|
||||
assert_eq!(d.variables.objects_of("v_text"), [text.unwrap()], "{notes:?}");
|
||||
}
|
||||
|
||||
/// `operators` as the page's content: a red fill of the same paths.
|
||||
fn page_of(operators: &str) -> String {
|
||||
let mut page = String::from("1 0 0 setrgbcolor\n");
|
||||
|
||||
@@ -133,11 +133,12 @@ private editing data. VectorCraft:
|
||||
editing data is read whole (#758), live effects that are not read or have settings that are not read, and
|
||||
brushes come in as their drawn look (#637; pattern fills are read since #1025, and symbols and their instances
|
||||
open live); legacy `.ai` without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868; guides
|
||||
open as ruler guides and guide paths since #5057 (#779). Type under a Warp effect is compared with the art
|
||||
saved with it: type that draws as that art stays editable with the effect live, and type that draws differently, or
|
||||
that the file's limit of checks has no check left for, opens as that art, with one note that counts them. Type stays
|
||||
live without a comparison when the saved art paints nothing or draws type of its own, in frames of threaded type, and
|
||||
when its text object doesn't come in as one text object.
|
||||
open as ruler guides and guide paths since #5057 (#779).
|
||||
Type under a Warp effect is compared with the art saved with it: type that draws as that art stays editable with
|
||||
the effect live, and type that draws differently, or that the file's limit of checks has no check left for, opens
|
||||
as that art, with one note that counts them. 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.
|
||||
Legacy `.ai` type keeps its paragraphs' letter and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and
|
||||
hyphenation (`Th`); its tab definitions (`Xb` … `XB`) set its paragraphs' tab stops. Paragraphs in a row with
|
||||
the same attributes share one copy of them while the text object is read, and those copies hold at most
|
||||
|
||||
@@ -1273,7 +1273,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Blends and envelopes in the definitions of a `.ai`'s symbols, patterns and brushes' art count against the same per-file limits on blend layout and envelope checks as the layers' art, so a file with many definitions does not get those limits again for each one (`crates/eps/src/import/ai/read/directives.rs`).
|
||||
A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art. A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. An Art brush in Tints keeps that colorization. Pattern brushes and brushes that can't be read are left out with a note. Paths with a Calligraphic brush, and paths with an Art brush at 100% in Tints that stretches art of solid-filled paths unflipped, open as the path with the brush on its stroke, and other brushed paths still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
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` 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, 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, 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 unstroked path or compound path with straight segments whose only effect is Twist. 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).
|
||||
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