diff --git a/crates/svg/src/import/text.rs b/crates/svg/src/import/text.rs index 32d37bf10..d2a6c46ed 100644 --- a/crates/svg/src/import/text.rs +++ b/crates/svg/src/import/text.rs @@ -271,7 +271,10 @@ fn on_path(cx: &mut Ctx, t: &usvg::Text, world: Affine, parent: NodeId) -> bool local.apply_affine(Affine::translate((-bb.x0, -bb.y0))); td.path = Some(Box::new(figcraft_doc::TextPath { start: f64::from(tp.start_offset()) / len, ..figcraft_doc::TextPath::new(&local) })); let transform = world * Affine::translate((bb.x0, bb.y0)); - add_layer(cx, t, &[line], td, transform, (bb.width().max(1.0), bb.height().max(1.0)), world, 1.0, parent); + add_layer( + cx, + TextLayer { source: t, block: &[line], data: td, transform, size: (bb.width().max(1.0), bb.height().max(1.0)), world, scale: 1.0, parent }, + ); true } @@ -293,23 +296,24 @@ fn layer(cx: &mut Ctx, t: &usvg::Text, block: &[Line], m: Affine, s: f64, world: let (w, h) = (lay.width.ceil().max(1.0), lay.height.max(1.0)); let baseline = lay.lines.first().map_or(0.0, |l| l.baseline); let transform = m * Affine::translate((s * first.x - anchor_k(first.anchor) * w, s * first.y - baseline)); - add_layer(cx, t, block, td, transform, (w, h), world, s, parent); + add_layer(cx, TextLayer { source: t, block, data: td, transform, size: (w, h), world, scale: s, parent }); +} + +struct TextLayer<'text, 'block, 'span> { + source: &'text usvg::Text, + block: &'block [Line<'span>], + data: TextData, + transform: Affine, + size: (f64, f64), + world: Affine, + scale: f64, + parent: NodeId, } /// Add text layer `td` of `block` at `transform` with box `(w, h)`, with the spans' fills and the /// first span's stroke (`world`: the SVG text's space; `s`: the scale folded into font sizes). -#[allow(clippy::too_many_arguments)] -fn add_layer( - cx: &mut Ctx, - t: &usvg::Text, - block: &[Line], - mut td: TextData, - transform: Affine, - (w, h): (f64, f64), - world: Affine, - s: f64, - parent: NodeId, -) { +fn add_layer(cx: &mut Ctx, layer: TextLayer<'_, '_, '_>) { + let TextLayer { source: t, block, data: mut td, transform, size: (w, h), world, scale: s, parent } = layer; let Some(first) = block.first() else { return }; let spans: Vec<&usvg::TextSpan> = block.iter().flat_map(|l| l.spans.iter().map(|sp| sp.2)).collect(); if spans.iter().all(|sp| !sp.is_visible()) { diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index 233da2182..25175d28e 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -61,6 +61,18 @@ impl Default for SvgOptions { } } +struct NodePaint<'a> { + node: &'a Node, + xf: Affine, + depth: usize, + paint_clip: String, + kid_clip: Option, + inherited: Option, + outline: Option<(BezPath, figcraft_render::FillRule)>, + grains: String, + size: (f64, f64), +} + struct W<'a> { doc: &'a Document, /// The exported layers, in z-order (what backdrops see). @@ -458,6 +470,12 @@ impl W<'_> { NodeKind::Group | NodeKind::Page { .. } => None, _ => figcraft_render::node_outline(self.doc, n), }; + self.render_node(NodePaint { node: n, xf, depth, paint_clip, kid_clip, inherited, outline, grains, size: (w, h) }); + self.body.push_str(""); + } + + fn render_node(&mut self, paint: NodePaint<'_>) { + let NodePaint { node: n, xf, depth, paint_clip, kid_clip, inherited, outline, grains, size: (w, h) } = paint; let closed = figcraft_doc::geometry::is_closed(n) || matches!(n.kind, NodeKind::BooleanOp { .. } | NodeKind::Text(_)); let live_text = if self.opts.outline_text { None } else { self.text_element(n, xf) }; let run_paths = if live_text.is_none() { text_run_paths(n) } else { None }; @@ -486,92 +504,117 @@ impl W<'_> { } // Inner shadows paint over the fills, under the children and strokes. if let Some(o) = &outline { - for e in n.effects.iter().filter(|e| e.visible) { - if let EffectKind::InnerShadow(s) = &e.kind { - self.inner_shadow(o, s, xf); - } - } + self.inner_shadows(n, o, xf); } if !matches!(n.kind, NodeKind::BooleanOp { .. }) && !n.children.is_empty() { - let mut kid_clip = kid_clip; - if n.clip - && n.kind.is_frame_like() - && let Some((p, _)) = &outline - { - // Its own clip, within the clips around it. - let cid = self.id("clip"); - let mut cp = p.clone(); - cp.apply_affine(xf); - let _ = write!(self.defs, "", clip_attr(&kid_clip), path_d(&cp)); - kid_clip = Some(cid); - } - self.clip = kid_clip; - let kids = n.children.clone(); - for run in self.doc.paint_runs(n) { - let Some(&c) = kids.get(run.start) else { continue }; - if self.doc.node(c).is_some_and(|cn| cn.is_mask && cn.visible) { - let kind = self.doc.node(c).map(|mn| mn.mask_type).unwrap_or_default(); - if kind == figcraft_doc::MaskType::Vector { - // A vector mask is a clip path: its outline. - let cid = self.id("clip"); - let outline = - self.doc.node(c).and_then(|mn| Some((xf * mn.transform, figcraft_render::subtree_outline(self.doc, mn, depth + 1)?))); - let (shape, cr) = match &outline { - Some((mxf, (mp, rule))) => ( - format!(" transform=\"{}\" d=\"{}\"", matrix(*mxf), path_d(mp)), - if *rule == figcraft_render::FillRule::EvenOdd { " clip-rule=\"evenodd\"" } else { "" }, - ), - None => (" d=\"\"".to_string(), ""), - }; - let _ = write!(self.defs, ""); - let _ = write!(self.body, ""); - } else { - // The layer as drawn: its alpha or luminance masks. - let mid = self.id("mask"); - let saved = std::mem::take(&mut self.body); - let outer = self.mask.replace(c); - let clip = self.clip.take(); - self.node(c, xf, depth + 1); - self.clip = clip; - self.mask = outer; - let mask_body = std::mem::replace(&mut self.body, saved); - let alpha = if kind == figcraft_doc::MaskType::Alpha { " style=\"mask-type:alpha\"" } else { "" }; - let _ = write!(self.defs, "{mask_body}"); - let _ = write!(self.body, ""); - } - for k in kids.get(run.start + 1..run.end).unwrap_or(&[]) { - self.node(*k, xf, depth + 1); - } - self.body.push_str(""); - } else { - self.node(c, xf, depth + 1); - } - } - self.clip = inherited; + self.children(n, xf, depth, outline.as_ref(), kid_clip, inherited); } if n.has_visible_stroke() && let Some(simple) = self.opts.simplify_stroke.then(|| self.simple_stroke(n, xf)).flatten() { self.body.push_str(&clipped(&paint_clip, simple)); } else if n.has_visible_stroke() { - let geom = match &n.kind { - NodeKind::BooleanOp { .. } | NodeKind::Text(_) => outline.as_ref().map(|(p, _)| p.clone()), - _ => figcraft_doc::geometry::stroke_geometry(n), + self.stroke_paths(n, xf, outline.as_ref(), closed, &paint_clip); + } + self.body.push_str(&grains); + } + + /// Paints the children of `n`, each mask layer wrapping the layers it covers. + fn children( + &mut self, + n: &Node, + xf: Affine, + depth: usize, + outline: Option<&(BezPath, figcraft_render::FillRule)>, + mut kid_clip: Option, + inherited: Option, + ) { + if n.clip + && n.kind.is_frame_like() + && let Some((p, _)) = &outline + { + // Its own clip, within the clips around it. + let cid = self.id("clip"); + let mut cp = p.clone(); + cp.apply_affine(xf); + let _ = write!(self.defs, "", clip_attr(&kid_clip), path_d(&cp)); + kid_clip = Some(cid); + } + self.clip = kid_clip; + let kids = n.children.clone(); + for run in self.doc.paint_runs(n) { + let Some(&c) = kids.get(run.start) else { continue }; + if self.doc.node(c).is_some_and(|cn| cn.is_mask && cn.visible) { + let kind = self.doc.node(c).map(|mn| mn.mask_type).unwrap_or_default(); + self.open_mask(c, kind, xf, depth); + for k in kids.get(run.start + 1..run.end).unwrap_or(&[]) { + self.node(*k, xf, depth + 1); + } + self.body.push_str(""); + } else { + self.node(c, xf, depth + 1); + } + } + self.clip = inherited; + } + + /// The inner shadows of `n` over its `outline`. + fn inner_shadows(&mut self, n: &Node, outline: &Outline, xf: Affine) { + for e in n.effects.iter().filter(|e| e.visible) { + if let EffectKind::InnerShadow(s) = &e.kind { + self.inner_shadow(outline, s, xf); + } + } + } + + /// Opens the `` that the mask layer `c` clips or masks the layers after it with. + fn open_mask(&mut self, c: NodeId, kind: figcraft_doc::MaskType, xf: Affine, depth: usize) { + if kind == figcraft_doc::MaskType::Vector { + // A vector mask is a clip path: its outline. + let cid = self.id("clip"); + let outline = self.doc.node(c).and_then(|mn| Some((xf * mn.transform, figcraft_render::subtree_outline(self.doc, mn, depth + 1)?))); + let (shape, cr) = match &outline { + Some((mxf, (mp, rule))) => ( + format!(" transform=\"{}\" d=\"{}\"", matrix(*mxf), path_d(mp)), + if *rule == figcraft_render::FillRule::EvenOdd { " clip-rule=\"evenodd\"" } else { "" }, + ), + None => (" d=\"\"".to_string(), ""), }; - if let Some(g) = geom { - let area = figcraft_render::draw::stroke_area(n, &g, closed); - let even = n.stroke.sides.is_some() || (n.stroke.dashes.is_empty() && closed && n.stroke.align != figcraft_doc::StrokeAlign::Center); - let fr = if even { " fill-rule=\"evenodd\"" } else { "" }; - for p in &n.strokes { - if let Some(a) = self.paint_attr(p, w, h) { - let el = format!("", matrix(xf), path_d(&area)); - self.body.push_str(&clipped(&paint_clip, el)); - } + let _ = write!(self.defs, ""); + let _ = write!(self.body, ""); + } else { + // The layer as drawn: its alpha or luminance masks. + let mid = self.id("mask"); + let saved = std::mem::take(&mut self.body); + let outer = self.mask.replace(c); + let clip = self.clip.take(); + self.node(c, xf, depth + 1); + self.clip = clip; + self.mask = outer; + let mask_body = std::mem::replace(&mut self.body, saved); + let alpha = if kind == figcraft_doc::MaskType::Alpha { " style=\"mask-type:alpha\"" } else { "" }; + let _ = write!(self.defs, "{mask_body}"); + let _ = write!(self.body, ""); + } + } + + /// The visible strokes of `n` as filled outline paths. + fn stroke_paths(&mut self, n: &Node, xf: Affine, outline: Option<&(BezPath, figcraft_render::FillRule)>, closed: bool, paint_clip: &str) { + let geom = match &n.kind { + NodeKind::BooleanOp { .. } | NodeKind::Text(_) => outline.map(|(p, _)| p.clone()), + _ => figcraft_doc::geometry::stroke_geometry(n), + }; + if let Some(g) = geom { + let area = figcraft_render::draw::stroke_area(n, &g, closed); + let even = n.stroke.sides.is_some() || (n.stroke.dashes.is_empty() && closed && n.stroke.align != figcraft_doc::StrokeAlign::Center); + let fr = if even { " fill-rule=\"evenodd\"" } else { "" }; + for p in &n.strokes { + if let Some(a) = self.paint_attr(p, n.width, n.height) { + let el = format!("", matrix(xf), path_d(&area)); + self.body.push_str(&clipped(paint_clip, el)); } } } - self.body.push_str(&grains); - self.body.push_str(""); } /// `name` as an `id` not used yet in this file (spaces as underscores; repeats get `_2`…).