Skip to content

Commit

Permalink
Move segmentsIntersect to the math folder (#1399)
Browse files Browse the repository at this point in the history
and add tests. There are some interesting edge cases!

Issue: LEMS-2135

## Test plan:
`yarn test`

Author: benchristel

Reviewers: jeremywiebe, benchristel, SonicScrewdriver

Required Reviewers:

Approved By: jeremywiebe

Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1399
  • Loading branch information
benchristel authored Jul 10, 2024
1 parent 3108f93 commit 147ab04
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-paws-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

Internal: refactor and test `segmentsIntersect` function
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {vec} from "mafs";
import * as React from "react";

import {clockwise} from "../../../../util/geometry";
import {findAngle} from "../../math";
import {findAngle, segmentsIntersect} from "../../math";
import {getIntersectionOfRayWithBox as getRangeIntersectionVertex} from "../utils";

import {MafsCssTransformWrapper} from "./css-transform-wrapper";
Expand Down Expand Up @@ -324,30 +324,14 @@ export const shouldDrawArcOutside = (

polygonLines.forEach(
(line) =>
linesIntersect([vertex, rangeIntersectionPoint], line) &&
segmentsIntersect([vertex, rangeIntersectionPoint], line) &&
lineIntersectionCount++,
);

// If the number of intersections is even, the angle is inside the polygon
return !isEven(lineIntersectionCount);
};

// https://stackoverflow.com/a/24392281/7347484
// The "intersects" function in geometry doesn't seem to work for this use case
const linesIntersect = (
[[a, b], [c, d]]: CollinearTuple,
[[p, q], [r, s]]: CollinearTuple,
) => {
const determinant = (c - a) * (s - q) - (r - p) * (d - b);
if (determinant === 0) {
return false;
} else {
const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
}
};

const isEven = (n: number) => n % 2 === 0;

// This function calculates the bisector point of an angle formed by two points
Expand Down Expand Up @@ -420,10 +404,10 @@ function calculateBisectorPoint(
}

// Scale the bisector direction by the radius
const bisectorPoint = [
const bisectorPoint: vec.Vector2 = [
bisectorDirection[0] * radius,
bisectorDirection[1] * radius,
] satisfies vec.Vector2;
];

// Add the vertex to the bisector point to get the final position
// to ensure that the angle label moves with the interactive element
Expand Down
178 changes: 178 additions & 0 deletions packages/perseus/src/widgets/interactive-graphs/math/geometry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {segmentsIntersect} from "./geometry";

import type {Segment} from "./geometry";

describe("segmentsIntersect", () => {
it("returns false when segments have zero length", () => {
const segment1: Segment = [
[0, 0],
[0, 0],
];
const segment2: Segment = [
[1, 1],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when segments are the same", () => {
// intersecting segments must have a SINGLE intersection point.
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[0, 0],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when an endpoint touches the other segment (lambda = 1)", () => {
// We treat segments as open (exclusive of their endpoints).
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[0, 2],
[2, 0],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when an endpoint touches the other segment (lambda = 0)", () => {
// We treat segments as open (exclusive of their endpoints).
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[-1, 1],
[1, -1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when endpoints touch (gamma = 0)", () => {
// We treat segments as open (exclusive of their endpoints).
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[2, 1],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when endpoints touch (gamma = 1)", () => {
// We treat segments as open (exclusive of their endpoints).
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[1, 1],
[2, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false given two horizontal segments", () => {
const segment1: Segment = [
[0, 0],
[1, 0],
];
const segment2: Segment = [
[0, 1],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false given two vertical segments", () => {
const segment1: Segment = [
[0, 0],
[0, 1],
];
const segment2: Segment = [
[1, 0],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false given two parallel diagonal segments", () => {
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[0, 1],
[1, 2],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns true given intersecting segments", () => {
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[1, 0],
[0, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(true);
});

it("returns false when segments are not parallel but do not intersect (lambda > 1)", () => {
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[9, 0],
[0, 9],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when segments are not parallel but do not intersect (lambda < 0)", () => {
const segment1: Segment = [
[0, 0],
[1, 1],
];
const segment2: Segment = [
[-9, 0],
[0, -9],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when segments are not parallel but do not intersect (gamma > 1)", () => {
const segment1: Segment = [
[-9, 0],
[0, -9],
];
const segment2: Segment = [
[0, 0],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});

it("returns false when segments are not parallel but do not intersect (gamma < 0)", () => {
const segment1: Segment = [
[9, 0],
[0, 9],
];
const segment2: Segment = [
[0, 0],
[1, 1],
];
expect(segmentsIntersect(segment1, segment2)).toBe(false);
});
});
21 changes: 21 additions & 0 deletions packages/perseus/src/widgets/interactive-graphs/math/geometry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {vec} from "mafs";

export type Segment = [vec.Vector2, vec.Vector2];

// Determines whether two line segments have exactly one intersection point.
// The segments are treated as "open" - that is, not inclusive of their
// endpoints.
// Algorithm from https://stackoverflow.com/a/24392281/7347484
export const segmentsIntersect = (
[[a, b], [c, d]]: Segment,
[[p, q], [r, s]]: Segment,
): boolean => {
const determinant = (c - a) * (s - q) - (r - p) * (d - b);
if (determinant === 0) {
return false;
} else {
const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {inset, clampToBox} from "./box";
export {X, Y} from "./coordinates";
export {MIN, MAX, size} from "./interval";
export {lerp} from "./interpolation";
export {segmentsIntersect} from "./geometry";
export {
calculateAngleInDegrees,
convertDegreesToRadians,
Expand Down

0 comments on commit 147ab04

Please sign in to comment.