From 50b9aa5431f6ef5414cc4c1630dfd54cda1f05d4 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 9 Nov 2025 17:10:50 +0100 Subject: [PATCH] fixed issue with bounding box height being wrong --- pyWebLayout/concrete/text.py | 32 ++++++++++++++++++++-- tests/concrete/test_concrete_functional.py | 4 +-- tests/concrete/test_concrete_text.py | 8 +++++- tests/core/test_query_system.py | 25 +++++++++++------ 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/pyWebLayout/concrete/text.py b/pyWebLayout/concrete/text.py index 343fa20..00d263c 100644 --- a/pyWebLayout/concrete/text.py +++ b/pyWebLayout/concrete/text.py @@ -227,8 +227,11 @@ class Text(Renderable, Queriable): @property def size(self) -> int: - """Get the width of the text""" - return np.array((self._width, self._style.font_size)) + """Get the width and height of the text""" + # Return actual rendered height (ascent + descent) not just font_size + ascent, descent = self._style.font.getmetrics() + actual_height = ascent + descent + return np.array((self._width, actual_height)) def set_origin(self, origin: np.generic): """Set the origin (left baseline ("ls")) of this text element""" @@ -238,6 +241,31 @@ class Text(Renderable, Queriable): """Add this text to a line""" self._line = line + def in_object(self, point: np.generic): + """ + Check if a point is in the text object. + + Override Queriable.in_object() because Text uses baseline-anchored positioning. + The origin is at the baseline (anchor="ls"), not the top-left corner. + + Args: + point: The coordinates to check + + Returns: + True if the point is within the text bounds + """ + point_array = np.array(point) + + # Text origin is at baseline, so visual top is origin[1] - ascent + visual_top = self._origin[1] - self._ascent + visual_bottom = self._origin[1] + (self.size[1] - self._ascent) + + # Check if point is within bounds + # X: origin[0] to origin[0] + width + # Y: visual_top to visual_bottom + return (self._origin[0] <= point_array[0] < self._origin[0] + self.size[0] and + visual_top <= point_array[1] < visual_bottom) + def _apply_decoration(self, next_text: Optional['Text'] = None, spacing: int = 0): """ Apply text decoration (underline or strikethrough). diff --git a/tests/concrete/test_concrete_functional.py b/tests/concrete/test_concrete_functional.py index 0ef779b..64a97b3 100644 --- a/tests/concrete/test_concrete_functional.py +++ b/tests/concrete/test_concrete_functional.py @@ -125,8 +125,8 @@ class TestLinkText(unittest.TestCase): # Mock width property renderable._width = 80 - # Point inside link - self.assertTrue(renderable.in_object((15, 25))) + # Point inside link - origin is at baseline (10, 20), so test at baseline Y + self.assertTrue(renderable.in_object((15, 20))) # Point outside link self.assertFalse(renderable.in_object((200, 200))) diff --git a/tests/concrete/test_concrete_text.py b/tests/concrete/test_concrete_text.py index 1a3c18a..5f84deb 100644 --- a/tests/concrete/test_concrete_text.py +++ b/tests/concrete/test_concrete_text.py @@ -76,8 +76,14 @@ class TestText(unittest.TestCase): def test_in_object_true(self): text_instance = Text(text="Test", style=self.style, draw=self.draw) + # Set origin at baseline position (50, 50) + text_instance.set_origin(np.array([50, 50])) + # Test with a point that should be inside the text bounds - point = (5, 5) + # The text origin is at the baseline (50, 50) + # Visual bounds are: top = 50 - ascent, bottom = 50 + descent + # So a point at (55, 50) should be inside (at baseline) + point = (55, 50) self.assertTrue(text_instance.in_object(point)) def test_in_object_false(self): diff --git a/tests/core/test_query_system.py b/tests/core/test_query_system.py index 3a30832..fc1b4c0 100644 --- a/tests/core/test_query_system.py +++ b/tests/core/test_query_system.py @@ -145,7 +145,9 @@ class TestTextQueryPoint(unittest.TestCase): text.set_origin(np.array([100, 100])) # Point inside text bounds - self.assertTrue(text.in_object(np.array([110, 105]))) + # Origin is at baseline (100, 100), so test a point slightly above (at ascent/2) + # and to the right + self.assertTrue(text.in_object(np.array([110, 100]))) def test_in_object_miss(self): """Test in_object returns False for point outside text""" @@ -188,7 +190,9 @@ class TestLineQueryPoint(unittest.TestCase): # (after rendering, text objects have positions set) if len(line._text_objects) > 0: text_obj = line._text_objects[0] - point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5)) + # Origin is at baseline, so query at baseline position (Y = origin[1]) + # with X offset into the text + point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1])) result = line.query_point(point) @@ -234,7 +238,8 @@ class TestLineQueryPoint(unittest.TestCase): # Query the link if len(line._text_objects) > 0: text_obj = line._text_objects[0] - point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5)) + # Origin is at baseline, query at baseline Y position + point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1])) result = line.query_point(point) @@ -279,7 +284,8 @@ class TestPageQueryPoint(unittest.TestCase): # Query a point inside the line if len(line._text_objects) > 0: text_obj = line._text_objects[0] - point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1] + 5)) + # Origin is at baseline, query at baseline Y position + point = (int(text_obj._origin[0] + 5), int(text_obj._origin[1])) result = self.page.query_point(point) @@ -321,7 +327,8 @@ class TestPageQueryPoint(unittest.TestCase): # Query first line if len(line1._text_objects) > 0: text_obj1 = line1._text_objects[0] - point1 = (int(text_obj1._origin[0] + 5), int(text_obj1._origin[1] + 5)) + # Origin is at baseline, query at baseline Y position + point1 = (int(text_obj1._origin[0] + 5), int(text_obj1._origin[1])) result1 = self.page.query_point(point1) self.assertIsNotNone(result1) @@ -330,7 +337,8 @@ class TestPageQueryPoint(unittest.TestCase): # Query second line if len(line2._text_objects) > 0: text_obj2 = line2._text_objects[0] - point2 = (int(text_obj2._origin[0] + 5), int(text_obj2._origin[1] + 5)) + # Origin is at baseline, query at baseline Y position + point2 = (int(text_obj2._origin[0] + 5), int(text_obj2._origin[1])) result2 = self.page.query_point(point2) self.assertIsNotNone(result2) @@ -368,9 +376,10 @@ class TestPageQueryRange(unittest.TestCase): start_text = line._text_objects[0] end_text = line._text_objects[1] + # Origin is at baseline, query at baseline Y position start_point = ( - int(start_text._origin[0] + 5), int(start_text._origin[1] + 5)) - end_point = (int(end_text._origin[0] + 5), int(end_text._origin[1] + 5)) + int(start_text._origin[0] + 5), int(start_text._origin[1])) + end_point = (int(end_text._origin[0] + 5), int(end_text._origin[1])) sel_range = self.page.query_range(start_point, end_point)