diff --git a/lib/test/hamcrest-core-1.3.jar b/lib/test/hamcrest-core-1.3.jar
new file mode 100644
index 0000000..9d5fe16
Binary files /dev/null and b/lib/test/hamcrest-core-1.3.jar differ
diff --git a/lib/test/junit-4.13.2.jar b/lib/test/junit-4.13.2.jar
new file mode 100644
index 0000000..6da55d8
Binary files /dev/null and b/lib/test/junit-4.13.2.jar differ
diff --git a/release/build-common.xml b/release/build-common.xml
index 48ad54c..294f48f 100644
--- a/release/build-common.xml
+++ b/release/build-common.xml
@@ -36,6 +36,9 @@
+
+
+
@@ -81,6 +84,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= rrx1 || y >= rry1) {
-// return false;
-// }
-// double aw = Math.min(getWidth(), Math.abs(getArcWidth())) / 2.0;
-// double ah = Math.min(getHeight(), Math.abs(getArcHeight())) / 2.0;
-// // Check which corner point is in and do circular containment
-// // test - otherwise simple acceptance
-// if (x >= (rrx0 += aw) && x < (rrx0 = rrx1 - aw)) {
-// return true;
-// }
-// if (y >= (rry0 += ah) && y < (rry0 = rry1 - ah)) {
-// return true;
-// }
-// x = (x - rrx0) / aw;
-// y = (y - rry0) / ah;
-// return (x * x + y * y <= 1.0);
-
- // TBD
- return false;
+ if (isEmpty()) {
+ return false;
+ }
+
+ double x0 = this.x;
+ double y0 = this.y;
+ double x1 = x0 + width;
+ double y1 = y0 + height;
+
+ // Check for trivial rejection - point is outside bounding rectangle
+ if (x < x0 || y < y0 || x >= x1 || y >= y1) {
+ return false;
+ }
+
+ // Check each corner. If the point is in a corner's arc zone, test against that corner's ellipse.
+ // Arc widths and heights are clamped so they don't exceed the rectangle dimensions.
+
+ // Top-left corner
+ double aw = Math.min(width, Math.abs(tlaw)) / 2.0;
+ double ah = Math.min(height, Math.abs(tlah)) / 2.0;
+ if (aw > 0 && ah > 0 && x < x0 + aw && y < y0 + ah) {
+ double nx = (x - (x0 + aw)) / aw;
+ double ny = (y - (y0 + ah)) / ah;
+ return nx * nx + ny * ny <= 1.0;
+ }
+
+ // Top-right corner
+ aw = Math.min(width, Math.abs(traw)) / 2.0;
+ ah = Math.min(height, Math.abs(trah)) / 2.0;
+ if (aw > 0 && ah > 0 && x >= x1 - aw && y < y0 + ah) {
+ double nx = (x - (x1 - aw)) / aw;
+ double ny = (y - (y0 + ah)) / ah;
+ return nx * nx + ny * ny <= 1.0;
+ }
+
+ // Bottom-right corner
+ aw = Math.min(width, Math.abs(braw)) / 2.0;
+ ah = Math.min(height, Math.abs(brah)) / 2.0;
+ if (aw > 0 && ah > 0 && x >= x1 - aw && y >= y1 - ah) {
+ double nx = (x - (x1 - aw)) / aw;
+ double ny = (y - (y1 - ah)) / ah;
+ return nx * nx + ny * ny <= 1.0;
+ }
+
+ // Bottom-left corner
+ aw = Math.min(width, Math.abs(blaw)) / 2.0;
+ ah = Math.min(height, Math.abs(blah)) / 2.0;
+ if (aw > 0 && ah > 0 && x < x0 + aw && y >= y1 - ah) {
+ double nx = (x - (x0 + aw)) / aw;
+ double ny = (y - (y1 - ah)) / ah;
+ return nx * nx + ny * ny <= 1.0;
+ }
+
+ // Not in any corner zone — must be in the body of the rectangle
+ return true;
}
-// private int classify(double coord, double left, double right,
-// double arcsize)
-// {
-// if (coord < left) {
-// return 0;
-// } else if (coord < left + arcsize) {
-// return 1;
-// } else if (coord < right - arcsize) {
-// return 2;
-// } else if (coord < right) {
-// return 3;
-// } else {
-// return 4;
-// }
-// }
+ /**
+ Test whether a corner's quarter-ellipse intersects with a rectangle. The corner point is at (cx, cy), the arc
+ zone extends inward by aw horizontally and ah vertically, and the rectangle to test is given by its edges.
+
+ @return true if the quarter-ellipse region intersects the given rectangle.
+ */
+ private static boolean cornerIntersects(double cx, double cy, double aw, double ah,
+ double rx0, double ry0, double rx1, double ry1,
+ double signX, double signY)
+ {
+ // Find the point in the rectangle nearest to the ellipse center
+ double ecx = cx + signX * aw;
+ double ecy = cy + signY * ah;
+ double nearestX = Math.max(rx0, Math.min(rx1, ecx));
+ double nearestY = Math.max(ry0, Math.min(ry1, ecy));
+ double nx = (nearestX - ecx) / aw;
+ double ny = (nearestY - ecy) / ah;
+ return nx * nx + ny * ny <= 1.0;
+ }
public boolean intersects(double x, double y, double w, double h) {
-// if (isEmpty() || w <= 0 || h <= 0) {
-// return false;
-// }
-// double rrx0 = getX();
-// double rry0 = getY();
-// double rrx1 = rrx0 + getWidth();
-// double rry1 = rry0 + getHeight();
-// // Check for trivial rejection - bounding rectangles do not intersect
-// if (x + w <= rrx0 || x >= rrx1 || y + h <= rry0 || y >= rry1) {
-// return false;
-// }
-// double aw = Math.min(getWidth(), Math.abs(getArcWidth())) / 2.0;
-// double ah = Math.min(getHeight(), Math.abs(getArcHeight())) / 2.0;
-// int x0class = classify(x, rrx0, rrx1, aw);
-// int x1class = classify(x + w, rrx0, rrx1, aw);
-// int y0class = classify(y, rry0, rry1, ah);
-// int y1class = classify(y + h, rry0, rry1, ah);
-// // Trivially accept if any point is inside inner rectangle
-// if (x0class == 2 || x1class == 2 || y0class == 2 || y1class == 2) {
-// return true;
-// }
-// // Trivially accept if either edge spans inner rectangle
-// if ((x0class < 2 && x1class > 2) || (y0class < 2 && y1class > 2)) {
-// return true;
-// }
-// // Since neither edge spans the center, then one of the corners
-// // must be in one of the rounded edges. We detect this case if
-// // a [xy]0class is 3 or a [xy]1class is 1. One of those two cases
-// // must be true for each direction.
-// // We now find a "nearest point" to test for being inside a rounded
-// // corner.
-// x = (x1class == 1) ? (x = x + w - (rrx0 + aw)) : (x = x - (rrx1 - aw));
-// y = (y1class == 1) ? (y = y + h - (rry0 + ah)) : (y = y - (rry1 - ah));
-// x = x / aw;
-// y = y / ah;
-// return (x * x + y * y <= 1.0);
-
- // TBD
- return false;
+ if (isEmpty() || w <= 0 || h <= 0) {
+ return false;
+ }
+
+ double x0 = this.x;
+ double y0 = this.y;
+ double x1 = x0 + width;
+ double y1 = y0 + height;
+
+ // Check for trivial rejection - bounding rectangles do not intersect
+ if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) {
+ return false;
+ }
+
+ // Clamp arc radii
+ double tlawH = Math.min(width, Math.abs(tlaw)) / 2.0;
+ double tlahH = Math.min(height, Math.abs(tlah)) / 2.0;
+ double trawH = Math.min(width, Math.abs(traw)) / 2.0;
+ double trahH = Math.min(height, Math.abs(trah)) / 2.0;
+ double brawH = Math.min(width, Math.abs(braw)) / 2.0;
+ double brahH = Math.min(height, Math.abs(brah)) / 2.0;
+ double blawH = Math.min(width, Math.abs(blaw)) / 2.0;
+ double blahH = Math.min(height, Math.abs(blah)) / 2.0;
+
+ // Fast path: if the query rect extends into the horizontal or vertical middle band,
+ // it definitely intersects. The middle band is the region clear of all corner arcs.
+ double leftMax = Math.max(tlawH, blawH);
+ double rightMax = Math.max(trawH, brawH);
+ double topMax = Math.max(tlahH, trahH);
+ double bottomMax = Math.max(blahH, brahH);
+
+ if (x + w > x0 + leftMax && x < x1 - rightMax) {
+ return true;
+ }
+ if (y + h > y0 + topMax && y < y1 - bottomMax) {
+ return true;
+ }
+
+ // Clip query rect to the bounding rect for corner zone checks.
+ double cx0 = Math.max(x, x0);
+ double cy0 = Math.max(y, y0);
+ double cx1 = Math.min(x + w, x1);
+ double cy1 = Math.min(y + h, y1);
+
+ // Check each corner. If the clipped rect overlaps a corner zone with a non-zero arc,
+ // test whether the rect reaches the elliptical arc. Sharp corners (arc=0) have a
+ // zero-size zone, so they are never entered — this is correct because a sharp corner
+ // cuts no area from the bounding rect.
+ boolean anyMiss = false;
+
+ if (cx0 < x0 + tlawH && cy0 < y0 + tlahH && tlawH > 0 && tlahH > 0) {
+ if (cornerIntersects(x0, y0, tlawH, tlahH, cx0, cy0, cx1, cy1, 1, 1)) {
+ return true;
+ }
+ anyMiss = true;
+ }
+
+ if (cx1 > x1 - trawH && cy0 < y0 + trahH && trawH > 0 && trahH > 0) {
+ if (cornerIntersects(x1, y0, trawH, trahH, cx0, cy0, cx1, cy1, -1, 1)) {
+ return true;
+ }
+ anyMiss = true;
+ }
+
+ if (cx1 > x1 - brawH && cy1 > y1 - brahH && brawH > 0 && brahH > 0) {
+ if (cornerIntersects(x1, y1, brawH, brahH, cx0, cy0, cx1, cy1, -1, -1)) {
+ return true;
+ }
+ anyMiss = true;
+ }
+
+ if (cx0 < x0 + blawH && cy1 > y1 - blahH && blawH > 0 && blahH > 0) {
+ if (cornerIntersects(x0, y1, blawH, blahH, cx0, cy0, cx1, cy1, 1, -1)) {
+ return true;
+ }
+ anyMiss = true;
+ }
+
+ // If no corner zones were overlapped, the clipped rect is entirely in the body.
+ if (!anyMiss) {
+ return true;
+ }
+
+ // The clipped rect missed one or more corner ellipses. It may still intersect if
+ // part of the rect extends beyond those corner zones into the body. Check whether
+ // the center of the clipped rect is inside the shape.
+ double midX = (cx0 + cx1) / 2;
+ double midY = (cy0 + cy1) / 2;
+
+ if (tlawH > 0 && tlahH > 0 && midX < x0 + tlawH && midY < y0 + tlahH) {
+ double nx = (midX - (x0 + tlawH)) / tlawH;
+ double ny = (midY - (y0 + tlahH)) / tlahH;
+ return nx * nx + ny * ny <= 1.0;
+ }
+ if (trawH > 0 && trahH > 0 && midX > x1 - trawH && midY < y0 + trahH) {
+ double nx = (midX - (x1 - trawH)) / trawH;
+ double ny = (midY - (y0 + trahH)) / trahH;
+ return nx * nx + ny * ny <= 1.0;
+ }
+ if (brawH > 0 && brahH > 0 && midX > x1 - brawH && midY > y1 - brahH) {
+ double nx = (midX - (x1 - brawH)) / brawH;
+ double ny = (midY - (y1 - brahH)) / brahH;
+ return nx * nx + ny * ny <= 1.0;
+ }
+ if (blawH > 0 && blahH > 0 && midX < x0 + blawH && midY > y1 - blahH) {
+ double nx = (midX - (x0 + blawH)) / blawH;
+ double ny = (midY - (y1 - blahH)) / blahH;
+ return nx * nx + ny * ny <= 1.0;
+ }
+
+ // Center is not in any corner zone — it's in the body.
+ return true;
}
public boolean contains(double x, double y, double w, double h) {
diff --git a/test/org/violetlib/geom/GeneralRoundRectangleTest.java b/test/org/violetlib/geom/GeneralRoundRectangleTest.java
new file mode 100644
index 0000000..49495e5
--- /dev/null
+++ b/test/org/violetlib/geom/GeneralRoundRectangleTest.java
@@ -0,0 +1,324 @@
+package org.violetlib.geom;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class GeneralRoundRectangleTest
+{
+ // A 100x100 rectangle at (0,0) with uniform arc width/height of 20
+ private static GeneralRoundRectangle uniform()
+ {
+ return new GeneralRoundRectangle(0, 0, 100, 100,
+ 20, 20, 20, 20, 20, 20, 20, 20);
+ }
+
+ // A 100x100 rectangle at (0,0) with no rounding
+ private static GeneralRoundRectangle sharp()
+ {
+ return new GeneralRoundRectangle(0, 0, 100, 100,
+ 0, 0, 0, 0, 0, 0, 0, 0);
+ }
+
+ // A 100x80 rectangle at (10,10) with different arcs per corner
+ private static GeneralRoundRectangle asymmetric()
+ {
+ return new GeneralRoundRectangle(10, 10, 100, 80,
+ 30, 20, // tlaw, tlah (top-left)
+ 10, 40, // traw, trah (top-right)
+ 20, 10, // braw, brah (bottom-right)
+ 40, 30); // blaw, blah (bottom-left)
+ }
+
+ // ---------- contains(x, y) ----------
+
+ @Test
+ public void contains_emptyRect_returnsFalse()
+ {
+ GeneralRoundRectangle rr = new GeneralRoundRectangle(0, 0, 0, 100,
+ 10, 10, 10, 10, 10, 10, 10, 10);
+ assertFalse(rr.contains(0, 50));
+ }
+
+ @Test
+ public void contains_outsideBounds_returnsFalse()
+ {
+ GeneralRoundRectangle rr = uniform();
+ assertFalse(rr.contains(-1, 50));
+ assertFalse(rr.contains(50, -1));
+ assertFalse(rr.contains(100, 50));
+ assertFalse(rr.contains(50, 100));
+ }
+
+ @Test
+ public void contains_center_returnsTrue()
+ {
+ assertTrue(uniform().contains(50, 50));
+ }
+
+ @Test
+ public void contains_midEdge_returnsTrue()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // Middle of each edge — well inside the body, outside any corner zone
+ assertTrue(rr.contains(50, 0)); // top edge center
+ assertTrue(rr.contains(50, 99)); // bottom edge center
+ assertTrue(rr.contains(0, 50)); // left edge center
+ assertTrue(rr.contains(99, 50)); // right edge center
+ }
+
+ @Test
+ public void contains_cornerOutsideArc_returnsFalse()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // Very close to the corner, outside the elliptical arc
+ // Arc radius is 10 (arcWidth=20, half=10). Point (0,0) is the corner.
+ // At (1, 1): nx = (1-10)/10 = -0.9, ny = (1-10)/10 = -0.9
+ // nx^2 + ny^2 = 0.81 + 0.81 = 1.62 > 1 → outside
+ assertFalse(rr.contains(1, 1));
+ assertFalse(rr.contains(99, 1));
+ assertFalse(rr.contains(99, 99));
+ assertFalse(rr.contains(1, 99));
+ }
+
+ @Test
+ public void contains_cornerInsideArc_returnsTrue()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // Arc radius is 10. Point (5, 5):
+ // nx = (5-10)/10 = -0.5, ny = (5-10)/10 = -0.5
+ // nx^2 + ny^2 = 0.25 + 0.25 = 0.5 <= 1 → inside
+ assertTrue(rr.contains(5, 5));
+ assertTrue(rr.contains(95, 5));
+ assertTrue(rr.contains(95, 95));
+ assertTrue(rr.contains(5, 95));
+ }
+
+ @Test
+ public void contains_sharpCorners_returnsTrue()
+ {
+ GeneralRoundRectangle rr = sharp();
+ // With no rounding, all corners should be contained
+ assertTrue(rr.contains(0, 0));
+ assertTrue(rr.contains(99, 0));
+ assertTrue(rr.contains(99, 99));
+ assertTrue(rr.contains(0, 99));
+ }
+
+ @Test
+ public void contains_onArcBoundary_returnsTrue()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // Point exactly on the ellipse boundary (nx^2 + ny^2 == 1.0)
+ // At the arc center entry point: (10, 0)
+ // nx = (10-10)/10 = 0, ny = (0-10)/10 = -1
+ // nx^2 + ny^2 = 0 + 1 = 1.0 → on boundary, should be contained (<= 1.0)
+ assertTrue(rr.contains(10, 0));
+ }
+
+ @Test
+ public void contains_asymmetricCorners()
+ {
+ GeneralRoundRectangle rr = asymmetric();
+ // Center is definitely inside
+ assertTrue(rr.contains(60, 50));
+
+ // Top-left corner: arc is 30w, 20h → half = 15, 10
+ // Corner zone: x < 10+15=25, y < 10+10=20
+ // Point (11, 11): nx = (11-25)/15 = -0.933, ny = (11-20)/10 = -0.9
+ // nx^2 + ny^2 = 0.871 + 0.81 = 1.681 > 1 → outside
+ assertFalse(rr.contains(11, 11));
+
+ // Point (20, 15): nx = (20-25)/15 = -0.333, ny = (15-20)/10 = -0.5
+ // nx^2 + ny^2 = 0.111 + 0.25 = 0.361 → inside
+ assertTrue(rr.contains(20, 15));
+ }
+
+ // ---------- contains(x, y, w, h) ----------
+
+ @Test
+ public void containsRect_fullyInside_returnsTrue()
+ {
+ assertTrue(uniform().contains(20, 20, 60, 60));
+ }
+
+ @Test
+ public void containsRect_overlapCorner_returnsFalse()
+ {
+ // Small rect in the very corner — all 4 corner points are outside the arc
+ assertFalse(uniform().contains(0, 0, 2, 2));
+ }
+
+ @Test
+ public void containsRect_empty_returnsFalse()
+ {
+ assertFalse(uniform().contains(50, 50, 0, 10));
+ assertFalse(uniform().contains(50, 50, 10, 0));
+ }
+
+ // ---------- intersects(x, y, w, h) ----------
+
+ @Test
+ public void intersects_emptyShape_returnsFalse()
+ {
+ GeneralRoundRectangle rr = new GeneralRoundRectangle(0, 0, 0, 100,
+ 10, 10, 10, 10, 10, 10, 10, 10);
+ assertFalse(rr.intersects(0, 0, 50, 50));
+ }
+
+ @Test
+ public void intersects_emptyQuery_returnsFalse()
+ {
+ assertFalse(uniform().intersects(50, 50, 0, 10));
+ assertFalse(uniform().intersects(50, 50, 10, 0));
+ }
+
+ @Test
+ public void intersects_noOverlap_returnsFalse()
+ {
+ GeneralRoundRectangle rr = uniform();
+ assertFalse(rr.intersects(200, 200, 10, 10));
+ assertFalse(rr.intersects(-20, 50, 10, 10));
+ assertFalse(rr.intersects(110, 50, 10, 10));
+ }
+
+ @Test
+ public void intersects_middleBand_returnsTrue()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // Rect that overlaps the horizontal middle band (extends from above into the top edge)
+ assertTrue(rr.intersects(40, -10, 20, 15));
+ // Rect that overlaps the vertical middle band (extends from left into the left edge)
+ assertTrue(rr.intersects(-10, 40, 15, 20));
+ }
+
+ @Test
+ public void intersects_fullyInside_returnsTrue()
+ {
+ assertTrue(uniform().intersects(20, 20, 60, 60));
+ }
+
+ @Test
+ public void intersects_cornerOverlap_returnsTrue()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // A rect entirely within the top-left corner zone that touches the arc.
+ // Arc radius = 10. Rect (2,2)-(9,9). Nearest point to ellipse center (10,10) is (9,9).
+ // nx = (9-10)/10 = -0.1, ny = (9-10)/10 = -0.1. nx^2+ny^2 = 0.02 <= 1 → hit
+ assertTrue(rr.intersects(2, 2, 7, 7));
+ }
+
+ @Test
+ public void intersects_cornerMiss_returnsFalse()
+ {
+ GeneralRoundRectangle rr = uniform();
+ // A tiny rect in the very corner that doesn't touch the arc.
+ // Arc center is at (10, 10). Rect (0,0)-(1,1).
+ // Nearest point to center (10,10) is (1,1).
+ // nx = (1-10)/10 = -0.9, ny = (1-10)/10 = -0.9
+ // nx^2 + ny^2 = 1.62 > 1 → miss
+ assertFalse(rr.intersects(0, 0, 1, 1));
+ }
+
+ @Test
+ public void intersects_allCornerMisses()
+ {
+ GeneralRoundRectangle rr = uniform();
+ assertFalse(rr.intersects(0, 0, 1, 1)); // top-left
+ assertFalse(rr.intersects(99, 0, 1, 1)); // top-right
+ assertFalse(rr.intersects(99, 99, 1, 1)); // bottom-right
+ assertFalse(rr.intersects(0, 99, 1, 1)); // bottom-left
+ }
+
+ @Test
+ public void intersects_sharpCorners_returnsTrue()
+ {
+ GeneralRoundRectangle rr = sharp();
+ // With no rounding, any overlap with the bounding rect should intersect
+ assertTrue(rr.intersects(0, 0, 1, 1));
+ assertTrue(rr.intersects(99, 99, 1, 1));
+ }
+
+ @Test
+ public void intersects_spanningEntireShape_returnsTrue()
+ {
+ assertTrue(uniform().intersects(-10, -10, 120, 120));
+ }
+
+ @Test
+ public void intersects_touchingEdge_returnsTrue()
+ {
+ // Rect that just touches the top edge in the middle (no corner involvement)
+ assertTrue(uniform().intersects(40, -5, 20, 6));
+ }
+
+ @Test
+ public void intersects_justOutside_returnsFalse()
+ {
+ // Rect that is just outside the right edge
+ assertFalse(uniform().intersects(100, 40, 10, 20));
+ }
+
+ @Test
+ public void intersects_sharpCornerWithOtherCornersRounded()
+ {
+ // Top-left is sharp (arc=0), all other corners are rounded (arc=20).
+ GeneralRoundRectangle rr = new GeneralRoundRectangle(0, 0, 100, 100,
+ 0, 0, 20, 20, 20, 20, 20, 20);
+
+ // A rect at the sharp top-left corner should intersect since there is no arc to miss.
+ assertTrue(rr.intersects(0, 0, 1, 1));
+
+ // A rect extending from outside into the sharp corner (tests clipping + !anyMiss path)
+ assertTrue(rr.intersects(-5, -5, 7, 7));
+
+ // A rect at each rounded corner should still miss the arc
+ assertFalse(rr.intersects(99, 0, 1, 1)); // top-right
+ assertFalse(rr.intersects(99, 99, 1, 1)); // bottom-right
+ assertFalse(rr.intersects(0, 99, 1, 1)); // bottom-left
+ }
+
+ @Test
+ public void intersects_overlappingArcs_centerInBody()
+ {
+ // Very large arcs that nearly meet in the middle. The arc zones from adjacent corners
+ // leave only a small body in the center. A rect spanning two adjacent corner zones
+ // should intersect because its center falls in the body.
+ GeneralRoundRectangle rr = new GeneralRoundRectangle(0, 0, 100, 100,
+ 90, 90, 90, 90, 90, 90, 90, 90);
+ // Arc half = 45. Middle band: x > 45 and x < 55 (only 10 wide).
+ // A rect from (0,48) to (100,52): horizontal band check 100>45 && 0<55 → caught by fast path.
+ // A rect confined to the top-left quadrant but extending past the zone:
+ // (0, 0, 50, 50) — horizontal band: 50>45 && 0<55 → caught by fast path.
+ // For center-in-body path, need to bypass fast path:
+ // Rect (40, 40, 20, 20) — horizontal: 60>45 && 40<55 → caught.
+ // It's very hard to reach the center-in-body path without the fast path catching it.
+ // This test verifies the shape works correctly with large arcs.
+ assertTrue(rr.intersects(40, 40, 20, 20));
+ // Corner miss with large arcs
+ assertFalse(rr.intersects(0, 0, 1, 1));
+ }
+
+ @Test
+ public void intersects_asymmetricCorners()
+ {
+ GeneralRoundRectangle rr = asymmetric();
+
+ // Center overlap
+ assertTrue(rr.intersects(50, 40, 20, 20));
+
+ // Top-left corner miss: arc is 30w, 20h → half = 15, 10
+ // Corner at (10, 10). Tiny rect at (10, 10) size 1x1.
+ // Nearest point to ellipse center (25, 20) is (11, 11).
+ // nx = (11-25)/15 = -0.933, ny = (11-20)/10 = -0.9
+ // nx^2 + ny^2 = 0.871 + 0.81 = 1.681 > 1 → miss
+ assertFalse(rr.intersects(10, 10, 1, 1));
+
+ // Bottom-right corner: arc is 20w, 10h → half = 10, 5
+ // Corner at (110, 90). Tiny rect at (109, 89) size 1x1.
+ // Nearest point to ellipse center (100, 85) is (109, 85).
+ // nx = (109-100)/10 = 0.9, ny = 0
+ // nx^2 + ny^2 = 0.81 → inside, so intersects
+ assertTrue(rr.intersects(109, 85, 1, 1));
+ }
+}