Skip to content

Instantly share code, notes, and snippets.

@bruce965
Last active February 19, 2017 16:20
Show Gist options
  • Save bruce965/910c84b0a64dbde1da3b2f7b64d047e9 to your computer and use it in GitHub Desktop.
Save bruce965/910c84b0a64dbde1da3b2f7b64d047e9 to your computer and use it in GitHub Desktop.
Algorithm to check if a point is inside a self-crossing polygon. http://codepen.io/anon/pen/ygdOWp
/// <reference path="https://cdn.rawgit.com/borisyankov/DefinitelyTyped/master/jquery/jquery.d.ts" />
interface Vector2 { x: number; y: number; }
interface Line { a: Vector2; b: Vector2; }
var drawString: (point: Vector2, string: string, color?: string) => void;
var drawPoint: (point: Vector2, color?: string, radius?: number) => void;
var drawLine: (line: Line, color?: string, thickness?: number) => void;
var intersect: (a: Line, b: Line, upInclusive?: boolean, downInclusive?: boolean) => Vector2 | null;
var angleBetweenNormalized: (a: Vector2, b: Vector2, origin?: Vector2) => number;
var dotProduct: (a: Vector2, b: Vector2) => number;
var magnitude: (vector: Vector2) => number;
var normalize: (vector: Vector2) => Vector2;
var vectorFromAngle: (angle: number) => Vector2;
var isSamePoint: (a: Vector2, b: Vector2) => boolean;
function DEBUG_drawAngle(point: Vector2, angle: number, color: string, thickness: number): void {
drawLine({ a: point, b: { x: point.x + vectorFromAngle(angle).x * 25, y: point.y + vectorFromAngle(angle).y * 25 } }, color, thickness);
}
function linkedPoints(lines: Line[], point: Vector2): Vector2[] {
const points: Vector2[] = [];
for (let line of lines) {
if (line.a.x == point.x && line.a.y == point.y)
points.push(line.b);
else if (line.b.x == point.x && line.b.y == point.y)
points.push(line.a);
}
return points;
}
function uppermost(points: Vector2[]): Vector2 {
var uppermost = points[0];
for (var i = 1; i < points.length; i++)
if (points[i].y < uppermost.y)
uppermost = points[i];
return uppermost;
}
function isInside(contour: Line[], point: Vector2): boolean {
var testLine: Line = { a: { x: 0, y: point.y }, b: point };
var collisions = 0;
for (let line of contour)
if (intersect(line, testLine, true))
collisions++;
return !!(collisions & 1);
}
function compute(points: Vector2[]): void {
var lines: Line[] = []; // Will split lines at intersection points.
var allPoints: Vector2[] = []; // Will include intesections.
// Find all lines between points.
for (let i = 0; i < points.length; i++) {
drawPoint(points[i], 'white', 8);
allPoints.push(points[i]);
if (points[i + 1])
lines.push({ a: points[i], b: points[i + 1] });
else
lines.push({ a: points[i], b: points[0] });
//drawLine(lines[lines.length - 1], 'yellow', 5);
}
// Split lines at intersection points.
let generatedSomething: boolean;
//var debug_i = 0;
do {
generatedSomething = false;
for (let i = 0; i < lines.length; i++) {
let line1 = lines[i];
for (let j = 0; j < lines.length; j++) {
//if (debug_i++ == 100000) throw new Error("Frozen 1"); // DEBUG: stop after Nth iteration.
let line2 = lines[j];
if (line1 == line2)
break;
let intersection = intersect(line1, line2);
if (intersection) {
let line1a = { a: line1.a, b: intersection };
let line1b = { a: intersection, b: line1.b };
let line2a = { a: line2.a, b: intersection };
let line2b = { a: intersection, b: line2.b };
lines.splice(i, 1, line1a, line1b); // NOTE: `i` is `lines.indexOf(line1)`.
lines.splice(lines.indexOf(line2), 1, line2a, line2b);
//drawPoint(intersection, 'yellow', 6);
allPoints.push(intersection);
generatedSomething = true;
i = Infinity; break; // break outer;
}
}
}
} while(generatedSomething);
if (points.length < 3) {
// If there is no polygon, no point in inside.
for (let y = 5; y < 600; y += 5)
for (let x = 5; x < 800; x += 5)
drawPoint({ x, y }, 'red', 1);
return;
}
// DEBUG: Ensure lines are splitted at the right points.
for (let line of lines)
drawLine(line, 'red', 2);
// Trace contour.
var point = uppermost(points);
const contour: Vector2[] = [point];
var angle = 0;
//debug_i = 0;
do {
//if (debug_i++ == 10000) throw new Error("Frozen 2"); // DEBUG: stop after Nth iteration.
//DEBUG_drawAngle(point, angle, 'red', 5);
//drawString(point, (debug_i).toString());
let smallestAngle = Infinity;
let bestCandidate: Vector2;
for (let point2 of linkedPoints(lines, point)) {
let angle2 = angleBetweenNormalized(vectorFromAngle(angle), normalize({ x: point2.x - point.x, y: point2.y - point.y }));
if (angle2 < 0.001)
continue; // No point in going back.
//DEBUG_drawAngle(point, angle2 + angle, 'cyan', 2);
if (angle2 < smallestAngle) {
smallestAngle = angle2;
bestCandidate = point2;
}
}
angle = angle + smallestAngle;
//DEBUG_drawAngle(point, angle, 'blue', 1);
angle = (angle + Math.PI)%(Math.PI * 2); // Flip angle.
point = bestCandidate;
contour.push(point);
} while(contour[0] != point); // Keep tracing contour until we link back to the starting point.
// Ensure contour is correct.
const contourLines: Line[] = [];
for (let i = 0; i < contour.length; i++) {
drawPoint(contour[i], 'blue', i == 0 ? 5 : 3);
if (contour[i + 1])
contourLines.push({ a: contour[i], b: contour[i + 1] });
else
contourLines.push({ a: contour[i], b: contour[0] });
//drawLine(contourLines[contourLines.length - 1], 'blue', 2);
}
for (let y = 5; y < 600; y += 5)
for (let x = 5; x < 800; x += 5)
drawPoint({ x, y }, isInside(contourLines, { x, y }) ? 'lime' : 'red', 1);
}
// Graphics stuff.
$(document).ready(() => {
const EPSILON = 0.000001;
const canvas = $('#theCanvas');
const resetButton = $('#resetButton');
const context = (canvas[0] as HTMLCanvasElement).getContext('2d');
var points: Vector2[] = [
{ x: 300, y: 90 },
{ x: 360, y: 360 },
{ x: 90, y: 150 },
{ x: 450, y: 210 },
{ x: 150, y: 300 }
];
function recompute(): void {
try {
context.clearRect(0, 0, canvas.width(), canvas.height());
compute(points);
} catch(e) {
drawString({ x: 10, y: 30 }, e.toString(), 'red');
throw e;
}
}
canvas.mouseup(e => {
points.push({ x: e.offsetX, y: e.offsetY });
recompute();
});
resetButton.click(() => {
points = [];
recompute();
});
drawString = (point, string, color = 'white') => {
context.fillStyle = color;
context.lineWidth = 2;
context.strokeStyle = 'black';
context.font = "12px monospace";
context.strokeText(string, point.x + 10, point.y - 3);
context.fillText(string, point.x + 10, point.y - 3);
};
drawPoint = (point, color = 'yellow', radius = 10) => {
context.beginPath();
context.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
};
drawLine = (line, color = 'yellow', thickness = 5) => {
const offset = normalize({ x: line.b.x - line.a.x, y: line.b.y - line.a.y });
context.beginPath();
context.beginPath();
context.moveTo(line.a.x + offset.x * thickness * 2, line.a.y + offset.y * thickness * 2);
context.lineTo(line.b.x - offset.x * thickness * 2, line.b.y - offset.y * thickness * 2);
context.lineWidth = thickness;
context.strokeStyle = color;
context.stroke();
};
// http://stackoverflow.com/a/1968345/1135019
intersect = (a, b, upInclusive = false, downInclusive = false) => {
var s1_x = a.b.x - a.a.x;
var s1_y = a.b.y - a.a.y;
var s2_x = b.b.x - b.a.x;
var s2_y = b.b.y - b.a.y;
var s = (-s1_y * (a.a.x - b.a.x) + s1_x * (a.a.y - b.a.y)) / (-s2_x * s1_y + s1_x * s2_y);
var t = (s2_x * (a.a.y - b.a.y) - s2_y * (a.a.x - b.a.x)) / (-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
let point = { x: a.a.x + (t * s1_x), y: a.a.y + (t * s1_y) };
if (!upInclusive && isSamePoint(point, a.a.y < a.b.y ? a.a : a.b))
return null;
if (!downInclusive && isSamePoint(point, a.a.y >= a.b.y ? a.a : a.b))
return null;
return point;
}
return null;
};
angleBetweenNormalized = (a, b, origin = { x: 0, y: 0 }) => {
//return Math.acos(dotProduct(a, b));
var ang = Math.atan2(b.y, b.x) - Math.atan2(a.y, a.x);
return (ang < 0 ? ang + (2 * Math.PI) : ang)%(2 * Math.PI);
};
dotProduct = (a, b) => {
return a.x * b.x + a.y * b.y;
};
function squareMagnitude(v: Vector2) {
return v.x * v.x + v.y * v.y;
}
magnitude = v => {
return Math.sqrt(squareMagnitude(v));
};
normalize = v => {
const im = 1 / magnitude(v);
return { x: v.x * im, y: v.y * im };
};
isSamePoint = (a, b) => {
return squareMagnitude({ x: a.x - b.x, y: a.y - b.y }) < EPSILON;
};
// 0 = up, 3.14 = down
vectorFromAngle = angle => {
return { x: Math.sin(angle), y: -Math.cos(angle) };
};
recompute();
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Point inside intersecting polygon</title>
<!--%CSS%-->
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<!--%Javascript%-->
</head>
<body>
<canvas id="theCanvas" width="800" height="600"></canvas>
<p>Click to add points.</p>
<button id="resetButton">Reset</button>
</body>
</html>
body {
font-family: sans-serif;
}
#theCanvas {
background: black;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment