Skip to content

Commit 2c106dd

Browse files
committed
Add boolean path intersection utilities
Introduces `findPathIntersections` and `findCurveIntersections` functions for detecting intersections between paths and curves. Also adds a new test suite for boolean intersections and updates the build and file size metadata.
1 parent 35e96dd commit 2c106dd

File tree

7 files changed

+832
-7
lines changed

7 files changed

+832
-7
lines changed

build/two.js

Lines changed: 330 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ var Two = (() => {
12201220
* @name Two.PublishDate
12211221
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
12221222
*/
1223-
PublishDate: "2025-11-26T06:59:54.712Z",
1223+
PublishDate: "2025-12-01T22:57:49.092Z",
12241224
/**
12251225
* @name Two.Identifier
12261226
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
@@ -12132,6 +12132,332 @@ var Two = (() => {
1213212132
return xhr2;
1213312133
}
1213412134

12135+
// src/utils/boolean-operations.js
12136+
var EPSILON3 = 1e-12;
12137+
function anchorToSegment(anchor2) {
12138+
const segment = {
12139+
point: { x: anchor2.x, y: anchor2.y },
12140+
handleIn: { x: 0, y: 0 },
12141+
handleOut: { x: 0, y: 0 }
12142+
};
12143+
if (anchor2.controls) {
12144+
if (anchor2.relative) {
12145+
segment.handleIn.x = anchor2.controls.left.x;
12146+
segment.handleIn.y = anchor2.controls.left.y;
12147+
segment.handleOut.x = anchor2.controls.right.x;
12148+
segment.handleOut.y = anchor2.controls.right.y;
12149+
} else {
12150+
segment.handleIn.x = anchor2.controls.left.x - anchor2.x;
12151+
segment.handleIn.y = anchor2.controls.left.y - anchor2.y;
12152+
segment.handleOut.x = anchor2.controls.right.x - anchor2.x;
12153+
segment.handleOut.y = anchor2.controls.right.y - anchor2.y;
12154+
}
12155+
}
12156+
return segment;
12157+
}
12158+
function subdivideCurve(v0, v1, v2, v3, t) {
12159+
const u = 1 - t;
12160+
const v01 = { x: u * v0.x + t * v1.x, y: u * v0.y + t * v1.y };
12161+
const v12 = { x: u * v1.x + t * v2.x, y: u * v1.y + t * v2.y };
12162+
const v23 = { x: u * v2.x + t * v3.x, y: u * v2.y + t * v3.y };
12163+
const v012 = { x: u * v01.x + t * v12.x, y: u * v01.y + t * v12.y };
12164+
const v123 = { x: u * v12.x + t * v23.x, y: u * v12.y + t * v23.y };
12165+
const v0123 = { x: u * v012.x + t * v123.x, y: u * v012.y + t * v123.y };
12166+
return {
12167+
left: [v0, v01, v012, v0123],
12168+
right: [v0123, v123, v23, v3]
12169+
};
12170+
}
12171+
function signedDistance(point, lineStart, lineEnd) {
12172+
const dx = lineEnd.x - lineStart.x;
12173+
const dy = lineEnd.y - lineStart.y;
12174+
const lineLengthSq = dx * dx + dy * dy;
12175+
if (lineLengthSq < EPSILON3) {
12176+
const px = point.x - lineStart.x;
12177+
const py = point.y - lineStart.y;
12178+
return Math.sqrt(px * px + py * py);
12179+
}
12180+
const nx = dy;
12181+
const ny = -dx;
12182+
const lineLength = Math.sqrt(lineLengthSq);
12183+
return ((point.x - lineStart.x) * nx + (point.y - lineStart.y) * ny) / lineLength;
12184+
}
12185+
function clipCurve(v1, v2, t1Min, t1Max, t2Min, t2Max, depth, intersections) {
12186+
const MAX_DEPTH = 32;
12187+
const FLATNESS_TOLERANCE = 0.5;
12188+
if (depth > MAX_DEPTH) {
12189+
return intersections;
12190+
}
12191+
const bbox1 = {
12192+
minX: Math.min(v1[0].x, v1[1].x, v1[2].x, v1[3].x),
12193+
maxX: Math.max(v1[0].x, v1[1].x, v1[2].x, v1[3].x),
12194+
minY: Math.min(v1[0].y, v1[1].y, v1[2].y, v1[3].y),
12195+
maxY: Math.max(v1[0].y, v1[1].y, v1[2].y, v1[3].y)
12196+
};
12197+
const bbox2 = {
12198+
minX: Math.min(v2[0].x, v2[1].x, v2[2].x, v2[3].x),
12199+
maxX: Math.max(v2[0].x, v2[1].x, v2[2].x, v2[3].x),
12200+
minY: Math.min(v2[0].y, v2[1].y, v2[2].y, v2[3].y),
12201+
maxY: Math.max(v2[0].y, v2[1].y, v2[2].y, v2[3].y)
12202+
};
12203+
if (bbox1.maxX < bbox2.minX || bbox2.maxX < bbox1.minX || bbox1.maxY < bbox2.minY || bbox2.maxY < bbox1.minY) {
12204+
return intersections;
12205+
}
12206+
const flatness1 = Math.max(
12207+
Math.abs(signedDistance(v1[1], v1[0], v1[3])),
12208+
Math.abs(signedDistance(v1[2], v1[0], v1[3]))
12209+
);
12210+
const flatness2 = Math.max(
12211+
Math.abs(signedDistance(v2[1], v2[0], v2[3])),
12212+
Math.abs(signedDistance(v2[2], v2[0], v2[3]))
12213+
);
12214+
if (flatness1 < FLATNESS_TOLERANCE && flatness2 < FLATNESS_TOLERANCE) {
12215+
const intersection = lineIntersection(v1[0], v1[3], v2[0], v2[3]);
12216+
if (intersection) {
12217+
const t1 = t1Min + intersection.t1 * (t1Max - t1Min);
12218+
const t2 = t2Min + intersection.t2 * (t2Max - t2Min);
12219+
let isDuplicate = false;
12220+
const DUPLICATE_TOLERANCE = 0.01;
12221+
for (let i = 0; i < intersections.length; i++) {
12222+
const dx = intersections[i].point.x - intersection.point.x;
12223+
const dy = intersections[i].point.y - intersection.point.y;
12224+
const distSq = dx * dx + dy * dy;
12225+
if (distSq < DUPLICATE_TOLERANCE * DUPLICATE_TOLERANCE) {
12226+
isDuplicate = true;
12227+
break;
12228+
}
12229+
}
12230+
if (!isDuplicate) {
12231+
intersections.push({
12232+
point: intersection.point,
12233+
t1,
12234+
t2
12235+
});
12236+
}
12237+
}
12238+
return intersections;
12239+
}
12240+
if (flatness1 > flatness2) {
12241+
const split = subdivideCurve(v1[0], v1[1], v1[2], v1[3], 0.5);
12242+
const mid = (t1Min + t1Max) / 2;
12243+
clipCurve(split.left, v2, t1Min, mid, t2Min, t2Max, depth + 1, intersections);
12244+
clipCurve(split.right, v2, mid, t1Max, t2Min, t2Max, depth + 1, intersections);
12245+
} else {
12246+
const split = subdivideCurve(v2[0], v2[1], v2[2], v2[3], 0.5);
12247+
const mid = (t2Min + t2Max) / 2;
12248+
clipCurve(v1, split.left, t1Min, t1Max, t2Min, mid, depth + 1, intersections);
12249+
clipCurve(v1, split.right, t1Min, t1Max, mid, t2Max, depth + 1, intersections);
12250+
}
12251+
return intersections;
12252+
}
12253+
function lineIntersection(p1, p2, p3, p4) {
12254+
const x1 = p1.x, y1 = p1.y;
12255+
const x2 = p2.x, y2 = p2.y;
12256+
const x3 = p3.x, y3 = p3.y;
12257+
const x4 = p4.x, y4 = p4.y;
12258+
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
12259+
if (Math.abs(denom) < EPSILON3) {
12260+
return null;
12261+
}
12262+
const t1 = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
12263+
const t2 = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
12264+
if (t1 >= -EPSILON3 && t1 <= 1 + EPSILON3 && t2 >= -EPSILON3 && t2 <= 1 + EPSILON3) {
12265+
return {
12266+
point: {
12267+
x: x1 + t1 * (x2 - x1),
12268+
y: y1 + t1 * (y2 - y1)
12269+
},
12270+
t1: Math.max(0, Math.min(1, t1)),
12271+
t2: Math.max(0, Math.min(1, t2))
12272+
};
12273+
}
12274+
return null;
12275+
}
12276+
function findCurveIntersections(a1, a2, b1, b2, matrix1, matrix2) {
12277+
const seg1Start = anchorToSegment(a1);
12278+
const seg1End = anchorToSegment(a2);
12279+
const seg2Start = anchorToSegment(b1);
12280+
const seg2End = anchorToSegment(b2);
12281+
let curve1 = [
12282+
seg1Start.point,
12283+
{ x: seg1Start.point.x + seg1Start.handleOut.x, y: seg1Start.point.y + seg1Start.handleOut.y },
12284+
{ x: seg1End.point.x + seg1End.handleIn.x, y: seg1End.point.y + seg1End.handleIn.y },
12285+
seg1End.point
12286+
];
12287+
let curve2 = [
12288+
seg2Start.point,
12289+
{ x: seg2Start.point.x + seg2Start.handleOut.x, y: seg2Start.point.y + seg2Start.handleOut.y },
12290+
{ x: seg2End.point.x + seg2End.handleIn.x, y: seg2End.point.y + seg2End.handleIn.y },
12291+
seg2End.point
12292+
];
12293+
if (matrix1) {
12294+
curve1 = curve1.map((p) => {
12295+
const [x, y] = matrix1.multiply(p.x, p.y);
12296+
return { x, y };
12297+
});
12298+
}
12299+
if (matrix2) {
12300+
curve2 = curve2.map((p) => {
12301+
const [x, y] = matrix2.multiply(p.x, p.y);
12302+
return { x, y };
12303+
});
12304+
}
12305+
const intersections = [];
12306+
clipCurve(curve1, curve2, 0, 1, 0, 1, 0, intersections);
12307+
return intersections;
12308+
}
12309+
function findPathIntersections(path1, path2) {
12310+
const intersections = [];
12311+
if (path1._update) {
12312+
path1._update();
12313+
}
12314+
if (path2._update) {
12315+
path2._update();
12316+
}
12317+
const vertices1 = path1.vertices;
12318+
const vertices2 = path2.vertices;
12319+
if (!vertices1 || !vertices2 || vertices1.length < 2 || vertices2.length < 2) {
12320+
return intersections;
12321+
}
12322+
const matrix1 = path1.worldMatrix || path1._matrix;
12323+
const matrix2 = path2.worldMatrix || path2._matrix;
12324+
for (let i = 0; i < vertices1.length - 1; i++) {
12325+
const a1 = vertices1[i];
12326+
const a2 = vertices1[i + 1];
12327+
if (i > 0 && a2.command === Commands.move) {
12328+
continue;
12329+
}
12330+
for (let j = 0; j < vertices2.length - 1; j++) {
12331+
const b1 = vertices2[j];
12332+
const b2 = vertices2[j + 1];
12333+
if (j > 0 && b2.command === Commands.move) {
12334+
continue;
12335+
}
12336+
const curveIntersections = findCurveIntersections(a1, a2, b1, b2, matrix1, matrix2);
12337+
for (const intersection of curveIntersections) {
12338+
const DUPLICATE_TOLERANCE = 0.1;
12339+
let isDuplicate = false;
12340+
for (let k = 0; k < intersections.length; k++) {
12341+
const dx = intersections[k].point.x - intersection.point.x;
12342+
const dy = intersections[k].point.y - intersection.point.y;
12343+
const distSq = dx * dx + dy * dy;
12344+
if (distSq < DUPLICATE_TOLERANCE * DUPLICATE_TOLERANCE) {
12345+
isDuplicate = true;
12346+
break;
12347+
}
12348+
}
12349+
if (!isDuplicate) {
12350+
intersections.push({
12351+
point: intersection.point,
12352+
t1: intersection.t1,
12353+
t2: intersection.t2,
12354+
index1: i,
12355+
index2: j
12356+
});
12357+
}
12358+
}
12359+
}
12360+
}
12361+
if (path1.closed && vertices1.length > 2) {
12362+
const a1 = vertices1[vertices1.length - 1];
12363+
const a2 = vertices1[0];
12364+
for (let j = 0; j < vertices2.length - 1; j++) {
12365+
const b1 = vertices2[j];
12366+
const b2 = vertices2[j + 1];
12367+
if (j > 0 && b2.command === Commands.move) {
12368+
continue;
12369+
}
12370+
const curveIntersections = findCurveIntersections(a1, a2, b1, b2, matrix1, matrix2);
12371+
for (const intersection of curveIntersections) {
12372+
const DUPLICATE_TOLERANCE = 0.1;
12373+
let isDuplicate = false;
12374+
for (let k = 0; k < intersections.length; k++) {
12375+
const dx = intersections[k].point.x - intersection.point.x;
12376+
const dy = intersections[k].point.y - intersection.point.y;
12377+
const distSq = dx * dx + dy * dy;
12378+
if (distSq < DUPLICATE_TOLERANCE * DUPLICATE_TOLERANCE) {
12379+
isDuplicate = true;
12380+
break;
12381+
}
12382+
}
12383+
if (!isDuplicate) {
12384+
intersections.push({
12385+
point: intersection.point,
12386+
t1: intersection.t1,
12387+
t2: intersection.t2,
12388+
index1: vertices1.length - 1,
12389+
index2: j
12390+
});
12391+
}
12392+
}
12393+
}
12394+
}
12395+
if (path2.closed && vertices2.length > 2) {
12396+
const b1 = vertices2[vertices2.length - 1];
12397+
const b2 = vertices2[0];
12398+
for (let i = 0; i < vertices1.length - 1; i++) {
12399+
const a1 = vertices1[i];
12400+
const a2 = vertices1[i + 1];
12401+
if (i > 0 && a2.command === Commands.move) {
12402+
continue;
12403+
}
12404+
const curveIntersections = findCurveIntersections(a1, a2, b1, b2, matrix1, matrix2);
12405+
for (const intersection of curveIntersections) {
12406+
const DUPLICATE_TOLERANCE = 0.1;
12407+
let isDuplicate = false;
12408+
for (let k = 0; k < intersections.length; k++) {
12409+
const dx = intersections[k].point.x - intersection.point.x;
12410+
const dy = intersections[k].point.y - intersection.point.y;
12411+
const distSq = dx * dx + dy * dy;
12412+
if (distSq < DUPLICATE_TOLERANCE * DUPLICATE_TOLERANCE) {
12413+
isDuplicate = true;
12414+
break;
12415+
}
12416+
}
12417+
if (!isDuplicate) {
12418+
intersections.push({
12419+
point: intersection.point,
12420+
t1: intersection.t1,
12421+
t2: intersection.t2,
12422+
index1: i,
12423+
index2: vertices2.length - 1
12424+
});
12425+
}
12426+
}
12427+
}
12428+
}
12429+
if (path1.closed && vertices1.length > 2 && path2.closed && vertices2.length > 2) {
12430+
const a1 = vertices1[vertices1.length - 1];
12431+
const a2 = vertices1[0];
12432+
const b1 = vertices2[vertices2.length - 1];
12433+
const b2 = vertices2[0];
12434+
const curveIntersections = findCurveIntersections(a1, a2, b1, b2, matrix1, matrix2);
12435+
for (const intersection of curveIntersections) {
12436+
const DUPLICATE_TOLERANCE = 0.1;
12437+
let isDuplicate = false;
12438+
for (let k = 0; k < intersections.length; k++) {
12439+
const dx = intersections[k].point.x - intersection.point.x;
12440+
const dy = intersections[k].point.y - intersection.point.y;
12441+
const distSq = dx * dx + dy * dy;
12442+
if (distSq < DUPLICATE_TOLERANCE * DUPLICATE_TOLERANCE) {
12443+
isDuplicate = true;
12444+
break;
12445+
}
12446+
}
12447+
if (!isDuplicate) {
12448+
intersections.push({
12449+
point: intersection.point,
12450+
t1: intersection.t1,
12451+
t2: intersection.t2,
12452+
index1: vertices1.length - 1,
12453+
index2: vertices2.length - 1
12454+
});
12455+
}
12456+
}
12457+
}
12458+
return intersections;
12459+
}
12460+
1213512461
// src/boolean-group.js
1213612462
var BooleanGroup = class _BooleanGroup extends Group {
1213712463
/**
@@ -15737,7 +16063,9 @@ var Two = (() => {
1573716063
Error: TwoError,
1573816064
getRatio,
1573916065
read,
15740-
xhr
16066+
xhr,
16067+
findPathIntersections,
16068+
findCurveIntersections
1574116069
},
1574216070
_,
1574316071
CanvasPolyfill,

build/two.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)