148 lines
4.1 KiB
TypeScript
148 lines
4.1 KiB
TypeScript
export interface Point {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export type Polygon = Point[];
|
|
|
|
export const signedDistanceToEdge = (edgeStart: Point, edgeEnd: Point, point: Point): number =>
|
|
(edgeEnd.x - edgeStart.x) * (point.y - edgeStart.y)
|
|
- (edgeEnd.y - edgeStart.y) * (point.x - edgeStart.x);
|
|
|
|
export const iterateEdges = (
|
|
polygon: Polygon,
|
|
callback: (current: Point, next: Point, index: number) => boolean | void,
|
|
): void => {
|
|
if (!Array.isArray(polygon) || polygon.length === 0) {
|
|
return;
|
|
}
|
|
for (let index = 0; index < polygon.length; index += 1) {
|
|
const current = polygon[index];
|
|
const next = polygon[(index + 1) % polygon.length];
|
|
if (callback(current, next, index) === false) {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const forEachVertex = (
|
|
polygon: Polygon,
|
|
callback: (current: Point, previous: Point, index: number) => boolean | void,
|
|
): void => {
|
|
if (!Array.isArray(polygon) || polygon.length === 0) {
|
|
return;
|
|
}
|
|
for (let index = 0; index < polygon.length; index += 1) {
|
|
const current = polygon[index];
|
|
const prev = polygon[(index - 1 + polygon.length) % polygon.length];
|
|
if (callback(current, prev, index) === false) {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const lineIntersection = (p1: Point, p2: Point, cp1: Point, cp2: Point): Point => {
|
|
const A1 = p2.y - p1.y;
|
|
const B1 = p1.x - p2.x;
|
|
const C1 = A1 * p1.x + B1 * p1.y;
|
|
|
|
const A2 = cp2.y - cp1.y;
|
|
const B2 = cp1.x - cp2.x;
|
|
const C2 = A2 * cp1.x + B2 * cp1.y;
|
|
|
|
const det = A1 * B2 - A2 * B1;
|
|
if (Math.abs(det) < 1e-6) {
|
|
return { x: cp1.x, y: cp1.y };
|
|
}
|
|
return {
|
|
x: (B2 * C1 - B1 * C2) / det,
|
|
y: (A1 * C2 - A2 * C1) / det,
|
|
};
|
|
};
|
|
|
|
export const clipPolygon = (subject: Polygon, clipper: Polygon): Polygon => {
|
|
if (!Array.isArray(subject) || !subject.length) {
|
|
return [];
|
|
}
|
|
let output = subject;
|
|
iterateEdges(clipper, (cp1, cp2) => {
|
|
const input = output;
|
|
output = [];
|
|
if (!Array.isArray(input) || !input.length) {
|
|
return false;
|
|
}
|
|
forEachVertex(input, (current, prev) => {
|
|
const currentInside = signedDistanceToEdge(cp1, cp2, current) >= 0;
|
|
const prevInside = signedDistanceToEdge(cp1, cp2, prev) >= 0;
|
|
if (currentInside) {
|
|
if (!prevInside) {
|
|
output.push(lineIntersection(prev, current, cp1, cp2));
|
|
}
|
|
output.push(current);
|
|
} else if (prevInside) {
|
|
output.push(lineIntersection(prev, current, cp1, cp2));
|
|
}
|
|
return true;
|
|
});
|
|
return output.length > 0;
|
|
});
|
|
return output;
|
|
};
|
|
|
|
export const isPointInsideConvex = (point: Point, polygon: Polygon): boolean => {
|
|
if (!polygon?.length) {
|
|
return false;
|
|
}
|
|
let sign = 0;
|
|
let inside = true;
|
|
iterateEdges(polygon, (a, b) => {
|
|
const cross = signedDistanceToEdge(a, b, point);
|
|
if (cross === 0) {
|
|
return true;
|
|
}
|
|
const currentSign = cross > 0 ? 1 : -1;
|
|
if (sign === 0) {
|
|
sign = currentSign;
|
|
return true;
|
|
}
|
|
if (sign !== currentSign) {
|
|
inside = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return inside;
|
|
};
|
|
|
|
export const polygonCentroid = (polygon: Polygon): Point => {
|
|
if (!polygon?.length) {
|
|
return { x: 0, y: 0 };
|
|
}
|
|
let area = 0;
|
|
let cx = 0;
|
|
let cy = 0;
|
|
iterateEdges(polygon, (current, next) => {
|
|
const cross = current.x * next.y - next.x * current.y;
|
|
area += cross;
|
|
cx += (current.x + next.x) * cross;
|
|
cy += (current.y + next.y) * cross;
|
|
});
|
|
if (Math.abs(area) < 1e-6) {
|
|
let sumX = 0;
|
|
let sumY = 0;
|
|
forEachVertex(polygon, (point) => {
|
|
sumX += point.x;
|
|
sumY += point.y;
|
|
});
|
|
return {
|
|
x: sumX / polygon.length,
|
|
y: sumY / polygon.length,
|
|
};
|
|
}
|
|
const areaFactor = 1 / (3 * area);
|
|
return {
|
|
x: cx * areaFactor,
|
|
y: cy * areaFactor,
|
|
};
|
|
};
|