From ef2c347a7a47aa014eadc93df76819414ceb6e5b Mon Sep 17 00:00:00 2001 From: ArtemShaturnyi Date: Thu, 21 May 2026 12:46:33 +0300 Subject: [PATCH 1/7] feat(renderer): implement basic drawing primitives for general visualization Added universal drawing methods to the Renderer class to support a wider range of visual elements: --- src/simulator/renderer.py | 51 +++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/simulator/renderer.py b/src/simulator/renderer.py index e7c4820..0755ba7 100644 --- a/src/simulator/renderer.py +++ b/src/simulator/renderer.py @@ -1,7 +1,7 @@ import numpy as np import matplotlib.pyplot as plt -from matplotlib.patches import Circle, Rectangle +from matplotlib.patches import Circle, Rectangle, Polygon # Added Polygon from matplotlib.collections import LineCollection @@ -35,6 +35,49 @@ def __init__(self, x_limits=(-10.0, 10.0), y_limits=(-10.0, 10.0), max_colors=20 self.base_patch = None + # New functions for drawing primitives + + def clear(self): + """Clears the current drawing, resetting the axes for the next frame.""" + self.ax.cla() + # Reset limits and aspect ratio after clearing + self.ax.set_xlim(-self.view_size, self.view_size) + self.ax.set_ylim(-self.view_size, self.view_size) + self.ax.set_aspect("equal") + + # Redraw world frame axes + self.ax.axhline(0, color='black', lw=1, alpha=0.2) + self.ax.axvline(0, color='black', lw=1, alpha=0.2) + + def draw_line(self, start, end, color='blue', linewidth=2): + """Draws a line between two points [x, y]""" + self.ax.plot([start[0], end[0]], [start[1], end[1]], color=color, linewidth=linewidth) + + def draw_circle(self, center, radius=0.2, color='lightblue', fill=True): + """Draws a circle (joint)""" + circle = Circle(center, radius, color=color, fill=fill) + self.ax.add_patch(circle) + return circle + + def draw_rectangle(self, bottom_left, width, height, color='gray', fill=True): + """Draws a rectangle (box)""" + rect = Rectangle(bottom_left, width, height, color=color, fill=fill) + self.ax.add_patch(rect) + return rect + + def draw_polygon(self, vertices, color='green', fill=True): + """Draws a polygon from a list of vertices [(x1, y1), (x2, y2), ...]""" + poly = Polygon(vertices, color=color, fill=fill) + self.ax.add_patch(poly) + return poly + + def draw_text(self, position, text, fontsize=10, color='black'): + """Draws text at the specified position [x, y]""" + self.ax.text(position[0], position[1], text, fontsize=fontsize, color=color) + return None + + # End of new functions for drawing primitives + def update(self, objects, dt=0.0001): if not objects: @@ -89,9 +132,7 @@ def draw_tree(obj, q): self.links_lines = LineCollection(links, colors=self.colors, linewidths=3) self.ax.add_collection(self.links_lines) - self.joints_circles = self.ax.scatter( - points[:, 0], points[:, 1], c="lightblue", s=40, zorder=10 - ) + self.joints_circles = self.ax.scatter(points[:, 0], points[:, 1], c="lightblue", s=40, zorder=10) self.ax.scatter(0.0, 0.0, c="blue", s=50, zorder=10) else: self.links_lines.set_segments(links) @@ -101,4 +142,4 @@ def draw_tree(obj, q): self.fig.canvas.flush_events() def close(self): - plt.close(self.fig) + plt.close(self.fig) \ No newline at end of file From fd9708c4d523478394ac3cffda2fa6c38269c381 Mon Sep 17 00:00:00 2001 From: basharo1 Date: Fri, 22 May 2026 10:35:15 +0300 Subject: [PATCH 2/7] test(renderer): add renderer primitives tests --- tests/test_renderer.py | 198 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 tests/test_renderer.py diff --git a/tests/test_renderer.py b/tests/test_renderer.py new file mode 100644 index 0000000..5691a4a --- /dev/null +++ b/tests/test_renderer.py @@ -0,0 +1,198 @@ +import pytest +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib.patches import Circle, Rectangle, Polygon +import numpy as np + +from renderer import Renderer + + +@pytest.fixture(autouse=True) +def close_figures(): + yield + plt.close('all') + + +@pytest.fixture +def renderer(): + return Renderer() + +# Tests for draw_line() +class TestDrawLine: + def test_adds_a_line_to_axes(self, renderer): + before = len(renderer.ax.lines) + renderer.draw_line([0, 0], [1, 1]) + assert len(renderer.ax.lines) == before + 1 + + def test_line_coordinates(self, renderer): + renderer.draw_line([0, 0], [3, 4]) + line = renderer.ax.lines[-1] + assert list(line.get_xdata()) == [0, 3] + assert list(line.get_ydata()) == [0, 4] + + def test_default_color_and_linewidth(self, renderer): + renderer.draw_line([0, 0], [1, 1]) + line = renderer.ax.lines[-1] + assert line.get_color() == 'black' + assert line.get_linewidth() == 2.0 + + def test_custom_color_and_linewidth(self, renderer): + renderer.draw_line([0, 0], [1, 1], color='red', linewidth=5) + line = renderer.ax.lines[-1] + assert line.get_color() == 'red' + assert line.get_linewidth() == 5 + + def test_negative_coordinates(self, renderer): + renderer.draw_line([-2, -3], [-1, -1]) + line = renderer.ax.lines[-1] + assert list(line.get_xdata()) == [-2, -1] + assert list(line.get_ydata()) == [-3, -1] + + def test_zero_length_line(self, renderer): + renderer.draw_line([5, 5], [5, 5]) + assert len(renderer.ax.lines) >= 1 + +# Tests for draw_Circle() +class TestDrawCircle: + def test_returns_circle_patch(self, renderer): + result = renderer.draw_circle([0, 0], radius=1.0) + assert isinstance(result, Circle) + + def test_circle_added_to_axes(self, renderer): + before = len(renderer.ax.patches) + renderer.draw_circle([1, 2], radius=1.0) + assert len(renderer.ax.patches) == before + 1 + + def test_center_and_radius(self, renderer): + circle = renderer.draw_circle([3, 4], radius=0.5) + assert tuple(circle.center) == (3, 4) + assert circle.radius == 0.5 + + def test_default_filled_true(self, renderer): + circle = renderer.draw_circle([0, 0], radius=1.0) + assert circle.get_fill() is True + + def test_custom_color(self, renderer): + circle = renderer.draw_circle([0, 0], radius=1.0, color='red') + assert circle.get_facecolor() is not None + + def test_custom_radius(self, renderer): + circle = renderer.draw_circle([0, 0], radius=2.5) + assert circle.radius == 2.5 + +# Tests for draw_Rectangle() +class TestDrawRectangle: + def test_returns_rectangle_patch(self, renderer): + result = renderer.draw_rectangle([0, 0], 2, 1) + assert isinstance(result, Rectangle) + + def test_rectangle_added_to_axes(self, renderer): + before = len(renderer.ax.patches) + renderer.draw_rectangle([0, 0], 3, 2) + assert len(renderer.ax.patches) == before + 1 + + def test_position_and_size(self, renderer): + rect = renderer.draw_rectangle([2, 4], 4, 3) + assert rect.get_x() == 2 - 4 / 2 + assert rect.get_y() == 4 - 3 / 2 + assert rect.get_width() == 4 + assert rect.get_height() == 3 + + def test_default_filled(self, renderer): + rect = renderer.draw_rectangle([0, 0], 1, 1) + assert rect.get_fill() is True + + def test_custom_color(self, renderer): + rect = renderer.draw_rectangle([0, 0], 1, 1, color='blue') + assert rect.get_facecolor() is not None + + def test_zero_size_rectangle(self, renderer): + rect = renderer.draw_rectangle([0, 0], 0, 0) + assert rect.get_width() == 0 + assert rect.get_height() == 0 + +# Tests for draw_Polygon() +class TestDrawPolygon: + def test_returns_polygon_patch(self, renderer): + result = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)]) + assert isinstance(result, Polygon) + + def test_polygon_added_to_axes(self, renderer): + before = len(renderer.ax.patches) + renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)]) + assert len(renderer.ax.patches) == before + 1 + + def test_default_filled(self, renderer): + poly = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)]) + assert poly.get_fill() is True + + def test_custom_color(self, renderer): + poly = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)], color='red') + assert poly.get_facecolor() is not None + + def test_quad_vertices(self, renderer): + poly = renderer.draw_polygon([(0, 0), (2, 0), (2, 2), (0, 2)]) + assert isinstance(poly, Polygon) + + def test_many_vertices(self, renderer): + angles = np.linspace(0, 2 * np.pi, 32, endpoint=False) + verts = [(np.cos(a), np.sin(a)) for a in angles] + poly = renderer.draw_polygon(verts) + assert isinstance(poly, Polygon) + +# Tests for draw_Text() +class TestDrawText: + def test_returns_text_object(self, renderer): + result = renderer.draw_text([0, 0], 'hello') + assert result is not None + + def test_text_added_to_axes(self, renderer): + before = len(renderer.ax.texts) + renderer.draw_text([1, 2], 'test') + assert len(renderer.ax.texts) == before + 1 + + def test_text_content_and_position(self, renderer): + renderer.draw_text([3, 4], 'robot') + txt = renderer.ax.texts[-1] + assert txt.get_text() == 'robot' + assert txt.get_position() == (3, 4) + + def test_default_fontsize(self, renderer): + renderer.draw_text([0, 0], 'x') + txt = renderer.ax.texts[-1] + assert txt.get_fontsize() == 10 + + def test_custom_fontsize(self, renderer): + renderer.draw_text([0, 0], 'x', fontsize=16) + txt = renderer.ax.texts[-1] + assert txt.get_fontsize() == 16 + + def test_empty_string(self, renderer): + renderer.draw_text([0, 0], '') + txt = renderer.ax.texts[-1] + assert txt.get_text() == '' + + def test_unicode_text(self, renderer): + renderer.draw_text([0, 0], 'Hello Robot') + txt = renderer.ax.texts[-1] + assert txt.get_text() == 'Hello Robot' + + +class TestIntegration: + def test_multiple_shapes_all_added(self, renderer): + renderer.draw_line([0, 0], [1, 1]) + renderer.draw_circle([2, 2], radius=1.0) + renderer.draw_rectangle([3, 3], 1, 1) + renderer.draw_polygon([(5, 5), (6, 5), (5.5, 6)]) + renderer.draw_text([7, 7], 'ok') + + assert len(renderer.ax.lines) >= 1 + assert len(renderer.ax.patches) >= 3 + assert len(renderer.ax.texts) >= 1 + + def test_draw_robot_arm_segment(self, renderer): + renderer.draw_line([0, 0], [1, 0]) + joint = renderer.draw_circle([1, 0], radius=0.1) + assert isinstance(joint, Circle) + assert tuple(joint.center) == (1, 0) From 51aa4436a696485a73452b6c4f9ddbc170ccac3e Mon Sep 17 00:00:00 2001 From: Dennis Burdin Date: Sat, 23 May 2026 09:48:18 +0300 Subject: [PATCH 3/7] enhance(renderer): improve core 2D primitives for test compliance #19 Extended existing draw methods: list/tuple positions, kwargs, canvas update, proper returns. All 33 unit tests pass. --- src/simulator/renderer.py | 57 +++++++++++++++++++++++++++------------ tests/test_renderer.py | 2 +- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/simulator/renderer.py b/src/simulator/renderer.py index 0755ba7..2ecbb92 100644 --- a/src/simulator/renderer.py +++ b/src/simulator/renderer.py @@ -49,32 +49,55 @@ def clear(self): self.ax.axhline(0, color='black', lw=1, alpha=0.2) self.ax.axvline(0, color='black', lw=1, alpha=0.2) - def draw_line(self, start, end, color='blue', linewidth=2): - """Draws a line between two points [x, y]""" - self.ax.plot([start[0], end[0]], [start[1], end[1]], color=color, linewidth=linewidth) - - def draw_circle(self, center, radius=0.2, color='lightblue', fill=True): - """Draws a circle (joint)""" - circle = Circle(center, radius, color=color, fill=fill) + def draw_line(self, start, end, color='black', linewidth=2.0, alpha=1.0, **kwargs): + """Draws a line between two points [x,y].""" + self.ax.plot([start[0], end[0]], [start[1], end[1]], + color=color, linewidth=linewidth, alpha=alpha, **kwargs) + self.fig.canvas.draw_idle() + + + def draw_circle(self, center, radius=1.0, color='lightblue', fill=True, + alpha=1.0, linewidth=1.0, **kwargs): + """Draws a circle with given center [x,y] and radius.""" + circle = Circle((center[0], center[1]), radius=radius, + color=color, fill=fill, alpha=alpha, + linewidth=linewidth, **kwargs) self.ax.add_patch(circle) + self.fig.canvas.draw_idle() return circle - def draw_rectangle(self, bottom_left, width, height, color='gray', fill=True): - """Draws a rectangle (box)""" - rect = Rectangle(bottom_left, width, height, color=color, fill=fill) + def draw_rectangle(self, pos, width, height, angle=0.0, color='gray', + fill=True, alpha=1.0, **kwargs): + """ + Draws a rectangle with center at `pos` (list/tuple [x,y]). + Width and height specify the dimensions. + """ + x = pos[0] - width / 2 + y = pos[1] - height / 2 + rect = Rectangle((x, y), width, height, angle=angle, + facecolor=color if fill else 'none', + edgecolor=color, fill=fill, alpha=alpha, **kwargs) self.ax.add_patch(rect) + self.fig.canvas.draw_idle() return rect - def draw_polygon(self, vertices, color='green', fill=True): - """Draws a polygon from a list of vertices [(x1, y1), (x2, y2), ...]""" - poly = Polygon(vertices, color=color, fill=fill) + def draw_polygon(self, vertices, color='green', + fill=True, alpha=1.0, linewidth=1.0, **kwargs): + """Draws a polygon from a list of vertices [(x1,y1), ...].""" + poly = Polygon(vertices, facecolor=color if fill else 'none', + edgecolor=color, fill=fill, alpha=alpha, + linewidth=linewidth, **kwargs) self.ax.add_patch(poly) + self.fig.canvas.draw_idle() return poly + - def draw_text(self, position, text, fontsize=10, color='black'): - """Draws text at the specified position [x, y]""" - self.ax.text(position[0], position[1], text, fontsize=fontsize, color=color) - return None + def draw_text(self, position, text, fontsize=10, color='black', alpha=1.0, **kwargs): + """Draws text at position [x,y].""" + txt = self.ax.text(position[0], position[1], text, + fontsize=fontsize, color=color, alpha=alpha, **kwargs) + self.fig.canvas.draw_idle() + return txt # End of new functions for drawing primitives diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5691a4a..df1902f 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -5,7 +5,7 @@ from matplotlib.patches import Circle, Rectangle, Polygon import numpy as np -from renderer import Renderer +from src.simulator.renderer import Renderer @pytest.fixture(autouse=True) From b4e09a602556d95653c9d53d83ab973ad64cf415 Mon Sep 17 00:00:00 2001 From: basharo1 Date: Sun, 24 May 2026 21:11:54 +0300 Subject: [PATCH 4/7] test(renderer): add filled=False tests for circle, rectangle and polygon --- tests/test_renderer.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index df1902f..8f64c17 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -18,6 +18,7 @@ def close_figures(): def renderer(): return Renderer() + # Tests for draw_line() class TestDrawLine: def test_adds_a_line_to_axes(self, renderer): @@ -53,7 +54,8 @@ def test_zero_length_line(self, renderer): renderer.draw_line([5, 5], [5, 5]) assert len(renderer.ax.lines) >= 1 -# Tests for draw_Circle() + +# Tests for draw_circle() class TestDrawCircle: def test_returns_circle_patch(self, renderer): result = renderer.draw_circle([0, 0], radius=1.0) @@ -73,6 +75,11 @@ def test_default_filled_true(self, renderer): circle = renderer.draw_circle([0, 0], radius=1.0) assert circle.get_fill() is True + # fill=False should render the circle as outline only + def test_filled_false(self, renderer): + circle = renderer.draw_circle([0, 0], radius=1.0, fill=False) + assert circle.get_fill() is False + def test_custom_color(self, renderer): circle = renderer.draw_circle([0, 0], radius=1.0, color='red') assert circle.get_facecolor() is not None @@ -81,7 +88,8 @@ def test_custom_radius(self, renderer): circle = renderer.draw_circle([0, 0], radius=2.5) assert circle.radius == 2.5 -# Tests for draw_Rectangle() + +# Tests for draw_rectangle() class TestDrawRectangle: def test_returns_rectangle_patch(self, renderer): result = renderer.draw_rectangle([0, 0], 2, 1) @@ -103,6 +111,11 @@ def test_default_filled(self, renderer): rect = renderer.draw_rectangle([0, 0], 1, 1) assert rect.get_fill() is True + # fill=False should render the rectangle as outline only + def test_filled_false(self, renderer): + rect = renderer.draw_rectangle([0, 0], 1, 1, fill=False) + assert rect.get_fill() is False + def test_custom_color(self, renderer): rect = renderer.draw_rectangle([0, 0], 1, 1, color='blue') assert rect.get_facecolor() is not None @@ -112,7 +125,8 @@ def test_zero_size_rectangle(self, renderer): assert rect.get_width() == 0 assert rect.get_height() == 0 -# Tests for draw_Polygon() + +# Tests for draw_polygon() class TestDrawPolygon: def test_returns_polygon_patch(self, renderer): result = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)]) @@ -127,6 +141,11 @@ def test_default_filled(self, renderer): poly = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)]) assert poly.get_fill() is True + # fill=False should render the polygon as outline only + def test_filled_false(self, renderer): + poly = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)], fill=False) + assert poly.get_fill() is False + def test_custom_color(self, renderer): poly = renderer.draw_polygon([(0, 0), (1, 0), (0.5, 1)], color='red') assert poly.get_facecolor() is not None @@ -141,7 +160,8 @@ def test_many_vertices(self, renderer): poly = renderer.draw_polygon(verts) assert isinstance(poly, Polygon) -# Tests for draw_Text() + +# Tests for draw_text() class TestDrawText: def test_returns_text_object(self, renderer): result = renderer.draw_text([0, 0], 'hello') @@ -179,6 +199,7 @@ def test_unicode_text(self, renderer): assert txt.get_text() == 'Hello Robot' +# Integration tests — verifies all primitives work together class TestIntegration: def test_multiple_shapes_all_added(self, renderer): renderer.draw_line([0, 0], [1, 1]) From e8dfc2c576e83e1576250e43696bc01f7fa82933 Mon Sep 17 00:00:00 2001 From: Dennis Burdin Date: Mon, 25 May 2026 16:41:03 +0300 Subject: [PATCH 5/7] docs(lead): add comprehensive README for renderer primitives #19 --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index e69de29..53394f1 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,46 @@ +# Robot Simulator – Core 2D Rendering Primitives #19 + +## Project Overview + +This repository contains a 2D robot simulation environment with a Matplotlib‑based renderer. +As part of Ticket #19, the renderer has been extended with a set of core 2D drawing primitives that can be used independently of the robot model. + +These functions allow you to visualise obstacles, target points, trajectories, labels, and any other auxiliary information directly inside the simulation window. + +## Team & Role + +- Lead: Dennis Alexander Burdin +- Dev: Artem Shaturnyi +- Tester: Bashar Alahmad + +## Implemented Drawing Primitives + +All drawing methods are part of the `Renderer` class (`src/simulator/renderer.py`) and share a common interface: + +- Accept positions as lists or tuples `[x, y]` +- Support `kwargs` to pass any Matplotlib style option +- Automatically update the canvas (`draw_idle()`) +- Return the created artist for later modifications + +Method and Description: + +`draw_line(start, end, color='black', linewidth=2.0, alpha=1.0, **kwargs)` +Line from `start` to `end` + +`draw_circle(center, radius=1.0, color='lightblue', fill=True, alpha=1.0, linewidth=1.0, **kwargs)` +Circle with given center and radius. Returns the `Circle` patch. + +`draw_rectangle(pos, width, height, angle=0.0, color='gray', fill=True, alpha=1.0, **kwargs)` +Rectangle with center at `pos`. Width and height are full extents. Returns the `Rectangle` patch. + +`draw_polygon(vertices, color='green', fill=True, alpha=1.0, linewidth=1.0, **kwargs)` +Polygon defined by a list of vertices `[(x1,y1), (x2,y2), ...]`. Returns the `Polygon` patch. + +`draw_text(position, text, fontsize=10, color='black', alpha=1.0, **kwargs)` + Text label at `position`. Returns the `Text` object. + +## Testing +All primitives are covered by 33 unit tests (see tests/test_renderer.py). + +Run the tests with: +`pytest tests/test_renderer.py -v` \ No newline at end of file From a1bb9be7c8f3dbc3a477a5c0d1292f97bf6af650 Mon Sep 17 00:00:00 2001 From: Dennis Burdin Date: Mon, 25 May 2026 17:01:24 +0300 Subject: [PATCH 6/7] fix(ci): add matplotlib to dependencies for tests #19 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 370ab92..7707a72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "pre-commit>=4.6.0", "pytest>=9.0.3", "ruff>=0.15.12", + "matplotlib>=3.10.0", ] [tool.ruff.lint] From e89f3d47ade8ea20f682adc127e30cfd4654b8a9 Mon Sep 17 00:00:00 2001 From: Dennis Burdin Date: Mon, 25 May 2026 17:20:32 +0300 Subject: [PATCH 7/7] fix(tests): correct import path for CI environment #19 Change from 'src.simulator.renderer' to 'simulator.renderer' to match package structure. --- tests/test_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 8f64c17..deb9654 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -5,7 +5,7 @@ from matplotlib.patches import Circle, Rectangle, Polygon import numpy as np -from src.simulator.renderer import Renderer +from simulator.renderer import Renderer @pytest.fixture(autouse=True)