99from dataclasses import replace
1010from pathlib import Path
1111
12+ import pytest
1213from PIL import Image
1314
15+ from roborock .exceptions import RoborockException
1416from roborock .map .b01_grid_layers import GridCalibration
1517from roborock .map .b01_q10_map_parser import (
1618 B01Q10MapParserConfig ,
3133 _Q10_RESOLUTIONS ,
3234 Q10MapOverlays ,
3335 Q10MapRender ,
36+ _erased_cells ,
3437 draw_path_on_map ,
3538 render_q10_map ,
3639 solve_q10_calibration ,
@@ -92,13 +95,10 @@ def _calibrated_inputs(*, heading: int = 0) -> tuple[Q10MapPacket, Q10TracePacke
9295
9396
9497def test_render_base_map_without_calibration () -> None :
95- """Without a calibration only the base raster/layers/rooms are produced."""
98+ """Without a calibration only the base raster is produced."""
9699 render = _render ()
97100 assert render .image_content [:8 ] == b"\x89 PNG\r \n \x1a \n "
98101 assert render .map_data is not None
99- assert render .calibration is None
100- assert {room .id : room .name for room in render .rooms } == {2 : "Living Room" , 3 : "Bedroom" }
101- assert render .layers .class_counts .get ("floor" ) == 26
102102 # Overlays are world-coordinate only, so nothing is placed yet.
103103 assert render .map_data .path is None
104104
@@ -107,13 +107,14 @@ def test_render_places_path_and_position() -> None:
107107 """The map and trace derive calibration, path and position together."""
108108 packet , trace = _calibrated_inputs (heading = 45 )
109109 render = _render (packet , trace = trace )
110- assert render .calibration is not None
110+ calibration = solve_q10_calibration (packet , trace )
111+ assert calibration is not None
111112 assert render .map_data .path is not None
112113 assert render .map_data .vacuum_position is not None
113114 assert trace .robot_position is not None
114- expected = render . calibration .world_to_pixel (trace .robot_position .x , trace .robot_position .y )
115+ expected = calibration .world_to_pixel (trace .robot_position .x , trace .robot_position .y )
115116 assert (render .map_data .vacuum_position .x , render .map_data .vacuum_position .y ) == expected
116- assert render .map_data .vacuum_position .a == 45
117+ assert render .map_data .vacuum_position .a == - 45
117118
118119
119120def test_render_places_zones_and_charger () -> None :
@@ -128,43 +129,51 @@ def test_render_places_zones_and_charger() -> None:
128129 assert len (render .map_data .no_go_areas or []) == 1
129130 assert len (render .map_data .no_mopping_areas or []) == 1
130131 assert len (render .map_data .walls or []) == 1
131- assert render .calibration is not None
132+ calibration = solve_q10_calibration (packet , trace )
133+ assert calibration is not None
132134 assert render .map_data .charger is not None
133- expected = render . calibration .world_to_pixel (trace .points [0 ].x , trace .points [0 ].y )
135+ expected = calibration .world_to_pixel (trace .points [0 ].x , trace .points [0 ].y )
134136 assert (render .map_data .charger .x , render .map_data .charger .y ) == expected
135137
136138
137139def test_render_applies_erase_zones () -> None :
138- """With a calibration, erase-zone cells are blanked from layers + image."""
140+ """With a calibration, erase-zone cells are blanked from the image."""
139141 packet , trace = _calibrated_inputs ()
140142 base = _render (packet , trace = trace )
141- before_floor = base .layers .class_counts .get ("floor" )
142- assert before_floor and before_floor > 0
143- assert base .calibration is not None
143+ calibration = solve_q10_calibration (packet , trace )
144+ assert calibration is not None
144145
145146 # A rectangle covering the whole grid in world coords erases every cell.
146147 corners = [(- 1 , - 1 ), (8 , - 1 ), (8 , 6 ), (- 1 , 6 )]
147- erase_zone = Q10EraseZone (vertices = _world_vertices (base .calibration , corners ))
148+ erase_zone = Q10EraseZone (vertices = _world_vertices (calibration , corners ))
149+ cells = _erased_cells (packet .layers , [erase_zone ], calibration )
148150 render = _render (replace (packet , erase_zones = [erase_zone ]), trace = trace )
149151
150- assert render . layers . class_counts . get ( "floor" , 0 ) == 0 # all floor erased
152+ assert len ( cells ) == packet . layers . width * packet . layers . height
151153 assert render .image_content != base .image_content # re-rendered
152154
153155
154156def test_render_partial_erase () -> None :
155157 """An erase rectangle only blanks the cells it covers, leaving the rest."""
156158 packet , trace = _calibrated_inputs ()
157159 base = _render (packet , trace = trace )
158- before_floor = base . layers . class_counts . get ( "floor" , 0 )
159- assert base . calibration is not None
160+ calibration = solve_q10_calibration ( packet , trace )
161+ assert calibration is not None
160162
161163 # Cover only the top two grid rows.
162164 corners = [(- 1 , - 1 ), (8 , - 1 ), (8 , 2 ), (- 1 , 2 )]
163- erase_zone = Q10EraseZone (vertices = _world_vertices (base .calibration , corners ))
165+ erase_zone = Q10EraseZone (vertices = _world_vertices (calibration , corners ))
166+ cells = _erased_cells (packet .layers , [erase_zone ], calibration )
164167 render = _render (replace (packet , erase_zones = [erase_zone ]), trace = trace )
165168
166- after_floor = render .layers .class_counts .get ("floor" , 0 )
167- assert 0 < after_floor < before_floor # some, not all, floor removed
169+ 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 )
168177
169178
170179def test_draw_path_on_map_draws_position () -> None :
0 commit comments