mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
PDF import: gradient-filled shapes open as one path, not a clipped rectangle (#1304)
A PDF, and the PDF part of a PDF-compatible .ai, writes a shape filled with a gradient as a clip followed by a shading fill: the shape, W n, then sh, which fills the clip's bounds. The importer kept that as a clip group of the shape (a clipping path) and a rectangle the size of its bounds filled with the gradient. The Shape Builder skips clipping paths, so it built its pieces from the rectangles: two overlapping gradient-filled diamonds gave three rectangles. The Fill swatch showed ? for such a selection, since it held groups. A clip whose only content is one rectangle covering the clip, filled with a gradient placed on the page and nothing else, now comes in as one path: the clip's outline, its subpaths closed, with the rectangle's fill, opacity, blend mode and mask. It draws as before and edits like a gradient-filled path drawn in the app. Clips holding anything else (a flat colour, several objects, a gradient that doesn't cover the clip, a shading that doesn't extend past its ends) stay clip groups. Tests: a diamond clipping a shading imports as one closed path of four anchors with the linear gradient where it was, and a gradient covering only part of the diamond or a flat colour stay clipped. Two overlapping diamonds in a hand-written PDF, clicked on their overlap with the Shape Builder, give what is left of each (9000 pt² each) and the overlap diamond (3000 pt²), each with its gradient; main gave three rectangles of 12000 pt². Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
21c580a934
commit
563ff62aa0
@@ -6,6 +6,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, FillRule, PathData, Rect};
|
||||
use vectorcraft_testkit::pdf::{PdfPage, pdf};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
|
||||
use super::*;
|
||||
@@ -172,6 +173,47 @@ fn shape_builder_tool_drives_the_command() {
|
||||
assert_eq!(layer_children(&s), 1);
|
||||
}
|
||||
|
||||
/// The first and last stop colours of a gradient paint.
|
||||
fn gradient_ends(p: &Paint) -> Option<([f32; 3], [f32; 3])> {
|
||||
let Paint::Gradient(g) = p else { return None };
|
||||
Some((g.gradient.stops.first()?.color.to_rgb(), g.gradient.stops.last()?.color.to_rgb()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_builder_cuts_gradient_shapes_from_a_pdf_along_their_outlines() {
|
||||
// Two overlapping diamonds (x 100–300 and 200–400, y 40–160), each written as gradient-filled
|
||||
// shapes are in a PDF or a PDF-compatible .ai: the diamond as a clip, then a shading (#1304).
|
||||
let shading = |x0: u32, c0: &str, c1: &str| {
|
||||
format!(
|
||||
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{x0} 0 {} 0] /Extend [true true] /Function << /FunctionType 2 /Domain [0 1] /C0 [{c0}] /C1 [{c1}] /N 1 >> >>",
|
||||
x0 + 200
|
||||
)
|
||||
};
|
||||
let diamond = |x0: u32| format!("q {x0} 100 m {} 160 l {} 100 l {} 40 l h W n", x0 + 100, x0 + 200, x0 + 100);
|
||||
let page = PdfPage {
|
||||
resources: format!("/Shading << /Sh0 {} /Sh1 {} >>", shading(100, "1 0 0", "1 1 0"), shading(200, "0 0 1", "0 1 1")),
|
||||
..PdfPage::new(500.0, 200.0, &format!("{} /Sh0 sh Q {} /Sh1 sh Q", diamond(100), diamond(200)))
|
||||
};
|
||||
let bytes = pdf(&[page], None);
|
||||
let mut s = Session::new();
|
||||
s.execute("document.open", &json!({"name": "diamonds.pdf", "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let r = s.execute("shapeBuilder.merge", &json!({"points": [[250, 100]]})).unwrap();
|
||||
let mut pieces: Vec<(f64, Rect, Paint)> =
|
||||
ids(&r).into_iter().map(|i| (area(&s, i), node(&s, i).geometric_bounds().unwrap(), node(&s, i).appearance.fill_paint())).collect();
|
||||
pieces.sort_by(|a, b| a.1.x0.total_cmp(&b.1.x0));
|
||||
let near = |a: Rect, b: Rect| [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|v| v.abs() < 0.01);
|
||||
let [(left, lb, lp), (mid, mb, mp), (right, rb, rp)] = pieces.as_slice() else { panic!("three pieces: {pieces:?}") };
|
||||
// What is left of each diamond, and the diamond where they overlap: not their bounding boxes.
|
||||
assert!((left - 9000.0).abs() < 1.0 && (mid - 3000.0).abs() < 1.0 && (right - 9000.0).abs() < 1.0, "{pieces:?}");
|
||||
assert!(near(*lb, Rect::new(100.0, 40.0, 250.0, 160.0)), "{lb:?}");
|
||||
assert!(near(*mb, Rect::new(200.0, 70.0, 300.0, 130.0)), "{mb:?}");
|
||||
assert!(near(*rb, Rect::new(250.0, 40.0, 400.0, 160.0)), "{rb:?}");
|
||||
// Each keeps its gradient; the overlap takes the front diamond's.
|
||||
let (first, second) = (Some(([1.0, 0.0, 0.0], [1.0, 1.0, 0.0])), Some(([0.0, 0.0, 1.0], [0.0, 1.0, 1.0])));
|
||||
assert_eq!((gradient_ends(lp), gradient_ends(mp), gradient_ends(rp)), (first, second, second));
|
||||
}
|
||||
|
||||
/// A red 100 × 100 square crossed by a stroke-only vertical line at x = 50, both selected.
|
||||
fn square_and_line(s: &mut Session) -> (NodeId, NodeId) {
|
||||
let a = rect(s, 0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
Reference in New Issue
Block a user