199 lines
6.4 KiB
TypeScript
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;
|
|
});
|
|
}
|