Merge remote-tracking branch 'upstream/main' into fix/aligned-lines-trailing-tracking

# Conflicts:
#	docs/file-format-parity.md
This commit is contained in:
jmverges
2026-11-11 16:38:59 +00:00
17 changed files with 740 additions and 38 deletions
@@ -903,6 +903,29 @@ fn gradients_and_patterns_on_a_picked_object_are_taken() {
assert_eq!((undo_steps(&s), &s.paint.fill), (undo, &fill), "nothing changes");
}
/// A stop selected on the whole instance's gradient is not selected once a part of its art is
/// picked, so the Gradient tool does not take Delete for it, and is selected again when the whole
/// instance is.
#[test]
fn picking_a_part_leaves_no_gradient_stop_selected() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
let g = json!({"kind": "linear", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]});
s.execute("select.set", &json!({"ids": [inst.0]})).unwrap();
s.execute("paint.setFill", &json!({"gradient": g})).unwrap();
s.fill_active = true;
let view = ViewInfo::default();
s.select_tool("gradient", view).unwrap();
s.execute("gradient.selectStop", &json!({"index": 1})).unwrap();
assert_eq!(s.selected_stop(), Some(1));
assert!(s.tool_claims_key(vectorcraft_tools::ToolKey::Delete, view));
s.execute("symbol.instance.selectPart", &json!({"id": inst.0, "path": [1]})).unwrap();
assert_eq!(s.selected_stop(), None);
assert!(!s.tool_claims_key(vectorcraft_tools::ToolKey::Delete, view));
s.execute("select.set", &json!({"ids": [inst.0]})).unwrap();
assert_eq!(s.selected_stop(), Some(1));
}
#[test]
fn opacity_and_blend_on_a_picked_object_go_through_transparency_set() {
let mut s = session();
+71 -6
View File
@@ -324,13 +324,13 @@ fn pasted_art_takes_no_name_the_documents_brushes_use() {
assert_eq!(names, ["Shift 3"]);
}
/// A pasted filter at the size limit with a one-letter name, when every free name it could take is
/// in use, is refused rather than pasted under the name of the document's filter.
/// A pasted filter at the size limit with a one-letter name takes a free capital letter when every
/// lowercase letter and digit is in use.
#[test]
fn a_paste_with_no_free_name_for_its_filter_is_refused() {
fn a_renamed_paste_takes_a_free_capital_letter() {
let mut s = session();
for n in ('a'..='z').chain('0'..='9') {
run(&mut s, "svgFilter.define", json!({"xml": format!(r#"<filter id="{n}"><feOffset dx="1" dy="1"/></filter>"#)}));
run(&mut s, "svgFilter.define", json!({"xml": one_char_filter(n)}));
}
run(&mut s, "svgFilter.define", json!({"xml": r#"<filter id="S"><feOffset dx="3" dy="3"/></filter>"#}));
let (open, close) = (r#"<filter id="S"><feOffset dx="9" dy="9" result=""#, r#""/></filter>"#);
@@ -339,9 +339,74 @@ fn a_paste_with_no_free_name_for_its_filter_is_refused() {
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{big}</defs><rect width="30" height="30" filter="url(#S)"/></svg>"#
);
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
let before = steps(&s);
run(&mut s, "edit.pasteInPlace", json!({}));
let pasted = filter_effects(&s);
assert_eq!(pasted.len(), 1);
assert_eq!(pasted[0].0, "A");
assert_eq!(vectorcraft_render::effects::svg_filter::check(&pasted[0].1), Ok("A".to_string()));
assert!(pasted[0].1.contains("dx=\"9\""), "its own filter");
}
/// A filter named `n`, with the characters a quoted attribute value cannot hold escaped.
fn one_char_filter(n: char) -> String {
let id = match n {
'"' => "&quot;".to_string(),
'&' => "&amp;".to_string(),
'<' => "&lt;".to_string(),
c => c.to_string(),
};
format!(r#"<filter id="{id}"><feOffset dx="1" dy="1"/></filter>"#)
}
/// A pasted filter at the size limit with a one-letter name takes a free punctuation character when
/// every letter and digit is in use.
#[test]
fn a_renamed_paste_takes_a_free_punctuation_character() {
let mut s = session();
for n in ('a'..='z').chain('0'..='9').chain('A'..='Z') {
run(&mut s, "svgFilter.define", json!({"xml": one_char_filter(n)}));
}
let (open, close) = (r#"<filter id="S"><feOffset dx="9" dy="9" result=""#, r#""/></filter>"#);
let big = format!("{open}{}{close}", "a".repeat(vectorcraft_doc::SvgFilter::MAX_XML - open.len() - close.len()));
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{big}</defs><rect width="30" height="30" filter="url(#S)"/></svg>"#
);
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
run(&mut s, "edit.pasteInPlace", json!({}));
let pasted = filter_effects(&s);
assert_eq!(pasted.len(), 1);
assert_eq!(pasted[0].0, "!");
assert_eq!(vectorcraft_render::effects::svg_filter::check(&pasted[0].1), Ok("!".to_string()));
assert!(pasted[0].1.contains("dx=\"9\""), "its own filter");
}
/// A pasted filter at the size limit with a one-letter name, when every free name it could take is
/// in use, is refused rather than pasted under the name of the document's filter.
#[test]
fn a_paste_with_no_free_name_for_its_filter_is_refused() {
let mut s = session();
for n in ('a'..='z').chain('0'..='9').chain('A'..='Z') {
run(&mut s, "svgFilter.define", json!({"xml": one_char_filter(n)}));
}
// A document holds at most 64 filters: the other one-character names are taken by effects
// whose filters were deleted.
for n in ('!'..='~').filter(|c| !c.is_ascii_alphanumeric()) {
let a = rect(&mut s, 10.0);
let name = run(&mut s, "svgFilter.define", json!({"xml": one_char_filter(n)}))["name"].as_str().unwrap().to_string();
assert_eq!(name, n.to_string());
run(&mut s, "svgFilter.apply", json!({"name": name, "ids": [a.0]}));
run(&mut s, "svgFilter.delete", json!({"name": name}));
}
let (open, close) = (r#"<filter id="S"><feOffset dx="9" dy="9" result=""#, r#""/></filter>"#);
let big = format!("{open}{}{close}", "a".repeat(vectorcraft_doc::SvgFilter::MAX_XML - open.len() - close.len()));
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{big}</defs><rect width="30" height="30" filter="url(#S)"/></svg>"#
);
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
let (before, effects) = (steps(&s), filter_effects(&s).len());
assert_eq!(effects, 32);
assert!(err(&mut s, "edit.pasteInPlace", json!({})).contains("no free name"));
assert!(filter_effects(&s).is_empty(), "nothing pasted");
assert_eq!(filter_effects(&s).len(), effects, "nothing pasted");
assert_eq!(steps(&s), before);
}