fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
333 lines (309 loc) • 9.76 kB
text/typescript
import { Point } from './Point';
import { createVector } from './util/misc/vectors';
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
export type IntersectionType = 'Intersection' | 'Coincident' | 'Parallel';
export class Intersection {
declare points: Point[];
declare status?: IntersectionType;
constructor(status?: IntersectionType) {
this.status = status;
this.points = [];
}
/**
* Used to verify if a point is alredy in the collection
* @param {Point} point
* @returns {boolean}
*/
private includes(point: Point): boolean {
return this.points.some((p) => p.eq(point));
}
/**
* Appends points of intersection
* @param {...Point[]} points
* @return {Intersection} thisArg
* @chainable
*/
private append(...points: Point[]): Intersection {
this.points = this.points.concat(
points.filter((point) => {
return !this.includes(point);
}),
);
return this;
}
/**
* check if point T is on the segment or line defined between A and B
*
* @param {Point} T the point we are checking for
* @param {Point} A one extremity of the segment
* @param {Point} B the other extremity of the segment
* @param [infinite] if true checks if `T` is on the line defined by `A` and `B`
* @returns true if `T` is contained
*/
static isPointContained(T: Point, A: Point, B: Point, infinite = false) {
if (A.eq(B)) {
// Edge case: the segment is a point, we check for coincidence,
// infinite param has no meaning because there are infinite lines to consider
return T.eq(A);
} else if (A.x === B.x) {
// Edge case: horizontal line.
// we first check if T.x has the same value, and then if T.y is contained between A.y and B.y
return (
T.x === A.x &&
(infinite || (T.y >= Math.min(A.y, B.y) && T.y <= Math.max(A.y, B.y)))
);
} else if (A.y === B.y) {
// Edge case: vertical line.
// we first check if T.y has the same value, and then if T.x is contained between A.x and B.x
return (
T.y === A.y &&
(infinite || (T.x >= Math.min(A.x, B.x) && T.x <= Math.max(A.x, B.x)))
);
} else {
// Generic case: sloped line.
// we check that AT has the same slope as AB
// for the segment case we need both the vectors to have the same direction and for AT to be lte AB in size
// for the infinite case we check the absolute value of the slope, since direction is meaningless
const AB = createVector(A, B);
const AT = createVector(A, T);
const s = AT.divide(AB);
return infinite
? Math.abs(s.x) === Math.abs(s.y)
: s.x === s.y && s.x >= 0 && s.x <= 1;
}
}
/**
* Use the ray casting algorithm to determine if {@link Point} is in the polygon defined by [points]{@link Point}
* @see https://en.wikipedia.org/wiki/Point_in_polygon
* @param point
* @param points polygon points
* @returns
*/
static isPointInPolygon(point: Point, points: Point[]) {
const other = new Point(point).setX(
Math.min(point.x - 1, ...points.map((p) => p.x)),
);
let hits = 0;
for (let index = 0; index < points.length; index++) {
const inter = this.intersectSegmentSegment(
// polygon side
points[index],
points[(index + 1) % points.length],
// ray
point,
other,
);
if (inter.includes(point)) {
// point is on the polygon side
return true;
}
hits += Number(inter.status === 'Intersection');
}
return hits % 2 === 1;
}
/**
* Checks if a line intersects another
* @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection line intersection}
* @see {@link https://en.wikipedia.org/wiki/Cramer%27s_rule Cramer's rule}
* @static
* @param {Point} a1
* @param {Point} a2
* @param {Point} b1
* @param {Point} b2
* @param {boolean} [aInfinite=true] check segment intersection by passing `false`
* @param {boolean} [bInfinite=true] check segment intersection by passing `false`
* @return {Intersection}
*/
static intersectLineLine(
a1: Point,
a2: Point,
b1: Point,
b2: Point,
aInfinite = true,
bInfinite = true,
): Intersection {
const a2xa1x = a2.x - a1.x,
a2ya1y = a2.y - a1.y,
b2xb1x = b2.x - b1.x,
b2yb1y = b2.y - b1.y,
a1xb1x = a1.x - b1.x,
a1yb1y = a1.y - b1.y,
uaT = b2xb1x * a1yb1y - b2yb1y * a1xb1x,
ubT = a2xa1x * a1yb1y - a2ya1y * a1xb1x,
uB = b2yb1y * a2xa1x - b2xb1x * a2ya1y;
if (uB !== 0) {
const ua = uaT / uB,
ub = ubT / uB;
if (
(aInfinite || (0 <= ua && ua <= 1)) &&
(bInfinite || (0 <= ub && ub <= 1))
) {
return new Intersection('Intersection').append(
new Point(a1.x + ua * a2xa1x, a1.y + ua * a2ya1y),
);
} else {
return new Intersection();
}
} else {
if (uaT === 0 || ubT === 0) {
const segmentsCoincide =
aInfinite ||
bInfinite ||
Intersection.isPointContained(a1, b1, b2) ||
Intersection.isPointContained(a2, b1, b2) ||
Intersection.isPointContained(b1, a1, a2) ||
Intersection.isPointContained(b2, a1, a2);
return new Intersection(segmentsCoincide ? 'Coincident' : undefined);
} else {
return new Intersection('Parallel');
}
}
}
/**
* Checks if a segment intersects a line
* @see {@link intersectLineLine} for line intersection
* @static
* @param {Point} s1 boundary point of segment
* @param {Point} s2 other boundary point of segment
* @param {Point} l1 point on line
* @param {Point} l2 other point on line
* @return {Intersection}
*/
static intersectSegmentLine(
s1: Point,
s2: Point,
l1: Point,
l2: Point,
): Intersection {
return Intersection.intersectLineLine(s1, s2, l1, l2, false, true);
}
/**
* Checks if a segment intersects another
* @see {@link intersectLineLine} for line intersection
* @static
* @param {Point} a1 boundary point of segment
* @param {Point} a2 other boundary point of segment
* @param {Point} b1 boundary point of segment
* @param {Point} b2 other boundary point of segment
* @return {Intersection}
*/
static intersectSegmentSegment(
a1: Point,
a2: Point,
b1: Point,
b2: Point,
): Intersection {
return Intersection.intersectLineLine(a1, a2, b1, b2, false, false);
}
/**
* Checks if line intersects polygon
*
* @todo account for stroke
*
* @static
* @see {@link intersectSegmentPolygon} for segment intersection
* @param {Point} a1 point on line
* @param {Point} a2 other point on line
* @param {Point[]} points polygon points
* @param {boolean} [infinite=true] check segment intersection by passing `false`
* @return {Intersection}
*/
static intersectLinePolygon(
a1: Point,
a2: Point,
points: Point[],
infinite = true,
): Intersection {
const result = new Intersection();
const length = points.length;
for (let i = 0, b1, b2, inter; i < length; i++) {
b1 = points[i];
b2 = points[(i + 1) % length];
inter = Intersection.intersectLineLine(a1, a2, b1, b2, infinite, false);
if (inter.status === 'Coincident') {
return inter;
}
result.append(...inter.points);
}
if (result.points.length > 0) {
result.status = 'Intersection';
}
return result;
}
/**
* Checks if segment intersects polygon
* @static
* @see {@link intersectLinePolygon} for line intersection
* @param {Point} a1 boundary point of segment
* @param {Point} a2 other boundary point of segment
* @param {Point[]} points polygon points
* @return {Intersection}
*/
static intersectSegmentPolygon(
a1: Point,
a2: Point,
points: Point[],
): Intersection {
return Intersection.intersectLinePolygon(a1, a2, points, false);
}
/**
* Checks if polygon intersects another polygon
*
* @todo account for stroke
*
* @static
* @param {Point[]} points1
* @param {Point[]} points2
* @return {Intersection}
*/
static intersectPolygonPolygon(
points1: Point[],
points2: Point[],
): Intersection {
const result = new Intersection(),
length = points1.length;
const coincidences: Intersection[] = [];
for (let i = 0; i < length; i++) {
const a1 = points1[i],
a2 = points1[(i + 1) % length],
inter = Intersection.intersectSegmentPolygon(a1, a2, points2);
if (inter.status === 'Coincident') {
coincidences.push(inter);
result.append(a1, a2);
} else {
result.append(...inter.points);
}
}
if (coincidences.length > 0 && coincidences.length === points1.length) {
return new Intersection('Coincident');
} else if (result.points.length > 0) {
result.status = 'Intersection';
}
return result;
}
/**
* Checks if polygon intersects rectangle
* @static
* @see {@link intersectPolygonPolygon} for polygon intersection
* @param {Point[]} points polygon points
* @param {Point} r1 top left point of rect
* @param {Point} r2 bottom right point of rect
* @return {Intersection}
*/
static intersectPolygonRectangle(
points: Point[],
r1: Point,
r2: Point,
): Intersection {
const min = r1.min(r2),
max = r1.max(r2),
topRight = new Point(max.x, min.y),
bottomLeft = new Point(min.x, max.y);
return Intersection.intersectPolygonPolygon(points, [
min,
topRight,
max,
bottomLeft,
]);
}
}