mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 04:09:53 -06:00
Walks the display list (as the PDF and PNG exports draw it) for glyph id 0 and names each by code point, capped at 256 distinct. Chart/SmartArt label runs and equation runs now carry per-glyph text ranges. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FypgCX8G9hbs6XYZFsgjbL
533 lines
23 KiB
Rust
533 lines
23 KiB
Rust
use super::options::DisplayOptions;
|
||
use super::paint::{Paint, paint_for};
|
||
use super::{BezPath, Document, Draw, FaceRef, Graphic, GraphicItem, GraphicKind, InlineObject, PathSeg, Rect, Rgb, ShapeKind, Spin, TextAlign};
|
||
use wordcraft_doc::connector::ArrowStyle;
|
||
|
||
/// The draws of a chart or diagram's items inside `rect` (page coordinates). The items were built
|
||
/// for the size `g.w` × `g.h` (0: `rect`'s own size), so they are scaled to `rect`; strokes and text
|
||
/// by the mean of the two scales.
|
||
pub(super) fn graphic_draws(doc: &Document, g: &Graphic, rect: Rect, alpha: f32) -> Vec<Draw> {
|
||
let (sx, sy) = (scale_of(rect.w, g.w), scale_of(rect.h, g.h));
|
||
let k = (sx + sy) / 2.0;
|
||
let inside = |r: &[f32; 4]| Rect::new(rect.x + r[0] * sx, rect.y + r[1] * sy, r[2] * sx, r[3] * sy);
|
||
let mut out = Vec::with_capacity(g.items.len());
|
||
for it in &g.items {
|
||
match it {
|
||
GraphicItem::Shape { rect: r, kind, fill, stroke, stroke_width } => out.push(Draw::Shape {
|
||
rect: inside(r),
|
||
kind: *kind,
|
||
fill: fill.map(Paint::solid),
|
||
stroke: *stroke,
|
||
stroke_width: stroke_width * k,
|
||
effects: Default::default(),
|
||
corner: None,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
}),
|
||
GraphicItem::Path { segs, fill, stroke, stroke_width } => {
|
||
let segs = page_segs(segs, |x, y| (rect.x + x * sx, rect.y + y * sy));
|
||
out.push(Draw::Path {
|
||
segs,
|
||
fill: fill.map(Paint::solid),
|
||
stroke: *stroke,
|
||
stroke_width: stroke_width * k,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
})
|
||
}
|
||
GraphicItem::Text { rect: r, .. } => out.extend(text_glyphs(doc, inside(r), it, alpha, k)),
|
||
GraphicItem::Image { rect: r, media } => out.push(Draw::Image {
|
||
rect: inside(r),
|
||
media: media.clone(),
|
||
crop: [0.0; 4],
|
||
alpha,
|
||
alt: String::new(),
|
||
fx: Default::default(),
|
||
decorative: false,
|
||
}),
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Scale from the size `built` points a graphic's items were made for to `len` points; 1 when
|
||
/// the built size is unknown (0).
|
||
fn scale_of(len: f32, built: f32) -> f32 {
|
||
if built.is_finite() && built > 0.0 && len.is_finite() { (len / built).max(0.0) } else { 1.0 }
|
||
}
|
||
|
||
/// Path segments mapped to page coordinates by `map`, cleaned for the rasterizer and PDF: a segment
|
||
/// with a non-finite number is dropped, and so is anything before a move (or after a broken one).
|
||
fn page_segs(segs: &[PathSeg], map: impl Fn(f32, f32) -> (f32, f32)) -> Vec<PathSeg> {
|
||
let fin = |p: (f32, f32)| p.0.is_finite() && p.1.is_finite();
|
||
let mut out = Vec::with_capacity(segs.len());
|
||
let mut open = false;
|
||
for s in segs {
|
||
match *s {
|
||
PathSeg::Move(x, y) => {
|
||
let p = map(x, y);
|
||
open = fin(p);
|
||
if open {
|
||
out.push(PathSeg::Move(p.0, p.1));
|
||
}
|
||
}
|
||
PathSeg::Line(x, y) => {
|
||
let p = map(x, y);
|
||
if open && fin(p) {
|
||
out.push(PathSeg::Line(p.0, p.1));
|
||
}
|
||
}
|
||
PathSeg::Cubic(a, b, c, d, e, f) => {
|
||
let (p1, p2, p3) = (map(a, b), map(c, d), map(e, f));
|
||
if open && fin(p1) && fin(p2) && fin(p3) {
|
||
out.push(PathSeg::Cubic(p1.0, p1.1, p2.0, p2.1, p3.0, p3.1));
|
||
}
|
||
}
|
||
PathSeg::Close => {
|
||
if open {
|
||
out.push(PathSeg::Close);
|
||
}
|
||
open = false;
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// A path of clean segments (see [`page_segs`]) as a kurbo path, for the rasteriser and PDF.
|
||
/// How a shape looks: its preset `kind`, fill, outline, effects, freeform paths and connector extras.
|
||
#[derive(Clone, Copy)]
|
||
pub(super) struct ShapeLook<'a> {
|
||
pub kind: ShapeKind,
|
||
pub fill: Option<&'a wordcraft_doc::fill::Fill>,
|
||
pub stroke: Option<Rgb>,
|
||
pub stroke_width: f32,
|
||
pub effects: wordcraft_doc::effects::ShapeEffects,
|
||
pub freeform: Option<&'a wordcraft_doc::freeform::Freeform>,
|
||
pub extra: &'a wordcraft_doc::connector::ShapeExtra,
|
||
}
|
||
|
||
/// The draws of a shape at `rect`: a preset outline, or a freeform's paths (an ink stroke left out
|
||
/// under Review › Hide Ink).
|
||
pub(super) fn shape_draws(rect: Rect, look: &ShapeLook, opts: &DisplayOptions, out: &mut Vec<Draw>) {
|
||
let ShapeLook { kind, fill, stroke, stroke_width, effects, freeform, extra } = *look;
|
||
// Freeforms are painted across the shape's frame, not each path's bounds.
|
||
let fill = fill.and_then(|f| paint_for(f, rect));
|
||
if kind.is_open() {
|
||
line_draws(rect, kind, stroke, stroke_width, effects, extra, out);
|
||
return;
|
||
}
|
||
let Some(f) = freeform.filter(|_| kind == ShapeKind::Freeform) else {
|
||
out.push(Draw::Shape {
|
||
rect,
|
||
kind,
|
||
fill,
|
||
stroke,
|
||
stroke_width,
|
||
effects,
|
||
corner: extra.corner,
|
||
dash: extra.dash.pattern(stroke_width),
|
||
cap: extra.cap,
|
||
});
|
||
return;
|
||
};
|
||
if f.is_ink() && opts.hide_ink {
|
||
return;
|
||
}
|
||
let width = if stroke_width.is_finite() { stroke_width.clamp(0.0, 200.0) } else { 0.75 };
|
||
for (pts, closed) in f.placed(rect.x, rect.y, rect.w, rect.h) {
|
||
if f.is_ink() || (!closed && fill.is_none()) {
|
||
if let Some(color) = stroke {
|
||
let mut pts: Vec<(f32, f32)> = pts.iter().map(|[x, y]| (*x, *y)).collect();
|
||
if closed && let Some(first) = pts.first().copied() {
|
||
pts.push(first);
|
||
}
|
||
if !closed
|
||
&& extra.arrow_end
|
||
&& let Some(arrow) = path_arrowhead(&pts, color, width, extra.end_style)
|
||
{
|
||
out.push(arrow);
|
||
}
|
||
out.push(Draw::Ink { pts, color, width: width.max(0.25), alpha: f.alpha });
|
||
}
|
||
continue;
|
||
}
|
||
let mut segs: Vec<PathSeg> = Vec::with_capacity(pts.len() + 1);
|
||
for (i, [x, y]) in pts.iter().enumerate() {
|
||
segs.push(if i == 0 { PathSeg::Move(*x, *y) } else { PathSeg::Line(*x, *y) });
|
||
}
|
||
if closed {
|
||
segs.push(PathSeg::Close);
|
||
}
|
||
let width = path_stroke_width(width);
|
||
out.push(Draw::Path {
|
||
segs,
|
||
fill: if closed { fill.clone() } else { None },
|
||
stroke,
|
||
stroke_width: width,
|
||
dash: extra.dash.pattern(width),
|
||
cap: extra.cap,
|
||
});
|
||
if !closed
|
||
&& extra.arrow_end
|
||
&& let Some(color) = stroke
|
||
{
|
||
let points: Vec<(f32, f32)> = pts.iter().map(|[x, y]| (*x, *y)).collect();
|
||
if let Some(arrow) = path_arrowhead(&points, color, width, extra.end_style) {
|
||
out.push(arrow);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dash lengths use the effective width used by both path painters.
|
||
fn path_stroke_width(width: f32) -> f32 {
|
||
if width.is_finite() && width > 0.0 { width.clamp(0.25, 200.0) } else { 0.75 }
|
||
}
|
||
|
||
/// Make a filled triangular arrowhead at the last point, in the points' coordinate space.
|
||
/// The last distinct segment supplies its direction; `style` and the stroke width set its size.
|
||
/// Returns `None` if there is no usable final segment. The triangle has no outline, so the path's
|
||
/// cap setting does not affect its appearance.
|
||
fn path_arrowhead(points: &[(f32, f32)], color: Rgb, stroke_width: f32, style: ArrowStyle) -> Option<Draw> {
|
||
let tip = *points.last()?;
|
||
let (prev_x, prev_y) = points.iter().rev().skip(1).copied().find(|p| *p != tip)?;
|
||
let dx = tip.0 - prev_x;
|
||
let dy = tip.1 - prev_y;
|
||
let length = dx.hypot(dy);
|
||
if !length.is_finite() || length <= f32::EPSILON {
|
||
return None;
|
||
}
|
||
Some(arrowhead(tip, (dx / length, dy / length), stroke_width, style, color))
|
||
}
|
||
|
||
/// A filled triangular arrowhead with its tip at `tip`, pointing along unit vector `dir`: as long
|
||
/// and as wide as `style` makes it for a `stroke_width` line ([`ArrowStyle::dims`]).
|
||
fn arrowhead(tip: (f32, f32), (dx, dy): (f32, f32), stroke_width: f32, style: ArrowStyle, color: Rgb) -> Draw {
|
||
let (len, wide) = style.dims(stroke_width);
|
||
let (bx, by) = (tip.0 - dx * len, tip.1 - dy * len);
|
||
let (nx, ny) = (-dy * wide / 2.0, dx * wide / 2.0);
|
||
let segs = vec![PathSeg::Move(tip.0, tip.1), PathSeg::Line(bx + nx, by + ny), PathSeg::Line(bx - nx, by - ny), PathSeg::Close];
|
||
Draw::Path { segs, fill: Some(Paint::solid(color)), stroke: None, stroke_width: 0.0, dash: Vec::new(), cap: Default::default() }
|
||
}
|
||
|
||
/// A line or connector at `rect` (its unflipped frame: flips are the frame's turn), with its
|
||
/// arrowheads.
|
||
fn line_draws(
|
||
rect: Rect,
|
||
kind: ShapeKind,
|
||
stroke: Option<Rgb>,
|
||
stroke_width: f32,
|
||
effects: wordcraft_doc::effects::ShapeEffects,
|
||
extra: &wordcraft_doc::connector::ShapeExtra,
|
||
out: &mut Vec<Draw>,
|
||
) {
|
||
use wordcraft_doc::connector::{connector_segs, end_direction};
|
||
match connector_segs(kind, rect.w, rect.h) {
|
||
Some(segs) => {
|
||
let segs = segs
|
||
.into_iter()
|
||
.map(|s| match s {
|
||
PathSeg::Move(x, y) => PathSeg::Move(rect.x + x, rect.y + y),
|
||
PathSeg::Line(x, y) => PathSeg::Line(rect.x + x, rect.y + y),
|
||
PathSeg::Cubic(a, b, c, d, e, f) => PathSeg::Cubic(rect.x + a, rect.y + b, rect.x + c, rect.y + d, rect.x + e, rect.y + f),
|
||
PathSeg::Close => PathSeg::Close,
|
||
})
|
||
.collect();
|
||
let width = path_stroke_width(stroke_width);
|
||
out.push(Draw::Path { segs, fill: None, stroke, stroke_width: width, dash: extra.dash.pattern(width), cap: extra.cap });
|
||
}
|
||
None => out.push(Draw::Shape {
|
||
rect,
|
||
kind,
|
||
fill: None,
|
||
stroke,
|
||
stroke_width,
|
||
effects,
|
||
corner: None,
|
||
dash: extra.dash.pattern(stroke_width),
|
||
cap: extra.cap,
|
||
}),
|
||
}
|
||
let Some(color) = stroke else { return };
|
||
// A line runs from the top-left corner to the bottom-right, as it is drawn (DrawingML's
|
||
// `line`), and so do connectors; flips are the frame's turn.
|
||
let (start, end) = ((rect.x, rect.y), (rect.right(), rect.bottom()));
|
||
for (on, tip, at_end, style) in [(extra.arrow_start, start, false, extra.start_style), (extra.arrow_end, end, true, extra.end_style)] {
|
||
if on {
|
||
out.push(arrowhead(tip, end_direction(kind, rect.w, rect.h, at_end), stroke_width, style, color));
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn seg_path(segs: &[PathSeg]) -> BezPath {
|
||
let mut p = BezPath::new();
|
||
for s in segs {
|
||
match *s {
|
||
PathSeg::Move(x, y) => p.move_to((x as f64, y as f64)),
|
||
PathSeg::Line(x, y) => p.line_to((x as f64, y as f64)),
|
||
PathSeg::Cubic(a, b, c, d, e, f) => p.curve_to((a as f64, b as f64), (c as f64, d as f64), (e as f64, f as f64)),
|
||
PathSeg::Close => p.close_path(),
|
||
}
|
||
}
|
||
p
|
||
}
|
||
|
||
/// One line of a `GraphicItem::Text` in `r`, shaped in its font (the document's body font when
|
||
/// unset) and aligned horizontally, centred vertically, at its size times `k`. Characters the font
|
||
/// lacks (Chinese in a chart drawn in Calibri's substitute, #4720) are drawn in a fallback face, as
|
||
/// in body text: one draw per run of a face, in reading order. Empty when there is nothing to draw,
|
||
/// or the item's rectangle or size isn't a finite number (a degenerate scale: `r` itself is always
|
||
/// finite, as [`Rect::new`] makes it).
|
||
fn text_glyphs(doc: &Document, r: Rect, it: &GraphicItem, alpha: f32, k: f32) -> Vec<Draw> {
|
||
let GraphicItem::Text { rect, text, size, color, bold, align, font } = it else { return Vec::new() };
|
||
let size = *size * k;
|
||
if !rect.iter().chain([&size]).all(|v| v.is_finite()) {
|
||
return Vec::new();
|
||
}
|
||
let family = font.as_deref().filter(|f| !f.is_empty()).unwrap_or(&doc.settings.minor_font);
|
||
let size = size.clamp(1.0, 400.0);
|
||
let resolved = wordcraft_fonts::word::resolve(family, *bold, false);
|
||
let primary = resolved.face;
|
||
let scale = |f: FaceRef| size / f.upem.max(1.0) as f32;
|
||
let runs: Vec<_> = face_runs(text, primary, *bold)
|
||
.into_iter()
|
||
.map(|(face, s)| (face, s, wordcraft_fonts::shape(face.get(), s, &[], |c| c)))
|
||
.filter(|(_, _, shaped)| !shaped.is_empty())
|
||
.collect();
|
||
let width: f32 = runs.iter().map(|(f, _, shaped)| shaped.iter().map(|g| g.x_advance as f32).sum::<f32>() * scale(*f)).sum();
|
||
// The font's own line metrics centre the line, whichever faces draw it.
|
||
let (ascent, descent) = wordcraft_fonts::word::line_metrics(primary.get());
|
||
let baseline = r.y + r.h / 2.0 + (ascent - descent) as f32 * scale(primary) / 2.0;
|
||
let mut pen = match align {
|
||
TextAlign::Left => r.x,
|
||
TextAlign::Center => r.x + (r.w - width) / 2.0,
|
||
TextAlign::Right => r.x + r.w - width,
|
||
};
|
||
let mut out = Vec::with_capacity(runs.len());
|
||
for (face, s, shaped) in runs {
|
||
let k = scale(face);
|
||
let ranges = crate::missing::cluster_ranges(s, &shaped);
|
||
let mut glyphs = Vec::with_capacity(shaped.len());
|
||
for g in &shaped {
|
||
glyphs.push((g.gid, pen + g.x_offset as f32 * k, baseline - g.y_offset as f32 * k));
|
||
pen += g.x_advance as f32 * k;
|
||
}
|
||
// A fallback face is emboldened when it has no bold of its own, as in body text.
|
||
let synth_bold = if face.id() == primary.id() { resolved.synth_bold } else { *bold && face.weight < 600.0 };
|
||
out.push(Draw::Glyphs {
|
||
face,
|
||
size,
|
||
glyphs,
|
||
color: *color,
|
||
alpha,
|
||
synth_bold,
|
||
synth_italic: false,
|
||
text: s.to_string(),
|
||
link: None,
|
||
ranges,
|
||
text_bg: None,
|
||
x_scale: 1.0,
|
||
});
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `text` split into runs of the face that draws them: `primary` where it has the character, else a
|
||
/// fallback face. A run the primary lacks takes one face covering all of it when one does (so a
|
||
/// Chinese label doesn't mix serif and sans glyphs), else each character its own fallback. Spaces
|
||
/// and combining marks stay in the run before them; a character no face has stays in `primary`.
|
||
fn face_runs(text: &str, primary: FaceRef, bold: bool) -> Vec<(FaceRef, &str)> {
|
||
let joins = |c: char| c.is_whitespace() || wordcraft_doc::math_linear::is_combining(c);
|
||
// (start, end, drawn by `primary`)
|
||
let mut spans: Vec<(usize, usize, bool)> = Vec::new();
|
||
for (i, c) in text.char_indices() {
|
||
let end = i + c.len_utf8();
|
||
let own = primary.covers(c);
|
||
match spans.last_mut() {
|
||
Some(last) if last.2 == own || joins(c) => last.1 = end,
|
||
_ => spans.push((i, end, own)),
|
||
}
|
||
}
|
||
let db = wordcraft_fonts::FontDb::global();
|
||
let mut out: Vec<(FaceRef, &str)> = Vec::with_capacity(spans.len());
|
||
for (start, end, own) in spans {
|
||
let Some(s) = text.get(start..end) else { continue };
|
||
if own {
|
||
out.push((primary, s));
|
||
continue;
|
||
}
|
||
let chars: Vec<char> = s.chars().filter(|c| !joins(*c)).collect();
|
||
if let Some(f) = db.fallback_for_run(&chars, primary.id(), bold, false) {
|
||
out.push((FaceRef::of(&f), s));
|
||
continue;
|
||
}
|
||
// No one face has them all: each character in its own fallback, neighbours sharing a face
|
||
// in one run.
|
||
let mut run: Option<(FaceRef, usize)> = None;
|
||
for (i, c) in s.char_indices() {
|
||
let face =
|
||
if joins(c) { None } else { Some(db.fallback_styled(c, primary.id(), bold, false).map(|f| FaceRef::of(&f)).unwrap_or(primary)) };
|
||
match (run, face) {
|
||
(Some((cur, from)), Some(f)) if f.id() != cur.id() => {
|
||
out.extend(s.get(from..i).map(|t| (cur, t)));
|
||
run = Some((f, i));
|
||
}
|
||
(None, f) => run = Some((f.unwrap_or(primary), i)),
|
||
_ => {}
|
||
}
|
||
}
|
||
if let Some((cur, from)) = run {
|
||
out.extend(s.get(from..).map(|t| (cur, t)));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `items` turned by `spin` about the centre of `rect` (as they are, unturned).
|
||
pub(super) fn spun(spin: Spin, rect: Rect, items: Vec<Draw>, out: &mut Vec<Draw>) {
|
||
if spin.is_identity() {
|
||
out.extend(items);
|
||
} else {
|
||
out.push(Draw::Rotated { cx: rect.x + rect.w / 2.0, cy: rect.y + rect.h / 2.0, spin, items });
|
||
}
|
||
}
|
||
|
||
/// A picture, shape or group drawn in `rect` (its unturned frame), turned by its own spin about
|
||
/// the rect's centre; a group's members are turned inside it. `outer` is the spin it is already
|
||
/// drawn inside (its group's). Nothing for anything else.
|
||
pub(super) fn object_draws(o: &InlineObject, rect: Rect, outer: Spin, alpha: f32, opts: &DisplayOptions, out: &mut Vec<Draw>) {
|
||
let own = o.frame().map(|(_, _, f)| f.spin()).unwrap_or_default();
|
||
match o {
|
||
InlineObject::Image { media, crop, alt, fx, .. } => {
|
||
let alpha = alpha * fx.opacity();
|
||
let decorative = o.is_decorative();
|
||
spun(own, rect, vec![Draw::Image { rect, media: media.clone(), crop: *crop, alpha, alt: alt.clone(), fx: *fx, decorative }], out)
|
||
}
|
||
InlineObject::Shape { kind, fill, stroke, stroke_width, effects, freeform, extra, .. } => {
|
||
let effects = effects_in(*effects, own.within(outer));
|
||
let mut items = Vec::new();
|
||
shape_draws(
|
||
rect,
|
||
&ShapeLook {
|
||
kind: *kind,
|
||
fill: fill.as_ref(),
|
||
stroke: *stroke,
|
||
stroke_width: *stroke_width,
|
||
effects,
|
||
freeform: freeform.as_deref(),
|
||
extra,
|
||
},
|
||
opts,
|
||
&mut items,
|
||
);
|
||
match crate::shape_figure_alt(o) {
|
||
// A described shape is one figure with its description (tagged PDF).
|
||
Some(alt) => {
|
||
let mut draws = Vec::new();
|
||
spun(own, rect, items, &mut draws);
|
||
out.push(Draw::Figure { alt: alt.to_string(), kind: None, draws })
|
||
}
|
||
None => spun(own, rect, items, out),
|
||
}
|
||
}
|
||
InlineObject::Group { .. } => {
|
||
let mut members = Vec::new();
|
||
for ([x, y, w, h], c) in o.group_rects(rect.x, rect.y, rect.w, rect.h) {
|
||
if c.frame().is_some_and(|(_, _, f)| f.hidden) {
|
||
continue;
|
||
}
|
||
object_draws(c, Rect::new(x, y, w, h), own.within(outer), alpha, opts, &mut members);
|
||
}
|
||
match o.canvas_style() {
|
||
// A Drawing Canvas: its background and outline, then its members clipped to its
|
||
// frame, all inside its turn.
|
||
Some(c) => spun(own, rect, vec![canvas_background(rect, c), Draw::Clip { rect, items: members }], out),
|
||
None => spun(own, rect, members, out),
|
||
}
|
||
}
|
||
// Native ink in a group (kept opaque): drawn as drawing-level ink is, from its pictures.
|
||
InlineObject::Graphic { graphic, .. } if graphic.kind == GraphicKind::Ink => {
|
||
let (sx, sy) = (scale_of(rect.w, graphic.w), scale_of(rect.h, graphic.h));
|
||
let items = graphic
|
||
.items
|
||
.iter()
|
||
.filter_map(|it| match it {
|
||
GraphicItem::Image { rect: r, media } => Some(Draw::Image {
|
||
rect: Rect::new(rect.x + r[0] * sx, rect.y + r[1] * sy, r[2] * sx, r[3] * sy),
|
||
media: media.clone(),
|
||
crop: [0.0; 4],
|
||
alpha,
|
||
alt: String::new(),
|
||
fx: Default::default(),
|
||
decorative: false,
|
||
}),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
spun(own, rect, items, out)
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// A shape's `effects` as drawn turned by `spin` (all the turns it is drawn inside): glows and
|
||
/// soft edges turn with the shape, and so does a shadow's offset when it rotates with the shape
|
||
/// (`rotWithShape`); one that doesn't keeps its direction on the page, so its angle is turned back.
|
||
pub(crate) fn effects_in(effects: wordcraft_doc::effects::ShapeEffects, spin: Spin) -> wordcraft_doc::effects::ShapeEffects {
|
||
let mut e = effects;
|
||
if let Some(s) = e.shadow.as_mut().filter(|s| !s.rot_with_shape && !spin.is_identity()) {
|
||
let a = s.angle.to_radians();
|
||
let (dx, dy) = spin.unapply(0.0, 0.0, a.cos(), a.sin());
|
||
s.angle = wordcraft_geom::normalize_degrees(dy.atan2(dx).to_degrees());
|
||
}
|
||
e
|
||
}
|
||
|
||
/// Where inline object `obj` is drawn, given its cluster's box (`adv` wide at `cx`, reaching
|
||
/// `obj_h` above the baseline and `obj_d` below it): inside the room kept for its effects.
|
||
/// A rotated one's unrotated frame, centred in that room.
|
||
///
|
||
/// `line` is the room a horizontal rule is placed in, from `cx`; `modern`: Word 2013+ layout
|
||
/// (see [`wordcraft_doc::para::Float::line_pad`]).
|
||
pub(crate) fn inline_rect(obj: Option<&InlineObject>, cx: f32, base: f32, adv: f32, (obj_h, obj_d): (f32, f32), line: f32, modern: bool) -> Rect {
|
||
let frame = obj.and_then(InlineObject::frame);
|
||
let [l, t, r, b] = frame.map_or([0.0; 4], |(_, _, float)| float.effect_extent());
|
||
// Lowered further than its height, it is drawn with its top on the baseline (Word).
|
||
let room = Rect::new(cx + l, base - obj_h.max(0.0) + t, (adv - l - r).max(0.0), (obj_h + obj_d - t - b).max(0.0));
|
||
// A horizontal rule is as wide as its share of the paragraph's width, whatever narrows its
|
||
// line, and is placed by its alignment in what the line has left: it runs past the margin
|
||
// beside a float or a first-line indent, as in Word (see `ParaLayout::rule_room`).
|
||
if let Some(InlineObject::Shape { w, extra, .. }) = obj
|
||
&& let Some(hr) = extra.hr
|
||
{
|
||
let rw = hr.width(room.w, *w);
|
||
return Rect::new(hr.left(room.x, line, rw), room.y, rw, room.h);
|
||
}
|
||
let Some((w, h, float)) = frame.filter(|(_, _, f)| f.rot != 0.0) else { return room };
|
||
// The room is what the line kept for the turned frame: undo that.
|
||
let (w, h) = (wordcraft_geom::finite(w).clamp(1.0, 4000.0), wordcraft_geom::finite(h).clamp(1.0, 4000.0));
|
||
let bw = w + 2.0 * float.line_pad(w, h, modern).0;
|
||
let k = if bw > 0.0 { room.w / bw } else { 1.0 };
|
||
let (fw, fh) = (w * k, h * k);
|
||
Rect::new(room.x + (room.w - fw) / 2.0, room.y + (room.h - fh) / 2.0, fw, fh)
|
||
}
|
||
|
||
/// A Drawing Canvas's background and outline over its frame `rect`.
|
||
fn canvas_background(rect: Rect, c: &wordcraft_doc::para::CanvasStyle) -> Draw {
|
||
Draw::Shape {
|
||
rect,
|
||
kind: ShapeKind::Rectangle,
|
||
fill: c.fill.as_ref().and_then(|f| paint_for(f, rect)),
|
||
stroke: c.stroke,
|
||
stroke_width: c.stroke_width,
|
||
effects: Default::default(),
|
||
corner: None,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod fill_alpha_tests;
|