blob: 49523ba0b29dce88f346c7d7393a15f27e72a790 [file] [log] [blame]
// 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::coarse::{Cmd, MODE_CPU, Wide, WideTile};
use vello_common::color::palette::css::BLACK;
use vello_common::fearless_simd::Level;
use vello_common::flatten::{FlattenCtx, Line};
use vello_common::kurbo::{Affine, BezPath, Cap, Join, Stroke};
use vello_common::peniko::Fill;
use vello_common::strip::Strip;
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());
let mut strip_buf = vec![];
let mut alpha_buf = vec![];
let mut wide = Wide::<MODE_CPU>::new(args.width, args.height);
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(),
);
} 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(),
);
}
}
if stages.iter().any(|s| s.requires_tiling()) {
tiles.make_tiles(&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,
true,
&line_buf,
);
}
if stages.iter().any(|s| s.requires_wide_tiles()) {
wide.generate(&strip_buf, args.fill_rule, BLACK.into(), 0);
}
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::WideTiles) {
draw_wide_tiles(&mut document, wide.tiles(), &alpha_buf);
}
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.winding != 0 {
"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.winding != 0 {
"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_wide_tiles(document: &mut Document, wide_tiles: &[WideTile], alphas: &[u8]) {
// TODO: account for multiple wide tiles per row.
for (tile_idx, tile) in wide_tiles.iter().enumerate() {
for cmd in &tile.cmds {
match cmd {
Cmd::Fill(f) => {
for x in 0..f.width {
for y in 0..Tile::HEIGHT {
let rect = Rectangle::new()
.set("x", f.x + x)
.set("y", tile_idx * usize::from(Tile::HEIGHT) + usize::from(y))
.set("width", 1)
.set("height", 1)
.set("fill", "blue");
document.append(rect);
}
}
}
Cmd::AlphaFill(s) => {
for x in 0..s.width {
for y in 0..Tile::HEIGHT {
let alpha = alphas[s.alpha_idx
+ usize::from(x) * usize::from(Tile::HEIGHT)
+ usize::from(y)];
let rect = Rectangle::new()
.set("x", s.x + x)
.set("y", tile_idx * usize::from(Tile::HEIGHT) + usize::from(y))
.set("width", 1)
.set("height", 1)
.set("fill", "yellow")
.set("fill-opacity", alpha as f32 / 255.0);
document.append(rect);
}
}
}
_ => {
unimplemented!("unsupported command: {:?}", cmd);
}
}
}
}
}
#[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 wide tiles.
WideTiles,
}
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)
|| matches!(self, Self::Strips)
|| self.requires_wide_tiles()
}
fn requires_wide_tiles(&self) -> bool {
matches!(self, Self::WideTiles)
}
}
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),
"wt" | "wide_tiles" => Ok(Self::WideTiles),
_ => Err(format!(
"invalid stage: {input}. Expected one of `line_segments`, `tile_areas`, `tile_intersections`, `strip_areas`, `strips`, or `wide_tiles`, 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`"
)),
}
}