mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-31 22:07:06 -06:00
Shape Format: Shape Effects — shadow, glow and soft edges (#275)
Shapes and text boxes get an effects model (outer shadow with colour, transparency, blur, distance and angle; glow with colour, size and transparency; soft-edge radius), the shape.effects command (presets, custom values or null per effect, applied to every selected shape, undoable, hostile numbers clamped), screen and PDF rendering with the same banded approximation of a blur, DOCX a:effectLst read/write (outerShdw, glow, softEdge) with a matching wp:effectExtent, and a contextual Shape Format tab with Shape Fill, Shape Outline and Shape Effects (Shadow, Glow, Soft Edges submenus) plus Arrange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40c5d51bf9
commit
2f8f304fc9
@@ -670,6 +670,7 @@ fn toc_field_skips_nested_stories() {
|
||||
stroke_width: 0.0,
|
||||
float: Float::default(),
|
||||
story: Some(story),
|
||||
effects: Default::default(),
|
||||
}
|
||||
};
|
||||
let mut head = Paragraph::with_text("Contents", CharProps::default()).styled("TOCHeading");
|
||||
@@ -791,6 +792,7 @@ fn shapes_textboxes_equations_dropcaps_round_trip() {
|
||||
stroke_width: 1.0,
|
||||
float: Float { wrap: Wrap::Square, h_rel: Anchor::Margin, v_rel: Anchor::Paragraph, x: 10.0, y: 20.0, dist: 0.0, ..Default::default() },
|
||||
story: Some(story),
|
||||
effects: Default::default(),
|
||||
};
|
||||
let star = InlineObject::Shape {
|
||||
kind: ShapeKind::Star,
|
||||
@@ -801,6 +803,7 @@ fn shapes_textboxes_equations_dropcaps_round_trip() {
|
||||
stroke_width: 0.0,
|
||||
float: Float::default(),
|
||||
story: None,
|
||||
effects: Default::default(),
|
||||
};
|
||||
let eq = InlineObject::Equation { linear: "x=(-b±√(b^2-4ac))/2a".into(), display: false, math: Default::default() };
|
||||
let mut p = Paragraph::with_text("shapes ", CharProps::default());
|
||||
@@ -816,7 +819,7 @@ fn shapes_textboxes_equations_dropcaps_round_trip() {
|
||||
let got = paras(&r);
|
||||
assert_eq!(got[0].objects.len(), 3);
|
||||
match &got[0].objects[0] {
|
||||
InlineObject::Shape { kind, w, h, fill, stroke, stroke_width, float, story } => {
|
||||
InlineObject::Shape { kind, w, h, fill, stroke, stroke_width, float, story, .. } => {
|
||||
assert_eq!(
|
||||
(*kind, *w, *h, *fill, *stroke, *stroke_width),
|
||||
(ShapeKind::TextBox, 144.0, 72.0, Some(Rgb(255, 255, 200)), Some(Rgb(0, 0, 0)), 1.0)
|
||||
@@ -1017,6 +1020,7 @@ fn self_showing_text_box_saves_bounded() {
|
||||
stroke_width: 0.0,
|
||||
float: Float::default(),
|
||||
story: Some(id),
|
||||
effects: Default::default(),
|
||||
};
|
||||
for _ in 0..30 {
|
||||
d.insert_object(
|
||||
@@ -1097,3 +1101,48 @@ fn list_level_overrides_round_trip() {
|
||||
assert_eq!(c.next_label(&back.numbering, restart, 0).unwrap().0, "1.");
|
||||
assert_eq!(c.next_label(&back.numbering, restart, 1).unwrap().0, "1.01");
|
||||
}
|
||||
|
||||
/// Shape effects (#275): `a:effectLst` with an outer shadow, a glow and soft edges round-trips,
|
||||
/// and the effect extent leaves room for them.
|
||||
#[test]
|
||||
fn shape_effects_round_trip() {
|
||||
use wordcraft_doc::effects::{Glow, Shadow, ShapeEffects};
|
||||
let effects = ShapeEffects {
|
||||
shadow: Some(Shadow { color: Rgb(0x20, 0x30, 0x40), transparency: 60.0, blur: 4.0, distance: 3.0, angle: 135.0 }),
|
||||
glow: Some(Glow { color: Rgb(0xC0, 0x50, 0x10), size: 8.0, transparency: 40.0 }),
|
||||
soft_edge: Some(2.5),
|
||||
};
|
||||
let shape = InlineObject::Shape {
|
||||
kind: ShapeKind::RoundedRectangle,
|
||||
w: 100.0,
|
||||
h: 50.0,
|
||||
fill: Some(Rgb(0x15, 0x60, 0x82)),
|
||||
stroke: None,
|
||||
stroke_width: 0.0,
|
||||
float: Float::default(),
|
||||
story: None,
|
||||
effects,
|
||||
};
|
||||
let mut p = Paragraph::with_text("x", CharProps::default());
|
||||
p.insert_object(1, shape, &CharProps::default()).unwrap();
|
||||
let bytes = wordcraft_docx::write(&doc_with(vec![p])).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
assert!(
|
||||
xml.contains(
|
||||
r#"<a:outerShdw blurRad="50800" dist="38100" dir="8100000" algn="tr" rotWithShape="0"><a:srgbClr val="203040"><a:alpha val="40000"/>"#
|
||||
),
|
||||
"{xml}"
|
||||
);
|
||||
assert!(xml.contains(r#"<a:glow rad="101600"><a:srgbClr val="C05010"><a:alpha val="60000"/>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:softEdge rad="31750"/>"#), "{xml}");
|
||||
let r = wordcraft_docx::read(&bytes).expect("read");
|
||||
match paras(&r)[0].objects.first() {
|
||||
Some(InlineObject::Shape { effects: got, float, .. }) => {
|
||||
assert_eq!(*got, effects);
|
||||
assert_eq!(float.effect_extent(), effects.extent());
|
||||
}
|
||||
o => panic!("{o:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user