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"); } } }