WIP
This commit is contained in:
parent
26bbf2c3d5
commit
c1407c3970
|
@ -17,6 +17,7 @@ export class Point2D {
|
||||||
constructor(x: number, y: number) {
|
constructor(x: number, y: number) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
approxEq(other: Point2D): boolean {
|
approxEq(other: Point2D): boolean {
|
||||||
|
@ -33,9 +34,55 @@ export interface Size2D {
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Rect {
|
export class Rect {
|
||||||
origin: Point2D;
|
origin: Point2D;
|
||||||
size: Size2D;
|
size: Size2D;
|
||||||
|
|
||||||
|
constructor(origin: Point2D, size: Size2D) {
|
||||||
|
this.origin = origin;
|
||||||
|
this.size = size;
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
unionWithPoint(point: Point2D): Rect {
|
||||||
|
let newOrigin = this.origin, newSize = this.size;
|
||||||
|
|
||||||
|
if (point.x < this.origin.x) {
|
||||||
|
newSize = {
|
||||||
|
width: newSize.width + newOrigin.x - point.x,
|
||||||
|
height: newSize.height,
|
||||||
|
};
|
||||||
|
newOrigin = new Point2D(point.x, newOrigin.y);
|
||||||
|
} else if (point.x > this.maxX()) {
|
||||||
|
newSize = {
|
||||||
|
width: newSize.width + point.x - this.maxX(),
|
||||||
|
height: newSize.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.y < this.origin.y) {
|
||||||
|
newSize = {
|
||||||
|
width: newSize.width,
|
||||||
|
height: newSize.height + newOrigin.y - point.y,
|
||||||
|
};
|
||||||
|
newOrigin = new Point2D(newOrigin.x, point.y);
|
||||||
|
} else if (point.y > this.maxY()) {
|
||||||
|
newSize = {
|
||||||
|
width: newSize.width,
|
||||||
|
height: newSize.height + point.y - this.maxY(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rect(newOrigin, newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
maxX(): number {
|
||||||
|
return this.origin.x + this.size.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxY(): number {
|
||||||
|
return this.origin.y + this.size.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Vector3D {
|
export interface Vector3D {
|
||||||
|
@ -63,3 +110,11 @@ export function approxEq(a: number, b: number): boolean {
|
||||||
export function lerp(a: number, b: number, t: number): number {
|
export function lerp(a: number, b: number, t: number): number {
|
||||||
return a + (b - a) * t;
|
return a + (b - a) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ import STENCIL_VERTEX_SHADER_SOURCE from "./stencil.vs.glsl";
|
||||||
import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
|
import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
|
||||||
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
|
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
|
||||||
import AREA_LUT from "../resources/textures/area-lut.png";
|
import AREA_LUT from "../resources/textures/area-lut.png";
|
||||||
import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, lerp} from "./geometry";
|
import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, cross, lerp} from "./geometry";
|
||||||
import {SVGPath, Tiler} from "./tiling";
|
import {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals} from "./tiling";
|
||||||
import {staticCast, unwrapNull} from "./util";
|
import {staticCast, unwrapNull} from "./util";
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
||||||
|
@ -23,7 +23,6 @@ const parseColor: (color: string) => any = require('parse-color');
|
||||||
|
|
||||||
const SVG_NS: string = "http://www.w3.org/2000/svg";
|
const SVG_NS: string = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
const TILE_SIZE: Size2D = {width: 32.0, height: 32.0};
|
|
||||||
const STENCIL_FRAMEBUFFER_SIZE: Size2D = {
|
const STENCIL_FRAMEBUFFER_SIZE: Size2D = {
|
||||||
width: TILE_SIZE.width * 256,
|
width: TILE_SIZE.width * 256,
|
||||||
height: TILE_SIZE.height * 256,
|
height: TILE_SIZE.height * 256,
|
||||||
|
@ -417,7 +416,9 @@ class Scene {
|
||||||
const pathElements = Array.from(document.getElementsByTagName('path'));
|
const pathElements = Array.from(document.getElementsByTagName('path'));
|
||||||
const tiles: Tile[] = [], pathColors = [];
|
const tiles: Tile[] = [], pathColors = [];
|
||||||
|
|
||||||
for (let pathElementIndex = 0;
|
const tileDebugger = new TileDebugger(document);
|
||||||
|
|
||||||
|
for (let pathElementIndex = 0, realPathIndex = 0;
|
||||||
pathElementIndex < pathElements.length;
|
pathElementIndex < pathElements.length;
|
||||||
pathElementIndex++) {
|
pathElementIndex++) {
|
||||||
const pathElement = pathElements[pathElementIndex];
|
const pathElement = pathElements[pathElementIndex];
|
||||||
|
@ -450,7 +451,15 @@ class Scene {
|
||||||
path = flattenPath(path);
|
path = flattenPath(path);
|
||||||
path = canonicalizePath(path);
|
path = canonicalizePath(path);
|
||||||
|
|
||||||
|
realPathIndex++;
|
||||||
|
|
||||||
|
//if (realPathIndex === 73) {
|
||||||
|
//console.log("path", pathElementIndex, "svg path", path);
|
||||||
const tiler = new Tiler(path);
|
const tiler = new Tiler(path);
|
||||||
|
tiler.tile();
|
||||||
|
tileDebugger.addTiler(tiler, paint);
|
||||||
|
console.log("path", pathElementIndex, "tiles", tiler.getTileStrips());
|
||||||
|
//}
|
||||||
|
|
||||||
const boundingRect = this.boundingRectOfPath(path);
|
const boundingRect = this.boundingRectOfPath(path);
|
||||||
|
|
||||||
|
@ -464,7 +473,7 @@ class Scene {
|
||||||
while (true) {
|
while (true) {
|
||||||
let x = boundingRect.origin.x - boundingRect.origin.x % TILE_SIZE.width;
|
let x = boundingRect.origin.x - boundingRect.origin.x % TILE_SIZE.width;
|
||||||
while (true) {
|
while (true) {
|
||||||
const tileBounds = {origin: new Point2D(x, y), size: TILE_SIZE};
|
const tileBounds = new Rect(new Point2D(x, y), TILE_SIZE);
|
||||||
const tilePath = this.clipPathToRect(path, tileBounds);
|
const tilePath = this.clipPathToRect(path, tileBounds);
|
||||||
|
|
||||||
if (tilePath.toString().length > 0) {
|
if (tilePath.toString().length > 0) {
|
||||||
|
@ -518,6 +527,13 @@ class Scene {
|
||||||
|
|
||||||
document.body.removeChild(svgElement);
|
document.body.removeChild(svgElement);
|
||||||
|
|
||||||
|
const svgContainer = document.createElement('div');
|
||||||
|
svgContainer.style.position = 'relative';
|
||||||
|
svgContainer.style.width = "2000px";
|
||||||
|
svgContainer.style.height = "2000px";
|
||||||
|
svgContainer.appendChild(tileDebugger.svg);
|
||||||
|
document.body.appendChild(svgContainer);
|
||||||
|
|
||||||
console.log(tiles);
|
console.log(tiles);
|
||||||
this.tiles = tiles;
|
this.tiles = tiles;
|
||||||
this.pathColors = pathColors;
|
this.pathColors = pathColors;
|
||||||
|
@ -537,8 +553,8 @@ class Scene {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (minX == null || minY == null || maxX == null || maxY == null)
|
if (minX == null || minY == null || maxX == null || maxY == null)
|
||||||
return {origin: new Point2D(0, 0), size: {width: 0, height: 0}};
|
return new Rect(new Point2D(0, 0), {width: 0, height: 0});
|
||||||
return {origin: new Point2D(minX, minY), size: {width: maxX - minX, height: maxY - minY}};
|
return new Rect(new Point2D(minX, minY), {width: maxX - minX, height: maxY - minY});
|
||||||
}
|
}
|
||||||
|
|
||||||
private clipPathToRect(path: SVGPath, tileBounds: Rect): SVGPath {
|
private clipPathToRect(path: SVGPath, tileBounds: Rect): SVGPath {
|
||||||
|
@ -772,14 +788,6 @@ function canonicalizePath(path: SVGPath): SVGPath {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForQuery(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, query: WebGLQuery):
|
function waitForQuery(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, query: WebGLQuery):
|
||||||
void {
|
void {
|
||||||
const queryResultAvailable = disjointTimerQueryExt.QUERY_RESULT_AVAILABLE_EXT;
|
const queryResultAvailable = disjointTimerQueryExt.QUERY_RESULT_AVAILABLE_EXT;
|
||||||
|
@ -812,6 +820,8 @@ function sampleBezier(from: Point2D, ctrl: Point2D, to: Point2D, t: number): Poi
|
||||||
function main(): void {
|
function main(): void {
|
||||||
window.fetch(SVG).then(svg => {
|
window.fetch(SVG).then(svg => {
|
||||||
svg.text().then(svgText => {
|
svg.text().then(svgText => {
|
||||||
|
testIntervals();
|
||||||
|
|
||||||
const svg = staticCast((new DOMParser).parseFromString(svgText, 'image/svg+xml'),
|
const svg = staticCast((new DOMParser).parseFromString(svgText, 'image/svg+xml'),
|
||||||
XMLDocument);
|
XMLDocument);
|
||||||
const image = new Image;
|
const image = new Image;
|
||||||
|
|
396
demo2/tiling.ts
396
demo2/tiling.ts
|
@ -8,8 +8,10 @@
|
||||||
// option. This file may not be copied, modified, or distributed
|
// option. This file may not be copied, modified, or distributed
|
||||||
// except according to those terms.
|
// except according to those terms.
|
||||||
|
|
||||||
import {Point2D} from "./geometry";
|
import {Point2D, Rect, Size2D, cross} from "./geometry";
|
||||||
import {panic, unwrapNull} from "./util";
|
import {panic, staticCast, unwrapNull} from "./util";
|
||||||
|
|
||||||
|
export const TILE_SIZE: Size2D = {width: 32.0, height: 32.0};
|
||||||
|
|
||||||
export interface SVGPath {
|
export interface SVGPath {
|
||||||
abs(): SVGPath;
|
abs(): SVGPath;
|
||||||
|
@ -29,16 +31,20 @@ interface EndpointIndex {
|
||||||
export class Tiler {
|
export class Tiler {
|
||||||
private path: SVGPath;
|
private path: SVGPath;
|
||||||
private endpoints: SubpathEndpoints[];
|
private endpoints: SubpathEndpoints[];
|
||||||
private sortedEndpointIndices: EndpointIndex[];
|
private sortedEdges: Edge[];
|
||||||
|
private boundingRect: Rect | null;
|
||||||
|
private tileStrips: TileStrip[];
|
||||||
|
|
||||||
constructor(path: SVGPath) {
|
constructor(path: SVGPath) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
//console.log("tiler: path=", path);
|
||||||
|
|
||||||
// Accumulate endpoints.
|
// Accumulate endpoints.
|
||||||
this.endpoints = [];
|
this.endpoints = [];
|
||||||
let currentSubpathEndpoints = new SubpathEndpoints;
|
let currentSubpathEndpoints = new SubpathEndpoints;
|
||||||
path.iterate(segString => {
|
path.iterate(segString => {
|
||||||
const segment = new PathSegment(segString);
|
const segment = new PathSegment(segString);
|
||||||
|
//console.log("segment", segment);
|
||||||
switch (segment.command) {
|
switch (segment.command) {
|
||||||
case 'M':
|
case 'M':
|
||||||
if (!currentSubpathEndpoints.isEmpty()) {
|
if (!currentSubpathEndpoints.isEmpty()) {
|
||||||
|
@ -64,23 +70,157 @@ export class Tiler {
|
||||||
if (!currentSubpathEndpoints.isEmpty())
|
if (!currentSubpathEndpoints.isEmpty())
|
||||||
this.endpoints.push(currentSubpathEndpoints);
|
this.endpoints.push(currentSubpathEndpoints);
|
||||||
|
|
||||||
// Sort endpoints.
|
// Sort edges, and accumulate bounding rect.
|
||||||
this.sortedEndpointIndices = [];
|
this.sortedEdges = [];
|
||||||
|
this.boundingRect = null;
|
||||||
for (let subpathIndex = 0; subpathIndex < this.endpoints.length; subpathIndex++) {
|
for (let subpathIndex = 0; subpathIndex < this.endpoints.length; subpathIndex++) {
|
||||||
const subpathEndpoints = this.endpoints[subpathIndex];
|
const subpathEndpoints = this.endpoints[subpathIndex];
|
||||||
for (let endpointIndex = 0;
|
for (let endpointIndex = 0;
|
||||||
endpointIndex < subpathEndpoints.endpoints.length;
|
endpointIndex < subpathEndpoints.endpoints.length;
|
||||||
endpointIndex++) {
|
endpointIndex++) {
|
||||||
this.sortedEndpointIndices.push({subpathIndex, endpointIndex});
|
this.sortedEdges.push(this.nextEdgeFromEndpoint({subpathIndex, endpointIndex}));
|
||||||
|
|
||||||
|
const endpoint = subpathEndpoints.endpoints[endpointIndex];
|
||||||
|
if (this.boundingRect == null)
|
||||||
|
this.boundingRect = new Rect(endpoint, {width: 0, height: 0});
|
||||||
|
else
|
||||||
|
this.boundingRect.unionWithPoint(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.sortedEdges.sort((edgeA, edgeB) => {
|
||||||
|
return Math.min(edgeA.from.y, edgeA.to.y) - Math.min(edgeB.from.y, edgeB.to.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Dump endpoints.
|
||||||
|
const allEndpoints = this.sortedEndpointIndices.map(index => {
|
||||||
|
return {
|
||||||
|
index: index,
|
||||||
|
endpoint: this.endpoints[index.subpathIndex].endpoints[index.endpointIndex],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
//console.log("allEndpoints", allEndpoints);
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.tileStrips = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
tile(): void {
|
tile(): void {
|
||||||
const activeEdges = [];
|
if (this.boundingRect == null)
|
||||||
for (const endpointIndex of this.sortedEndpointIndices) {
|
return;
|
||||||
|
|
||||||
|
const activeIntervals = new Intervals(this.boundingRect.maxY());;
|
||||||
|
let activeEdges: Edge[] = [];
|
||||||
|
let nextEdgeIndex = 0;
|
||||||
|
this.tileStrips = [];
|
||||||
|
|
||||||
|
let tileTop = this.boundingRect.origin.y - this.boundingRect.origin.y % TILE_SIZE.height;
|
||||||
|
while (tileTop < this.boundingRect.maxY()) {
|
||||||
|
const tileBottom = tileTop + TILE_SIZE.height;
|
||||||
|
|
||||||
|
// Populate tile strip with active intervals.
|
||||||
|
const tileStrip = new TileStrip(tileTop);
|
||||||
|
for (const interval of activeIntervals.intervalRanges()) {
|
||||||
|
if (interval.winding === 0)
|
||||||
|
continue;
|
||||||
|
const startPoint = new Point2D(interval.start, tileTop);
|
||||||
|
const endPoint = new Point2D(interval.end, tileTop);
|
||||||
|
if (interval.winding < 0)
|
||||||
|
tileStrip.pushEdge(new Edge(startPoint, endPoint));
|
||||||
|
else
|
||||||
|
tileStrip.pushEdge(new Edge(endPoint, startPoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate tile strip with active edges.
|
||||||
|
const oldEdges = activeEdges;
|
||||||
|
activeEdges = [];
|
||||||
|
for (const activeEdge of oldEdges)
|
||||||
|
this.processEdge(activeEdge, tileStrip, activeEdges, activeIntervals, tileTop);
|
||||||
|
|
||||||
|
while (nextEdgeIndex < this.sortedEdges.length) {
|
||||||
|
const edge = this.sortedEdges[nextEdgeIndex];
|
||||||
|
if (edge.from.y > tileBottom && edge.to.y > tileBottom)
|
||||||
|
break;
|
||||||
|
|
||||||
|
this.processEdge(edge, tileStrip, activeEdges, activeIntervals, tileTop);
|
||||||
|
nextEdgeIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileStrips.push(tileStrip);
|
||||||
|
tileTop = tileBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTileStrips(): TileStrip[] {
|
||||||
|
return this.tileStrips;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoundingRect(): Rect {
|
||||||
|
if (this.boundingRect == null)
|
||||||
|
return new Rect(new Point2D(0, 0), {width: 0, height: 0});
|
||||||
|
|
||||||
|
const tileLeft = this.boundingRect.origin.x - this.boundingRect.origin.x % TILE_SIZE.width;
|
||||||
|
const tileRight = Math.ceil(this.boundingRect.maxX() / TILE_SIZE.width) * TILE_SIZE.width;
|
||||||
|
const tileTop = this.boundingRect.origin.y - this.boundingRect.origin.y % TILE_SIZE.height;
|
||||||
|
const tileBottom = Math.ceil(this.boundingRect.maxY() / TILE_SIZE.height) *
|
||||||
|
TILE_SIZE.height;
|
||||||
|
return new Rect(new Point2D(tileLeft, tileTop),
|
||||||
|
{width: tileRight - tileLeft, height: tileBottom - tileTop});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processEdge(edge: Edge,
|
||||||
|
tileStrip: TileStrip,
|
||||||
|
activeEdges: Edge[],
|
||||||
|
intervals: Intervals,
|
||||||
|
tileTop: number):
|
||||||
|
void {
|
||||||
|
const tileBottom = tileTop + TILE_SIZE.height;
|
||||||
|
const clipped = this.clipEdgeY(edge, tileBottom);
|
||||||
|
|
||||||
|
if (clipped.upper != null) {
|
||||||
|
tileStrip.pushEdge(clipped.upper);
|
||||||
|
|
||||||
|
if (edge.from.x <= edge.to.x)
|
||||||
|
intervals.add(new IntervalRange(edge.from.x, edge.to.x, 1));
|
||||||
|
else
|
||||||
|
intervals.add(new IntervalRange(edge.to.x, edge.from.x, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipped.lower != null)
|
||||||
|
activeEdges.push(clipped.lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clipEdgeY(edge: Edge, y: number): ClippedEdgesY {
|
||||||
|
if (edge.from.y < y && edge.to.y < y)
|
||||||
|
return {upper: edge, lower: null};
|
||||||
|
if (edge.from.y > y && edge.to.y > y)
|
||||||
|
return {upper: null, lower: edge};
|
||||||
|
|
||||||
|
const from = {x: edge.from.x, y: edge.from.y, z: 1.0};
|
||||||
|
const to = {x: edge.to.x, y: edge.to.y, z: 1.0};
|
||||||
|
const clipLine = {x: 0.0, y: 1.0, z: -y };
|
||||||
|
|
||||||
|
const intersectionHC = cross(cross(from, to), clipLine);
|
||||||
|
const intersection = new Point2D(intersectionHC.x / intersectionHC.z,
|
||||||
|
intersectionHC.y / intersectionHC.z);
|
||||||
|
const fromEdge = new Edge(edge.from, intersection);
|
||||||
|
const toEdge = new Edge(intersection, edge.to);
|
||||||
|
|
||||||
|
if (edge.from.y < y)
|
||||||
|
return {upper: fromEdge, lower: toEdge};
|
||||||
|
return {upper: toEdge, lower: fromEdge};
|
||||||
|
}
|
||||||
|
|
||||||
|
private prevEdgeFromEndpoint(endpointIndex: EndpointIndex): Edge {
|
||||||
|
const subpathEndpoints = this.endpoints[endpointIndex.subpathIndex];
|
||||||
|
return new Edge(subpathEndpoints.prevEndpointOf(endpointIndex.endpointIndex),
|
||||||
|
subpathEndpoints.endpoints[endpointIndex.endpointIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextEdgeFromEndpoint(endpointIndex: EndpointIndex): Edge {
|
||||||
|
const subpathEndpoints = this.endpoints[endpointIndex.subpathIndex];
|
||||||
|
return new Edge(subpathEndpoints.endpoints[endpointIndex.endpointIndex],
|
||||||
|
subpathEndpoints.nextEndpointOf(endpointIndex.endpointIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +263,7 @@ export class PathSegment {
|
||||||
for (let i = 1; i < segment.length; i += 2)
|
for (let i = 1; i < segment.length; i += 2)
|
||||||
points.push(new Point2D(parseFloat(segment[i]), parseFloat(segment[i + 1])));
|
points.push(new Point2D(parseFloat(segment[i]), parseFloat(segment[i + 1])));
|
||||||
this.points = points;
|
this.points = points;
|
||||||
|
//console.log("PathSegment, segment=", segment, "points=", points);
|
||||||
this.command = segment[0];
|
this.command = segment[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,3 +271,242 @@ export class PathSegment {
|
||||||
return this.points[this.points.length - 1];
|
return this.points[this.points.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Edge {
|
||||||
|
from: Point2D;
|
||||||
|
to: Point2D;
|
||||||
|
|
||||||
|
constructor(from: Point2D, to: Point2D) {
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TileStrip {
|
||||||
|
edges: Edge[];
|
||||||
|
tileTop: number;
|
||||||
|
|
||||||
|
constructor(tileTop: number) {
|
||||||
|
this.edges = [];
|
||||||
|
this.tileTop = tileTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushEdge(edge: Edge): void {
|
||||||
|
this.edges.push(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
tileBottom(): number {
|
||||||
|
return this.tileTop + TILE_SIZE.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClippedEdgesY {
|
||||||
|
upper: Edge | null;
|
||||||
|
lower: Edge | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Intervals {
|
||||||
|
private ranges: IntervalRange[];
|
||||||
|
|
||||||
|
constructor(width: number) {
|
||||||
|
this.ranges = [new IntervalRange(0, width, 0)];
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalRanges(): IntervalRange[] {
|
||||||
|
return this.ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(range: IntervalRange): void {
|
||||||
|
this.splitAt(range.start);
|
||||||
|
this.splitAt(range.end);
|
||||||
|
|
||||||
|
let startIndex = this.ranges.length, endIndex = this.ranges.length;
|
||||||
|
for (let i = 0; i < this.ranges.length; i++) {
|
||||||
|
if (range.start === this.ranges[i].start)
|
||||||
|
startIndex = i;
|
||||||
|
if (range.end === this.ranges[i].end)
|
||||||
|
endIndex = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust winding numbers.
|
||||||
|
for (let i = startIndex; i < endIndex; i++)
|
||||||
|
this.ranges[i].winding += range.winding;
|
||||||
|
|
||||||
|
this.mergeAdjacent();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.ranges = [new IntervalRange(0, this.ranges[this.ranges.length - 1].end, 0)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private splitAt(value: number): void {
|
||||||
|
for (let i = 0; i < this.ranges.length; i++) {
|
||||||
|
if (this.ranges[i].start < value && value < this.ranges[i].end) {
|
||||||
|
const firstRange = this.ranges[i];
|
||||||
|
const secondRange = new IntervalRange(value, firstRange.end, firstRange.winding);
|
||||||
|
this.ranges.splice(i + 1, 0, secondRange);
|
||||||
|
firstRange.end = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeAdjacent(): void {
|
||||||
|
let i = 0;
|
||||||
|
while (i + 1 < this.ranges.length) {
|
||||||
|
if (this.ranges[i].end === this.ranges[i + 1].start &&
|
||||||
|
this.ranges[i].winding === this.ranges[i + 1].winding) {
|
||||||
|
this.ranges[i].end = this.ranges[i + 1].end;
|
||||||
|
this.ranges.splice(i + 1, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntervalRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
winding: number;
|
||||||
|
|
||||||
|
constructor(start: number, end: number, winding: number) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.winding = winding;
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(value: number): boolean {
|
||||||
|
return value >= this.start && value < this.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging
|
||||||
|
|
||||||
|
const SVG_NS: string = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
export class TileDebugger {
|
||||||
|
svg: SVGElement;
|
||||||
|
size: Size2D;
|
||||||
|
|
||||||
|
constructor(document: HTMLDocument) {
|
||||||
|
this.svg = staticCast(document.createElementNS(SVG_NS, 'svg'), SVGElement);
|
||||||
|
|
||||||
|
this.size = {width: 0, height: 0};
|
||||||
|
|
||||||
|
this.svg.style.position = 'absolute';
|
||||||
|
this.svg.style.left = "0";
|
||||||
|
this.svg.style.top = "0";
|
||||||
|
this.updateSVGSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTiler(tiler: Tiler, fillColor: string): void {
|
||||||
|
const boundingRect = tiler.getBoundingRect();
|
||||||
|
this.size.width = Math.max(this.size.width, boundingRect.maxX());
|
||||||
|
this.size.height = Math.max(this.size.height, boundingRect.maxY());
|
||||||
|
|
||||||
|
for (const tileStrip of tiler.getTileStrips()) {
|
||||||
|
const tileBottom = tileStrip.tileBottom();
|
||||||
|
let path = "";
|
||||||
|
for (const edge of tileStrip.edges) {
|
||||||
|
path += "M " + edge.from.x + " " + edge.from.y + " ";
|
||||||
|
path += "L " + edge.to.x + " " + edge.to.y + " ";
|
||||||
|
path += "L " + edge.to.x + " " + tileBottom + " ";
|
||||||
|
path += "L " + edge.from.x + " " + tileBottom + " ";
|
||||||
|
path += "Z ";
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathElement = staticCast(document.createElementNS(SVG_NS, 'path'),
|
||||||
|
SVGPathElement);
|
||||||
|
pathElement.setAttribute('d', path);
|
||||||
|
pathElement.setAttribute('fill', fillColor);
|
||||||
|
pathElement.setAttribute('stroke', "rgb(0, 128.0, 0)");
|
||||||
|
this.svg.appendChild(pathElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSVGSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSVGSize(): void {
|
||||||
|
this.svg.style.width = this.size.width + "px";
|
||||||
|
this.svg.style.height = this.size.height + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEq<T>(actual: T, expected: T): void {
|
||||||
|
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
||||||
|
console.error("expected", expected, "but found", actual);
|
||||||
|
throw new Error("Assertion failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testIntervals(): void {
|
||||||
|
const intervals = new Intervals(7);
|
||||||
|
intervals.add(new IntervalRange(1, 2, 1));
|
||||||
|
intervals.add(new IntervalRange(3, 4, 1));
|
||||||
|
intervals.add(new IntervalRange(5, 6, 1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 1, 0),
|
||||||
|
new IntervalRange(1, 2, 1),
|
||||||
|
new IntervalRange(2, 3, 0),
|
||||||
|
new IntervalRange(3, 4, 1),
|
||||||
|
new IntervalRange(4, 5, 0),
|
||||||
|
new IntervalRange(5, 6, 1),
|
||||||
|
new IntervalRange(6, 7, 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
intervals.clear();
|
||||||
|
intervals.add(new IntervalRange(1, 2, 1));
|
||||||
|
intervals.add(new IntervalRange(2, 3, 1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 1, 0),
|
||||||
|
new IntervalRange(1, 3, 1),
|
||||||
|
new IntervalRange(3, 7, 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
intervals.clear();
|
||||||
|
intervals.add(new IntervalRange(1, 4, 1));
|
||||||
|
intervals.add(new IntervalRange(3, 5, 1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 1, 0),
|
||||||
|
new IntervalRange(1, 3, 1),
|
||||||
|
new IntervalRange(3, 4, 2),
|
||||||
|
new IntervalRange(4, 5, 1),
|
||||||
|
new IntervalRange(5, 7, 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
intervals.clear();
|
||||||
|
intervals.add(new IntervalRange(2, 3.5, 1));
|
||||||
|
intervals.add(new IntervalRange(3, 5, 1));
|
||||||
|
intervals.add(new IntervalRange(6, 7, 1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 2, 0),
|
||||||
|
new IntervalRange(2, 3, 1),
|
||||||
|
new IntervalRange(3, 3.5, 2),
|
||||||
|
new IntervalRange(3.5, 5, 1),
|
||||||
|
new IntervalRange(5, 6, 0),
|
||||||
|
new IntervalRange(6, 7, 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
intervals.clear();
|
||||||
|
intervals.add(new IntervalRange(2, 5, 1));
|
||||||
|
intervals.add(new IntervalRange(3, 3.5, -1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 2, 0),
|
||||||
|
new IntervalRange(2, 3, 1),
|
||||||
|
new IntervalRange(3, 3.5, 0),
|
||||||
|
new IntervalRange(3.5, 5, 1),
|
||||||
|
new IntervalRange(5, 7, 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
intervals.clear();
|
||||||
|
intervals.add(new IntervalRange(2, 5, 1));
|
||||||
|
intervals.add(new IntervalRange(3, 3.5, -1));
|
||||||
|
intervals.add(new IntervalRange(3, 3.5, 1));
|
||||||
|
assertEq(intervals.intervalRanges(), [
|
||||||
|
new IntervalRange(0, 2, 0),
|
||||||
|
new IntervalRange(2, 5, 1),
|
||||||
|
new IntervalRange(5, 7, 0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue