Files
new-shapes/src/test/java/ovh/gasser/newshapes/shapes/ContainsPointTest.java
Thibaud b87deecf40
All checks were successful
CI / build-and-test (pull_request) Successful in 17s
test: add contains(Point) hit-testing tests for all shapes
Add 36 tests covering bounds-based hit-testing (getBounds().contains(Point))
for SRectangle, SCircle, STriangle, SText, and SCollection:
- Point inside, outside, on boundary (inclusive top-left, exclusive bottom-right)
- Point after translate to verify position update
- SCollection union bounds, gap between children, single-child bounds

Closes #11
2026-03-27 23:23:57 +00:00

388 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ovh.gasser.newshapes.shapes;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests bounds-based hit-testing for all shape types.
* Hit-testing is performed via {@code shape.getBounds().contains(Point)},
* delegating to {@link java.awt.Rectangle#contains(Point)}.
*
* <p>Note on {@link java.awt.Rectangle#contains} boundary semantics:
* A rectangle {@code (x, y, w, h)} contains a point {@code (px, py)} when:
* {@code x <= px < x+w} and {@code y <= py < y+h}.
* The top-left corner is <em>inclusive</em>; the bottom-right corner is <em>exclusive</em>.
*/
class ContainsPointTest {
// -----------------------------------------------------------------------
// SRectangle — bounds = (x, y, width, height)
// -----------------------------------------------------------------------
@Nested
class SRectangleContainsPoint {
// Shape: origin (10, 20), size 80×60 → right-exclusive: x=90, y=80
private final SRectangle rect = SRectangle.create(10, 20, 80, 60);
@Test
void pointInsideShouldBeContained() {
Point inside = new Point(50, 50);
assertTrue(rect.getBounds().contains(inside),
"A point clearly inside the rectangle should be contained");
}
@Test
void pointOutsideShouldNotBeContained() {
Point outside = new Point(5, 5);
assertFalse(rect.getBounds().contains(outside),
"A point outside the rectangle should not be contained");
}
@Test
void pointFarOutsideShouldNotBeContained() {
Point farOutside = new Point(200, 300);
assertFalse(rect.getBounds().contains(farOutside),
"A point far outside the rectangle should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
// Rectangle.contains is inclusive of the top-left corner
Point topLeft = new Point(10, 20);
assertTrue(rect.getBounds().contains(topLeft),
"The top-left corner (inclusive boundary) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
// Rectangle.contains is exclusive of the bottom-right corner
Point bottomRight = new Point(90, 80); // x + width, y + height
assertFalse(rect.getBounds().contains(bottomRight),
"The bottom-right corner (exclusive boundary) should not be contained");
}
@Test
void pointJustInsideBottomRightShouldBeContained() {
Point justInside = new Point(89, 79); // one pixel before exclusive boundary
assertTrue(rect.getBounds().contains(justInside),
"A point one pixel before the bottom-right boundary should be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
SRectangle moved = SRectangle.create(10, 20, 80, 60);
moved.translate(30, 40); // new bounds: (40, 60, 80, 60)
Point oldCenter = new Point(50, 50);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(80, 90);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
@Test
void pointOnTopEdgeShouldBeContained() {
Point topEdge = new Point(50, 20); // y == rect.y, inside x range
assertTrue(rect.getBounds().contains(topEdge),
"A point on the top edge should be contained");
}
@Test
void pointOnLeftEdgeShouldBeContained() {
Point leftEdge = new Point(10, 50); // x == rect.x, inside y range
assertTrue(rect.getBounds().contains(leftEdge),
"A point on the left edge should be contained");
}
@Test
void pointOnRightEdgeShouldNotBeContained() {
Point rightEdge = new Point(90, 50); // x == rect.x + rect.width (exclusive)
assertFalse(rect.getBounds().contains(rightEdge),
"A point exactly on the right edge (exclusive) should not be contained");
}
@Test
void pointOnBottomEdgeShouldNotBeContained() {
Point bottomEdge = new Point(50, 80); // y == rect.y + rect.height (exclusive)
assertFalse(rect.getBounds().contains(bottomEdge),
"A point exactly on the bottom edge (exclusive) should not be contained");
}
}
// -----------------------------------------------------------------------
// SCircle — bounds = (x, y, radius*2, radius*2)
// -----------------------------------------------------------------------
@Nested
class SCircleContainsPoint {
// Shape: origin (20, 30), radius=40 → bounds (20, 30, 80, 80)
// right-exclusive corner: (100, 110)
private final SCircle circle = SCircle.create(20, 30, 40);
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(60, 70);
assertTrue(circle.getBounds().contains(inside),
"A point inside the bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(10, 10);
assertFalse(circle.getBounds().contains(outside),
"A point outside the bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(20, 30);
assertTrue(circle.getBounds().contains(topLeft),
"The top-left corner of the bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
Point bottomRight = new Point(100, 110); // x + 2r, y + 2r
assertFalse(circle.getBounds().contains(bottomRight),
"The bottom-right corner of the bounding box (exclusive) should not be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
SCircle moved = SCircle.create(20, 30, 40);
moved.translate(50, 50); // new bounds: (70, 80, 80, 80)
Point oldCenter = new Point(60, 70);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(110, 120);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
}
// -----------------------------------------------------------------------
// STriangle — bounds = (x, y, size, size)
// -----------------------------------------------------------------------
@Nested
class STriangleContainsPoint {
// Shape: origin (5, 15), size=60 → bounds (5, 15, 60, 60)
// right-exclusive corner: (65, 75)
private final STriangle triangle = STriangle.create(5, 15, 60, Color.RED, Color.BLACK);
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(35, 45);
assertTrue(triangle.getBounds().contains(inside),
"A point inside the bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(0, 0);
assertFalse(triangle.getBounds().contains(outside),
"A point outside the bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(5, 15);
assertTrue(triangle.getBounds().contains(topLeft),
"The top-left corner of the bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
Point bottomRight = new Point(65, 75); // x + size, y + size
assertFalse(triangle.getBounds().contains(bottomRight),
"The bottom-right corner of the bounding box (exclusive) should not be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
STriangle moved = STriangle.create(5, 15, 60, Color.RED, Color.BLACK);
moved.translate(100, 100); // new bounds: (105, 115, 60, 60)
Point oldCenter = new Point(35, 45);
assertFalse(moved.getBounds().contains(oldCenter),
"After translate, old center should no longer be contained");
Point newCenter = new Point(135, 145);
assertTrue(moved.getBounds().contains(newCenter),
"After translate, a point at the new center should be contained");
}
}
// -----------------------------------------------------------------------
// SText — bounds = (x, y, 100, 20)
// -----------------------------------------------------------------------
@Nested
class STextContainsPoint {
// Shape: origin (50, 100) → bounds (50, 100, 100, 20)
// right-exclusive corner: (150, 120)
private final SText text = SText.create(50, 100, "Hello");
@Test
void pointInsideBoundsShouldBeContained() {
Point inside = new Point(100, 110);
assertTrue(text.getBounds().contains(inside),
"A point inside the default text bounding box should be contained");
}
@Test
void pointOutsideBoundsShouldNotBeContained() {
Point outside = new Point(10, 10);
assertFalse(text.getBounds().contains(outside),
"A point outside the text bounding box should not be contained");
}
@Test
void topLeftCornerShouldBeContained() {
Point topLeft = new Point(50, 100);
assertTrue(text.getBounds().contains(topLeft),
"The top-left corner of the text bounding box (inclusive) should be contained");
}
@Test
void bottomRightCornerShouldNotBeContained() {
// Default bounds width=100, height=20 → exclusive corner at (150, 120)
Point bottomRight = new Point(150, 120);
assertFalse(text.getBounds().contains(bottomRight),
"The bottom-right corner of the text bounding box (exclusive) should not be contained");
}
@Test
void pointJustBeforeBottomRightShouldBeContained() {
Point justInside = new Point(149, 119);
assertTrue(text.getBounds().contains(justInside),
"A point one pixel before the exclusive bottom-right boundary should be contained");
}
@Test
void pointAfterTranslateShouldBeAtNewPosition() {
// Original bounds: (50, 100, 100, 20) — covers x∈[50,150), y∈[100,120)
SText moved = SText.create(50, 100, "Hello");
moved.translate(-40, 10); // new bounds: (10, 110, 100, 20) — covers x∈[10,110), y∈[110,130)
// (130, 105) was inside original bounds but is now outside (x=130 >= 110, y=105 < 110)
Point oldPosition = new Point(130, 105);
assertFalse(moved.getBounds().contains(oldPosition),
"After translate, a point that was in the old bounds should no longer be contained");
// (50, 115) is inside new bounds: x∈[10,110), y∈[110,130) ✓
Point newPosition = new Point(50, 115);
assertTrue(moved.getBounds().contains(newPosition),
"After translate, a point in the new bounds should be contained");
}
@Test
void blankTextUsesDefaultPlaceholderAndSameBounds() {
// Even blank/null text falls back to the placeholder — bounds remain (x, y, 100, 20)
SText blankText = SText.create(0, 0, "");
Point inside = new Point(50, 10);
assertTrue(blankText.getBounds().contains(inside),
"Blank text still uses default bounds; interior point should be contained");
}
}
// -----------------------------------------------------------------------
// SCollection — bounds = union of children bounds
// -----------------------------------------------------------------------
@Nested
class SCollectionContainsPoint {
// Two non-overlapping children:
// rect1: (10, 10, 50, 50) → covers x∈[10,60), y∈[10,60)
// rect2: (100, 100, 40, 40) → covers x∈[100,140), y∈[100,140)
// Union bounds: (10, 10, 130, 130) → exclusive corner: (140, 140)
private final SRectangle child1 = SRectangle.create(10, 10, 50, 50);
private final SRectangle child2 = SRectangle.create(100, 100, 40, 40);
private final SCollection collection = SCollection.of(child1, child2);
@Test
void pointInsideFirstChildBoundsShouldBeContained() {
Point insideChild1 = new Point(30, 30);
assertTrue(collection.getBounds().contains(insideChild1),
"A point inside the first child's bounds is within the union and should be contained");
}
@Test
void pointInsideSecondChildBoundsShouldBeContained() {
Point insideChild2 = new Point(120, 120);
assertTrue(collection.getBounds().contains(insideChild2),
"A point inside the second child's bounds is within the union and should be contained");
}
@Test
void pointInGapBetweenChildrenShouldBeContained() {
// The union rectangle spans the gap between the two children
Point inGap = new Point(70, 70);
assertTrue(collection.getBounds().contains(inGap),
"A point in the gap between children is still inside the union bounds and should be contained");
}
@Test
void pointOutsideUnionShouldNotBeContained() {
Point outside = new Point(0, 0);
assertFalse(collection.getBounds().contains(outside),
"A point outside the union bounds should not be contained");
}
@Test
void topLeftCornerOfUnionShouldBeContained() {
// Union top-left = (10, 10) — inclusive
Point topLeft = new Point(10, 10);
assertTrue(collection.getBounds().contains(topLeft),
"The top-left corner of the union bounds (inclusive) should be contained");
}
@Test
void bottomRightCornerOfUnionShouldNotBeContained() {
// Union: x=10, y=10, w=130, h=130 → exclusive corner (140, 140)
Point bottomRight = new Point(140, 140);
assertFalse(collection.getBounds().contains(bottomRight),
"The bottom-right corner of the union bounds (exclusive) should not be contained");
}
@Test
void pointAfterTranslatingChildrenShouldBeAtNewPosition() {
SRectangle movableChild = SRectangle.create(0, 0, 30, 30);
SCollection movable = SCollection.of(movableChild);
movable.translate(50, 50); // child now at (50, 50, 30, 30)
Point oldPoint = new Point(15, 15);
assertFalse(movable.getBounds().contains(oldPoint),
"After translate, a point at the old location should not be contained");
Point newPoint = new Point(65, 65);
assertTrue(movable.getBounds().contains(newPoint),
"After translate, a point at the new location should be contained");
}
@Test
void singleChildCollectionBoundsMatchChildBounds() {
SRectangle only = SRectangle.create(5, 5, 20, 20);
SCollection single = SCollection.of(only);
assertEquals(only.getBounds(), single.getBounds(),
"A single-child collection's bounds should equal the child's own bounds");
}
}
}