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