Files
papercrate/frontend/src/desktop/utils/geometry.ts
T

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,
};
};