This commit is contained in:
Brandon Thomas
2026-10-14 22:05:11 -04:00
4 changed files with 62 additions and 34 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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) {
+48
View File
@@ -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));
}