mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 20:56:47 -06:00
Merge PR #698
This commit is contained in:
@@ -148,6 +148,7 @@ Newest first.
|
||||
- **2026-10-12 (PDFW)** · PDF, booklet and book composition share guarded writing, metadata and font preparation through craft-pdf. Layout, shaping, placed graphics, colour policy, tagging and PDF/X postprocessing remain in DesignCraft. Failed native drawing returns an export error; failed writer cleanup cannot serialize an incomplete document. No readiness percentage change.
|
||||
|
||||
- **2026-10-13** · Copy with a placed graphic selected (Direct Selection) copies just the graphic, uncropped, where it sits on the page and without the frame's stroke, so Paste Into puts the whole image in place; Cut and Delete then remove only the graphic and leave its frame empty. Nested selections copy in their spread coordinates. No readiness percentage change.
|
||||
- **2026-10-13** · Object effects (Basic Feather, Inner Glow, shadows, glows, bevel, satin, directional and gradient feathers) apply to frames with pasted-in content, on screen and in image exports: the frame, its clipped content and its stroke are one object to the effect. PDF export still has no soft effects for any object. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Renderer {
|
||||
let reach = xf.transform_rect_bbox(bp.bounding_box()).inflate(sw + 1.0, sw + 1.0);
|
||||
// The silhouette is the shape when the object paints its area (fill / image); text frames
|
||||
// without a fill cast the shadow of their text only, which can't be grown.
|
||||
let solid = !it.fill.is_none() || matches!(it.content, designcraft_doc::Content::Graphic(_));
|
||||
let solid = !it.fill.is_none() || matches!(it.content, designcraft_doc::Content::Graphic(_)) || it.has_nested_items();
|
||||
let mut below = vec![];
|
||||
if e.outer_glow.on {
|
||||
let g = &e.outer_glow;
|
||||
|
||||
+12
-33
@@ -446,39 +446,9 @@ impl Renderer {
|
||||
}
|
||||
let xf = parent * it.xf;
|
||||
let vb = xf.transform_rect_bbox(it.inner_bounds()).inflate(it.stroke.extent() + 2.0, it.stroke.extent() + 2.0);
|
||||
if it.has_nested_items() {
|
||||
// A frame with items pasted into it: fill, the items clipped to the frame, stroke.
|
||||
if !rect_overlaps(vb, f.visible) {
|
||||
return;
|
||||
}
|
||||
let bp = if it.corners.is_none() { it.path.to_bezpath() } else { corners::apply(&it.path, &it.corners) };
|
||||
let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal;
|
||||
if layered {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(blend_mode(it.blend)), Some(it.opacity), None, None);
|
||||
}
|
||||
if !it.fill.is_none() {
|
||||
ctx.set_transform(f.view * xf);
|
||||
if set_fill_paint(ctx, f.doc, &it.fill, bp.bounding_box()) {
|
||||
ctx.fill_path(&bp);
|
||||
}
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
ctx.set_transform(f.view * xf);
|
||||
ctx.push_clip_layer(&bp);
|
||||
for c in it.children() {
|
||||
self.draw_item(ctx, f, c, xf, page);
|
||||
}
|
||||
ctx.pop_layer();
|
||||
if !it.stroke.is_none() {
|
||||
self.draw_stroke(ctx, f, it, &bp, xf);
|
||||
}
|
||||
if layered {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !it.children().is_empty() {
|
||||
// A frame with items pasted into it draws like any other object (draw_body: fill, the items
|
||||
// clipped to the frame, stroke), so its effects apply to all of it.
|
||||
if !it.children().is_empty() && !it.has_nested_items() {
|
||||
// Groups: children carry their own transforms relative to the group.
|
||||
// Isolate Blending / Knockout: the group is its own layer (blend modes stop at it).
|
||||
let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal || it.isolate || it.knockout;
|
||||
@@ -593,6 +563,15 @@ impl Renderer {
|
||||
}
|
||||
ctx.pop_layer();
|
||||
}
|
||||
// Items pasted into the frame, clipped by it.
|
||||
Content::Group { items } if it.has_nested_items() => {
|
||||
ctx.set_transform(f.view * xf);
|
||||
ctx.push_clip_layer(bp);
|
||||
for c in items {
|
||||
self.draw_item(ctx, f, c, xf, page);
|
||||
}
|
||||
ctx.pop_layer();
|
||||
}
|
||||
Content::Text(tf) => {
|
||||
let cs = self.story(f, tf.story, page);
|
||||
if let Some(ft) = cs.frame(it.id) {
|
||||
|
||||
@@ -222,3 +222,51 @@ fn multithreaded_effects_match_single_threaded() {
|
||||
assert!(worst <= 4, "case {i}: max channel difference {worst}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame (30..130) holding a pasted-in black square that overhangs it (Edit › Paste Into).
|
||||
fn pasted_into(d: &mut Document) -> ItemId {
|
||||
let id = rect_item(d, Rect::new(30.0, 30.0, 130.0, 130.0), "[None]");
|
||||
let kid = Item {
|
||||
fill: Fill::swatch("[Black]"),
|
||||
..Item::new(ItemId(d.alloc()), d.default_layer(), Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 160.0, 160.0)))
|
||||
};
|
||||
d.item_mut(id).unwrap().content = designcraft_doc::Content::Group { items: vec![std::sync::Arc::new(kid)] };
|
||||
assert!(d.item(id).unwrap().has_nested_items());
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_apply_to_a_frame_with_pasted_in_items() {
|
||||
let mut d = doc();
|
||||
let id = pasted_into(&mut d);
|
||||
let plain = render(&d);
|
||||
let black = lum(plain.pixel(80, 80));
|
||||
assert!(black < 200 && lum(plain.pixel(31, 80)) <= black + 6, "the content fills the frame: {:?} {:?}", plain.pixel(31, 80), plain.pixel(80, 80));
|
||||
assert_eq!(lum(plain.pixel(27, 80)), WHITE, "clipped by the frame");
|
||||
// Basic Feather fades the frame's edge, content and all.
|
||||
d.item_mut(id).unwrap().effects.feather = 16.0;
|
||||
let img = render(&d);
|
||||
assert!(lum(img.pixel(30, 80)) > 600, "nearly transparent at the path: {:?}", img.pixel(30, 80));
|
||||
assert!(lum(img.pixel(80, 80)) <= black + 6, "solid inside");
|
||||
assert_eq!(lum(img.pixel(27, 80)), WHITE, "still clipped");
|
||||
// Inner Glow: white screened in from the edges.
|
||||
d.item_mut(id).unwrap().effects.feather = 0.0;
|
||||
let g = &mut d.item_mut(id).unwrap().effects.inner_glow;
|
||||
g.on = true;
|
||||
g.size = 12.0;
|
||||
g.opacity = 1.0;
|
||||
let img = render(&d);
|
||||
assert!(lum(img.pixel(32, 80)) > black + 150, "lit at the edge: {:?}", img.pixel(32, 80));
|
||||
assert!(lum(img.pixel(80, 80)) <= black + 6, "dark in the middle");
|
||||
// Drop shadow: cast by the content's silhouette, outside the frame.
|
||||
d.item_mut(id).unwrap().effects.inner_glow.on = false;
|
||||
let ds = &mut d.item_mut(id).unwrap().effects.drop_shadow;
|
||||
ds.on = true;
|
||||
ds.angle = 180.0;
|
||||
ds.global_light = false;
|
||||
ds.distance = 12.0;
|
||||
ds.size = 0.0;
|
||||
ds.opacity = 1.0;
|
||||
let img = render(&d);
|
||||
assert!(lum(img.pixel(136, 80)) < 300, "the shadow to the right: {:?}", img.pixel(136, 80));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user