This commit is contained in:
Patrick Walton 2018-11-14 10:33:53 -08:00
parent 3a5069b1e7
commit a2522e3845
12 changed files with 3341 additions and 22 deletions

2
.gitignore vendored
View File

@ -14,6 +14,8 @@
/demo/server/Rocket.toml
.DS_Store
target
node_modules
.cache
# Editors
*.swp

View File

@ -48,6 +48,7 @@ use lyon_path::PathEvent;
use lyon_path::builder::{FlatPathBuilder, PathBuilder};
use lyon_path::iterator::PathIter;
use pathfinder_partitioner::FillRule;
use pathfinder_partitioner::mesh::Mesh;
use pathfinder_partitioner::mesh_pack::MeshPack;
use pathfinder_partitioner::partitioner::Partitioner;
use pathfinder_path_utils::cubic_to_quadratic::CubicToQuadraticTransformer;
@ -273,11 +274,13 @@ impl PathPartitioningResult {
fn compute(pack: &mut MeshPack,
path_descriptors: &[PathDescriptor],
paths: &[Vec<PathEvent>],
approx_tolerance: Option<f32>)
approx_tolerance: Option<f32>,
tile: bool)
-> PathPartitioningResult {
let timestamp_before = Instant::now();
for (path, path_descriptor) in paths.iter().zip(path_descriptors.iter()) {
if !tile {
let mut partitioner = Partitioner::new();
if let Some(tolerance) = approx_tolerance {
partitioner.builder_mut().set_approx_tolerance(tolerance);
@ -293,11 +296,24 @@ impl PathPartitioningResult {
partitioner.mesh_mut().push_stencil_normals(
CubicToQuadraticTransformer::new(path.iter().cloned(),
CUBIC_TO_QUADRATIC_APPROX_TOLERANCE));
pack.push(partitioner.into_mesh());
} else {
let tiles = tiling::tile_path(path);
for tile_info in tiles {
let mut mesh = Mesh::new();
mesh.push_stencil_segments(tile_info.events.into_iter());
pack.push(mesh);
}
}
}
let time_elapsed = timestamp_before.elapsed();
eprintln!("path partitioning time: {}ms",
time_elapsed.as_secs() as f64 * 1000.0 +
time_elapsed.subsec_nanos() as f64 * 1e-6);
let mut data_buffer = Cursor::new(vec![]);
drop(pack.serialize_into(&mut data_buffer));
@ -452,7 +468,8 @@ fn partition_font(request: Json<PartitionFontRequest>) -> Result<PartitionRespon
let path_partitioning_result = PathPartitioningResult::compute(&mut pack,
&path_descriptors,
&paths,
None);
None,
false);
// Build the response.
let elapsed_ms = path_partitioning_result.elapsed_ms();
@ -538,7 +555,8 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
let path_partitioning_result = PathPartitioningResult::compute(&mut pack,
&path_descriptors,
&paths,
Some(tolerance));
Some(tolerance),
false);
// Return the response.
let elapsed_ms = path_partitioning_result.elapsed_ms();
@ -822,3 +840,113 @@ fn main() {
static_textures,
]).launch();
}
mod tiling {
use euclid::{Point2D, Rect, Size2D, Vector2D};
use lyon_path::PathEvent;
use pathfinder_path_utils::clip::RectClipper;
use std::f32;
use std::mem;
const TILE_SIZE: f32 = 8.0;
pub fn tile_path(path: &[PathEvent]) -> Vec<TileInfo> {
let mut tiles = vec![];
let mut all_points = vec![];
for event in path {
match *event {
PathEvent::MoveTo(point) | PathEvent::LineTo(point) => all_points.push(point),
PathEvent::QuadraticTo(point0, point1) => {
all_points.extend_from_slice(&[point0, point1])
}
PathEvent::CubicTo(point0, point1, point2) => {
all_points.extend_from_slice(&[point0, point1, point2])
}
PathEvent::Arc(..) | PathEvent::Close => {}
}
}
let bounding_rect = Rect::from_points(all_points);
//eprintln!("path {}: bounding rect = {:?}", path_index, bounding_rect);
let tile_size = Size2D::new(TILE_SIZE, TILE_SIZE);
let (mut full_tile_count, mut tile_count) = (0, 0);
let mut y = bounding_rect.origin.y - bounding_rect.origin.y % TILE_SIZE;
loop {
let mut x = bounding_rect.origin.x - bounding_rect.origin.x % TILE_SIZE;
loop {
let origin = Point2D::new(x, y);
let clipper = RectClipper::new(&Rect::new(origin, tile_size), path);
let mut tile_path = clipper.clip();
simplify_path(&mut tile_path);
translate_path(&mut tile_path, &Vector2D::new(-x, -y));
//eprintln!("({},{}): {:?}", x, y, tile_path);
if !tile_path.is_empty() {
tiles.push(TileInfo {
events: tile_path,
origin,
});
full_tile_count += 1;
} else {
tile_count += 1;
}
if x < bounding_rect.max_x() {
x += TILE_SIZE;
} else {
break
}
}
if y < bounding_rect.max_y() {
y += TILE_SIZE;
} else {
break
}
}
tiles
}
fn simplify_path(output: &mut Vec<PathEvent>) {
let mut subpath = vec![];
for event in mem::replace(output, vec![]) {
subpath.push(event);
if let PathEvent::Close = event {
if subpath.len() > 2 {
output.extend_from_slice(&subpath);
}
subpath.clear();
}
}
}
fn translate_path(output: &mut [PathEvent], vector: &Vector2D<f32>) {
for event in output {
match *event {
PathEvent::Close => {}
PathEvent::MoveTo(ref mut to) | PathEvent::LineTo(ref mut to) => *to += *vector,
PathEvent::QuadraticTo(ref mut cp, ref mut to) => {
*cp += *vector;
*to += *vector;
}
PathEvent::CubicTo(ref mut cp0, ref mut cp1, ref mut to) => {
*cp0 += *vector;
*cp1 += *vector;
*to += *vector;
}
PathEvent::Arc(ref mut center, _, _, _) => *center += *vector,
}
}
}
#[derive(Debug)]
pub struct TileInfo {
pub events: Vec<PathEvent>,
pub origin: Point2D<f32>,
}
}

14
demo2/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Pathfinder Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="pathfinder.css" />
</head>
<body>
<canvas id=canvas width=640 height=480></canvas>
<script src="pathfinder.ts"></script>
</body>
</html>

5
demo2/pathfinder.css Normal file
View File

@ -0,0 +1,5 @@
.tile {
position: absolute;
top: 0;
left: 0;
}

275
demo2/pathfinder.ts Normal file
View File

@ -0,0 +1,275 @@
// pathfinder/demo2/pathfinder.ts
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
const SVGPath = require('svgpath');
const TILE_SIZE: number = 16.0;
const GLOBAL_OFFSET: Point2D = {x: 400.0, y: 200.0};
const SVG_NS: string = "http://www.w3.org/2000/svg";
type Point2D = {x: number, y: number};
type Size2D = {width: number, height: number};
type Rect = {origin: Point2D, size: Size2D};
type Vector3D = {x: number, y: number, z: number};
type Edge = 'left' | 'top' | 'right' | 'bottom';
type SVGPath = any;
class App {
private svg: XMLDocument;
constructor(svg: XMLDocument) {
this.svg = svg;
}
run(): void {
const svgElement = unwrapNull(this.svg.documentElement).cloneNode(true);
document.body.appendChild(svgElement);
const pathElements = Array.from(document.getElementsByTagName('path'));
const tiles: Tile[] = [];
for (let pathElementIndex = 0;
pathElementIndex < 15;
pathElementIndex++) {
const pathElement = pathElements[pathElementIndex];
const path = canonicalizePath(SVGPath(unwrapNull(pathElement.getAttribute('d'))));
const boundingRect = this.boundingRectOfPath(path);
//console.log("path " + pathElementIndex, path.toString(), ":", boundingRect);
let y = boundingRect.origin.y;
while (true) {
let x = boundingRect.origin.x;
while (true) {
const tileBounds = {
origin: {x, y},
size: {width: TILE_SIZE, height: TILE_SIZE},
};
const tilePath = this.clipPathToRect(path, tileBounds);
tiles.push(new Tile(pathElementIndex, tilePath, tileBounds.origin));
if (x >= boundingRect.origin.x + boundingRect.size.width)
break;
x += TILE_SIZE;
}
if (y >= boundingRect.origin.y + boundingRect.size.height)
break;
y += TILE_SIZE;
}
for (const tile of tiles) {
const newSVG = staticCast(document.createElementNS(SVG_NS, 'svg'), SVGElement);
newSVG.setAttribute('class', "tile");
newSVG.style.left = (GLOBAL_OFFSET.x + tile.origin.x) + "px";
newSVG.style.top = (GLOBAL_OFFSET.y + tile.origin.y) + "px";
newSVG.style.width = TILE_SIZE + "px";
newSVG.style.height = TILE_SIZE + "px";
const newPath = document.createElementNS(SVG_NS, 'path');
newPath.setAttribute('d',
tile.path
.translate(-tile.origin.x, -tile.origin.y)
.toString());
let color = "#";
for (let i = 0; i < 6; i++)
color += Math.floor(Math.random() * 16).toString(16);
newPath.setAttribute('fill', color);
newSVG.appendChild(newPath);
document.body.appendChild(newSVG);
}
}
document.body.removeChild(svgElement);
}
private clipPathToRect(path: SVGPath, tileBounds: Rect): SVGPath {
path = this.clipPathToEdge('left', tileBounds.origin.x, path);
path = this.clipPathToEdge('top', tileBounds.origin.y, path);
path = this.clipPathToEdge('right', tileBounds.origin.x + tileBounds.size.width, path);
path = this.clipPathToEdge('bottom', tileBounds.origin.y + tileBounds.size.height, path);
return path;
}
private clipPathToEdge(edge: Edge, edgePos: number, input: SVGPath): SVGPath {
let pathStart: Point2D | null = null, from = {x: 0, y: 0}, firstPoint = false;
let output: string[][] = [];
input.iterate((segment: string[], index: number, x: number, y: number) => {
const event = segment[0];
let to;
switch (event) {
case 'M':
from = {
x: parseFloat(segment[segment.length - 2]),
y: parseFloat(segment[segment.length - 1]),
};
pathStart = from;
firstPoint = true;
return;
case 'Z':
if (pathStart == null)
return;
to = pathStart;
break;
default:
to = {
x: parseFloat(segment[segment.length - 2]),
y: parseFloat(segment[segment.length - 1]),
};
break;
}
if (this.pointIsInside(edge, edgePos, to)) {
if (!this.pointIsInside(edge, edgePos, from)) {
this.addLine(this.computeLineIntersection(edge, edgePos, from, to),
output,
firstPoint);
firstPoint = false;
}
this.addLine(to, output, firstPoint);
firstPoint = false;
} else if (this.pointIsInside(edge, edgePos, from)) {
this.addLine(this.computeLineIntersection(edge, edgePos, from, to),
output,
firstPoint);
firstPoint = false;
}
from = to;
if (event === 'Z') {
output.push(['Z']);
pathStart = null;
}
});
return SVGPath(output.map(segment => segment.join(" ")).join(" "));
}
private addLine(to: Point2D, output: string[][], firstPoint: boolean) {
if (firstPoint)
output.push(['M', "" + to.x, "" + to.y]);
else
output.push(['L', "" + to.x, "" + to.y]);
}
private pointIsInside(edge: Edge, edgePos: number, point: Point2D): boolean {
switch (edge) {
case 'left': return point.x >= edgePos;
case 'top': return point.y >= edgePos;
case 'right': return point.x <= edgePos;
case 'bottom': return point.y <= edgePos;
}
}
private computeLineIntersection(edge: Edge,
edgePos: number,
startPoint: Point2D,
endpoint: Point2D):
Point2D {
const start = {x: startPoint.x, y: startPoint.y, z: 1.0};
const end = {x: endpoint.x, y: endpoint.y, z: 1.0};
let edgeVector: Vector3D;
switch (edge) {
case 'left':
case 'right':
edgeVector = {x: 1.0, y: 0.0, z: -edgePos};
break;
default:
edgeVector = {x: 0.0, y: 1.0, z: -edgePos};
break;
}
const intersection = this.cross(this.cross(start, end), edgeVector);
return {x: intersection.x / intersection.z, y: intersection.y / intersection.z};
}
private boundingRectOfPath(path: SVGPath): Rect {
let minX: number | null = null, minY: number | null = null;
let maxX: number | null = null, maxY: number | null = null;
path.iterate((segment: string[], index: number, x: number, y: number) => {
for (let i = 1; i < segment.length; i += 2) {
const x = parseFloat(segment[i]), y = parseFloat(segment[i + 1]);
minX = minX == null ? x : Math.min(minX, x);
minY = minY == null ? y : Math.min(minY, y);
maxX = maxX == null ? x : Math.max(maxX, x);
maxY = maxY == null ? y : Math.max(maxY, y);
//console.log("x", x, "y", y, "maxX", maxX, "maxY", maxY, "segment", segment);
}
});
if (minX == null || minY == null || maxX == null || maxY == null)
return {origin: {x: 0, y: 0}, size: {width: 0, height: 0}};
return {origin: {x: minX, y: minY}, size: {width: maxX - minX, height: maxY - minY}};
}
private cross(a: Vector3D, b: Vector3D): Vector3D {
return {
x: a.y*b.z - a.z*b.y,
y: a.z*b.x - a.x*b.z,
z: a.x*b.y - a.y*b.x,
};
}
}
class Tile {
pathIndex: number;
path: SVGPath;
origin: Point2D;
constructor(pathIndex: number, path: SVGPath, origin: Point2D) {
this.pathIndex = pathIndex;
this.path = path;
this.origin = origin;
}
}
function canonicalizePath(path: SVGPath): SVGPath {
return path.abs().iterate((segment: string[], index: number, x: number, y: number) => {
if (segment[0] === 'H')
return [['L', segment[1], '0']];
if (segment[0] === 'V')
return [['L', '0', segment[1]]];
return [segment];
});
}
function main(): void {
window.fetch(SVG).then(svg => {
svg.text().then(svgText => {
const svg = staticCast((new DOMParser).parseFromString(svgText, 'image/svg+xml'),
XMLDocument);
new App(svg).run();
});
});
}
document.addEventListener('DOMContentLoaded', () => main(), false);
function staticCast<T>(value: any, constructor: { new(...args: any[]): T }): T {
if (!(value instanceof constructor))
throw new Error("Invalid dynamic cast");
return value;
}
function unwrapNull<T>(value: T | null): T {
if (value == null)
throw new Error("Unexpected null");
return value;
}

6
demo2/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"target": "es6"
},
}

2757
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@ pub struct Mesh {
pub b_boxes: Vec<BBox>,
pub stencil_segments: Vec<StencilSegment>,
pub stencil_normals: Vec<StencilNormals>,
pub tile_metadata: Option<TileMetadata>,
}
impl Mesh {
@ -43,6 +44,7 @@ impl Mesh {
b_boxes: vec![],
stencil_segments: vec![],
stencil_normals: vec![],
tile_metadata: None,
}
}
@ -55,6 +57,7 @@ impl Mesh {
self.b_boxes.clear();
self.stencil_segments.clear();
self.stencil_normals.clear();
self.tile_metadata = None;
}
pub(crate) fn add_b_vertex(&mut self,
@ -279,6 +282,12 @@ pub struct StencilNormals {
pub to: Vector2D<f32>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct TileMetadata {
pub origin: Point2D<f32>,
pub path_index: u32,
}
#[derive(Clone, Copy, Debug)]
struct CornerValues {
upper_left: Point2D<f32>,

View File

@ -10,7 +10,7 @@
use bincode;
use byteorder::{LittleEndian, WriteBytesExt};
use mesh::Mesh;
use mesh::{Mesh, TileMetadata};
use serde::Serialize;
use std::io::{self, ErrorKind, Seek, SeekFrom, Write};
use std::u32;
@ -53,6 +53,10 @@ impl MeshPack {
try!(write_simple_chunk(writer, b"bbox", &mesh.b_boxes));
try!(write_simple_chunk(writer, b"sseg", &mesh.stencil_segments));
try!(write_simple_chunk(writer, b"snor", &mesh.stencil_normals));
match mesh.tile_metadata {
None => try!(write_simple_chunk::<_, TileMetadata>(writer, b"tile", &[])),
Some(metadata) => try!(write_simple_chunk(writer, b"tile", &[metadata])),
}
Ok(())
}));
}

118
path-utils/src/clip.rs Normal file
View File

@ -0,0 +1,118 @@
// pathfinder/partitioner/src/clip.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use euclid::{Point2D, Rect, Vector3D};
use lyon_path::PathEvent;
use std::mem;
use std::ops::Range;
pub struct RectClipper<'a> {
clip_rect: Rect<f32>,
subject: &'a [PathEvent],
}
impl<'a> RectClipper<'a> {
pub fn new<'aa>(clip_rect: &Rect<f32>, subject: &'aa [PathEvent]) -> RectClipper<'aa> {
RectClipper {
clip_rect: *clip_rect,
subject,
}
}
pub fn clip(&self) -> Vec<PathEvent> {
let mut output = self.subject.to_vec();
self.clip_against(Edge::Left(self.clip_rect.origin.x), &mut output);
self.clip_against(Edge::Top(self.clip_rect.origin.y), &mut output);
self.clip_against(Edge::Right(self.clip_rect.max_x()), &mut output);
self.clip_against(Edge::Bottom(self.clip_rect.max_y()), &mut output);
output
}
fn clip_against(&self, edge: Edge, output: &mut Vec<PathEvent>) {
let (mut from, mut path_start, mut first_point) = (Point2D::zero(), None, false);
let input = mem::replace(output, vec![]);
for event in input {
let to = match event {
PathEvent::MoveTo(to) => {
path_start = Some(to);
from = to;
first_point = true;
continue
}
PathEvent::Close => {
match path_start {
None => continue,
Some(path_start) => path_start,
}
}
PathEvent::LineTo(to) |
PathEvent::QuadraticTo(_, to) |
PathEvent::CubicTo(_, _, to) => to,
PathEvent::Arc(..) => panic!("Arcs unsupported!"),
};
if edge.point_is_inside(&to) {
if !edge.point_is_inside(&from) {
add_line(&edge.line_intersection(&from, &to), output, &mut first_point);
}
add_line(&to, output, &mut first_point);
} else if edge.point_is_inside(&from) {
add_line(&edge.line_intersection(&from, &to), output, &mut first_point);
}
from = to;
if let PathEvent::Close = event {
output.push(PathEvent::Close);
path_start = None;
}
}
fn add_line(to: &Point2D<f32>, output: &mut Vec<PathEvent>, first_point: &mut bool) {
if *first_point {
output.push(PathEvent::MoveTo(*to));
*first_point = false;
} else {
output.push(PathEvent::LineTo(*to));
}
}
}
}
#[derive(Clone, Copy)]
enum Edge {
Left(f32),
Top(f32),
Right(f32),
Bottom(f32),
}
impl Edge {
fn point_is_inside(&self, point: &Point2D<f32>) -> bool {
match *self {
Edge::Left(x_edge) => point.x >= x_edge,
Edge::Top(y_edge) => point.y >= y_edge,
Edge::Right(x_edge) => point.x <= x_edge,
Edge::Bottom(y_edge) => point.y <= y_edge,
}
}
fn line_intersection(&self, start_point: &Point2D<f32>, endpoint: &Point2D<f32>)
-> Point2D<f32> {
let start_point = Vector3D::new(start_point.x, start_point.y, 1.0);
let endpoint = Vector3D::new(endpoint.x, endpoint.y, 1.0);
let edge = match *self {
Edge::Left(x_edge) | Edge::Right(x_edge) => Vector3D::new(1.0, 0.0, -x_edge),
Edge::Top(y_edge) | Edge::Bottom(y_edge) => Vector3D::new(0.0, 1.0, -y_edge),
};
let intersection = start_point.cross(endpoint).cross(edge);
Point2D::new(intersection.x / intersection.z, intersection.y / intersection.z)
}
}

View File

@ -18,6 +18,7 @@ extern crate lyon_path;
use lyon_path::geom as lyon_geom;
use lyon_path::geom::euclid;
pub mod clip;
pub mod cubic_to_quadratic;
pub mod normals;
pub mod orientation;