From cbc75e4bc2a0ac22e3aaef7183ae2a9bf0d08a31 Mon Sep 17 00:00:00 2001 From: Thibaud Date: Fri, 27 Mar 2026 16:37:27 +0100 Subject: [PATCH] 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 --- .../newshapes/shapes/ContainsPointTest.java | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 src/test/java/ovh/gasser/newshapes/shapes/ContainsPointTest.java diff --git a/src/test/java/ovh/gasser/newshapes/shapes/ContainsPointTest.java b/src/test/java/ovh/gasser/newshapes/shapes/ContainsPointTest.java new file mode 100644 index 0000000..e501272 --- /dev/null +++ b/src/test/java/ovh/gasser/newshapes/shapes/ContainsPointTest.java @@ -0,0 +1,387 @@ +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)}. + * + *

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 inclusive; the bottom-right corner is exclusive. + */ +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"); + } + } +}