99from dataclasses import replace
1010from pathlib import Path
1111
12- import pytest
1312from PIL import Image
1413
15- from roborock .exceptions import RoborockException
1614from roborock .map .b01_grid_layers import GridCalibration
1715from roborock .map .b01_q10_map_parser import (
1816 B01Q10MapParserConfig ,
3230from roborock .map .b01_q10_render import (
3331 _Q10_RESOLUTIONS ,
3432 Q10MapOverlays ,
35- Q10MapRender ,
3633 _erased_cells ,
37- draw_path_on_map ,
3834 render_q10_map ,
3935 solve_q10_calibration ,
4036)
@@ -58,7 +54,7 @@ def _render(
5854 * ,
5955 trace : Q10TracePacket | None = None ,
6056 overlays : Q10MapOverlays | None = None ,
61- ) -> Q10MapRender :
57+ ) -> bytes :
6258 return render_q10_map (
6359 packet if packet is not None else _packet (),
6460 trace ,
@@ -96,44 +92,35 @@ def _calibrated_inputs(*, heading: int = 0) -> tuple[Q10MapPacket, Q10TracePacke
9692
9793def test_render_base_map_without_calibration () -> None :
9894 """Without a calibration only the base raster is produced."""
99- render = _render ()
100- assert render .image_content [:8 ] == b"\x89 PNG\r \n \x1a \n "
101- assert render .map_data is not None
102- # Overlays are world-coordinate only, so nothing is placed yet.
103- assert render .map_data .path is None
95+ image = _render ()
96+ assert image [:8 ] == b"\x89 PNG\r \n \x1a \n "
10497
10598
106- def test_render_places_path_and_position () -> None :
107- """The map and trace derive calibration, path and position together ."""
108- packet , trace = _calibrated_inputs (heading = 45 )
109- render = _render (packet , trace = trace )
99+ def test_render_draws_path_and_position () -> None :
100+ """The map and trace derive calibration and draw the robot position ."""
101+ packet , trace = _calibrated_inputs ()
102+ image = _render (packet , trace = trace )
110103 calibration = solve_q10_calibration (packet , trace )
111104 assert calibration is not None
112- assert render .map_data .path is not None
113- assert render .map_data .vacuum_position is not None
114105 assert trace .robot_position is not None
115- expected = calibration .world_to_pixel (trace .robot_position .x , trace .robot_position .y )
116- assert (render .map_data .vacuum_position .x , render .map_data .vacuum_position .y ) == expected
117- assert render .map_data .vacuum_position .a == - 45
106+ px , py = calibration .world_to_pixel (trace .robot_position .x , trace .robot_position .y )
107+ image_position = (round (px * CONFIG .map_scale ), round (py * CONFIG .map_scale ))
108+ rendered = Image .open (io .BytesIO (image )).convert ("RGBA" )
109+ assert rendered .size == (8 * 4 , 6 * 4 )
110+ assert rendered .getpixel (image_position ) == (255 , 211 , 0 , 255 )
118111
119112
120- def test_render_places_zones_and_charger () -> None :
121- """Decoded no-go / no-mop zones become pixel-space MapData areas + charger ."""
113+ def test_render_draws_zones_and_virtual_walls () -> None :
114+ """Decoded DPS overlays are included in the composed image ."""
122115 packet , trace = _calibrated_inputs ()
123116 zones = [
124117 Q10Zone (type = ZONE_TYPE_NO_GO , vertices = [(0 , 0 ), (4 , 0 ), (4 , 4 ), (0 , 4 )]),
125118 Q10Zone (type = ZONE_TYPE_NO_MOP , vertices = [(1 , 1 ), (2 , 1 ), (2 , 2 ), (1 , 2 )]),
126119 ]
127120 walls = [Q10Zone (type = ZONE_TYPE_VIRTUAL_WALL , vertices = [(0 , 0 ), (4 , 0 )])]
128- render = _render (packet , trace = trace , overlays = Q10MapOverlays (zones = zones , virtual_walls = walls ))
129- assert len (render .map_data .no_go_areas or []) == 1
130- assert len (render .map_data .no_mopping_areas or []) == 1
131- assert len (render .map_data .walls or []) == 1
132- calibration = solve_q10_calibration (packet , trace )
133- assert calibration is not None
134- assert render .map_data .charger is not None
135- expected = calibration .world_to_pixel (trace .points [0 ].x , trace .points [0 ].y )
136- assert (render .map_data .charger .x , render .map_data .charger .y ) == expected
121+ base = _render (packet , trace = trace )
122+ rendered = _render (packet , trace = trace , overlays = Q10MapOverlays (zones = zones , virtual_walls = walls ))
123+ assert rendered != base
137124
138125
139126def test_render_applies_erase_zones () -> None :
@@ -150,7 +137,7 @@ def test_render_applies_erase_zones() -> None:
150137 render = _render (replace (packet , erase_zones = [erase_zone ]), trace = trace )
151138
152139 assert len (cells ) == packet .layers .width * packet .layers .height
153- assert render . image_content != base . image_content # re-rendered
140+ assert render != base
154141
155142
156143def test_render_partial_erase () -> None :
@@ -167,55 +154,29 @@ def test_render_partial_erase() -> None:
167154 render = _render (replace (packet , erase_zones = [erase_zone ]), trace = trace )
168155
169156 assert 0 < len (cells ) < packet .layers .width * packet .layers .height
170- assert render .image_content != base .image_content
171-
172-
173- def test_draw_path_on_map_requires_projected_path () -> None :
174- """Drawing fails clearly when the source streams could not calibrate."""
175- with pytest .raises (RoborockException , match = "No calibration available" ):
176- draw_path_on_map (_render (), config = CONFIG )
177-
178-
179- def test_draw_path_on_map_draws_position () -> None :
180- """The robot position is drawn at the mapped pixel."""
181- packet , trace = _calibrated_inputs ()
182- render = _render (packet , trace = trace )
183- png = draw_path_on_map (
184- render ,
185- config = CONFIG ,
186- position_color = (255 , 211 , 0 , 255 ),
187- )
188- img = Image .open (io .BytesIO (png )).convert ("RGBA" )
189- position = render .map_data .vacuum_position
190- assert position is not None
191- image_position = (round (position .x * CONFIG .map_scale ), round (position .y * CONFIG .map_scale ))
192- assert img .size == (8 * 4 , 6 * 4 )
193- assert img .getpixel (image_position ) == (255 , 211 , 0 , 255 )
157+ assert render != base
194158
195159
196- def test_draw_path_on_map_draws_heading_indicator () -> None :
160+ def test_render_draws_heading_indicator () -> None :
197161 """A known heading draws a facing tick from the robot marker.
198162
199163 With heading 0 (= +x world) and the identity-ish calibration, the tick
200164 extends to the right of the robot pixel; with the marker at image (12, 12)
201165 the tick covers pixels at x > 12 along y == 12.
202166 """
203167 packet , trace = _calibrated_inputs (heading = 0 )
204- render = _render (packet , trace = trace )
205- png = draw_path_on_map (
206- render ,
207- config = CONFIG ,
208- position_color = (255 , 211 , 0 , 255 ),
209- )
210- img = Image .open (io .BytesIO (png )).convert ("RGBA" )
211- position = render .map_data .vacuum_position
212- assert position is not None
213- cx = round (position .x * CONFIG .map_scale )
214- cy = round (position .y * CONFIG .map_scale )
168+ image = _render (packet , trace = trace )
169+ calibration = solve_q10_calibration (packet , trace )
170+ assert calibration is not None
171+ assert trace .robot_position is not None
172+ px , py = calibration .world_to_pixel (trace .robot_position .x , trace .robot_position .y )
173+ cx = round (px * CONFIG .map_scale )
174+ cy = round (py * CONFIG .map_scale )
175+ rendered = Image .open (io .BytesIO (image )).convert ("RGBA" )
215176 # Tick runs +x from the marker (4 * radius = 16 px at scale 4).
216- assert img .getpixel ((cx + 8 , cy )) == (255 , 211 , 0 , 255 )
177+ assert rendered .getpixel ((cx + 8 , cy )) == (255 , 211 , 0 , 255 )
217178 # ...and not behind it (the marker is a small disc; sample well to the left)
218- assert img .getpixel ((cx - 8 , cy )) != (255 , 211 , 0 , 255 )
179+ assert rendered .getpixel ((cx - 8 , cy )) != (255 , 211 , 0 , 255 )
219180
220181
221182def test_solve_q10_calibration_uses_header_origin_with_short_path () -> None :
0 commit comments