Skip to content

Commit

Permalink
[pico_svg] Encode group transform in scene (#609)
Browse files Browse the repository at this point in the history
Build the scene recursively, applying the transform at group nodes,
rather than flattening to a single list of items. This causes the
transform to be applied correctly to stroke styles, which among other
things fixes the rendering of the waves example from the Nehab timings
data set.

We're deliberately keeping things minimal, but if we were to add other
properties to groups (clips, opacity, etc), then this change would
support that.
  • Loading branch information
raphlinus authored Jun 20, 2024
1 parent 7f766ff commit fb947d6
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 44 deletions.
51 changes: 32 additions & 19 deletions examples/scenes/src/pico_svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct PicoSvg {
pub enum Item {
Fill(FillItem),
Stroke(StrokeItem),
Group(GroupItem),
}

pub struct StrokeItem {
Expand All @@ -32,17 +33,20 @@ pub struct FillItem {
pub path: BezPath,
}

struct Parser<'a> {
pub struct GroupItem {
pub affine: Affine,
pub children: Vec<Item>,
}

struct Parser {
scale: f64,
items: &'a mut Vec<Item>,
}

impl PicoSvg {
pub fn load(xml_string: &str, scale: f64) -> Result<PicoSvg, Box<dyn std::error::Error>> {
let doc = Document::parse(xml_string)?;
let root = doc.root_element();
let mut items = Vec::new();
let mut parser = Parser::new(&mut items, scale);
let mut parser = Parser::new(scale);
let width = root.attribute("width").and_then(|s| f64::from_str(s).ok());
let height = root.attribute("height").and_then(|s| f64::from_str(s).ok());
let (origin, viewbox_size) = root
Expand Down Expand Up @@ -99,32 +103,39 @@ impl PicoSvg {
Affine::new([-scale, 0.0, 0.0, scale, 0.0, 0.0])
};
let props = RecursiveProperties {
transform,
fill: Some(Color::BLACK),
};
// The root element is the svg document element, which we don't care about
let mut items = Vec::new();
for node in root.children() {
parser.rec_parse(node, &props)?;
parser.rec_parse(node, &props, &mut items)?;
}
Ok(PicoSvg { items, size })
let root_group = Item::Group(GroupItem {
affine: transform,
children: items,
});
Ok(PicoSvg {
items: vec![root_group],
size,
})
}
}

#[derive(Clone)]
struct RecursiveProperties {
transform: Affine,
fill: Option<Color>,
}

impl<'a> Parser<'a> {
fn new(items: &'a mut Vec<Item>, scale: f64) -> Parser<'a> {
Parser { scale, items }
impl Parser {
fn new(scale: f64) -> Parser {
Parser { scale }
}

fn rec_parse(
&mut self,
node: Node,
properties: &RecursiveProperties,
items: &mut Vec<Item>,
) -> Result<(), Box<dyn std::error::Error>> {
if node.is_element() {
let mut properties = properties.clone();
Expand All @@ -139,21 +150,24 @@ impl<'a> Parser<'a> {
properties.fill = Some(color);
}
}
if let Some(transform) = node.attribute("transform") {
properties.transform *= parse_transform(transform);
}
match node.tag_name().name() {
"g" => {
let mut children = Vec::new();
let mut affine = Affine::default();
if let Some(transform) = node.attribute("transform") {
affine = parse_transform(transform);
}
for child in node.children() {
self.rec_parse(child, &properties)?;
self.rec_parse(child, &properties, &mut children)?;
}
items.push(Item::Group(GroupItem { affine, children }));
}
"path" => {
let d = node.attribute("d").ok_or("missing 'd' attribute")?;
let bp = BezPath::from_svg(d)?;
let path = properties.transform * bp;
let path = bp;
if let Some(color) = properties.fill {
self.items.push(Item::Fill(FillItem {
items.push(Item::Fill(FillItem {
color,
path: path.clone(),
}));
Expand All @@ -169,8 +183,7 @@ impl<'a> Parser<'a> {
let color = modify_opacity(color, "stroke-opacity", node);
// TODO: Handle recursive opacity properly
let color = modify_opacity(color, "opacity", node);
self.items
.push(Item::Stroke(StrokeItem { width, color, path }));
items.push(Item::Stroke(StrokeItem { width, color, path }));
}
}
}
Expand Down
60 changes: 35 additions & 25 deletions examples/scenes/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,38 @@ fn example_scene_of(file: PathBuf) -> ExampleScene {
}
}

fn render_svg_rec(items: &[crate::pico_svg::Item]) -> Scene {
let mut scene = Scene::new();
for item in items {
use crate::pico_svg::Item;
match item {
Item::Fill(fill) => {
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
fill.color,
None,
&fill.path,
);
}
Item::Stroke(stroke) => {
scene.stroke(
&Stroke::new(stroke.width),
Affine::IDENTITY,
stroke.color,
None,
&stroke.path,
);
}
Item::Group(group) => {
let child_scene = render_svg_rec(&group.children);
scene.append(&child_scene, Some(group.affine));
}
}
}
scene
}

pub fn svg_function_of<R: AsRef<str>>(
name: String,
contents: impl FnOnce() -> R + Send + 'static,
Expand All @@ -79,34 +111,12 @@ pub fn svg_function_of<R: AsRef<str>>(
use crate::pico_svg::*;
let start = Instant::now();
match PicoSvg::load(contents, 1.0) {
Ok(PicoSvg { items, size }) => {
Ok(svg) => {
eprintln!("Parsed svg {name} in {:?}", start.elapsed());
let start = Instant::now();
let mut new_scene = Scene::new();
for item in items {
match item {
Item::Fill(fill) => {
new_scene.fill(
Fill::NonZero,
Affine::IDENTITY,
fill.color,
None,
&fill.path,
);
}
Item::Stroke(stroke) => {
new_scene.stroke(
&Stroke::new(stroke.width),
Affine::IDENTITY,
stroke.color,
None,
&stroke.path,
);
}
}
}
let scene = render_svg_rec(&svg.items);
eprintln!("Encoded svg {name} in {:?}", start.elapsed());
(new_scene, size.to_vec2())
(scene, svg.size.to_vec2())
}
Err(e) => {
eprintln!("Failed to load svg: {e}");
Expand Down

0 comments on commit fb947d6

Please sign in to comment.