Fix issues with clip paths for frames (#2406)

Fixes an issue with frame clipping paths. In fact, this should also
solve other issues we might have with intersect. Seems like our
`pointInPolygon` did not correctly detect that points in the corners or
on the edges of the polygon are in fact part of the polygon.

When calculating the intersection of two regular, intersecting
rectangles the`intersectPolygonPolygon` was returning a polygon with 2,
3, or sometimes even 0 points, which also could result in an error when
dragging one frame out of another frame.

It seems that for all intents and purposes the `pointInPolygon` function
should also consider corners and edges, but maybe we might want to
rename it?

Before:


https://github.com/tldraw/tldraw/assets/2523721/155d351d-8ceb-47c3-a263-024cab487d03

After:


https://github.com/tldraw/tldraw/assets/2523721/338b923a-f902-4dc4-a1b7-e954f906fb8d




Fixes https://github.com/tldraw/tldraw/issues/2387

### Change Type

- [x] `patch` — Bug fix
- [ ] `minor` — New feature
- [ ] `major` — Breaking change
- [ ] `dependencies` — Changes to package dependencies[^1]
- [ ] `documentation` — Changes to the documentation only[^2]
- [ ] `tests` — Changes to any test code only[^2]
- [ ] `internal` — Any other changes that don't affect the published
package[^2]
- [ ] I don't know

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. Add a step-by-step description of how to test your PR here.
2.

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Add a brief release note for your PR here.
This commit is contained in:
Mitja Bezenšek 2024-01-05 08:55:38 +01:00 committed by GitHub
parent d66b4af69d
commit 466a9a2088
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 31 additions and 12 deletions

View file

@ -4066,7 +4066,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (pageMask.length === 0) return undefined if (pageMask.length === 0) return undefined
const { corners } = pageBounds const { corners } = pageBounds
if (corners.every((p, i) => Vec.Equals(p, pageMask[i]))) return pageBounds.clone() if (corners.every((p, i) => p && Vec.Equals(p, pageMask[i]))) return pageBounds.clone()
// todo: find out why intersect polygon polygon for identical polygons produces zero w/h intersections // todo: find out why intersect polygon polygon for identical polygons produces zero w/h intersections
const intersection = intersectPolygonPolygon(pageMask, corners) const intersection = intersectPolygonPolygon(pageMask, corners)

View file

@ -243,26 +243,32 @@ export function intersectPolygonPolygon(
polygonA: VecLike[], polygonA: VecLike[],
polygonB: VecLike[] polygonB: VecLike[]
): VecLike[] | null { ): VecLike[] | null {
// Create an empty polygon as P // Create an empty polygon as result
const result: VecLike[] = [] const result: Map<string, VecLike> = new Map()
let a: VecLike, b: VecLike, c: VecLike, d: VecLike let a: VecLike, b: VecLike, c: VecLike, d: VecLike
// Add all corners of PolygonA that is inside PolygonB to P // Add all corners of PolygonA that is inside PolygonB to result
for (let i = 0, n = polygonA.length; i < n; i++) { for (let i = 0, n = polygonA.length; i < n; i++) {
a = polygonA[i] a = polygonA[i]
if (pointInPolygon(a, polygonB)) { if (pointInPolygon(a, polygonB)) {
result.push(a) const id = getPointId(a)
if (!result.has(id)) {
result.set(id, a)
}
} }
} }
// Add all corners of PolygonB that is inside PolygonA to P // Add all corners of PolygonB that is inside PolygonA to result
for (let i = 0, n = polygonB.length; i < n; i++) { for (let i = 0, n = polygonB.length; i < n; i++) {
a = polygonB[i] a = polygonB[i]
if (pointInPolygon(a, polygonA)) { if (pointInPolygon(a, polygonA)) {
result.push(a) const id = getPointId(a)
if (!result.has(id)) {
result.set(id, a)
}
} }
} }
// Add all intersection points to P // Add all intersection points to result
for (let i = 0, n = polygonA.length; i < n; i++) { for (let i = 0, n = polygonA.length; i < n; i++) {
a = polygonA[i] a = polygonA[i]
b = polygonA[(i + 1) % polygonA.length] b = polygonA[(i + 1) % polygonA.length]
@ -273,15 +279,22 @@ export function intersectPolygonPolygon(
const intersection = intersectLineSegmentLineSegment(a, b, c, d) const intersection = intersectLineSegmentLineSegment(a, b, c, d)
if (intersection !== null) { if (intersection !== null) {
result.push(intersection) const id = getPointId(intersection)
if (!result.has(id)) {
result.set(id, intersection)
}
} }
} }
} }
if (result.length === 0) return null // no intersection if (result.size === 0) return null // no intersection
// Order all points in the P counter-clockwise. // Order all points in the result counter-clockwise.
return orderClockwise(result) return orderClockwise([...result.values()])
}
function getPointId(point: VecLike) {
return `${point.x},${point.y}`
} }
function orderClockwise(points: VecLike[]): VecLike[] { function orderClockwise(points: VecLike[]): VecLike[] {

View file

@ -313,8 +313,14 @@ export function pointInPolygon(A: VecLike, points: VecLike[]): boolean {
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
a = points[i] a = points[i]
// Point is the same as one of the corners of the polygon
if (a.x === A.x && a.y === A.y) return true
b = points[(i + 1) % points.length] b = points[(i + 1) % points.length]
// Point is on the polygon edge
if (Vec.Dist(A, a) + Vec.Dist(A, b) === Vec.Dist(a, b)) return true
if (a.y <= A.y) { if (a.y <= A.y) {
if (b.y > A.y && cross(a, b, A) > 0) { if (b.y > A.y && cross(a, b, A) > 0) {
windingNumber += 1 windingNumber += 1