BigUI WIP: Curve Speed Border + SLC
This commit is contained in:
@@ -222,7 +222,8 @@ purge_host_desktop_ui_artifacts() {
|
||||
"${WORK_DIR}/selfdrive/ui/main.o" \
|
||||
"${WORK_DIR}/selfdrive/ui/moc_ui.o" \
|
||||
"${WORK_DIR}/selfdrive/ui/ui.o" \
|
||||
"${WORK_DIR}/selfdrive/ui/ui"
|
||||
"${WORK_DIR}/selfdrive/ui/ui" \
|
||||
"${WORK_DIR}/cereal/gen/cpp/"*.capnp.o
|
||||
}
|
||||
|
||||
ensure_host_python_tools() {
|
||||
@@ -324,7 +325,14 @@ sync_worktree() {
|
||||
rsync_args+=(--exclude "${pattern}")
|
||||
done
|
||||
|
||||
local _capnp_before
|
||||
_capnp_before="$(stat -c '%Y' "${WORK_DIR}/cereal/custom.capnp" 2>/dev/null || echo 0)"
|
||||
rsync "${rsync_args[@]}" "${ROOT_DIR}/" "${WORK_DIR}/"
|
||||
local _capnp_after
|
||||
_capnp_after="$(stat -c '%Y' "${WORK_DIR}/cereal/custom.capnp" 2>/dev/null || echo 0)"
|
||||
if [[ "${_capnp_before}" != "${_capnp_after}" ]]; then
|
||||
rm -rf "${SP_SCONS_CACHE_DIR:-${HOST_ROOT}/scons_cache}"
|
||||
fi
|
||||
purge_host_desktop_ui_artifacts
|
||||
rm -f "${WORK_DIR}/third_party/libjson11.a" "${WORK_DIR}/third_party/libkaitai.a"
|
||||
sync_host_generated_headers
|
||||
|
||||
@@ -260,6 +260,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Curve Speed Controller"),
|
||||
"desc": tr_noop("Automatically slow down for upcoming curves using data learned from your driving style."),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("CurveSpeedController"),
|
||||
"set_state": lambda s: self._params.put_bool("CurveSpeedController", s),
|
||||
@@ -268,6 +269,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Status Widget"),
|
||||
"desc": tr_noop("Show the Curve Speed Controller ambient effect on the driving screen."),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("ShowCSCStatus"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowCSCStatus", s),
|
||||
@@ -275,6 +277,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Calibrated Lateral Accel"),
|
||||
"desc": tr_noop("The learned lateral acceleration from collected driving data. Higher values allow faster cornering."),
|
||||
"type": "value",
|
||||
"get_value": lambda: f"{self._params_memory.get_float('CalibratedLateralAcceleration'):.2f} m/s²",
|
||||
"on_click": lambda: None,
|
||||
@@ -282,6 +285,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Calibration Progress"),
|
||||
"desc": tr_noop("How much curve data has been collected. Normal for the value to stay low."),
|
||||
"type": "value",
|
||||
"get_value": lambda: f"{self._params_memory.get_float('CalibrationProgress'):.2f}%",
|
||||
"on_click": lambda: None,
|
||||
@@ -289,6 +293,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Reset Curve Data"),
|
||||
"desc": tr_noop("Reset collected user data for Curve Speed Controller."),
|
||||
"type": "hub",
|
||||
"on_click": lambda: self._reset_curve_data(),
|
||||
"color": "#1BA1E2",
|
||||
@@ -299,8 +304,9 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
|
||||
def _reset_curve_data(self):
|
||||
def on_close(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.remove("CalibratedLateralAcceleration")
|
||||
self._params.put_float("CalibratedLateralAcceleration", 2.00)
|
||||
self._params.remove("CalibrationProgress")
|
||||
self._params.remove("CurvatureData")
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close))
|
||||
@@ -576,6 +582,15 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Speed Limit Controller"),
|
||||
"desc": tr_noop("Limit the car's maximum speed to the current speed limit."),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("SpeedLimitController"),
|
||||
"set_state": lambda s: self._params.put_bool("SpeedLimitController", s),
|
||||
"icon": "toggle_icons/icon_speed_limit.png",
|
||||
"color": "#1BA1E2",
|
||||
},
|
||||
{"title": tr_noop("SLC Offsets"), "panel": "slc_offsets", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"},
|
||||
{"title": tr_noop("SLC Quality of Life"), "panel": "slc_qol", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"},
|
||||
{"title": tr_noop("SLC Visuals"), "panel": "slc_visuals", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"},
|
||||
|
||||
@@ -90,14 +90,17 @@ class HubTile(MetroTile):
|
||||
|
||||
|
||||
class ToggleTile(MetroTile):
|
||||
def __init__(self, title: str, get_state: Callable[[], bool], set_state: Callable[[bool], None], icon_path: str | None = None, bg_color: rl.Color | str | None = None):
|
||||
def __init__(self, title: str, get_state: Callable[[], bool], set_state: Callable[[bool], None], icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None, desc: str = ""):
|
||||
if bg_color: super().__init__(bg_color=bg_color)
|
||||
else: super().__init__(bg_color=rl.Color(0, 163, 0, 255))
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.get_state = get_state
|
||||
self.set_state = set_state
|
||||
self._icon = gui_app.starpilot_texture(icon_path, 80, 80) if icon_path else None
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_desc = gui_app.font(FontWeight.NORMAL)
|
||||
self._active_color = self.bg_color
|
||||
self._inactive_color = rl.Color(120, 120, 120, 255)
|
||||
|
||||
@@ -122,6 +125,8 @@ class ToggleTile(MetroTile):
|
||||
title_x = rect.x + padding + (55 if self._icon else 0)
|
||||
max_title_width = rect.width - (title_x - rect.x) - padding
|
||||
self._draw_text_fit(self._font, self.title, rl.Vector2(title_x, rect.y + padding + 2), max_title_width, 35)
|
||||
if self.desc:
|
||||
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(title_x, rect.y + padding + 40), max_title_width, 22)
|
||||
state_text = tr("ON") if active else tr("OFF")
|
||||
ts = measure_text_cached(self._font, state_text, 30)
|
||||
rl.draw_text_ex(self._font, state_text, rl.Vector2(rect.x + rect.width - ts.x - padding, rect.y + rect.height - 50), 30, 0, rl.WHITE)
|
||||
@@ -129,34 +134,22 @@ class ToggleTile(MetroTile):
|
||||
|
||||
class ValueTile(MetroTile):
|
||||
def __init__(self, title: str, get_value: Callable[[], str], on_click: Callable, icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None, is_enabled: Callable[[], bool] | None = None):
|
||||
bg_color: rl.Color | str | None = None, is_enabled: Callable[[], bool] | None = None, desc: str = ""):
|
||||
super().__init__(bg_color=bg_color, on_click=on_click)
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.get_value = get_value
|
||||
self.is_enabled = is_enabled or (lambda: True)
|
||||
# Wire is_enabled into the parent Widget.enabled property
|
||||
self._enabled = is_enabled or (lambda: True)
|
||||
self._icon = gui_app.starpilot_texture(icon_path, 80, 80) if icon_path else None
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_desc = gui_app.font(FontWeight.NORMAL)
|
||||
self._active_color = self.bg_color
|
||||
self._disabled_color = rl.Color(120, 120, 120, 255)
|
||||
|
||||
def _enabled(self) -> bool:
|
||||
return self.is_enabled() if callable(self.is_enabled) else bool(self.is_enabled)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if not self._enabled():
|
||||
self._is_pressed = False
|
||||
return
|
||||
super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if not self._enabled():
|
||||
self._is_pressed = False
|
||||
return
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
enabled = self._enabled()
|
||||
enabled = self.enabled
|
||||
base_color = self._active_color if enabled else self._disabled_color
|
||||
r, g, b = max(0, base_color.r - 20), max(0, base_color.g - 20), max(0, base_color.b - 20)
|
||||
color = rl.Color(r, g, b, 255) if self._is_pressed and enabled else base_color
|
||||
@@ -169,7 +162,9 @@ class ValueTile(MetroTile):
|
||||
title_x = rect.x + padding + (55 if self._icon else 0)
|
||||
max_title_width = rect.width - (title_x - rect.x) - padding
|
||||
self._draw_text_fit(self._font, self.title, rl.Vector2(title_x, rect.y + padding + 2), max_title_width, 35)
|
||||
|
||||
if self.desc:
|
||||
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(title_x, rect.y + padding + 40), max_title_width, 22)
|
||||
|
||||
val_text = self.get_value()
|
||||
# Bottom value: scale to fit if it's too long (common for Car Models)
|
||||
max_val_width = rect.width - 2 * padding
|
||||
|
||||
@@ -84,9 +84,9 @@ class StarPilotPanel(Widget):
|
||||
bg_color=cat.get("color"),
|
||||
)
|
||||
elif tile_type == "toggle":
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")))
|
||||
elif tile_type == "value":
|
||||
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")))
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import math
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
# Amber palette
|
||||
AMBER = rl.Color(251, 191, 36, 255)
|
||||
|
||||
# Filament
|
||||
_FILAMENT_WIDTH = 3.0
|
||||
_FILAMENT_PASSES = 3
|
||||
_FILAMENT_SPEED_BASE = 2.0
|
||||
_FILAMENT_SEGMENTS = 120
|
||||
|
||||
# Glow
|
||||
_GLOW_LAYERS = 8
|
||||
_GLOW_MAX_ALPHA = 60
|
||||
_BREATHING_PERIOD = 4.5 # seconds — resting heart rate ~0.22 Hz
|
||||
|
||||
# Curvature mapping
|
||||
_CURVATURE_MIN = 0.001
|
||||
_CURVATURE_MAX = 0.1
|
||||
|
||||
|
||||
# ── State ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _csc_state():
|
||||
"""Read CSC state from starpilotPlan. Returns dict or None if stale/hidden."""
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["starpilotPlan"] < ui_state.started_frame:
|
||||
return None
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return None
|
||||
if sm.recv_frame["controlsState"] < ui_state.started_frame:
|
||||
return None
|
||||
|
||||
plan = sm["starpilotPlan"]
|
||||
if plan.speedLimitChanged or not ui_state.params.get_bool("ShowCSCStatus"):
|
||||
return None
|
||||
|
||||
car_state = sm["carState"]
|
||||
v_cruise = car_state.vCruiseCluster
|
||||
if v_cruise == 0.0:
|
||||
v_cruise = sm["controlsState"].vCruiseDEPRECATED
|
||||
is_cruise_set = 0 < v_cruise < 255
|
||||
|
||||
return {
|
||||
'training': plan.cscTraining,
|
||||
'active': is_cruise_set and plan.cscControllingSpeed,
|
||||
'curvature': plan.roadCurvature,
|
||||
}
|
||||
|
||||
|
||||
# ── Math ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _intensity(curvature: float) -> float:
|
||||
"""Map abs(curvature) from [CURVATURE_MIN, CURVATURE_MAX] → [0, 1]."""
|
||||
return max(0.0, min(1.0, (abs(curvature) - _CURVATURE_MIN) / (_CURVATURE_MAX - _CURVATURE_MIN)))
|
||||
|
||||
|
||||
def _perimeter(r: rl.Rectangle, roundness: float) -> list[tuple[float, float]]:
|
||||
"""Generate evenly-spaced points around a rounded rectangle perimeter."""
|
||||
rx = roundness * min(r.width, r.height) / 2.0
|
||||
rx = min(rx, r.width / 2.0, r.height / 2.0)
|
||||
x, y, w, h = r.x, r.y, r.width, r.height
|
||||
|
||||
# Corner centers
|
||||
corners = [
|
||||
(x + w - rx, y + rx), # top-right
|
||||
(x + w - rx, y + h - rx), # bottom-right
|
||||
(x + rx, y + h - rx), # bottom-left
|
||||
(x + rx, y + rx), # top-left
|
||||
]
|
||||
|
||||
# Straight edge lengths (connecting corner tangent points)
|
||||
edges = [
|
||||
w - 2 * rx, # top
|
||||
h - 2 * rx, # right
|
||||
w - 2 * rx, # bottom
|
||||
h - 2 * rx, # left
|
||||
]
|
||||
|
||||
arc_len = math.pi * rx / 2.0
|
||||
total = 4 * arc_len + sum(edges)
|
||||
if total <= 0:
|
||||
return [(x, y)] * _FILAMENT_SEGMENTS
|
||||
|
||||
points = []
|
||||
for i in range(_FILAMENT_SEGMENTS):
|
||||
d = (i / _FILAMENT_SEGMENTS) * total
|
||||
# Walk corners and edges in order
|
||||
for ci in range(4):
|
||||
if d < arc_len:
|
||||
frac = d / arc_len
|
||||
# Sweep 90° clockwise: 270→0, 0→90, 90→180, 180→270
|
||||
angle = math.radians(270 + ci * 90 + frac * 90)
|
||||
cx, cy = corners[ci]
|
||||
points.append((cx + rx * math.cos(angle), cy + rx * math.sin(angle)))
|
||||
break
|
||||
d -= arc_len
|
||||
|
||||
edge = edges[ci]
|
||||
if d < edge:
|
||||
frac = d / edge
|
||||
cx, cy = corners[ci]
|
||||
# Next corner in CW order
|
||||
nx, ny = corners[(ci + 1) % 4]
|
||||
# Tangent exit point from current corner
|
||||
dx = (1.0, 0.0, -1.0, 0.0)[ci]
|
||||
dy = (0.0, 1.0, 0.0, -1.0)[ci]
|
||||
ex, ey = cx + rx * dx, cy + rx * dy
|
||||
# Tangent entry point to next corner
|
||||
nnx, nny = nx - rx * dx, ny - rx * dy
|
||||
points.append((ex + (nnx - ex) * frac, ey + (nny - ey) * frac))
|
||||
break
|
||||
d -= edge
|
||||
else:
|
||||
points.append((x, y))
|
||||
|
||||
return points
|
||||
|
||||
|
||||
# ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
def render_glow(border_rect: rl.Rectangle):
|
||||
"""Layer 3: Amber glow behind the standard border. Call BEFORE drawing standard border."""
|
||||
state = _csc_state()
|
||||
if state is None or not state['active']:
|
||||
return
|
||||
|
||||
intensity = _intensity(state['curvature'])
|
||||
if intensity <= 0.0:
|
||||
return
|
||||
|
||||
phase = (rl.get_time() % _BREATHING_PERIOD) / _BREATHING_PERIOD
|
||||
breath = 0.5 + 0.5 * math.sin(phase * 2 * math.pi)
|
||||
alpha = _GLOW_MAX_ALPHA * intensity * (0.5 + 0.5 * breath)
|
||||
|
||||
for i in range(_GLOW_LAYERS):
|
||||
inset = (UI_BORDER_SIZE / _GLOW_LAYERS) * i
|
||||
falloff = 1.0 - (i / _GLOW_LAYERS)
|
||||
a = int(alpha * falloff * falloff)
|
||||
if a < 2:
|
||||
continue
|
||||
|
||||
glow_rect = rl.Rectangle(
|
||||
border_rect.x + inset,
|
||||
border_rect.y + inset,
|
||||
border_rect.width - 2 * inset,
|
||||
border_rect.height - 2 * inset,
|
||||
)
|
||||
rl.draw_rectangle_rounded(glow_rect, 0.12, 10, rl.Color(251, 191, 36, a))
|
||||
|
||||
|
||||
def render_filament(border_rect: rl.Rectangle):
|
||||
"""Layer 5: Amber filament on top of the standard border. Call AFTER drawing standard border."""
|
||||
state = _csc_state()
|
||||
if state is None:
|
||||
return
|
||||
|
||||
if not state['active'] and not state['training']:
|
||||
return
|
||||
|
||||
curvature = state['curvature']
|
||||
inner = rl.Rectangle(
|
||||
border_rect.x + UI_BORDER_SIZE,
|
||||
border_rect.y + UI_BORDER_SIZE,
|
||||
border_rect.width - 2 * UI_BORDER_SIZE,
|
||||
border_rect.height - 2 * UI_BORDER_SIZE,
|
||||
)
|
||||
|
||||
points = _perimeter(inner, 0.12)
|
||||
n = len(points)
|
||||
|
||||
direction = -1.0 if curvature < 0 else 1.0
|
||||
speed = _FILAMENT_SPEED_BASE + _intensity(curvature) * 3.0
|
||||
time_offset = rl.get_time() * speed * direction
|
||||
|
||||
base_alpha = 120 if state['training'] else 200
|
||||
|
||||
for pass_idx in range(_FILAMENT_PASSES):
|
||||
width = _FILAMENT_WIDTH + pass_idx * 1.5
|
||||
alpha_scale = 1.0 - (pass_idx / _FILAMENT_PASSES) * 0.5
|
||||
|
||||
for i in range(n):
|
||||
j = (i + 1) % n
|
||||
t = ((i + time_offset) / n) % 1.0
|
||||
|
||||
# Single pulse: ramp up 0→25%, hold 25→50%, ramp down 50→75%, off 75→100%
|
||||
if t < 0.25:
|
||||
a = t / 0.25
|
||||
elif t < 0.5:
|
||||
a = 1.0
|
||||
elif t < 0.75:
|
||||
a = (0.75 - t) / 0.25
|
||||
else:
|
||||
continue
|
||||
|
||||
alpha = int(base_alpha * a * alpha_scale)
|
||||
if alpha < 2:
|
||||
continue
|
||||
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(points[i][0], points[i][1]),
|
||||
rl.Vector2(points[j][0], points[j][1]),
|
||||
width,
|
||||
rl.Color(251, 191, 36, alpha),
|
||||
)
|
||||
@@ -0,0 +1,407 @@
|
||||
import pyray as rl
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
# ── Constants (matching Qt exactly) ───────────────────────────────────
|
||||
|
||||
# Set speed rect layout (from hud_renderer.py UI_CONFIG)
|
||||
SET_SPEED_X_OFFSET = 60
|
||||
SET_SPEED_Y_OFFSET = 45
|
||||
SET_SPEED_HEIGHT = 204
|
||||
SET_SPEED_WIDTH_IMP = 172
|
||||
SET_SPEED_WIDTH_MET = 200
|
||||
|
||||
# US MUTCD sign
|
||||
US_SIGN_HEIGHT = 186
|
||||
US_SIGN_RADIUS = 24
|
||||
US_INNER_RADIUS = 16
|
||||
US_BORDER_WIDTH = 6
|
||||
US_INSET = 9
|
||||
|
||||
# EU Vienna sign
|
||||
EU_SIGN_SIZE = 176
|
||||
EU_SIGN_WIDTH = 176
|
||||
RED_RING_WIDTH = 20
|
||||
|
||||
# Pending sign
|
||||
PENDING_BLINK_MS = 500
|
||||
PENDING_US_WIDE = 200
|
||||
|
||||
# Sources panel
|
||||
SOURCE_ROW_W = 450
|
||||
SOURCE_ROW_H = 60
|
||||
SOURCE_ROW_GAP = UI_BORDER_SIZE // 2
|
||||
|
||||
# Fonts
|
||||
FONT_LABEL = 28
|
||||
FONT_SPEED = 70
|
||||
FONT_OFFSET = 50
|
||||
FONT_EU_LARGE = 70
|
||||
FONT_EU_SMALL = 60
|
||||
FONT_EU_OFFSET = 40
|
||||
|
||||
# Layout
|
||||
SIGN_MARGIN = 12
|
||||
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_slc_state():
|
||||
"""Extract SLC state from SubMaster. Returns dict or None if stale."""
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["starpilotPlan"] < ui_state.started_frame:
|
||||
return None
|
||||
|
||||
plan = sm["starpilotPlan"]
|
||||
speed_limit_changed = plan.speedLimitChanged
|
||||
|
||||
show_slc = ui_state.params.get_bool("ShowSpeedLimits") or ui_state.params.get_bool("SpeedLimitController")
|
||||
hide_sl = ui_state.params.get_bool("HideSpeedLimit")
|
||||
hide = not speed_limit_changed and hide_sl
|
||||
|
||||
if not show_slc and not speed_limit_changed:
|
||||
return None
|
||||
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
show_offset = ui_state.params.get_bool("ShowSLCOffset")
|
||||
|
||||
dashboard_sl = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0
|
||||
vision_sl = ui_state.params_memory.get_float("VisionSpeedLimit") if ui_state.params.get_bool("VisionSpeedLimitDetection") else 0.0
|
||||
|
||||
slc_overridden_speed = plan.slcOverriddenSpeed
|
||||
speed_limit = plan.slcSpeedLimit
|
||||
|
||||
# Offset calculation (matches Qt lines 199-204)
|
||||
if slc_overridden_speed == 0 and not show_offset:
|
||||
speed_limit += plan.slcSpeedLimitOffset
|
||||
speed_limit *= speed_conversion
|
||||
|
||||
speed_limit_offset = plan.slcSpeedLimitOffset * speed_conversion
|
||||
offset_str = f"{'+' if speed_limit_offset > 0 else '-'}{abs(int(round(speed_limit_offset)))}" if speed_limit_offset != 0 else "\u2013"
|
||||
|
||||
return {
|
||||
'speed_limit': speed_limit,
|
||||
'speed_limit_str': "\u2013" if speed_limit <= 1 else str(int(round(speed_limit))),
|
||||
'slc_overridden_speed': slc_overridden_speed,
|
||||
'speed_limit_source': plan.slcSpeedLimitSource,
|
||||
'unconfirmed_speed_limit': max(0.0, plan.unconfirmedSlcSpeedLimit * speed_conversion),
|
||||
'speed_limit_changed': speed_limit_changed,
|
||||
'hide': hide,
|
||||
'show_offset': show_offset,
|
||||
'use_vienna': ui_state.params.get_bool("UseVienna"),
|
||||
'offset_str': offset_str,
|
||||
'speed_conversion': speed_conversion,
|
||||
'show_sources': ui_state.params.get_bool("SpeedLimitSources"),
|
||||
# Per-source raw values
|
||||
'dashboard_sl': max(0.0, dashboard_sl * speed_conversion),
|
||||
'map_sl': max(0.0, plan.slcMapSpeedLimit * speed_conversion),
|
||||
'vision_sl': max(0.0, vision_sl * speed_conversion),
|
||||
'mapbox_sl': max(0.0, plan.slcMapboxSpeedLimit * speed_conversion),
|
||||
'next_sl': max(0.0, plan.slcNextSpeedLimit * speed_conversion),
|
||||
}
|
||||
|
||||
|
||||
# ── Fonts ─────────────────────────────────────────────────────────────
|
||||
|
||||
_font_bold = None
|
||||
_font_semi_bold = None
|
||||
|
||||
def _get_bold():
|
||||
global _font_bold
|
||||
if _font_bold is None:
|
||||
_font_bold = gui_app.font(FontWeight.BOLD)
|
||||
return _font_bold
|
||||
|
||||
def _get_semi_bold():
|
||||
global _font_semi_bold
|
||||
if _font_semi_bold is None:
|
||||
_font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
return _font_semi_bold
|
||||
|
||||
|
||||
# ── US MUTCD Sign ─────────────────────────────────────────────────────
|
||||
|
||||
def _draw_us_sign(x: float, y: float, sign_width: float, speed_text: str, offset_str: str,
|
||||
alpha: int, show_offset: bool):
|
||||
"""Draw US-style speed limit sign at (x, y) with given width."""
|
||||
sign_rect = rl.Rectangle(x, y, sign_width, US_SIGN_HEIGHT)
|
||||
border_rect = rl.Rectangle(x + US_INSET, y + US_INSET,
|
||||
sign_width - 2 * US_INSET,
|
||||
US_SIGN_HEIGHT - 2 * US_INSET)
|
||||
|
||||
# White background
|
||||
rl.draw_rectangle_rounded(sign_rect, US_SIGN_RADIUS / US_SIGN_HEIGHT, 16,
|
||||
rl.Color(255, 255, 255, alpha))
|
||||
# Black border
|
||||
rl.draw_rectangle_rounded_lines_ex(border_rect, US_INNER_RADIUS / (US_SIGN_HEIGHT - 18), 16,
|
||||
US_BORDER_WIDTH, rl.Color(0, 0, 0, alpha))
|
||||
|
||||
font_bold = _get_bold()
|
||||
font_semi = _get_semi_bold()
|
||||
text_color = rl.Color(0, 0, 0, alpha)
|
||||
cx = x + sign_width / 2
|
||||
|
||||
if show_offset:
|
||||
# Offset ON: "LIMIT" at y=22, speed at y=51, offset at y=120
|
||||
limit_size = measure_text_cached(font_semi, tr("LIMIT"), FONT_LABEL)
|
||||
rl.draw_text_ex(font_semi, tr("LIMIT"), rl.Vector2(cx - limit_size.x / 2, y + 22), FONT_LABEL, 0, text_color)
|
||||
|
||||
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED)
|
||||
rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 51), FONT_SPEED, 0, text_color)
|
||||
|
||||
offset_size = measure_text_cached(font_semi, offset_str, FONT_OFFSET)
|
||||
rl.draw_text_ex(font_semi, offset_str, rl.Vector2(cx - offset_size.x / 2, y + 120), FONT_OFFSET, 0, text_color)
|
||||
else:
|
||||
# Offset OFF: "SPEED" at y=22, "LIMIT" at y=51, speed at y=85
|
||||
speed_label_size = measure_text_cached(font_semi, tr("SPEED"), FONT_LABEL)
|
||||
rl.draw_text_ex(font_semi, tr("SPEED"), rl.Vector2(cx - speed_label_size.x / 2, y + 22), FONT_LABEL, 0, text_color)
|
||||
|
||||
limit_size = measure_text_cached(font_semi, tr("LIMIT"), FONT_LABEL)
|
||||
rl.draw_text_ex(font_semi, tr("LIMIT"), rl.Vector2(cx - limit_size.x / 2, y + 51), FONT_LABEL, 0, text_color)
|
||||
|
||||
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED)
|
||||
rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 85), FONT_SPEED, 0, text_color)
|
||||
|
||||
|
||||
# ── EU Vienna Sign ────────────────────────────────────────────────────
|
||||
|
||||
def _draw_eu_sign(x: float, y: float, speed_text: str, offset_str: str,
|
||||
text_alpha: int, show_offset: bool):
|
||||
"""Draw EU-style speed limit sign at (x, y)."""
|
||||
center_x = x + EU_SIGN_SIZE / 2
|
||||
center_y = y + EU_SIGN_SIZE / 2
|
||||
radius = EU_SIGN_SIZE / 2
|
||||
|
||||
# White circle (full opacity)
|
||||
rl.draw_circle(int(center_x), int(center_y), radius, rl.Color(255, 255, 255, 255))
|
||||
# Red ring (full opacity, static)
|
||||
rl.draw_ring(rl.Vector2(center_x, center_y), radius - RED_RING_WIDTH, radius,
|
||||
0, 360, 64, rl.Color(201, 34, 49, 255))
|
||||
|
||||
font_bold = _get_bold()
|
||||
font_semi = _get_semi_bold()
|
||||
text_color = rl.Color(0, 0, 0, text_alpha)
|
||||
|
||||
eu_font = FONT_EU_LARGE if len(speed_text) <= 2 else FONT_EU_SMALL
|
||||
|
||||
if show_offset:
|
||||
# Offset ON: speed shifted up, offset below
|
||||
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2 - 25)
|
||||
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||
|
||||
offset_size = measure_text_cached(font_semi, offset_str, FONT_EU_OFFSET)
|
||||
offset_pos = rl.Vector2(center_x - offset_size.x / 2, y + 100)
|
||||
rl.draw_text_ex(font_semi, offset_str, offset_pos, FONT_EU_OFFSET, 0, text_color)
|
||||
else:
|
||||
# Centered speed value
|
||||
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2)
|
||||
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||
|
||||
|
||||
# ── Main Speed Limit Sign ────────────────────────────────────────────
|
||||
|
||||
def _draw_speed_limit_sign(state: dict, sign_x: float, sign_y: float, sign_width: float):
|
||||
"""Draw the main speed limit sign (US or EU based on setting)."""
|
||||
speed_text = state['speed_limit_str']
|
||||
offset_str = state['offset_str']
|
||||
alpha = 72 if state['slc_overridden_speed'] != 0 else 255
|
||||
|
||||
if state['use_vienna']:
|
||||
_draw_eu_sign(sign_x, sign_y, speed_text, offset_str, alpha, state['show_offset'])
|
||||
else:
|
||||
_draw_us_sign(sign_x, sign_y, sign_width, speed_text, offset_str, alpha, state['show_offset'])
|
||||
|
||||
|
||||
# ── Pending Speed Limit Sign ─────────────────────────────────────────
|
||||
|
||||
def _draw_pending_sign(state: dict, pending_x: float, pending_y: float, sign_width: float):
|
||||
"""Draw the blinking pending speed limit sign."""
|
||||
pending_speed = state['unconfirmed_speed_limit']
|
||||
if pending_speed <= 0:
|
||||
return
|
||||
|
||||
speed_text = "\u2013" if pending_speed <= 1 else str(int(round(pending_speed)))
|
||||
use_vienna = state['use_vienna']
|
||||
blink_on = int(rl.get_time() * 1000) % (PENDING_BLINK_MS * 2) < PENDING_BLINK_MS
|
||||
|
||||
if use_vienna:
|
||||
# EU pending: white circle, STATIC red ring, TEXT blinks black/red
|
||||
size = EU_SIGN_SIZE
|
||||
center_x = pending_x + size / 2
|
||||
center_y = pending_y + size / 2
|
||||
radius = size / 2
|
||||
|
||||
rl.draw_circle(int(center_x), int(center_y), radius, rl.Color(255, 255, 255, 255))
|
||||
rl.draw_ring(rl.Vector2(center_x, center_y), radius - RED_RING_WIDTH, radius,
|
||||
0, 360, 64, rl.Color(201, 34, 49, 255))
|
||||
|
||||
eu_font = FONT_EU_LARGE if len(speed_text) <= 2 else FONT_EU_SMALL
|
||||
text_color = rl.Color(0, 0, 0) if blink_on else rl.Color(201, 34, 49)
|
||||
font_bold = _get_bold()
|
||||
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2)
|
||||
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||
|
||||
else:
|
||||
# US pending: white rounded rect, BORDER blinks black/red
|
||||
w = PENDING_US_WIDE if len(speed_text) >= 3 else sign_width
|
||||
h = US_SIGN_HEIGHT
|
||||
sign_rect = rl.Rectangle(pending_x, pending_y, w, h)
|
||||
border_rect = rl.Rectangle(pending_x + US_INSET, pending_y + US_INSET,
|
||||
w - 2 * US_INSET, h - 2 * US_INSET)
|
||||
|
||||
rl.draw_rectangle_rounded(sign_rect, US_SIGN_RADIUS / h, 16, rl.Color(255, 255, 255, 255))
|
||||
|
||||
border_color = rl.Color(0, 0, 0) if blink_on else rl.Color(201, 34, 49)
|
||||
rl.draw_rectangle_rounded_lines_ex(border_rect, US_INNER_RADIUS / (h - 18), 16,
|
||||
US_BORDER_WIDTH, border_color)
|
||||
|
||||
font_bold = _get_bold()
|
||||
font_semi = _get_semi_bold()
|
||||
black = rl.Color(0, 0, 0, 255)
|
||||
cx = pending_x + w / 2
|
||||
|
||||
pending_label = measure_text_cached(font_semi, tr("PENDING"), FONT_LABEL)
|
||||
rl.draw_text_ex(font_semi, tr("PENDING"),
|
||||
rl.Vector2(cx - pending_label.x / 2, pending_y + 22),
|
||||
FONT_LABEL, 0, black)
|
||||
|
||||
limit_label = measure_text_cached(font_semi, tr("LIMIT"), FONT_LABEL)
|
||||
rl.draw_text_ex(font_semi, tr("LIMIT"),
|
||||
rl.Vector2(cx - limit_label.x / 2, pending_y + 51),
|
||||
FONT_LABEL, 0, black)
|
||||
|
||||
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED)
|
||||
rl.draw_text_ex(font_bold, speed_text,
|
||||
rl.Vector2(cx - speed_size.x / 2, pending_y + 85),
|
||||
FONT_SPEED, 0, black)
|
||||
|
||||
|
||||
# ── Sources Panel ─────────────────────────────────────────────────────
|
||||
|
||||
def _draw_sources_panel(state: dict, panel_x: float, panel_y: float):
|
||||
"""Draw the 5 source indicator rows below the speed limit sign."""
|
||||
font_bold = _get_bold()
|
||||
font_semi = _get_semi_bold()
|
||||
active_source = state['speed_limit_source']
|
||||
|
||||
sources = [
|
||||
("Dashboard", state['dashboard_sl']),
|
||||
("Map Data", state['map_sl']),
|
||||
("Vision", state['vision_sl']),
|
||||
("Mapbox", state['mapbox_sl']),
|
||||
("Upcoming", state['next_sl']),
|
||||
]
|
||||
|
||||
y = panel_y
|
||||
for title, value in sources:
|
||||
is_active = active_source == title and value != 0
|
||||
|
||||
rect = rl.Rectangle(panel_x, y, SOURCE_ROW_W, SOURCE_ROW_H)
|
||||
|
||||
if is_active:
|
||||
bg = rl.Color(201, 34, 49, 166)
|
||||
border = rl.Color(201, 34, 49, 255)
|
||||
text_font = font_bold
|
||||
else:
|
||||
bg = rl.Color(0, 0, 0, 166)
|
||||
border = rl.Color(0, 0, 0, 255)
|
||||
text_font = font_semi
|
||||
|
||||
rl.draw_rectangle_rounded(rect, 24 / SOURCE_ROW_H, 16, bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 24 / SOURCE_ROW_H, 16, 10, border)
|
||||
|
||||
speed_text = f"{int(round(value))}" if value != 0 else "N/A"
|
||||
full_text = f"{tr(title)} - {speed_text}"
|
||||
|
||||
if is_active:
|
||||
# Draw with black stroke outline
|
||||
text_pos = rl.Vector2(rect.x + 20, rect.y + (SOURCE_ROW_H - 35) / 2)
|
||||
rl.draw_text_ex(text_font, full_text, text_pos, 35, 0, rl.Color(255, 255, 255, 255))
|
||||
else:
|
||||
text_pos = rl.Vector2(rect.x + 20, rect.y + (SOURCE_ROW_H - 35) / 2)
|
||||
rl.draw_text_ex(text_font, full_text, text_pos, 35, 0, rl.Color(255, 255, 255, 255))
|
||||
|
||||
y += SOURCE_ROW_H + SOURCE_ROW_GAP
|
||||
|
||||
|
||||
# ── Sign Rect Calculations ───────────────────────────────────────────
|
||||
|
||||
def _calc_sign_rect(sign_x: float, sign_y: float, sign_width: float, use_vienna: bool) -> rl.Rectangle:
|
||||
if use_vienna:
|
||||
return rl.Rectangle(sign_x, sign_y, EU_SIGN_SIZE, EU_SIGN_SIZE)
|
||||
return rl.Rectangle(sign_x, sign_y, sign_width, US_SIGN_HEIGHT)
|
||||
|
||||
|
||||
def _calc_pending_rect(sign_x: float, sign_y: float, sign_width: float,
|
||||
unconfirmed_speed: float, use_vienna: bool) -> rl.Rectangle:
|
||||
# Pending sign is adjacent to main sign (Qt: translated by sign.width + UI_BORDER_SIZE)
|
||||
pending_x = sign_x + sign_width + UI_BORDER_SIZE
|
||||
|
||||
if use_vienna:
|
||||
return rl.Rectangle(pending_x, sign_y, EU_SIGN_SIZE, EU_SIGN_SIZE)
|
||||
|
||||
speed_text = str(int(round(unconfirmed_speed)))
|
||||
w = PENDING_US_WIDE if len(speed_text) >= 3 else sign_width
|
||||
return rl.Rectangle(pending_x, sign_y, w, US_SIGN_HEIGHT)
|
||||
|
||||
|
||||
# ── Click Handling ────────────────────────────────────────────────────
|
||||
|
||||
def handle_slc_click(mouse_pos, sign_x: float, sign_y: float, sign_width: float):
|
||||
"""Handle mouse click for accepting pending speed limit. Call from _handle_mouse_press."""
|
||||
state = _get_slc_state()
|
||||
if state is None or not state['speed_limit_changed']:
|
||||
return
|
||||
|
||||
pending_rect = _calc_pending_rect(sign_x, sign_y, sign_width,
|
||||
state['unconfirmed_speed_limit'], state['use_vienna'])
|
||||
if (pending_rect.x <= mouse_pos.x <= pending_rect.x + pending_rect.width and
|
||||
pending_rect.y <= mouse_pos.y <= pending_rect.y + pending_rect.height):
|
||||
from openpilot.common.params import Params
|
||||
Params(memory=True).put_bool("SpeedLimitAccepted", True)
|
||||
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────
|
||||
|
||||
def render_speed_limit(content_rect: rl.Rectangle):
|
||||
"""Render SLC speed limit signs. Call from onroad view render loop."""
|
||||
state = _get_slc_state()
|
||||
if state is None:
|
||||
return
|
||||
|
||||
# Compute sign rect matching Qt:
|
||||
# signRect = QRect(setSpeedRect.x() + signMargin, setSpeedRect.bottom() + signMargin,
|
||||
# setSpeedRect.width() - 2*signMargin, usSignHeight)
|
||||
ss_width = SET_SPEED_WIDTH_MET if ui_state.is_metric else SET_SPEED_WIDTH_IMP
|
||||
ss_x = content_rect.x + SET_SPEED_X_OFFSET + (SET_SPEED_WIDTH_IMP - ss_width) // 2
|
||||
ss_y = content_rect.y + SET_SPEED_Y_OFFSET
|
||||
|
||||
sign_width = ss_width - 2 * SIGN_MARGIN
|
||||
sign_x = ss_x + SIGN_MARGIN
|
||||
sign_y = ss_y + SET_SPEED_HEIGHT + SIGN_MARGIN
|
||||
|
||||
use_vienna = state['use_vienna']
|
||||
|
||||
# 1. Pending sign (always drawn when speedLimitChanged, even if main sign hidden)
|
||||
if state['speed_limit_changed']:
|
||||
pending_rect = _calc_pending_rect(sign_x, sign_y, sign_width,
|
||||
state['unconfirmed_speed_limit'], use_vienna)
|
||||
_draw_pending_sign(state, pending_rect.x, pending_rect.y, sign_width)
|
||||
|
||||
# 2. Main speed limit sign (unless hidden)
|
||||
if not state['hide']:
|
||||
_draw_speed_limit_sign(state, sign_x, sign_y, sign_width)
|
||||
|
||||
# 3. Sources panel
|
||||
if state['show_sources']:
|
||||
sign_rect = _calc_sign_rect(sign_x, sign_y, sign_width, use_vienna)
|
||||
sources_x = sign_rect.x - SIGN_MARGIN
|
||||
sources_y = sign_rect.y + sign_rect.height + UI_BORDER_SIZE
|
||||
_draw_sources_panel(state, sources_x, sources_y)
|
||||
@@ -3,8 +3,14 @@ from msgq.visionipc import VisionStreamType
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView, BORDER_COLORS
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.curve_speed_border import render_glow, render_filament
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.personality_button import PersonalityButton, BTN_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import (
|
||||
render_speed_limit, handle_slc_click, SET_SPEED_X_OFFSET, SET_SPEED_Y_OFFSET,
|
||||
SET_SPEED_WIDTH_IMP, SET_SPEED_WIDTH_MET, SET_SPEED_HEIGHT, SIGN_MARGIN,
|
||||
)
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.system.ui.lib.application import MousePos, gui_app
|
||||
|
||||
AOL_COLOR = rl.Color(10, 186, 181, 255)
|
||||
|
||||
@@ -22,8 +28,21 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
if not ui_state.started:
|
||||
return
|
||||
|
||||
self._render_slc(rect)
|
||||
self._render_overlays()
|
||||
|
||||
def _render_slc(self, rect: rl.Rectangle):
|
||||
content_rect = rl.Rectangle(
|
||||
rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
|
||||
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE,
|
||||
)
|
||||
rl.begin_scissor_mode(
|
||||
int(content_rect.x), int(content_rect.y),
|
||||
int(content_rect.width), int(content_rect.height),
|
||||
)
|
||||
render_speed_limit(content_rect)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _render_overlays(self):
|
||||
self._position_personality_button()
|
||||
self._personality_button.render()
|
||||
@@ -52,10 +71,29 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
|
||||
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
|
||||
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
|
||||
|
||||
# Layer 3: Amber glow (behind standard border)
|
||||
render_glow(border_rect)
|
||||
|
||||
# Layer 4: Standard border
|
||||
border_color = AOL_COLOR if ui_state.always_on_lateral_active else BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
rl.draw_rectangle_rounded_lines_ex(border_rect, 0.12, 10, UI_BORDER_SIZE, border_color)
|
||||
|
||||
def _handle_mouse_press(self, _):
|
||||
# Layer 5: Amber filament (on top of standard border)
|
||||
render_filament(border_rect)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
content_rect = rl.Rectangle(
|
||||
UI_BORDER_SIZE, UI_BORDER_SIZE,
|
||||
gui_app.width - 2 * UI_BORDER_SIZE, gui_app.height - 2 * UI_BORDER_SIZE,
|
||||
)
|
||||
ss_width = SET_SPEED_WIDTH_MET if ui_state.is_metric else SET_SPEED_WIDTH_IMP
|
||||
ss_x = content_rect.x + SET_SPEED_X_OFFSET + (SET_SPEED_WIDTH_IMP - ss_width) // 2
|
||||
sign_x = ss_x + SIGN_MARGIN
|
||||
sign_y = content_rect.y + SET_SPEED_Y_OFFSET + SET_SPEED_HEIGHT + SIGN_MARGIN
|
||||
sign_width = ss_width - 2 * SIGN_MARGIN
|
||||
handle_slc_click(mouse_pos, sign_x, sign_y, sign_width)
|
||||
|
||||
if self._personality_button.is_interacting:
|
||||
return
|
||||
super()._handle_mouse_press(_)
|
||||
super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
Reference in New Issue
Block a user