@countertype/clipper2-ts
Version:
TypeScript port of Clipper2 polygon clipping and offsetting library
641 lines • 24.7 kB
JavaScript
;
/*******************************************************************************
* Author : Angus Johnson *
* Date : 12 October 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : Core structures and functions for the Clipper Library *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvalidRectD = exports.InvalidRect64 = exports.PathsUtils = exports.PathUtils = exports.RectDUtils = exports.Rect64Utils = exports.PointDUtils = exports.Point64Utils = exports.InternalClipper = exports.PointInPolygonResult = exports.FillRule = exports.PathType = exports.ClipType = void 0;
// Note: all clipping operations except for Difference are commutative.
var ClipType;
(function (ClipType) {
ClipType[ClipType["NoClip"] = 0] = "NoClip";
ClipType[ClipType["Intersection"] = 1] = "Intersection";
ClipType[ClipType["Union"] = 2] = "Union";
ClipType[ClipType["Difference"] = 3] = "Difference";
ClipType[ClipType["Xor"] = 4] = "Xor";
})(ClipType || (exports.ClipType = ClipType = {}));
var PathType;
(function (PathType) {
PathType[PathType["Subject"] = 0] = "Subject";
PathType[PathType["Clip"] = 1] = "Clip";
})(PathType || (exports.PathType = PathType = {}));
// By far the most widely used filling rules for polygons are EvenOdd
// and NonZero, sometimes called Alternate and Winding respectively.
// https://en.wikipedia.org/wiki/Nonzero-rule
var FillRule;
(function (FillRule) {
FillRule[FillRule["EvenOdd"] = 0] = "EvenOdd";
FillRule[FillRule["NonZero"] = 1] = "NonZero";
FillRule[FillRule["Positive"] = 2] = "Positive";
FillRule[FillRule["Negative"] = 3] = "Negative";
})(FillRule || (exports.FillRule = FillRule = {}));
// PointInPolygon
var PointInPolygonResult;
(function (PointInPolygonResult) {
PointInPolygonResult[PointInPolygonResult["IsOn"] = 0] = "IsOn";
PointInPolygonResult[PointInPolygonResult["IsInside"] = 1] = "IsInside";
PointInPolygonResult[PointInPolygonResult["IsOutside"] = 2] = "IsOutside";
})(PointInPolygonResult || (exports.PointInPolygonResult = PointInPolygonResult = {}));
var InternalClipper;
(function (InternalClipper) {
InternalClipper.MaxInt64 = 9223372036854775807n;
InternalClipper.MaxCoord = Number(InternalClipper.MaxInt64 / 4n);
InternalClipper.max_coord = InternalClipper.MaxCoord;
InternalClipper.min_coord = -InternalClipper.MaxCoord;
InternalClipper.Invalid64 = Number(InternalClipper.MaxInt64);
InternalClipper.floatingPointTolerance = 1E-12;
InternalClipper.defaultMinimumEdgeLength = 0.1;
function crossProduct(pt1, pt2, pt3) {
// typecast to avoid potential int overflow
return ((pt2.x - pt1.x) * (pt3.y - pt2.y) -
(pt2.y - pt1.y) * (pt3.x - pt2.x));
}
InternalClipper.crossProduct = crossProduct;
function crossProductSign(pt1, pt2, pt3) {
const a = pt2.x - pt1.x;
const b = pt3.y - pt2.y;
const c = pt2.y - pt1.y;
const d = pt3.x - pt2.x;
const ab = multiplyUInt64(Math.abs(a), Math.abs(b));
const cd = multiplyUInt64(Math.abs(c), Math.abs(d));
const signAB = triSign(a) * triSign(b);
const signCD = triSign(c) * triSign(d);
if (signAB === signCD) {
let result;
if (ab.hi64 === cd.hi64) {
if (ab.lo64 === cd.lo64)
return 0;
result = (ab.lo64 > cd.lo64) ? 1 : -1;
}
else {
result = (ab.hi64 > cd.hi64) ? 1 : -1;
}
return (signAB > 0) ? result : -result;
}
return (signAB > signCD) ? 1 : -1;
}
InternalClipper.crossProductSign = crossProductSign;
function checkPrecision(precision) {
if (precision < -8 || precision > 8) {
throw new Error("Error: Precision is out of range.");
}
}
InternalClipper.checkPrecision = checkPrecision;
function isAlmostZero(value) {
return Math.abs(value) <= InternalClipper.floatingPointTolerance;
}
InternalClipper.isAlmostZero = isAlmostZero;
function triSign(x) {
return (x < 0) ? -1 : (x > 0) ? 1 : 0;
}
InternalClipper.triSign = triSign;
function multiplyUInt64(a, b) {
// Convert to BigInt for accurate 64-bit multiplication
const aBig = BigInt(a >>> 0); // Ensure unsigned
const bBig = BigInt(b >>> 0);
const x1 = (aBig & 0xffffffffn) * (bBig & 0xffffffffn);
const x2 = (aBig >> 32n) * (bBig & 0xffffffffn) + (x1 >> 32n);
const x3 = (aBig & 0xffffffffn) * (bBig >> 32n) + (x2 & 0xffffffffn);
const lobits = (x3 & 0xffffffffn) << 32n | (x1 & 0xffffffffn);
const hibits = (aBig >> 32n) * (bBig >> 32n) + (x2 >> 32n) + (x3 >> 32n);
return {
lo64: Number(lobits & 0xffffffffffffffffn),
hi64: Number(hibits & 0xffffffffffffffffn)
};
}
InternalClipper.multiplyUInt64 = multiplyUInt64;
// returns true if (and only if) a * b == c * d
function productsAreEqual(a, b, c, d) {
// nb: unsigned values will be needed for CalcOverflowCarry()
const absA = Math.abs(a);
const absB = Math.abs(b);
const absC = Math.abs(c);
const absD = Math.abs(d);
const mulAb = multiplyUInt64(absA, absB);
const mulCd = multiplyUInt64(absC, absD);
// nb: it's important to differentiate 0 values here from other values
const signAb = triSign(a) * triSign(b);
const signCd = triSign(c) * triSign(d);
return mulAb.lo64 === mulCd.lo64 && mulAb.hi64 === mulCd.hi64 && signAb === signCd;
}
InternalClipper.productsAreEqual = productsAreEqual;
function isCollinear(pt1, sharedPt, pt2) {
const a = sharedPt.x - pt1.x;
const b = pt2.y - sharedPt.y;
const c = sharedPt.y - pt1.y;
const d = pt2.x - sharedPt.x;
// When checking for collinearity with very large coordinate values
// then ProductsAreEqual is more accurate than using CrossProduct.
return productsAreEqual(a, b, c, d);
}
InternalClipper.isCollinear = isCollinear;
function dotProduct(pt1, pt2, pt3) {
// typecast to avoid potential int overflow
return ((pt2.x - pt1.x) * (pt3.x - pt2.x) +
(pt2.y - pt1.y) * (pt3.y - pt2.y));
}
InternalClipper.dotProduct = dotProduct;
function crossProductD(vec1, vec2) {
return (vec1.y * vec2.x - vec2.y * vec1.x);
}
InternalClipper.crossProductD = crossProductD;
function dotProductD(vec1, vec2) {
return (vec1.x * vec2.x + vec1.y * vec2.y);
}
InternalClipper.dotProductD = dotProductD;
// Banker's rounding (round half to even) to match C# MidpointRounding.ToEven
function roundToEven(value) {
// Use the built-in behavior that's closer to C# MidpointRounding.ToEven
// JavaScript's Math.round actually implements "round half away from zero"
// but for most practical cases, the difference is minimal
const floor = Math.floor(value);
const diff = value - floor;
if (Math.abs(diff - 0.5) < 1e-10) {
// Exactly halfway - round to even
return floor % 2 === 0 ? floor : floor + 1;
}
return Math.round(value);
}
InternalClipper.roundToEven = roundToEven;
function checkCastInt64(val) {
if ((val >= InternalClipper.max_coord) || (val <= InternalClipper.min_coord))
return InternalClipper.Invalid64;
return Math.round(val);
}
InternalClipper.checkCastInt64 = checkCastInt64;
// GetLineIntersectPt - a 'true' result is non-parallel. The 'ip' will also
// be constrained to seg1. However, it's possible that 'ip' won't be inside
// seg2, even when 'ip' hasn't been constrained (ie 'ip' is inside seg1).
function getLineIntersectPt(ln1a, ln1b, ln2a, ln2b) {
const dy1 = (ln1b.y - ln1a.y);
const dx1 = (ln1b.x - ln1a.x);
const dy2 = (ln2b.y - ln2a.y);
const dx2 = (ln2b.x - ln2a.x);
const det = dy1 * dx2 - dy2 * dx1;
if (det === 0.0) {
return { intersects: false, point: { x: 0, y: 0 } };
}
const t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det;
let ip;
if (t <= 0.0) {
ip = { x: ln1a.x, y: ln1a.y }; // Create a copy to avoid mutating original
}
else if (t >= 1.0) {
ip = { x: ln1b.x, y: ln1b.y }; // Create a copy to avoid mutating original
}
else {
// avoid using constructor (and rounding too) as they affect performance
// Use Math.trunc to match C# (long) cast behavior which truncates towards zero
const rawX = ln1a.x + t * dx1;
const rawY = ln1a.y + t * dy1;
ip = {
x: Math.trunc(rawX),
y: Math.trunc(rawY)
};
}
return { intersects: true, point: ip };
}
InternalClipper.getLineIntersectPt = getLineIntersectPt;
function getLineIntersectPtD(ln1a, ln1b, ln2a, ln2b) {
const dy1 = ln1b.y - ln1a.y;
const dx1 = ln1b.x - ln1a.x;
const dy2 = ln2b.y - ln2a.y;
const dx2 = ln2b.x - ln2a.x;
const det = dy1 * dx2 - dy2 * dx1;
if (det === 0.0) {
return { success: false, ip: { x: 0, y: 0 } };
}
const t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det;
let ip;
if (t <= 0.0) {
ip = { ...ln1a };
}
else if (t >= 1.0) {
ip = { ...ln1b };
}
else {
ip = {
x: ln1a.x + t * dx1,
y: ln1a.y + t * dy1
};
}
return { success: true, ip };
}
InternalClipper.getLineIntersectPtD = getLineIntersectPtD;
function segsIntersect(seg1a, seg1b, seg2a, seg2b, inclusive = false) {
if (!inclusive) {
// Match C# fast path - use cross product multiplication
// This avoids floating point equality checks (safer than === 0)
return (crossProduct(seg1a, seg2a, seg2b) *
crossProduct(seg1b, seg2a, seg2b) < 0) &&
(crossProduct(seg2a, seg1a, seg1b) *
crossProduct(seg2b, seg1a, seg1b) < 0);
}
// Inclusive case - match C# implementation
const res1 = crossProduct(seg1a, seg2a, seg2b);
const res2 = crossProduct(seg1b, seg2a, seg2b);
if (res1 * res2 > 0)
return false;
const res3 = crossProduct(seg2a, seg1a, seg1b);
const res4 = crossProduct(seg2b, seg1a, seg1b);
if (res3 * res4 > 0)
return false;
// ensure NOT collinear
return (res1 !== 0 || res2 !== 0 || res3 !== 0 || res4 !== 0);
}
InternalClipper.segsIntersect = segsIntersect;
function getBounds(path) {
if (path.length === 0)
return { left: 0, top: 0, right: 0, bottom: 0 };
const result = {
left: Number.MAX_SAFE_INTEGER,
top: Number.MAX_SAFE_INTEGER,
right: Number.MIN_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER
};
for (const pt of path) {
if (pt.x < result.left)
result.left = pt.x;
if (pt.x > result.right)
result.right = pt.x;
if (pt.y < result.top)
result.top = pt.y;
if (pt.y > result.bottom)
result.bottom = pt.y;
}
return result.left === Number.MAX_SAFE_INTEGER ?
{ left: 0, top: 0, right: 0, bottom: 0 } : result;
}
InternalClipper.getBounds = getBounds;
function getClosestPtOnSegment(offPt, seg1, seg2) {
if (seg1.x === seg2.x && seg1.y === seg2.y)
return { x: seg1.x, y: seg1.y }; // Return copy, not reference
const dx = (seg2.x - seg1.x);
const dy = (seg2.y - seg1.y);
const q = ((offPt.x - seg1.x) * dx + (offPt.y - seg1.y) * dy) / ((dx * dx) + (dy * dy));
const qClamped = q < 0 ? 0 : (q > 1 ? 1 : q);
return {
// use Math.round to match the C# MidpointRounding.ToEven behavior
x: Math.round(seg1.x + qClamped * dx),
y: Math.round(seg1.y + qClamped * dy)
};
}
InternalClipper.getClosestPtOnSegment = getClosestPtOnSegment;
function pointInPolygon(pt, polygon) {
const len = polygon.length;
let start = 0;
if (len < 3)
return PointInPolygonResult.IsOutside;
while (start < len && polygon[start].y === pt.y)
start++;
if (start === len)
return PointInPolygonResult.IsOutside;
let isAbove = polygon[start].y < pt.y;
const startingAbove = isAbove;
let val = 0;
let i = start + 1;
let end = len;
while (true) {
if (i === end) {
if (end === 0 || start === 0)
break;
end = start;
i = 0;
}
if (isAbove) {
while (i < end && polygon[i].y < pt.y)
i++;
}
else {
while (i < end && polygon[i].y > pt.y)
i++;
}
if (i === end)
continue;
const curr = polygon[i];
const prev = i > 0 ? polygon[i - 1] : polygon[len - 1];
if (curr.y === pt.y) {
if (curr.x === pt.x || (curr.y === prev.y &&
((pt.x < prev.x) !== (pt.x < curr.x)))) {
return PointInPolygonResult.IsOn;
}
i++;
if (i === start)
break;
continue;
}
if (pt.x < curr.x && pt.x < prev.x) {
// we're only interested in edges crossing on the left
}
else if (pt.x > prev.x && pt.x > curr.x) {
val = 1 - val; // toggle val
}
else {
const cps = crossProductSign(prev, curr, pt);
if (cps === 0)
return PointInPolygonResult.IsOn;
if ((cps < 0) === isAbove)
val = 1 - val;
}
isAbove = !isAbove;
i++;
}
if (isAbove === startingAbove) {
return val === 0 ? PointInPolygonResult.IsOutside : PointInPolygonResult.IsInside;
}
if (i === len)
i = 0;
const cps = i === 0 ?
crossProductSign(polygon[len - 1], polygon[0], pt) :
crossProductSign(polygon[i - 1], polygon[i], pt);
if (cps === 0)
return PointInPolygonResult.IsOn;
if ((cps < 0) === isAbove)
val = 1 - val;
return val === 0 ? PointInPolygonResult.IsOutside : PointInPolygonResult.IsInside;
}
InternalClipper.pointInPolygon = pointInPolygon;
function path2ContainsPath1(path1, path2) {
// we need to make some accommodation for rounding errors
// so we won't jump if the first vertex is found outside
let pip = PointInPolygonResult.IsOn;
for (const pt of path1) {
switch (pointInPolygon(pt, path2)) {
case PointInPolygonResult.IsOutside:
if (pip === PointInPolygonResult.IsOutside)
return false;
pip = PointInPolygonResult.IsOutside;
break;
case PointInPolygonResult.IsInside:
if (pip === PointInPolygonResult.IsInside)
return true;
pip = PointInPolygonResult.IsInside;
break;
default:
break;
}
}
// since path1's location is still equivocal, check its midpoint
const mp = getBounds(path1);
const midPt = {
x: Math.round((mp.left + mp.right) / 2),
y: Math.round((mp.top + mp.bottom) / 2)
};
return pointInPolygon(midPt, path2) !== PointInPolygonResult.IsOutside;
}
InternalClipper.path2ContainsPath1 = path2ContainsPath1;
})(InternalClipper || (exports.InternalClipper = InternalClipper = {}));
// Point64 utility functions
var Point64Utils;
(function (Point64Utils) {
function create(x = 0, y = 0) {
return { x: Math.round(x), y: Math.round(y) };
}
Point64Utils.create = create;
function fromPointD(pt) {
return { x: Math.round(pt.x), y: Math.round(pt.y) };
}
Point64Utils.fromPointD = fromPointD;
function scale(pt, scale) {
return {
x: Math.round(pt.x * scale),
y: Math.round(pt.y * scale)
};
}
Point64Utils.scale = scale;
function equals(a, b) {
return a.x === b.x && a.y === b.y;
}
Point64Utils.equals = equals;
function add(a, b) {
return { x: a.x + b.x, y: a.y + b.y };
}
Point64Utils.add = add;
function subtract(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
Point64Utils.subtract = subtract;
function toString(pt) {
return `${pt.x},${pt.y} `;
}
Point64Utils.toString = toString;
})(Point64Utils || (exports.Point64Utils = Point64Utils = {}));
// PointD utility functions
var PointDUtils;
(function (PointDUtils) {
function create(x = 0, y = 0) {
return { x, y };
}
PointDUtils.create = create;
function fromPoint64(pt) {
return { x: pt.x, y: pt.y };
}
PointDUtils.fromPoint64 = fromPoint64;
function scale(pt, scale) {
return { x: pt.x * scale, y: pt.y * scale };
}
PointDUtils.scale = scale;
function equals(a, b) {
return InternalClipper.isAlmostZero(a.x - b.x) &&
InternalClipper.isAlmostZero(a.y - b.y);
}
PointDUtils.equals = equals;
function negate(pt) {
pt.x = -pt.x;
pt.y = -pt.y;
}
PointDUtils.negate = negate;
function toString(pt, precision = 2) {
return `${pt.x.toFixed(precision)},${pt.y.toFixed(precision)}`;
}
PointDUtils.toString = toString;
})(PointDUtils || (exports.PointDUtils = PointDUtils = {}));
// Rect64 utility functions
var Rect64Utils;
(function (Rect64Utils) {
function create(l = 0, t = 0, r = 0, b = 0) {
return { left: l, top: t, right: r, bottom: b };
}
Rect64Utils.create = create;
function createInvalid() {
return {
left: Number.MAX_SAFE_INTEGER,
top: Number.MAX_SAFE_INTEGER,
right: Number.MIN_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER
};
}
Rect64Utils.createInvalid = createInvalid;
function width(rect) {
return rect.right - rect.left;
}
Rect64Utils.width = width;
function height(rect) {
return rect.bottom - rect.top;
}
Rect64Utils.height = height;
function isEmpty(rect) {
return rect.bottom <= rect.top || rect.right <= rect.left;
}
Rect64Utils.isEmpty = isEmpty;
function isValid(rect) {
return rect.left < Number.MAX_SAFE_INTEGER;
}
Rect64Utils.isValid = isValid;
function midPoint(rect) {
return {
x: Math.round((rect.left + rect.right) / 2),
y: Math.round((rect.top + rect.bottom) / 2)
};
}
Rect64Utils.midPoint = midPoint;
function contains(rect, pt) {
return pt.x > rect.left && pt.x < rect.right &&
pt.y > rect.top && pt.y < rect.bottom;
}
Rect64Utils.contains = contains;
function containsRect(rect, rec) {
return rec.left >= rect.left && rec.right <= rect.right &&
rec.top >= rect.top && rec.bottom <= rect.bottom;
}
Rect64Utils.containsRect = containsRect;
function intersects(rect, rec) {
return (Math.max(rect.left, rec.left) <= Math.min(rect.right, rec.right)) &&
(Math.max(rect.top, rec.top) <= Math.min(rect.bottom, rec.bottom));
}
Rect64Utils.intersects = intersects;
function asPath(rect) {
return [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.right, y: rect.bottom },
{ x: rect.left, y: rect.bottom }
];
}
Rect64Utils.asPath = asPath;
})(Rect64Utils || (exports.Rect64Utils = Rect64Utils = {}));
// RectD utility functions
var RectDUtils;
(function (RectDUtils) {
function create(l = 0, t = 0, r = 0, b = 0) {
return { left: l, top: t, right: r, bottom: b };
}
RectDUtils.create = create;
function createInvalid() {
return {
left: Number.MAX_VALUE,
top: Number.MAX_VALUE,
right: -Number.MAX_VALUE,
bottom: -Number.MAX_VALUE
};
}
RectDUtils.createInvalid = createInvalid;
function width(rect) {
return rect.right - rect.left;
}
RectDUtils.width = width;
function height(rect) {
return rect.bottom - rect.top;
}
RectDUtils.height = height;
function isEmpty(rect) {
return rect.bottom <= rect.top || rect.right <= rect.left;
}
RectDUtils.isEmpty = isEmpty;
function midPoint(rect) {
return {
x: (rect.left + rect.right) / 2,
y: (rect.top + rect.bottom) / 2
};
}
RectDUtils.midPoint = midPoint;
function contains(rect, pt) {
return pt.x > rect.left && pt.x < rect.right &&
pt.y > rect.top && pt.y < rect.bottom;
}
RectDUtils.contains = contains;
function containsRect(rect, rec) {
return rec.left >= rect.left && rec.right <= rect.right &&
rec.top >= rect.top && rec.bottom <= rect.bottom;
}
RectDUtils.containsRect = containsRect;
function intersects(rect, rec) {
return (Math.max(rect.left, rec.left) < Math.min(rect.right, rec.right)) &&
(Math.max(rect.top, rec.top) < Math.min(rect.bottom, rec.bottom));
}
RectDUtils.intersects = intersects;
function asPath(rect) {
return [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.right, y: rect.bottom },
{ x: rect.left, y: rect.bottom }
];
}
RectDUtils.asPath = asPath;
})(RectDUtils || (exports.RectDUtils = RectDUtils = {}));
// Path utility functions
var PathUtils;
(function (PathUtils) {
function toString64(path) {
let result = "";
for (const pt of path) {
result += Point64Utils.toString(pt);
}
return result + '\n';
}
PathUtils.toString64 = toString64;
function toStringD(path, precision = 2) {
let result = "";
for (const pt of path) {
result += PointDUtils.toString(pt, precision) + ", ";
}
if (result !== "")
result = result.slice(0, -2);
return result;
}
PathUtils.toStringD = toStringD;
function reverse64(path) {
return [...path].reverse();
}
PathUtils.reverse64 = reverse64;
function reverseD(path) {
return [...path].reverse();
}
PathUtils.reverseD = reverseD;
})(PathUtils || (exports.PathUtils = PathUtils = {}));
var PathsUtils;
(function (PathsUtils) {
function toString64(paths) {
let result = "";
for (const path of paths) {
result += PathUtils.toString64(path);
}
return result;
}
PathsUtils.toString64 = toString64;
function toStringD(paths, precision = 2) {
let result = "";
for (const path of paths) {
result += PathUtils.toStringD(path, precision) + "\n";
}
return result;
}
PathsUtils.toStringD = toStringD;
function reverse64(paths) {
return paths.map(path => PathUtils.reverse64(path));
}
PathsUtils.reverse64 = reverse64;
function reverseD(paths) {
return paths.map(path => PathUtils.reverseD(path));
}
PathsUtils.reverseD = reverseD;
})(PathsUtils || (exports.PathsUtils = PathsUtils = {}));
// Constants
exports.InvalidRect64 = Rect64Utils.createInvalid();
exports.InvalidRectD = RectDUtils.createInvalid();
//# sourceMappingURL=Core.js.map