Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
HARP-11233: Store the clipping states in the ring vertices
Browse files Browse the repository at this point in the history
This change modifies `clipPolygon` to store the state of the clipping
in the vertices. Also, this change modifies the clipping planes
to ensure that vertices on the edge are not marked as "clipped".

Signed-off-by: Roberto Raggi <roberto.raggi@here.com>
  • Loading branch information
robertoraggi committed Jan 20, 2021
1 parent d2c4078 commit 0661e04
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 60 deletions.
59 changes: 46 additions & 13 deletions @here/harp-geometry/lib/ClipPolygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

import { Vector2 } from "three";

interface ClipInfo {
/**
* `true` if this vertex was clipped.
*/
isClipped?: boolean;
}

export type ClippedVertex = Vector2 & ClipInfo;

/**
* Abstract helper class used to implement the Sutherland-Hodgman clipping algorithm.
*
Expand Down Expand Up @@ -35,7 +44,7 @@ export abstract class ClippingEdge {
* @param b A point of the segment to clip.
* @param extent The extent of the bounding box.
*/
abstract computeIntersection(a: Vector2, b: Vector2, extent: number): Vector2;
abstract computeIntersection(a: Vector2, b: Vector2, extent: number): ClippedVertex;

/**
* Clip the polygon against this clipping edge.
Expand All @@ -48,8 +57,14 @@ export abstract class ClippingEdge {

polygon = [];

const pushPoint = (point: Vector2) => {
if (polygon.length === 0 || !polygon[polygon.length - 1].equals(point)) {
const pushPoint = (point: ClippedVertex) => {
const lastAddedPoint: ClippedVertex = polygon[polygon.length - 1];

if (
!lastAddedPoint?.equals(point) ||
(point.isClipped === true && !lastAddedPoint?.isClipped) ||
(!point.isClipped && lastAddedPoint?.isClipped === true)
) {
polygon.push(point);
}
};
Expand All @@ -59,11 +74,15 @@ export abstract class ClippingEdge {
const prevPoint = inputList[(i + inputList.length - 1) % inputList.length];
if (this.inside(currentPoint, extent)) {
if (!this.inside(prevPoint, extent)) {
pushPoint(this.computeIntersection(prevPoint, currentPoint, extent));
const p = this.computeIntersection(prevPoint, currentPoint, extent);
p.isClipped = true;
pushPoint(p);
}
pushPoint(currentPoint);
} else if (this.inside(prevPoint, extent)) {
pushPoint(this.computeIntersection(prevPoint, currentPoint, extent));
const p = this.computeIntersection(prevPoint, currentPoint, extent);
p.isClipped = true;
pushPoint(p);
}
}

Expand All @@ -74,7 +93,7 @@ export abstract class ClippingEdge {
class TopClippingEdge extends ClippingEdge {
/** @override */
inside(point: Vector2): boolean {
return point.y > 0;
return point.y >= 0;
}

/**
Expand All @@ -90,14 +109,15 @@ class TopClippingEdge extends ClippingEdge {
computeIntersection(a: Vector2, b: Vector2): Vector2 {
const { x: x1, y: y1 } = a;
const { x: x2, y: y2 } = b;
return new Vector2((x1 * y2 - y1 * x2) / -(y1 - y2), 0).round();
const v: Vector2 = new Vector2((x1 * y2 - y1 * x2) / -(y1 - y2), 0).round();
return v;
}
}

class RightClippingEdge extends ClippingEdge {
/** @override */
inside(point: Vector2, extent: number): boolean {
return point.x < extent;
return point.x <= extent;
}

/**
Expand All @@ -113,14 +133,18 @@ class RightClippingEdge extends ClippingEdge {
computeIntersection(a: Vector2, b: Vector2, extent: number): Vector2 {
const { x: x1, y: y1 } = a;
const { x: x2, y: y2 } = b;
return new Vector2(extent, (x1 * y2 - y1 * x2 - (y1 - y2) * -extent) / (x1 - x2)).round();
const v: Vector2 = new Vector2(
extent,
(x1 * y2 - y1 * x2 - (y1 - y2) * -extent) / (x1 - x2)
).round();
return v;
}
}

class BottomClipEdge extends ClippingEdge {
/** @override */
inside(point: Vector2, extent: number): boolean {
return point.y < extent;
return point.y <= extent;
}

/**
Expand All @@ -136,14 +160,18 @@ class BottomClipEdge extends ClippingEdge {
computeIntersection(a: Vector2, b: Vector2, extent: number): Vector2 {
const { x: x1, y: y1 } = a;
const { x: x2, y: y2 } = b;
return new Vector2((x1 * y2 - y1 * x2 - (x1 - x2) * extent) / -(y1 - y2), extent).round();
const v: Vector2 = new Vector2(
(x1 * y2 - y1 * x2 - (x1 - x2) * extent) / -(y1 - y2),
extent
).round();
return v;
}
}

class LeftClippingEdge extends ClippingEdge {
/** @override */
inside(point: Vector2) {
return point.x > 0;
return point.x >= 0;
}

/**
Expand All @@ -159,7 +187,8 @@ class LeftClippingEdge extends ClippingEdge {
computeIntersection(a: Vector2, b: Vector2): Vector2 {
const { x: x1, y: y1 } = a;
const { x: x2, y: y2 } = b;
return new Vector2(0, (x1 * y2 - y1 * x2) / (x1 - x2)).round();
const v: Vector2 = new Vector2(0, (x1 * y2 - y1 * x2) / (x1 - x2)).round();
return v;
}
}

Expand Down Expand Up @@ -193,5 +222,9 @@ export function clipPolygon(polygon: Vector2[], extent: number): Vector2[] {
polygon = clip.clipPolygon(polygon, extent);
}

if (polygon.length < 3) {
return [];
}

return polygon;
}
112 changes: 96 additions & 16 deletions @here/harp-geometry/test/ClipPolygonTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,82 @@ describe("ClipPolygon", () => {
new Vector2(0, 0),
new Vector2(extents, 0),
new Vector2(extents, extents),
new Vector2(0, extents)
new Vector2(0, extents),
new Vector2(0, 0)
];

it("Full quad convering the tile (outer ring)", () => {
const polygon = [...tileBounds];
const clippedPolygon = clipPolygon(polygon, extents);
const expectedPolygon = [
{ x: 0, y: 0 },
{ x: 4096, y: 0 },
{ x: 4096, y: 4096 },
{ x: 0, y: 4096 },
{ x: 0, y: 0 }
];
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(ShapeUtils.isClockWise(clippedPolygon), ShapeUtils.isClockWise(polygon));
assert.strictEqual(ShapeUtils.area(clippedPolygon), ShapeUtils.area(polygon));
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Full quad convering the tile (inter ring)", () => {
const polygon = [...tileBounds].reverse();
const clippedPolygon = clipPolygon(polygon, extents);
const expectedPolygon = [
{ x: 0, y: 0 },
{ x: 0, y: 4096 },
{ x: 4096, y: 4096 },
{ x: 4096, y: 0 },
{ x: 0, y: 0 }
];
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(ShapeUtils.isClockWise(clippedPolygon), ShapeUtils.isClockWise(polygon));
assert.strictEqual(ShapeUtils.area(clippedPolygon), ShapeUtils.area(polygon));
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Full quad with margin (outer ring)", () => {
const polygon = [
new Vector2(-20, -20),
new Vector2(extents + 20, -20),
new Vector2(extents + 20, extents + 20),
new Vector2(-20, extents + 20)
new Vector2(-20, extents + 20),
new Vector2(-20, -20)
];
const expectedPolygon = [
{ x: 0, y: 0, isClipped: true },
{ x: 4096, y: 0, isClipped: true },
{ x: 4096, y: 4096, isClipped: true },
{ x: 0, y: 4096, isClipped: true }
];
const clippedPolygon = clipPolygon(polygon, extents);
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(ShapeUtils.isClockWise(clippedPolygon), ShapeUtils.isClockWise(polygon));
assert.strictEqual(ShapeUtils.area(clippedPolygon), ShapeUtils.area(tileBounds));
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Full quad with margin (inner ring)", () => {
const polygon = [
new Vector2(-20, -20),
new Vector2(extents + 20, -20),
new Vector2(extents + 20, extents + 20),
new Vector2(-20, extents + 20)
new Vector2(-20, extents + 20),
new Vector2(-20, -20)
].reverse();
const expectedPolygon = [
{ x: 0, y: 4096, isClipped: true },
{ x: 4096, y: 4096, isClipped: true },
{ x: 4096, y: 0, isClipped: true },
{ x: 0, y: 0, isClipped: true }
];
const clippedPolygon = clipPolygon(polygon, extents);
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(ShapeUtils.isClockWise(clippedPolygon), ShapeUtils.isClockWise(polygon));
assert.strictEqual(ShapeUtils.area(clippedPolygon), -ShapeUtils.area(tileBounds));
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

// a big triangle covering the entire tile bounds
Expand All @@ -72,6 +105,7 @@ describe("ClipPolygon", () => {
];

const clippedPolygon = clipPolygon(polygon, extents);

assert.notStrictEqual(clippedPolygon, polygon);

assert.strictEqual(clippedPolygon.length, 4);
Expand All @@ -98,46 +132,67 @@ describe("ClipPolygon", () => {
new Vector2(-1000, 0),
new Vector2(0, 0),
new Vector2(0, 1000),
new Vector2(-1000, 1000)
new Vector2(-1000, 1000),
new Vector2(-1000, 0)
];

// the result of clipping the polygon touching the boundary
// of one tile is one line.
const expectedPolygon = [
{ x: 0, y: 0, isClipped: true }, // a vertex introduced during clipping
{ x: 0, y: 0 }, // proper vertex, it was a vertex in the original geometry
{ x: 0, y: 1000 }, // proper vertex, it was a vertex in the original geometry
{ x: 0, y: 1000, isClipped: true } // a vertex introduced during clipping
];

const clippedPolygon = clipPolygon(polygon, extents);
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(clippedPolygon.length, 0);

assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Non overlapping adjacent polygon (inner ring)", () => {
const polygon: Vector2[] = [
new Vector2(-1000, 0),
new Vector2(0, 0),
new Vector2(0, 1000),
new Vector2(-1000, 1000)
new Vector2(-1000, 1000),
new Vector2(-1000, 0)
].reverse();

// the result of clipping the polygon touching the boundary
// of one tile is one line.
const expectedPolygon = [
{ x: 0, y: 1000, isClipped: true }, // a vertex introduced during clipping
{ x: 0, y: 1000 }, // proper vertex, it was a vertex in the original geometry
{ x: 0, y: 0 }, // proper vertex, it was a vertex in the original geometry
{ x: 0, y: 0, isClipped: true } // a vertex introduced during clippings
];

const clippedPolygon = clipPolygon(polygon, extents);
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(clippedPolygon.length, 0);

assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Overlapping adjacent polygon (outer ring)", () => {
const polygon: Vector2[] = [
new Vector2(-1000, 0),
new Vector2(20, 0),
new Vector2(20, 1000),
new Vector2(-1000, 1000)
new Vector2(-1000, 1000),
new Vector2(-1000, 0)
];

const expectedClippedPolygon: Vector2[] = [
new Vector2(0, 0),
new Vector2(20, 0),
new Vector2(20, 1000),
new Vector2(0, 1000)
const expectedPolygon = [
{ x: 0, y: 0, isClipped: true },
{ x: 20, y: 0 },
{ x: 20, y: 1000 },
{ x: 0, y: 1000, isClipped: true }
];

const clippedPolygon = clipPolygon(polygon, extents);
assert.notStrictEqual(clippedPolygon, polygon);
assert.strictEqual(clippedPolygon.length, 4);
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedClippedPolygon));
assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Overlapping adjacent polygon (inner ring)", () => {
Expand Down Expand Up @@ -176,6 +231,29 @@ describe("ClipPolygon", () => {
new Vector2(-33, 4829)
];

const expectedPolygon = [
{ x: 0, y: 0, isClipped: true },
{ x: 419, y: 0, isClipped: true },
{ x: 412, y: 92 },
{ x: 102, y: 0, isClipped: true },
{ x: 30, y: 0, isClipped: true },
{ x: 94, y: 908 },
{ x: 6, y: 894 },
{ x: 8, y: 961 },
{ x: 8, y: 952 },
{ x: 6, y: 1026 },
{ x: 312, y: 1170 },
{ x: 1286, y: 1440 },
{ x: 1220, y: 2124 },
{ x: 1204, y: 2252 },
{ x: 1118, y: 2680 },
{ x: 1092, y: 2866 },
{ x: 4096, y: 3356, isClipped: true },
{ x: 4096, y: 3537, isClipped: true },
{ x: 2718, y: 4096, isClipped: true },
{ x: 0, y: 4096, isClipped: true }
];

assert.isTrue(polygon.some(vert => vert.x < 0));
assert.isTrue(polygon.some(vert => vert.x > extents));
assert.isTrue(polygon.some(vert => vert.y < 0));
Expand Down Expand Up @@ -204,6 +282,8 @@ describe("ClipPolygon", () => {
verticesInsideTile.forEach(v => {
assert.isDefined(clippedPolygon.find(p => p.equals(v)));
});

assert.strictEqual(JSON.stringify(clippedPolygon), JSON.stringify(expectedPolygon));
});

it("Concave polygon resulting into 2 parts after clipping", () => {
Expand Down
Loading

0 comments on commit 0661e04

Please sign in to comment.