Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions ui/canvas/items/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,17 @@ def __init__(
self.index_in_model = index_in_model
rw = getattr(self.canvas_view, "robot_length_m", 0.60)
rh = getattr(self.canvas_view, "robot_width_m", 0.60)
protrusion_front = max(0.0, float(getattr(self.canvas_view, "robot_protrusion_front_m", 0.0)))
protrusion_back = max(0.0, float(getattr(self.canvas_view, "robot_protrusion_back_m", 0.0)))
protrusion_left = max(0.0, float(getattr(self.canvas_view, "robot_protrusion_left_m", 0.0)))
protrusion_right = max(0.0, float(getattr(self.canvas_view, "robot_protrusion_right_m", 0.0)))
pen_width_m = OUTLINE_THICK_M if (outline_color and not dashed_outline) else OUTLINE_THIN_M
inset = (pen_width_m if outline_color else 0.0) * 0.5
self.setRect(-(rw / 2.0) + inset, -(rh / 2.0) + inset, rw - inset * 2, rh - inset * 2)
x_min = -(rw / 2.0) - protrusion_back
x_max = (rw / 2.0) + protrusion_front
y_min = -(rh / 2.0) - protrusion_right
y_max = (rh / 2.0) + protrusion_left
self.setRect(x_min + inset, y_min + inset, (x_max - x_min) - inset * 2, (y_max - y_min) - inset * 2)
self.setPos(self.canvas_view._scene_from_model(center_m.x(), center_m.y()))
pen = QPen(outline_color or QColor("#000"), pen_width_m if outline_color else 0.0)
if dashed_outline:
Expand Down Expand Up @@ -192,8 +200,9 @@ def __init__(
self._angle_radians: float = 0.0

def _build_triangle(self, color: QColor):
rw = getattr(self.canvas_view, "robot_length_m", 0.60)
rh = getattr(self.canvas_view, "robot_width_m", 0.60)
r = self.rect()
rw = float(r.width())
rh = float(r.height())
base_size = min(rw, rh) * TRIANGLE_REL_SIZE
half_base = base_size * 0.5
height = base_size
Expand Down
34 changes: 29 additions & 5 deletions ui/canvas/items/sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def __init__(self, canvas_view: "CanvasView"):
self.canvas_view = canvas_view
robot_width_m = 0.5
robot_length_m = 0.5
protrusion_front_m = 0.0
protrusion_back_m = 0.0
protrusion_left_m = 0.0
protrusion_right_m = 0.0
try:
if hasattr(canvas_view, "_project_manager") and canvas_view._project_manager:
if hasattr(canvas_view._project_manager, "config_as_dict"):
Expand All @@ -28,21 +32,41 @@ def __init__(self, canvas_view: "CanvasView"):
cfg = dict(getattr(canvas_view._project_manager, "config", {}) or {})
robot_width_m = float(cfg.get("robot_width_meters", robot_width_m))
robot_length_m = float(cfg.get("robot_length_meters", robot_length_m))
protrusion_front_m = max(0.0, float(cfg.get("robot_protrusion_front_meters", protrusion_front_m)))
protrusion_back_m = max(0.0, float(cfg.get("robot_protrusion_back_meters", protrusion_back_m)))
protrusion_left_m = max(0.0, float(cfg.get("robot_protrusion_left_meters", protrusion_left_m)))
protrusion_right_m = max(0.0, float(cfg.get("robot_protrusion_right_meters", protrusion_right_m)))
except Exception:
pass
self.setRect(-robot_length_m / 2, -robot_width_m / 2, robot_length_m, robot_width_m)
x_min = -(robot_length_m / 2.0) - protrusion_back_m
x_max = (robot_length_m / 2.0) + protrusion_front_m
y_min = -(robot_width_m / 2.0) - protrusion_right_m
y_max = (robot_width_m / 2.0) + protrusion_left_m
self.setRect(x_min, y_min, x_max - x_min, y_max - y_min)
self.setBrush(QBrush(QColor(255, 165, 0, 120)))
self.setPen(QPen(QColor("#000000"), 0.03))
self.setZValue(15)
self.setFlag(QGraphicsItem.ItemIsMovable, False)
self.setFlag(QGraphicsItem.ItemIsSelectable, False)
self.triangle_item = QGraphicsPolygonItem(self)
self._build_triangle(robot_length_m, robot_width_m)
self._build_triangle(float(self.rect().width()), float(self.rect().height()))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sim triangle offset uses protrusion-inclusive width

Low Severity

_build_triangle is now called with self.rect().width() and self.rect().height() which include protrusions. Inside _build_triangle, triangle_offset = robot_length_m * 0.3 uses this inflated value to position the direction indicator. With asymmetric protrusions, the triangle shifts away from the robot body center into the protrusion area, since the rect origin (0,0) is the body center but the offset is scaled to the full protrusion-inclusive extent. Previously, the actual body dimensions were passed, keeping the triangle within the robot body.

Additional Locations (1)

Fix in Cursor Fix in Web

self._angle_radians = 0.0

def set_dimensions(self, length_m: float, width_m: float):
self.setRect(-length_m / 2.0, -width_m / 2.0, length_m, width_m)
self._build_triangle(length_m, width_m)
def set_dimensions(
self,
length_m: float,
width_m: float,
protrusion_front_m: float = 0.0,
protrusion_back_m: float = 0.0,
protrusion_left_m: float = 0.0,
protrusion_right_m: float = 0.0,
):
x_min = -(length_m / 2.0) - max(0.0, protrusion_back_m)
x_max = (length_m / 2.0) + max(0.0, protrusion_front_m)
y_min = -(width_m / 2.0) - max(0.0, protrusion_right_m)
y_max = (width_m / 2.0) + max(0.0, protrusion_left_m)
self.setRect(x_min, y_min, x_max - x_min, y_max - y_min)
self._build_triangle(float(self.rect().width()), float(self.rect().height()))

def _build_triangle(self, robot_length_m: float, robot_width_m: float):
if not self.triangle_item:
Expand Down
36 changes: 32 additions & 4 deletions ui/canvas/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ def __init__(self, parent=None):
self._pan_start: Optional[QPoint] = None
self.robot_length_m = ELEMENT_RECT_WIDTH_M
self.robot_width_m = ELEMENT_RECT_HEIGHT_M
self.robot_protrusion_front_m = 0.0
self.robot_protrusion_back_m = 0.0
self.robot_protrusion_left_m = 0.0
self.robot_protrusion_right_m = 0.0
self._field_offset: float = FIELD_OFFSET_M # 0.5m for 2026
self.graphics_scene = QGraphicsScene(self)
self.setScene(self.graphics_scene)
Expand Down Expand Up @@ -189,6 +193,16 @@ def set_robot_dimensions(self, length_m: float, width_m: float):
try:
self.robot_length_m = float(length_m)
self.robot_width_m = float(width_m)
cfg = {}
if hasattr(self, "_project_manager") and self._project_manager:
if hasattr(self._project_manager, "config_as_dict"):
cfg = self._project_manager.config_as_dict()
else:
cfg = dict(getattr(self._project_manager, "config", {}) or {})
self.robot_protrusion_front_m = max(0.0, float(cfg.get("robot_protrusion_front_meters", 0.0)))
self.robot_protrusion_back_m = max(0.0, float(cfg.get("robot_protrusion_back_meters", 0.0)))
self.robot_protrusion_left_m = max(0.0, float(cfg.get("robot_protrusion_left_meters", 0.0)))
self.robot_protrusion_right_m = max(0.0, float(cfg.get("robot_protrusion_right_meters", 0.0)))
except Exception:
return
self._rebuild_items()
Expand All @@ -197,7 +211,7 @@ def set_robot_dimensions(self, length_m: float, width_m: float):
try:
self._ensure_sim_robot_item()
if self._sim_robot_item:
self._sim_robot_item.set_dimensions(self.robot_length_m, self.robot_width_m)
self._sim_robot_item.set_dimensions(self.robot_length_m, self.robot_width_m, self.robot_protrusion_front_m, self.robot_protrusion_back_m, self.robot_protrusion_left_m, self.robot_protrusion_right_m)
except Exception:
pass

Expand Down Expand Up @@ -671,9 +685,23 @@ def _model_from_scene(self, x_s: float, y_s: float) -> Tuple[float, float]:
"""
return float(x_s - self._field_offset), float(FIELD_WIDTH_METERS - y_s - self._field_offset)

def _robot_half_extents(self) -> Tuple[float, float]:
half_x = max(0.0, self.robot_length_m * 0.5 + self.robot_protrusion_front_m)
half_x_back = max(0.0, self.robot_length_m * 0.5 + self.robot_protrusion_back_m)
half_y = max(0.0, self.robot_width_m * 0.5 + self.robot_protrusion_left_m)
half_y_right = max(0.0, self.robot_width_m * 0.5 + self.robot_protrusion_right_m)
return max(half_x, half_x_back), max(half_y, half_y_right)

def _clamp_scene_coords(self, x_s: float, y_s: float) -> Tuple[float, float]:
return max(0.0, min(x_s, FIELD_LENGTH_METERS)), max(0.0, min(y_s, FIELD_WIDTH_METERS))

def _clamp_scene_coords_with_robot_perimeter(self, x_s: float, y_s: float) -> Tuple[float, float]:
hx, hy = self._robot_half_extents()
return (
max(hx, min(x_s, FIELD_LENGTH_METERS - hx)),
max(hy, min(y_s, FIELD_WIDTH_METERS - hy)),
)

def _constrain_scene_coords_for_index(
self, index: int, x_s: float, y_s: float
) -> Tuple[float, float]:
Expand All @@ -685,7 +713,7 @@ def _constrain_scene_coords_for_index(
except Exception:
return x_s, y_s
if kind not in ("rotation", "event_trigger"):
return x_s, y_s
return self._clamp_scene_coords_with_robot_perimeter(x_s, y_s)
prev_pos, next_pos = self._find_neighbor_item_positions(index)
if prev_pos is None or next_pos is None:
return x_s, y_s
Expand All @@ -700,7 +728,7 @@ def _constrain_scene_coords_for_index(
t = max(0.0, min(1.0, t))
proj_x = ax + t * dx
proj_y = ay + t * dy
return self._clamp_scene_coords(proj_x, proj_y)
return self._clamp_scene_coords_with_robot_perimeter(proj_x, proj_y)

def _find_neighbor_item_positions(
self, index: int
Expand Down Expand Up @@ -821,7 +849,7 @@ def _ensure_sim_robot_item(self):
self._sim_robot_item = item
item.setVisible(False)
try:
item.set_dimensions(self.robot_length_m, self.robot_width_m)
item.set_dimensions(self.robot_length_m, self.robot_width_m, self.robot_protrusion_front_m, self.robot_protrusion_back_m, self.robot_protrusion_left_m, self.robot_protrusion_right_m)
except Exception:
pass
except Exception:
Expand Down
28 changes: 28 additions & 0 deletions ui/config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,34 @@ def add_spin(
(0.05, 5.0),
0.01,
)
add_spin(
"robot_protrusion_front_meters",
"Robot Protrusion Front (m)",
cfg.get("robot_protrusion_front_meters", 0.0) or 0.0,
(0.0, 2.0),
0.01,
)
add_spin(
"robot_protrusion_back_meters",
"Robot Protrusion Back (m)",
cfg.get("robot_protrusion_back_meters", 0.0) or 0.0,
(0.0, 2.0),
0.01,
)
add_spin(
"robot_protrusion_left_meters",
"Robot Protrusion Left (m)",
cfg.get("robot_protrusion_left_meters", 0.0) or 0.0,
(0.0, 2.0),
0.01,
)
add_spin(
"robot_protrusion_right_meters",
"Robot Protrusion Right (m)",
cfg.get("robot_protrusion_right_meters", 0.0) or 0.0,
(0.0, 2.0),
0.01,
)

# Optional defaults
add_spin(
Expand Down
4 changes: 4 additions & 0 deletions ui/main_window/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,10 @@ def _get_config_key_label(self, key: str) -> str:
labels = {
"robot_length_meters": "Robot Length",
"robot_width_meters": "Robot Width",
"robot_protrusion_front_meters": "Robot Protrusion Front",
"robot_protrusion_back_meters": "Robot Protrusion Back",
"robot_protrusion_left_meters": "Robot Protrusion Left",
"robot_protrusion_right_meters": "Robot Protrusion Right",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protrusion config changes skip live canvas update

Medium Severity

The protrusion config keys are added to _get_config_key_label and the config dialog spinners, but _on_config_live_change at line 589 only triggers _apply_robot_dims_from_config when key in ("robot_length_meters", "robot_width_meters"). When a user changes any robot_protrusion_*_meters spinner, the value is saved to disk but _apply_robot_dims_from_config is never called, so the canvas view's robot_protrusion_*_m attributes remain stale, items aren't rebuilt, and the sim robot item isn't resized — no live visual feedback occurs for protrusion changes unlike dimension changes.

Fix in Cursor Fix in Web

"max_velocity_meters_per_sec": "Default Max Velocity",
"max_acceleration_meters_per_sec2": "Default Max Accel",
"intermediate_handoff_radius_meters": "Default Handoff Radius",
Expand Down
6 changes: 6 additions & 0 deletions ui/sidebar/components/element_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ def get_robot_dimensions(self) -> Tuple[float, float]:
cfg = dict(getattr(self.project_manager, "config", {}) or {})
length_m = float(cfg.get("robot_length_meters", length_m))
width_m = float(cfg.get("robot_width_meters", width_m))
length_m += max(0.0, float(cfg.get("robot_protrusion_front_meters", 0.0))) + max(
0.0, float(cfg.get("robot_protrusion_back_meters", 0.0))
)
width_m += max(0.0, float(cfg.get("robot_protrusion_left_meters", 0.0))) + max(
0.0, float(cfg.get("robot_protrusion_right_meters", 0.0))
)
except Exception:
pass
return length_m, width_m
Expand Down
4 changes: 4 additions & 0 deletions utils/project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
class ProjectConfig:
robot_length_meters: float = 0.5
robot_width_meters: float = 0.5
robot_protrusion_front_meters: float = 0.0
robot_protrusion_back_meters: float = 0.0
robot_protrusion_left_meters: float = 0.0
robot_protrusion_right_meters: float = 0.0
default_max_velocity_meters_per_sec: float = 4.5
default_max_acceleration_meters_per_sec2: float = 7.0
default_intermediate_handoff_radius_meters: float = 0.2
Expand Down