From f18af6d650ec99d76bf6c4f420349b92d39f9af0 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 4 Dec 2018 13:35:50 -0500 Subject: [PATCH] Approximation --- demo2/path-utils.ts | 130 +++++++++++++++++++++++++++++++++++++++++--- demo2/pathfinder.ts | 7 ++- demo2/tiling.ts | 8 ++- demo2/util.ts | 6 ++ 4 files changed, 139 insertions(+), 12 deletions(-) diff --git a/demo2/path-utils.ts b/demo2/path-utils.ts index 765485e7..69109fc5 100644 --- a/demo2/path-utils.ts +++ b/demo2/path-utils.ts @@ -8,8 +8,11 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -import {Point2D} from "./geometry"; -import {SVGPath} from "./tiling"; +import {Point2D, Rect, EPSILON} from "./geometry"; +import {SVGPath, Edge} from "./tiling"; +import { ENGINE_METHOD_DIGESTS } from "constants"; +import { AssertionError } from "assert"; +import { unwrapNull, unwrapUndef } from "./util"; const SVGPath: (path: string) => SVGPath = require('svgpath'); @@ -48,11 +51,16 @@ export function flattenPath(path: SVGPath): SVGPath { return path.unshort().abs().iterate(segmentPieces => { let segment = new PathSegment(segmentPieces); if (segment.command === 'C' && lastPoint != null) { - const ctrl10 = segment.points[0].scale(3.0).sub(lastPoint).scale(0.5); - const ctrl11 = segment.points[1].scale(3.0).sub(segment.points[2]).scale(0.5); - const to = segment.points[2]; - const ctrl = ctrl10.lerp(ctrl11, 0.5); - segment = new PathSegment(['Q', "" + ctrl.x, "" + ctrl.y, "" + to.x, "" + to.y]); + const cubicEdge = new CubicEdge(lastPoint, + segment.points[0], + segment.points[1], + segment.points[2]); + //console.log("cubic edge", cubicEdge); + const edges: Edge[] = cubicEdge.toQuadraticEdges(); + const newSegments = edges.map(edge => edge.toSVGPieces()); + //console.log("... resulting new segments:", newSegments); + lastPoint = segment.to(); + return newSegments; } if (segment.command === 'H' && lastPoint != null) segment = new PathSegment(['L', segmentPieces[1], "" + lastPoint.y]); @@ -63,6 +71,53 @@ export function flattenPath(path: SVGPath): SVGPath { }); } +export function makePathMonotonic(path: SVGPath): SVGPath { + let lastPoint: Point2D | null = null; + return path.iterate(segmentPieces => { + let segment = new PathSegment(segmentPieces); + if (segment.command === 'Q' && lastPoint != null) { + const edge = new Edge(lastPoint, segment.points[0], segment.points[1]); + const minX = Math.min(edge.from.x, edge.to.x); + const maxX = Math.max(edge.from.x, edge.to.x); + + const edgesX: Edge[] = []; + if (edge.ctrl!.x < minX || edge.ctrl!.x > maxX) { + const t = (edge.from.x - edge.ctrl!.x) / + (edge.from.x - 2.0 * edge.ctrl!.x + edge.to.x); + const subdivided = edge.subdivideAt(t); + if (t < -EPSILON || t > 1.0 + EPSILON) + throw new Error("Bad t value when making monotonic X!"); + edgesX.push(subdivided.prev, subdivided.next); + } else { + edgesX.push(edge); + } + + const newEdges = []; + for (const edge of edgesX) { + const minY = Math.min(edge.from.y, edge.to.y); + const maxY = Math.max(edge.from.y, edge.to.y); + + if (edge.ctrl!.y < minY || edge.ctrl!.y > maxY) { + const t = (edge.from.y - edge.ctrl!.y) / + (edge.from.y - 2.0 * edge.ctrl!.y + edge.to.y); + if (t < -EPSILON || t > 1.0 + EPSILON) + throw new Error("Bad t value when making monotonic Y!"); + const subdivided = edge.subdivideAt(t); + newEdges.push(subdivided.prev, subdivided.next); + } else { + newEdges.push(edge); + } + } + + lastPoint = segment.to(); + return newEdges.map(newEdge => newEdge.toSVGPieces()); + } + + lastPoint = segment.to(); + return [segment.toStringPieces()]; + }); +} + export class Outline { suboutlines: Suboutline[]; @@ -92,7 +147,6 @@ export class Outline { } stroke(radius: number): void { - console.log("stroking, radius=", radius); for (const suboutline of this.suboutlines) suboutline.stroke(radius); } @@ -180,3 +234,63 @@ export class OutlinePoint { this.offCurve = offCurve; } } + +class CubicEdge { + from: Point2D; + ctrl0: Point2D; + ctrl1: Point2D; + to: Point2D; + + constructor(from: Point2D, ctrl0: Point2D, ctrl1: Point2D, to: Point2D) { + this.from = from; + this.ctrl0 = ctrl0; + this.ctrl1 = ctrl1; + this.to = to; + } + + subdivideAt(t: number): SubdividedCubicEdges { + const p0 = this.from, p1 = this.ctrl0, p2 = this.ctrl1, p3 = this.to; + const p01 = p0.lerp(p1, t), p12 = p1.lerp(p2, t), p23 = p2.lerp(p3, t); + const p012 = p01.lerp(p12, t), p123 = p12.lerp(p23, t); + const p0123 = p012.lerp(p123, t); + return { + prev: new CubicEdge(p0, p01, p012, p0123), + next: new CubicEdge(p0123, p123, p23, p3), + }; + } + + toQuadraticEdges(): Edge[] { + const MAX_APPROXIMATION_ITERATIONS: number = 32; + const TOLERANCE: number = 0.1; + + const results = [], worklist: CubicEdge[] = [this]; + while (worklist.length > 0) { + let current = unwrapUndef(worklist.pop()); + for (let iteration = 0; iteration < MAX_APPROXIMATION_ITERATIONS; iteration++) { + const deltaCtrl0 = current.from.sub(current.ctrl0.scale(3.0)) + .add(current.ctrl1.scale(3.0).sub(current.to)); + const deltaCtrl1 = current.ctrl0.scale(3.0) + .sub(current.from) + .add(current.to.sub(current.ctrl1.scale(3.0))); + const maxError = Math.max(deltaCtrl0.length(), deltaCtrl1.length()) / 6.0; + if (maxError < TOLERANCE) + break; + + const subdivided = current.subdivideAt(0.5); + worklist.push(subdivided.next); + current = subdivided.prev; + } + + const approxCtrl0 = current.ctrl0.scale(3.0).sub(current.from).scale(0.5); + const approxCtrl1 = current.ctrl1.scale(3.0).sub(current.to).scale(0.5); + results.push(new Edge(current.from, approxCtrl0.lerp(approxCtrl1, 0.5), current.to)); + } + + return results; + } +} + +interface SubdividedCubicEdges { + prev: CubicEdge; + next: CubicEdge; +} diff --git a/demo2/pathfinder.ts b/demo2/pathfinder.ts index 2b6a8658..ecc939d3 100644 --- a/demo2/pathfinder.ts +++ b/demo2/pathfinder.ts @@ -17,7 +17,7 @@ import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl"; import SVG from "../resources/svg/Ghostscript_Tiger.svg"; import AREA_LUT from "../resources/textures/area-lut.png"; import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, cross, lerp} from "./geometry"; -import {flattenPath, Outline} from "./path-utils"; +import {flattenPath, Outline, makePathMonotonic} from "./path-utils"; import {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals, TileStrip} from "./tiling"; import {staticCast, unwrapNull} from "./util"; @@ -318,7 +318,7 @@ class App { gl.bindFramebuffer(gl.FRAMEBUFFER, null); const framebufferSize = {width: canvas.width, height: canvas.height}; gl.viewport(0, 0, framebufferSize.width, framebufferSize.height); - gl.clearColor(1.0, 1.0, 1.0, 1.0); + gl.clearColor(0.85, 0.85, 0.85, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.bindVertexArray(this.opaqueVertexArray); @@ -573,7 +573,7 @@ class Scene { if (strokeWidth != null) { const outline = new Outline(path); outline.calculateNormals(); - outline.stroke(strokeWidth * GLOBAL_TRANSFORM.a * 5.0); + outline.stroke(strokeWidth * GLOBAL_TRANSFORM.a); const strokedPathString = outline.toSVGPathString(); /* @@ -594,6 +594,7 @@ class Scene { */ path = SVGPath(strokedPathString); + path = makePathMonotonic(path); } paths.push(path); diff --git a/demo2/tiling.ts b/demo2/tiling.ts index 3478387c..02112983 100644 --- a/demo2/tiling.ts +++ b/demo2/tiling.ts @@ -361,7 +361,7 @@ class SubpathEndpoints { } } -class Edge { +export class Edge { from: Point2D; ctrl: Point2D | null; to: Point2D; @@ -390,6 +390,12 @@ class Edge { next: new Edge(mid, ctrlB, this.to), } } + + toSVGPieces(): string[] { + if (this.ctrl == null) + return ['L', "" + this.to.x, "" + this.to.y]; + return ['Q', "" + this.ctrl.x, "" + this.ctrl.y, "" + this.to.x, "" + this.to.y]; + } } interface SubdividedEdges { diff --git a/demo2/util.ts b/demo2/util.ts index 7a9c07ee..5312d61e 100644 --- a/demo2/util.ts +++ b/demo2/util.ts @@ -23,3 +23,9 @@ export function unwrapNull(value: T | null): T { throw new Error("Unexpected null"); return value; } + +export function unwrapUndef(value: T | undefined): T { + if (value == null) + throw new Error("Unexpected undefined"); + return value; +}