valetudo-map-parser 0.1.9b49__tar.gz → 0.1.9b51__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/PKG-INFO +1 -1
  2. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/__init__.py +2 -0
  3. valetudo_map_parser-0.1.9b51/SCR/valetudo_map_parser/config/color_utils.py +105 -0
  4. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/colors.py +55 -71
  5. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/drawable.py +55 -37
  6. valetudo_map_parser-0.1.9b51/SCR/valetudo_map_parser/config/drawable_elements.py +307 -0
  7. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/enhanced_drawable.py +75 -193
  8. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/shared.py +0 -1
  9. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/types.py +19 -33
  10. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/utils.py +0 -67
  11. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/hypfer_draw.py +222 -57
  12. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/hypfer_handler.py +48 -155
  13. valetudo_map_parser-0.1.9b51/SCR/valetudo_map_parser/hypfer_rooms_handler.py +406 -0
  14. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/map_data.py +0 -30
  15. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/rand25_handler.py +2 -84
  16. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/pyproject.toml +1 -1
  17. valetudo_map_parser-0.1.9b49/SCR/valetudo_map_parser/config/color_utils.py +0 -51
  18. valetudo_map_parser-0.1.9b49/SCR/valetudo_map_parser/config/drawable_elements.py +0 -983
  19. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/LICENSE +0 -0
  20. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/NOTICE.txt +0 -0
  21. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/README.md +0 -0
  22. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  23. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  24. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  25. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
  26. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/config/room_outline.py +0 -0
  27. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/py.typed +0 -0
  28. {valetudo_map_parser-0.1.9b49 → valetudo_map_parser-0.1.9b51}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b49
3
+ Version: 0.1.9b51
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -15,11 +15,13 @@ from .config.types import (
15
15
  TrimCropData,
16
16
  UserLanguageStore,
17
17
  )
18
+ from .hypfer_rooms_handler import HypferRoomsHandler
18
19
  from .hypfer_handler import HypferMapImageHandler
19
20
  from .rand25_handler import ReImageHandler
20
21
 
21
22
 
22
23
  __all__ = [
24
+ "HypferRoomsHandler",
23
25
  "HypferMapImageHandler",
24
26
  "ReImageHandler",
25
27
  "RRMapParser",
@@ -0,0 +1,105 @@
1
+ """Utility functions for color operations in the map parser."""
2
+
3
+ from typing import Tuple, Optional
4
+
5
+ from .colors import ColorsManagement
6
+ from .types import NumpyArray, Color
7
+
8
+
9
+ def get_blended_color(
10
+ x0: int,
11
+ y0: int,
12
+ x1: int,
13
+ y1: int,
14
+ arr: Optional[NumpyArray],
15
+ color: Color,
16
+ ) -> Color:
17
+ """
18
+ Get a blended color for a pixel based on the current element map and the new element to draw.
19
+
20
+ This function:
21
+ 1. Gets the background colors at the start and end points (with offset to avoid sampling already drawn pixels)
22
+ 2. Directly blends the foreground color with the background using straight alpha
23
+ 3. Returns the average of the two blended colors
24
+
25
+ Returns:
26
+ Blended RGBA color to use for drawing
27
+ """
28
+ # Extract foreground color components
29
+ fg_r, fg_g, fg_b, fg_a = color
30
+ fg_alpha = fg_a / 255.0 # Convert to 0-1 range
31
+
32
+ # Fast path for fully opaque or transparent foreground
33
+ if fg_a == 255:
34
+ return color
35
+ if fg_a == 0:
36
+ # Sample background at midpoint
37
+ mid_x, mid_y = (x0 + x1) // 2, (y0 + y1) // 2
38
+ if 0 <= mid_y < arr.shape[0] and 0 <= mid_x < arr.shape[1]:
39
+ return tuple(arr[mid_y, mid_x])
40
+ return (0, 0, 0, 0) # Default if out of bounds
41
+
42
+ # Calculate direction vector for offset sampling
43
+ dx = x1 - x0
44
+ dy = y1 - y0
45
+ length = max(1, (dx**2 + dy**2) ** 0.5) # Avoid division by zero
46
+ offset = 5 # 5-pixel offset to avoid sampling already drawn pixels
47
+
48
+ # Calculate offset coordinates for start point (move away from the line)
49
+ offset_x0 = int(x0 - (offset * dx / length))
50
+ offset_y0 = int(y0 - (offset * dy / length))
51
+
52
+ # Calculate offset coordinates for end point (move away from the line)
53
+ offset_x1 = int(x1 + (offset * dx / length))
54
+ offset_y1 = int(y1 + (offset * dy / length))
55
+
56
+ # Sample background at offset start point
57
+ if 0 <= offset_y0 < arr.shape[0] and 0 <= offset_x0 < arr.shape[1]:
58
+ bg_color_start = arr[offset_y0, offset_x0]
59
+ # Direct straight alpha blending
60
+ start_r = int(fg_r * fg_alpha + bg_color_start[0] * (1 - fg_alpha))
61
+ start_g = int(fg_g * fg_alpha + bg_color_start[1] * (1 - fg_alpha))
62
+ start_b = int(fg_b * fg_alpha + bg_color_start[2] * (1 - fg_alpha))
63
+ start_a = int(fg_a + bg_color_start[3] * (1 - fg_alpha))
64
+ start_blended_color = (start_r, start_g, start_b, start_a)
65
+ else:
66
+ # If offset point is out of bounds, try original point
67
+ if 0 <= y0 < arr.shape[0] and 0 <= x0 < arr.shape[1]:
68
+ bg_color_start = arr[y0, x0]
69
+ start_r = int(fg_r * fg_alpha + bg_color_start[0] * (1 - fg_alpha))
70
+ start_g = int(fg_g * fg_alpha + bg_color_start[1] * (1 - fg_alpha))
71
+ start_b = int(fg_b * fg_alpha + bg_color_start[2] * (1 - fg_alpha))
72
+ start_a = int(fg_a + bg_color_start[3] * (1 - fg_alpha))
73
+ start_blended_color = (start_r, start_g, start_b, start_a)
74
+ else:
75
+ start_blended_color = color
76
+
77
+ # Sample background at offset end point
78
+ if 0 <= offset_y1 < arr.shape[0] and 0 <= offset_x1 < arr.shape[1]:
79
+ bg_color_end = arr[offset_y1, offset_x1]
80
+ # Direct straight alpha blending
81
+ end_r = int(fg_r * fg_alpha + bg_color_end[0] * (1 - fg_alpha))
82
+ end_g = int(fg_g * fg_alpha + bg_color_end[1] * (1 - fg_alpha))
83
+ end_b = int(fg_b * fg_alpha + bg_color_end[2] * (1 - fg_alpha))
84
+ end_a = int(fg_a + bg_color_end[3] * (1 - fg_alpha))
85
+ end_blended_color = (end_r, end_g, end_b, end_a)
86
+ else:
87
+ # If offset point is out of bounds, try original point
88
+ if 0 <= y1 < arr.shape[0] and 0 <= x1 < arr.shape[1]:
89
+ bg_color_end = arr[y1, x1]
90
+ end_r = int(fg_r * fg_alpha + bg_color_end[0] * (1 - fg_alpha))
91
+ end_g = int(fg_g * fg_alpha + bg_color_end[1] * (1 - fg_alpha))
92
+ end_b = int(fg_b * fg_alpha + bg_color_end[2] * (1 - fg_alpha))
93
+ end_a = int(fg_a + bg_color_end[3] * (1 - fg_alpha))
94
+ end_blended_color = (end_r, end_g, end_b, end_a)
95
+ else:
96
+ end_blended_color = color
97
+
98
+ # Use the average of the two blended colors
99
+ blended_color = (
100
+ (start_blended_color[0] + end_blended_color[0]) // 2,
101
+ (start_blended_color[1] + end_blended_color[1]) // 2,
102
+ (start_blended_color[2] + end_blended_color[2]) // 2,
103
+ (start_blended_color[3] + end_blended_color[3]) // 2,
104
+ )
105
+ return blended_color
@@ -139,7 +139,6 @@ color_array = [
139
139
  ]
140
140
 
141
141
 
142
-
143
142
  class SupportedColor(StrEnum):
144
143
  """Color of a supported map element."""
145
144
 
@@ -408,61 +407,56 @@ class ColorsManagement:
408
407
  def blend_colors(background: Color, foreground: Color) -> Color:
409
408
  """
410
409
  Blend foreground color with background color based on alpha values.
411
-
412
- This is used when drawing elements that overlap on the map.
413
- The alpha channel determines how much of the foreground color is visible.
414
- Uses optimized calculations for better performance.
410
+ Optimized version with more fast paths and simplified calculations.
415
411
 
416
412
  :param background: Background RGBA color (r,g,b,a)
417
413
  :param foreground: Foreground RGBA color (r,g,b,a) to blend on top
418
414
  :return: Blended RGBA color
419
415
  """
420
- # Extract components
421
- bg_r, bg_g, bg_b, bg_a = background
422
- fg_r, fg_g, fg_b, fg_a = foreground
416
+ # Fast paths for common cases
417
+ fg_a = foreground[3]
423
418
 
424
- # Fast path for common cases
425
- if fg_a == 255:
419
+ if fg_a == 255: # Fully opaque foreground
426
420
  return foreground
427
- if fg_a == 0:
428
- return background
429
421
 
430
- # Calculate alpha blending
431
- # Convert alpha from [0-255] to [0-1] for calculations
432
- fg_alpha = fg_a / 255.0
433
- bg_alpha = bg_a / 255.0
422
+ if fg_a == 0: # Fully transparent foreground
423
+ return background
434
424
 
435
- # Calculate resulting alpha
436
- out_alpha = fg_alpha + bg_alpha * (1 - fg_alpha)
425
+ bg_a = background[3]
426
+ if bg_a == 0: # Fully transparent background
427
+ return foreground
437
428
 
438
- # Avoid division by zero
439
- if out_alpha < 0.0001:
440
- return Color[0, 0, 0, 0] # Fully transparent result
429
+ # Extract components (only after fast paths)
430
+ bg_r, bg_g, bg_b = background[:3]
431
+ fg_r, fg_g, fg_b = foreground[:3]
441
432
 
442
- # Calculate blended RGB components
443
- # Using a more efficient calculation method
444
- alpha_ratio = fg_alpha / out_alpha
445
- inv_alpha_ratio = 1.0 - alpha_ratio
433
+ # Pre-calculate the blend factor once (avoid repeated division)
434
+ blend = fg_a / 255.0
435
+ inv_blend = 1.0 - blend
446
436
 
447
- out_r = int(fg_r * alpha_ratio + bg_r * inv_alpha_ratio)
448
- out_g = int(fg_g * alpha_ratio + bg_g * inv_alpha_ratio)
449
- out_b = int(fg_b * alpha_ratio + bg_b * inv_alpha_ratio)
437
+ # Simple linear interpolation for RGB channels
438
+ # This is faster than the previous implementation
439
+ out_r = int(fg_r * blend + bg_r * inv_blend)
440
+ out_g = int(fg_g * blend + bg_g * inv_blend)
441
+ out_b = int(fg_b * blend + bg_b * inv_blend)
450
442
 
451
- # Convert alpha back to [0-255] range
452
- out_a = int(out_alpha * 255)
443
+ # Alpha blending - simplified calculation
444
+ out_a = int(fg_a + bg_a * inv_blend)
453
445
 
454
- # Ensure values are in valid range (using min/max for efficiency)
455
- out_r = max(0, min(255, out_r))
456
- out_g = max(0, min(255, out_g))
457
- out_b = max(0, min(255, out_b))
446
+ # No need for min/max checks as the blend math keeps values in range
447
+ # when input values are valid (0-255)
458
448
 
459
449
  return [out_r, out_g, out_b, out_a]
460
450
 
451
+ # Cache for recently sampled background colors
452
+ _bg_color_cache = {}
453
+ _cache_size = 1024 # Limit cache size to avoid memory issues
454
+
461
455
  @staticmethod
462
456
  def sample_and_blend_color(array, x: int, y: int, foreground: Color) -> Color:
463
457
  """
464
458
  Sample the background color from the array at coordinates (x,y) and blend with foreground color.
465
- Uses scipy.ndimage for efficient sampling when appropriate.
459
+ Optimized version with caching and faster sampling.
466
460
 
467
461
  Args:
468
462
  array: The RGBA numpy array representing the image
@@ -473,53 +467,43 @@ class ColorsManagement:
473
467
  Returns:
474
468
  Blended RGBA color
475
469
  """
476
- # Ensure coordinates are within bounds
470
+ # Fast path for fully opaque foreground - no need to sample or blend
471
+ if foreground[3] == 255:
472
+ return foreground
473
+
474
+ # Ensure array exists
477
475
  if array is None:
478
476
  return foreground
479
477
 
478
+ # Check if coordinates are within bounds
480
479
  height, width = array.shape[:2]
481
480
  if not (0 <= y < height and 0 <= x < width):
482
- return foreground # Return foreground if coordinates are out of bounds
483
-
484
- # Fast path for fully opaque foreground
485
- if foreground[3] == 255:
486
481
  return foreground
487
482
 
488
- # The array is in RGBA format with shape (height, width, 4)
489
- try:
490
- # Use scipy.ndimage for sampling with boundary handling
491
- # This is more efficient for large arrays and handles edge cases better
492
- if (
493
- array.size > 1000000
494
- ): # Only use for larger arrays where the overhead is worth it
495
- # Create coordinates array for the sampling point
496
- coordinates = np.array([[y, x]])
497
-
498
- # Sample each channel separately with nearest neighbor interpolation
499
- # This is faster than sampling all channels at once for large arrays
500
- r = ndimage.map_coordinates(
501
- array[..., 0], coordinates.T, order=0, mode="nearest"
502
- )[0]
503
- g = ndimage.map_coordinates(
504
- array[..., 1], coordinates.T, order=0, mode="nearest"
505
- )[0]
506
- b = ndimage.map_coordinates(
507
- array[..., 2], coordinates.T, order=0, mode="nearest"
508
- )[0]
509
- a = ndimage.map_coordinates(
510
- array[..., 3], coordinates.T, order=0, mode="nearest"
511
- )[0]
512
- background = (int(r), int(g), int(b), int(a))
513
- else:
514
- # For smaller arrays, direct indexing is faster
515
- background = tuple(array[y, x])
516
- except (IndexError, ValueError):
517
- # Fallback to direct indexing if ndimage fails
483
+ # Check cache for this coordinate
484
+ cache_key = (id(array), x, y)
485
+ cache = ColorsManagement._bg_color_cache
486
+
487
+ if cache_key in cache:
488
+ background = cache[cache_key]
489
+ else:
490
+ # Sample the background color using direct indexing (fastest method)
518
491
  try:
519
- background = tuple(array[y, x])
492
+ background = tuple(map(int, array[y, x]))
493
+
494
+ # Update cache (with simple LRU-like behavior)
495
+ if len(cache) >= ColorsManagement._cache_size:
496
+ # Remove a random entry if cache is full
497
+ cache.pop(next(iter(cache)))
498
+ cache[cache_key] = background
499
+
520
500
  except (IndexError, ValueError):
521
501
  return foreground
522
502
 
503
+ # Fast path for fully transparent foreground
504
+ if foreground[3] == 0:
505
+ return background
506
+
523
507
  # Blend the colors
524
508
  return ColorsManagement.blend_colors(background, foreground)
525
509
 
@@ -75,7 +75,7 @@ class Drawable:
75
75
  # Get the region to update
76
76
  region_slice = (
77
77
  slice(row, row + pixel_size),
78
- slice(col + i * pixel_size, col + (i + 1) * pixel_size)
78
+ slice(col + i * pixel_size, col + (i + 1) * pixel_size),
79
79
  )
80
80
 
81
81
  if need_blending:
@@ -84,8 +84,10 @@ class Drawable:
84
84
  center_x = col + i * pixel_size + pixel_size // 2
85
85
 
86
86
  # Only blend if coordinates are valid
87
- if (0 <= center_y < image_array.shape[0] and
88
- 0 <= center_x < image_array.shape[1]):
87
+ if (
88
+ 0 <= center_y < image_array.shape[0]
89
+ and 0 <= center_x < image_array.shape[1]
90
+ ):
89
91
  # Get blended color
90
92
  blended_color = ColorsManagement.sample_and_blend_color(
91
93
  image_array, center_x, center_y, full_color
@@ -296,9 +298,11 @@ class Drawable:
296
298
  for x, y in zip(x_coords, y_coords):
297
299
  for i in range(-half_width, half_width + 1):
298
300
  for j in range(-half_width, half_width + 1):
299
- if (i*i + j*j <= half_width*half_width and # Make it round
300
- 0 <= x + i < layer.shape[1] and
301
- 0 <= y + j < layer.shape[0]):
301
+ if (
302
+ i * i + j * j <= half_width * half_width # Make it round
303
+ and 0 <= x + i < layer.shape[1]
304
+ and 0 <= y + j < layer.shape[0]
305
+ ):
302
306
  layer[y + j, x + i] = blended_color
303
307
 
304
308
  return layer
@@ -371,7 +375,7 @@ class Drawable:
371
375
  dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
372
376
 
373
377
  # Create masks for the circle and outline
374
- circle_mask = dist_sq <= radius ** 2
378
+ circle_mask = dist_sq <= radius**2
375
379
 
376
380
  # Apply the fill color
377
381
  image[min_y:max_y, min_x:max_x][circle_mask] = color
@@ -468,7 +472,7 @@ class Drawable:
468
472
  adjusted_points = [(p[0] - min_x, p[1] - min_y) for p in points]
469
473
 
470
474
  # Create a grid of coordinates and use it to test all points at once
471
- y_indices, x_indices = np.mgrid[0:mask.shape[0], 0:mask.shape[1]]
475
+ y_indices, x_indices = np.mgrid[0 : mask.shape[0], 0 : mask.shape[1]]
472
476
 
473
477
  # Test each point in the grid
474
478
  for i in range(mask.shape[0]):
@@ -476,7 +480,7 @@ class Drawable:
476
480
  mask[i, j] = Drawable.point_inside(j, i, adjusted_points)
477
481
 
478
482
  # Apply the fill color to the masked region
479
- arr[min_y:max_y+1, min_x:max_x+1][mask] = fill_color
483
+ arr[min_y : max_y + 1, min_x : max_x + 1][mask] = fill_color
480
484
 
481
485
  return arr
482
486
 
@@ -530,7 +534,7 @@ class Drawable:
530
534
  y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
531
535
 
532
536
  # Create a circular mask
533
- mask = (y_indices - y)**2 + (x_indices - x)**2 <= dot_radius**2
537
+ mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
534
538
 
535
539
  # Apply the color to the masked region
536
540
  layers[y_min:y_max, x_min:x_max][mask] = blended_color
@@ -607,8 +611,12 @@ class Drawable:
607
611
  y2 = int(tmp_y + r_cover * math.cos(a2))
608
612
 
609
613
  # Draw the direction line
610
- if (0 <= x1 < tmp_width and 0 <= y1 < tmp_height and
611
- 0 <= x2 < tmp_width and 0 <= y2 < tmp_height):
614
+ if (
615
+ 0 <= x1 < tmp_width
616
+ and 0 <= y1 < tmp_height
617
+ and 0 <= x2 < tmp_width
618
+ and 0 <= y2 < tmp_height
619
+ ):
612
620
  tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
613
621
 
614
622
  # Draw the lidar indicator
@@ -694,14 +702,14 @@ class Drawable:
694
702
  # Get image dimensions
695
703
  height, width = image.shape[:2]
696
704
 
697
- if element_type == 'circle':
705
+ if element_type == "circle":
698
706
  # Extract circle centers and radii
699
707
  centers = []
700
708
  radii = []
701
709
  for elem in elements:
702
- if isinstance(elem, dict) and 'center' in elem and 'radius' in elem:
703
- centers.append(elem['center'])
704
- radii.append(elem['radius'])
710
+ if isinstance(elem, dict) and "center" in elem and "radius" in elem:
711
+ centers.append(elem["center"])
712
+ radii.append(elem["radius"])
705
713
  elif isinstance(elem, (list, tuple)) and len(elem) >= 3:
706
714
  # Format: (x, y, radius)
707
715
  centers.append((elem[0], elem[1]))
@@ -709,7 +717,9 @@ class Drawable:
709
717
 
710
718
  # Process circles with the same radius together
711
719
  for radius in set(radii):
712
- same_radius_centers = [centers[i] for i in range(len(centers)) if radii[i] == radius]
720
+ same_radius_centers = [
721
+ centers[i] for i in range(len(centers)) if radii[i] == radius
722
+ ]
713
723
  if same_radius_centers:
714
724
  # Create a combined mask for all circles with this radius
715
725
  mask = np.zeros((height, width), dtype=bool)
@@ -725,20 +735,22 @@ class Drawable:
725
735
  y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
726
736
 
727
737
  # Add this circle to the mask
728
- circle_mask = (y_indices - cy)**2 + (x_indices - cx)**2 <= radius**2
738
+ circle_mask = (y_indices - cy) ** 2 + (
739
+ x_indices - cx
740
+ ) ** 2 <= radius**2
729
741
  mask[min_y:max_y, min_x:max_x] |= circle_mask
730
742
 
731
743
  # Apply color to all circles at once
732
744
  image[mask] = color
733
745
 
734
- elif element_type == 'line':
746
+ elif element_type == "line":
735
747
  # Extract line endpoints
736
748
  lines = []
737
749
  widths = []
738
750
  for elem in elements:
739
- if isinstance(elem, dict) and 'start' in elem and 'end' in elem:
740
- lines.append((elem['start'], elem['end']))
741
- widths.append(elem.get('width', 1))
751
+ if isinstance(elem, dict) and "start" in elem and "end" in elem:
752
+ lines.append((elem["start"], elem["end"]))
753
+ widths.append(elem.get("width", 1))
742
754
  elif isinstance(elem, (list, tuple)) and len(elem) >= 4:
743
755
  # Format: (x1, y1, x2, y2, [width])
744
756
  lines.append(((elem[0], elem[1]), (elem[2], elem[3])))
@@ -746,19 +758,25 @@ class Drawable:
746
758
 
747
759
  # Process lines with the same width together
748
760
  for width in set(widths):
749
- same_width_lines = [lines[i] for i in range(len(lines)) if widths[i] == width]
761
+ same_width_lines = [
762
+ lines[i] for i in range(len(lines)) if widths[i] == width
763
+ ]
750
764
  if same_width_lines:
751
765
  # Create a combined mask for all lines with this width
752
766
  mask = np.zeros((height, width), dtype=bool)
753
767
 
754
768
  # Draw all lines into the mask
755
- for (start, end) in same_width_lines:
769
+ for start, end in same_width_lines:
756
770
  x1, y1 = start
757
771
  x2, y2 = end
758
772
 
759
773
  # Skip invalid lines
760
- if not (0 <= x1 < width and 0 <= y1 < height and
761
- 0 <= x2 < width and 0 <= y2 < height):
774
+ if not (
775
+ 0 <= x1 < width
776
+ and 0 <= y1 < height
777
+ and 0 <= x2 < width
778
+ and 0 <= y2 < height
779
+ ):
762
780
  continue
763
781
 
764
782
  # Use Bresenham's algorithm to get line points
@@ -783,8 +801,12 @@ class Drawable:
783
801
  max_x = min(width, x + half_width + 1)
784
802
 
785
803
  # Create a circular brush
786
- y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
787
- brush = (y_indices - y)**2 + (x_indices - x)**2 <= half_width**2
804
+ y_indices, x_indices = np.ogrid[
805
+ min_y:max_y, min_x:max_x
806
+ ]
807
+ brush = (y_indices - y) ** 2 + (
808
+ x_indices - x
809
+ ) ** 2 <= half_width**2
788
810
  mask[min_y:max_y, min_x:max_x] |= brush
789
811
 
790
812
  # Apply color to all lines at once
@@ -826,20 +848,16 @@ class Drawable:
826
848
  )
827
849
 
828
850
  # Add to centers list with radius
829
- centers.append({
830
- 'center': (x, y),
831
- 'radius': 6,
832
- 'color': obstacle_color
833
- })
851
+ centers.append({"center": (x, y), "radius": 6, "color": obstacle_color})
834
852
  except (KeyError, TypeError):
835
853
  continue
836
854
 
837
855
  # Draw each obstacle with its blended color
838
856
  if centers:
839
857
  for obstacle in centers:
840
- cx, cy = obstacle['center']
841
- radius = obstacle['radius']
842
- obs_color = obstacle['color']
858
+ cx, cy = obstacle["center"]
859
+ radius = obstacle["radius"]
860
+ obs_color = obstacle["color"]
843
861
 
844
862
  # Create a small mask for the obstacle
845
863
  min_y = max(0, cy - radius)
@@ -851,7 +869,7 @@ class Drawable:
851
869
  y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
852
870
 
853
871
  # Create a circular mask
854
- mask = (y_indices - cy)**2 + (x_indices - cx)**2 <= radius**2
872
+ mask = (y_indices - cy) ** 2 + (x_indices - cx) ** 2 <= radius**2
855
873
 
856
874
  # Apply the color to the masked region
857
875
  image[min_y:max_y, min_x:max_x][mask] = obs_color