Last active
February 19, 2017 16:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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(); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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