| // Copyright 2025 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Visualize the intermediate stages of `vello_common` in an SVG. | |
| #![allow( | |
| clippy::cast_possible_truncation, | |
| reason = "this is only a debug tool, so we can ignore them" | |
| )] | |
| use clap::Parser; | |
| use std::collections::HashSet; | |
| use std::path; | |
| use svg::node::element::path::Data; | |
| use svg::node::element::{Line as SvgLine, Path, Rectangle}; | |
| use svg::{Document, Node}; | |
| use vello_common::fearless_simd::Level; | |
| use vello_common::flatten::{FlattenCtx, Line}; | |
| use vello_common::geometry::RectU16; | |
| use vello_common::kurbo::{Affine, BezPath, Cap, Join, Stroke, StrokeCtx}; | |
| use vello_common::peniko::Fill; | |
| use vello_common::strip::{ | |
| Strip, StripAlphaFillSegment, StripFillSegment, visit_strip_fill_segments, | |
| }; | |
| use vello_common::tile::{Tile, Tiles}; | |
| use vello_common::{flatten, strip}; | |
| fn main() { | |
| let args = Args::parse(); | |
| let mut document = | |
| Document::new().set("viewBox", (-10, -10, args.width + 20, args.height + 20)); | |
| let mut line_buf = vec![]; | |
| let mut tiles = Tiles::new(Level::new(), args.width, args.height); | |
| let mut strip_buf = vec![]; | |
| let mut alpha_buf = vec![]; | |
| let stages = &args.stages; | |
| // Not super efficient doing it this way, but it doesn't really matter. | |
| if stages.iter().any(|s| s.requires_flatten()) { | |
| if !args.stroke { | |
| flatten::fill( | |
| Level::new(), | |
| &args.path, | |
| Affine::IDENTITY, | |
| &mut line_buf, | |
| &mut FlattenCtx::default(), | |
| RectU16::new(0, 0, args.width, args.height), | |
| ); | |
| } else { | |
| let stroke = Stroke { | |
| width: args.stroke_width as f64, | |
| join: Join::Bevel, | |
| start_cap: Cap::Butt, | |
| end_cap: Cap::Butt, | |
| ..Default::default() | |
| }; | |
| flatten::stroke( | |
| Level::new(), | |
| &args.path, | |
| &stroke, | |
| Affine::IDENTITY, | |
| &mut line_buf, | |
| &mut FlattenCtx::default(), | |
| &mut StrokeCtx::default(), | |
| RectU16::new(0, 0, args.width, args.height), | |
| ); | |
| } | |
| } | |
| if stages.iter().any(|s| s.requires_tiling()) { | |
| tiles.make_tiles_analytic_aa(Level::new(), &line_buf, args.width, args.height); | |
| tiles.sort_tiles(); | |
| } | |
| if stages.iter().any(|s| s.requires_strips()) { | |
| strip::render( | |
| Level::new(), | |
| &tiles, | |
| &mut strip_buf, | |
| &mut alpha_buf, | |
| args.fill_rule, | |
| None, | |
| &line_buf, | |
| ); | |
| } | |
| draw_grid(&mut document, args.width, args.height); | |
| if stages.contains(&Stage::LineSegments) { | |
| draw_line_segments(&mut document, &line_buf); | |
| } | |
| if stages.contains(&Stage::TileAreas) { | |
| draw_tile_areas(&mut document, &tiles); | |
| } | |
| if stages.contains(&Stage::StripAreas) { | |
| draw_strip_areas(&mut document, &strip_buf, &alpha_buf); | |
| } | |
| if stages.contains(&Stage::Strips) { | |
| draw_strips(&mut document, &strip_buf, &alpha_buf); | |
| } | |
| if stages.contains(&Stage::FillSegments) { | |
| draw_fill_segments( | |
| &mut document, | |
| &strip_buf, | |
| &alpha_buf, | |
| args.width, | |
| args.height, | |
| ); | |
| } | |
| let path = path::absolute("debug.svg").unwrap(); | |
| eprintln!("Saved debug output to '{}'", path.display()); | |
| svg::save(path, &document).unwrap(); | |
| } | |
| fn draw_grid(document: &mut Document, width: u16, height: u16) { | |
| let border_data = Data::new() | |
| .move_to((0, 0)) | |
| .line_to((width, 0)) | |
| .line_to((width, height)) | |
| .line_to((0, height)) | |
| .close(); | |
| let border = Path::new() | |
| .set("stroke-width", 0.2) | |
| .set("fill", "none") | |
| .set("vectorEffect", "non-scaling-stroke") | |
| .set("stroke", "black") | |
| .set("d", border_data); | |
| let grid_line = |data: Data| { | |
| Path::new() | |
| .set("stroke", "grey") | |
| .set("stroke-opacity", 0.3) | |
| .set("stroke-width", 0.1) | |
| .set("vectorEffect", "non-scaling-stroke") | |
| .set("d", data) | |
| }; | |
| for i in 1..height { | |
| let data = Data::new().move_to((0, i)).line_to((width, i)); | |
| document.append(grid_line(data)); | |
| } | |
| for i in 1..width { | |
| let data = Data::new().move_to((i, 0)).line_to((i, height)); | |
| document.append(grid_line(data)); | |
| } | |
| document.append(border); | |
| } | |
| fn draw_line_segments(document: &mut Document, line_buf: &[Line]) { | |
| let svg_line = SvgLine::new() | |
| .set("stroke-width", 0.1) | |
| .set("fill", "none") | |
| .set("fill-opacity", 0.1); | |
| for line in line_buf { | |
| let dy = line.p1.y - line.p0.y; | |
| let color = if dy < 0. { | |
| // Lines oriented upwards add to winding. | |
| "green" | |
| } else if dy > 0. { | |
| // Lines oriented upwards subtract from winding. | |
| "red" | |
| } else { | |
| // Horizontal lines don't impact winding. | |
| "grey" | |
| }; | |
| let svg_line = svg_line | |
| .clone() | |
| .set("x1", line.p0.x) | |
| .set("y1", line.p0.y) | |
| .set("x2", line.p1.x) | |
| .set("y2", line.p1.y) | |
| .set("stroke", color); | |
| document.append(svg_line); | |
| } | |
| } | |
| fn draw_tile_areas(document: &mut Document, tiles: &Tiles) { | |
| let mut seen = HashSet::new(); | |
| for i in 0..tiles.len() { | |
| let tile = tiles.get(i); | |
| let x = tile.x * Tile::WIDTH; | |
| let y = tile.y * Tile::HEIGHT; | |
| if seen.contains(&(x, y)) { | |
| continue; | |
| } | |
| let color = "darkblue"; | |
| let rect = Rectangle::new() | |
| .set("x", x) | |
| .set("y", y) | |
| .set("width", Tile::WIDTH) | |
| .set("height", Tile::HEIGHT) | |
| .set("fill", color) | |
| .set("stroke", color) | |
| .set("stroke-opacity", 1.0) | |
| .set("stroke-width", 0.2) | |
| .set("fill-opacity", 0.1); | |
| document.append(rect); | |
| seen.insert((x, y)); | |
| } | |
| } | |
| fn draw_strip_areas(document: &mut Document, strips: &[Strip], alphas: &[u8]) { | |
| for i in 0..strips.len() { | |
| let strip = &strips[i]; | |
| let x = strip.x; | |
| let y = strip.strip_y(); | |
| let end = strips | |
| .get(i + 1) | |
| .map(|s| s.alpha_idx() / u32::from(Tile::HEIGHT)) | |
| .unwrap_or(alphas.len() as u32); | |
| let width = end - strip.alpha_idx() / u32::from(Tile::HEIGHT); | |
| // TODO: Account for even-odd? | |
| let color = if strip.fill_gap() { "red" } else { "limegreen" }; | |
| let rect = Rectangle::new() | |
| .set("x", x) | |
| .set("y", y * Tile::HEIGHT) | |
| .set("width", width) | |
| .set("height", Tile::HEIGHT) | |
| .set("stroke", color) | |
| .set("fill", color) | |
| .set("fill-opacity", 0.4) | |
| .set("stroke-opacity", 0.6) | |
| .set("stroke-width", 0.2); | |
| document.append(rect); | |
| } | |
| } | |
| fn draw_strips(document: &mut Document, strips: &[Strip], alphas: &[u8]) { | |
| for s in 0..strips.len() { | |
| let strip = &strips[s]; | |
| let end = strips | |
| .get(s + 1) | |
| .map(|st| st.alpha_idx() / u32::from(Tile::HEIGHT)) | |
| .unwrap_or(alphas.len() as u32); | |
| let width = u16::try_from(end - strip.alpha_idx() / u32::from(Tile::HEIGHT)).unwrap(); | |
| // TODO: Account for even-odd? | |
| let color = if strip.fill_gap() { "red" } else { "limegreen" }; | |
| for x in 0..width { | |
| for y in 0..Tile::HEIGHT { | |
| let alpha = alphas[strip.alpha_idx() as usize | |
| + usize::from(x) * usize::from(Tile::HEIGHT) | |
| + usize::from(y)]; | |
| let rect = Rectangle::new() | |
| .set("x", strip.x + x) | |
| .set("y", strip.y + y) | |
| .set("width", 1) | |
| .set("height", 1) | |
| .set("fill", color) | |
| .set("fill-opacity", alpha as f32 / 255.0); | |
| document.append(rect); | |
| } | |
| } | |
| } | |
| } | |
| fn draw_fill_segments( | |
| document: &mut Document, | |
| strips: &[Strip], | |
| alphas: &[u8], | |
| width: u16, | |
| height: u16, | |
| ) { | |
| let tile_bounds = RectU16::new( | |
| 0, | |
| 0, | |
| width.div_ceil(Tile::WIDTH), | |
| height.div_ceil(Tile::HEIGHT), | |
| ); | |
| visit_strip_fill_segments( | |
| strips, | |
| tile_bounds, | |
| document, | |
| |document, segment| draw_alpha_fill_segment(document, segment, alphas, width, height), | |
| |document, segment| draw_fill_segment(document, segment, width, height), | |
| ); | |
| } | |
| fn draw_alpha_fill_segment( | |
| document: &mut Document, | |
| segment: StripAlphaFillSegment, | |
| alphas: &[u8], | |
| width: u16, | |
| height: u16, | |
| ) { | |
| let x0 = segment.x0(); | |
| let x1 = segment.x1().min(width); | |
| let y0 = segment.y(); | |
| let y1 = y0.saturating_add(Tile::HEIGHT).min(height); | |
| for x in x0..x1 { | |
| for y in y0..y1 { | |
| let alpha_idx = segment.alpha_idx as usize | |
| + usize::from(x - x0) * usize::from(Tile::HEIGHT) | |
| + usize::from(y - y0); | |
| let rect = Rectangle::new() | |
| .set("x", x) | |
| .set("y", y) | |
| .set("width", 1) | |
| .set("height", 1) | |
| .set("fill", "yellow") | |
| .set("fill-opacity", f32::from(alphas[alpha_idx]) / 255.0); | |
| document.append(rect); | |
| } | |
| } | |
| } | |
| fn draw_fill_segment(document: &mut Document, segment: StripFillSegment, width: u16, height: u16) { | |
| let x0 = segment.x0(); | |
| let x1 = segment.x1().min(width); | |
| let y0 = segment.y(); | |
| let y1 = y0.saturating_add(Tile::HEIGHT).min(height); | |
| let rect = Rectangle::new() | |
| .set("x", x0) | |
| .set("y", y0) | |
| .set("width", x1 - x0) | |
| .set("height", y1 - y0) | |
| .set("fill", "blue"); | |
| document.append(rect); | |
| } | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| enum Stage { | |
| /// Draw the flattened lines of the path. | |
| LineSegments, | |
| /// Draw the tile areas covered by the path. | |
| TileAreas, | |
| /// Draw the stripped areas. | |
| StripAreas, | |
| /// Draw the strips with their alpha masks. | |
| Strips, | |
| /// Draw the fill segments formed by the strips. | |
| FillSegments, | |
| } | |
| impl Stage { | |
| fn requires_flatten(&self) -> bool { | |
| matches!(self, Self::LineSegments) || self.requires_tiling() | |
| } | |
| fn requires_tiling(&self) -> bool { | |
| matches!(self, Self::TileAreas) || self.requires_strips() | |
| } | |
| fn requires_strips(&self) -> bool { | |
| matches!(self, Self::StripAreas | Self::Strips | Self::FillSegments) | |
| } | |
| } | |
| impl std::str::FromStr for Stage { | |
| type Err = String; | |
| fn from_str(input: &str) -> Result<Self, Self::Err> { | |
| match input.to_lowercase().as_str() { | |
| "ls" | "line_segments" => Ok(Self::LineSegments), | |
| "ta" | "tile_areas" => Ok(Self::TileAreas), | |
| "sa" | "strip_areas" => Ok(Self::StripAreas), | |
| "s" | "strips" => Ok(Self::Strips), | |
| "fs" | "fill_segments" => Ok(Self::FillSegments), | |
| _ => Err(format!( | |
| "invalid stage: {input}. Expected one of `line_segments`, `tile_areas`, `strip_areas`, `strips`, or `fill_segments`, or their acronym" | |
| )), | |
| } | |
| } | |
| } | |
| #[derive(Parser, Debug)] | |
| struct Args { | |
| /// The width of the viewport. | |
| #[arg(long, default_value_t = 50, value_parser = parse_dim)] | |
| pub width: u16, | |
| /// The height of the viewport. | |
| #[arg(long, default_value_t = 50, value_parser = parse_dim)] | |
| pub height: u16, | |
| /// The SVG path that should be drawn. | |
| #[arg(short, long, value_parser = parse_path)] | |
| pub path: BezPath, | |
| /// Whether the path should be stroked (if false, it will be filled). | |
| #[arg(short, long, default_value_t = false)] | |
| pub stroke: bool, | |
| /// The stroke width for stroking operations. | |
| #[arg(long, default_value_t = 1.0)] | |
| pub stroke_width: f32, | |
| /// The fill rule used for fill operations. | |
| #[arg(long, default_value = "nonzero", value_parser = parse_fill_rule)] | |
| pub fill_rule: Fill, | |
| /// The stages of the pipeline that should be included in the SVG. | |
| #[arg(long, num_args = 1.., value_delimiter = ',', default_value = "ls,ta,ti,sa,s", value_parser = parse_stages)] | |
| pub stages: Vec<Stage>, | |
| } | |
| fn parse_stages(val: &str) -> Result<Stage, String> { | |
| val.parse::<Stage>() | |
| } | |
| fn parse_dim(val: &str) -> Result<u16, String> { | |
| let parsed = val | |
| .parse::<u16>() | |
| .map_err(|_| "width/height must be a positive integer")?; | |
| if parsed > 500 { | |
| Err( | |
| "the width/height cannot be larger than 500 (otherwise the SVG will be very slow)." | |
| .to_string(), | |
| ) | |
| } else { | |
| Ok(parsed) | |
| } | |
| } | |
| fn parse_path(val: &str) -> Result<BezPath, String> { | |
| BezPath::from_svg(val).map_err(|_| "failed to parse the SVG path".to_string()) | |
| } | |
| fn parse_fill_rule(val: &str) -> Result<Fill, String> { | |
| match val { | |
| "nonzero" => Ok(Fill::NonZero), | |
| "evenodd" => Ok(Fill::EvenOdd), | |
| _ => Err(format!( | |
| "unsupported fill rule: {val}. Expected one of `nonzero` or `evenodd`" | |
| )), | |
| } | |
| } |