Files
AFFiNE/libs/components/board-shapes/src/shared/polygon-utils.ts
T

199 lines
6.4 KiB
TypeScript

import { intersectLineLine } from '@tldraw/intersect';
import Vec from '@tldraw/vec';
const PI2 = Math.PI * 2;
type Vert = number[];
type Edge = Vert[];
type Polygon = Vert[];
export class PolygonUtils {
static inward_edge_normal(edge: Edge) {
// Assuming that polygon vertices are in clockwise order
const delta = Vec.sub(edge[1], edge[0]);
const len = Vec.len2(delta);
return [-delta[0] / len, delta[1] / len];
}
static outward_edge_normal(edge: Edge) {
return Vec.neg(PolygonUtils.inward_edge_normal(edge));
}
// If the slope of line v1,v2 greater than the slope of v1,p then p is on the left side of v1,v2 and the return value is > 0.
// If p is colinear with v1,v2 then return 0, otherwise return a value < 0.
static left_side = Vec.isLeft;
static is_reflex_vertex(polygon: Polygon, index: number) {
const len = polygon.length;
// Assuming that polygon vertices are in clockwise order
const v0 = polygon[(index + len - 1) % len];
const v1 = polygon[index];
const v2 = polygon[(index + 1) % len];
if (PolygonUtils.left_side(v0, v2, v1) < 0) return true;
return false;
}
static get_edges(vertices: Vert[]) {
return vertices.map((vert, i) => [
vert,
vertices[(i + 1) % vertices.length],
]);
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, A => "line a", B => "line b"
static edges_intersection([A1, A2]: number[][], [B1, B2]: number[][]) {
const den =
(B2[1] - B1[1]) * (A2[0] - A1[0]) -
(B2[0] - B1[0]) * (A2[1] - A1[1]);
if (den == 0) return null; // lines are parallel or conincident
const ua =
((B2[0] - B1[0]) * (A1[1] - B1[1]) -
(B2[1] - B1[1]) * (A1[0] - B1[0])) /
den;
const ub =
((A2[0] - A1[0]) * (A1[1] - B1[1]) -
(A2[1] - A1[1]) * (A1[0] - B1[0])) /
den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1) return null;
return [A1[0] + ua * (A2[0] - A1[0]), A1[1] + ua * (A2[1] - A1[1])];
}
static append_arc(
polygon: number[][],
center: number[],
radius: number,
startVertex: number[],
endVertex: number[],
isPaddingBoundary = false
) {
const vertices = [...polygon];
let startAngle = Math.atan2(
startVertex[1] - center[1],
startVertex[0] - center[0]
);
let endAngle = Math.atan2(
endVertex[1] - center[1],
endVertex[0] - center[0]
);
if (startAngle < 0) startAngle += PI2;
if (endAngle < 0) endAngle += PI2;
const arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
const angle =
startAngle > endAngle
? startAngle - endAngle
: startAngle + PI2 - endAngle;
const angle5 =
(isPaddingBoundary ? -angle : PI2 - angle) / arcSegmentCount;
vertices.push(startVertex);
for (let i = 1; i < arcSegmentCount; ++i) {
const angle = startAngle + angle5 * i;
vertices.push([
center[0] + Math.cos(angle) * radius,
center[1] + Math.sin(angle) * radius,
]);
}
vertices.push(endVertex);
return vertices;
}
static create_offset_edge(edge: Edge, offset: number[]) {
return edge.map(vert => Vec.add(vert, offset));
}
static get_offset_polygon(polygon: Polygon, offset = 0) {
const edges = PolygonUtils.get_edges(polygon);
const offsetEdges = edges.map(edge =>
PolygonUtils.create_offset_edge(
edge,
Vec.mul(PolygonUtils.outward_edge_normal(edge), offset)
)
);
const vertices = [];
for (let i = 0; i < offsetEdges.length; i++) {
const thisEdge = offsetEdges[i];
const prevEdge =
offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
const vertex = PolygonUtils.edges_intersection(prevEdge, thisEdge);
if (vertex) vertices.push(vertex);
else {
PolygonUtils.append_arc(
vertices,
edges[i][0],
offset,
prevEdge[1],
thisEdge[0],
false
);
}
}
// var marginPolygon = PolygonUtils.createPolygon(vertices)
// marginPolygon.offsetEdges = offsetEdges
return vertices;
}
static create_padding_polygon(polygon: number[][][], shapePadding = 0) {
const offsetEdges = polygon.map(edge =>
PolygonUtils.create_offset_edge(
edge,
PolygonUtils.inward_edge_normal(edge)
)
);
const vertices = [];
for (let i = 0; i < offsetEdges.length; i++) {
const thisEdge = offsetEdges[i];
const prevEdge =
offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
const vertex = PolygonUtils.edges_intersection(prevEdge, thisEdge);
if (vertex) vertices.push(vertex);
else {
PolygonUtils.append_arc(
vertices,
polygon[i][0],
shapePadding,
prevEdge[1],
thisEdge[0],
true
);
}
}
return vertices;
}
}
export function getOffsetPolygon(points: number[][], offset: number) {
if (points.length < 3) throw Error('Polygon must have at least 3 points');
const len = points.length;
return points
.map((point, i) => [point, points[(i + 1) % len]])
.map(([A, B]) => {
const offsetVector = Vec.mul(
Vec.per(Vec.uni(Vec.sub(B, A))),
offset
);
return [Vec.add(A, offsetVector), Vec.add(B, offsetVector)];
})
.map((edge, i, edges) => {
const intersection = intersectLineLine(
edge,
edges[(i + 1) % edges.length]
);
if (intersection === undefined)
throw Error('Expected an intersection');
return intersection;
});
}