mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 15:33:45 +08:00
Align speed HUD for clearer SLC prompts
This commit is contained in:
@@ -10,11 +10,11 @@ print(json.dumps(dict(document=default_document(), defaults=default_document(),
|
||||
revision="native", editable=True, valid=True, activeProfile="large", supportedProfiles=["large"])))
|
||||
`], { encoding: "utf8" }))
|
||||
assert.equal(validSnapshot(data), true)
|
||||
for (const version of [11, 12, 13]) {
|
||||
for (const version of [11, 12, 13, 14]) {
|
||||
assert.equal(validSnapshot({ ...data, document: { ...data.document, version } }), true)
|
||||
assert.equal(validSnapshot({ ...data, defaults: { ...data.defaults, version } }), true)
|
||||
}
|
||||
for (const version of [9, 10, 14]) {
|
||||
for (const version of [9, 10, 15]) {
|
||||
assert.equal(validSnapshot({ ...data, document: { ...data.document, version } }), false)
|
||||
assert.equal(validSnapshot({ ...data, defaults: { ...data.defaults, version } }), false)
|
||||
}
|
||||
|
||||
@@ -1220,15 +1220,17 @@ console.log('Display options, target switching and reload during polling passed'
|
||||
assert.equal(documentHelpers.overlapsReserved(profile, 'cruise_limits', 88, 500, sample.document.layouts.large), true)
|
||||
}
|
||||
|
||||
// Portrait sizing and its attached chooser use the same compact bounds as native input.
|
||||
// Reserve the taller notice while keeping the opened buttons at their existing size.
|
||||
{
|
||||
const sample = snapshot(), profile = sample.metadata.profiles.compact
|
||||
profile.widgets.max_speed.width = 146
|
||||
profile.widgets.speed_limit = { ...profile.widgets.speed_limit, width: 128, height: 144,
|
||||
confirmation: { x: -172, y: -8, width: 328, height: 152 } }
|
||||
sample.document.version = 13
|
||||
confirmation: { x: -172, y: -8, width: 328, height: 159 } }
|
||||
sample.document.version = 14
|
||||
sample.document.layouts.compact.speed_limit = { x: 320, y: 16, enabled: true }
|
||||
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 16, sample.document.layouts.compact), false)
|
||||
assert.equal(validDocument(sample.document, sample.metadata), true)
|
||||
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 20, sample.document.layouts.compact), true)
|
||||
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 320, 24, sample.document.layouts.compact), true)
|
||||
assert.equal(documentHelpers.overlapsReserved(profile, 'speed_limit', 317, 16, sample.document.layouts.compact), true)
|
||||
assert.equal(clampPlacement(profile, 'speed_limit', 330, 16, sample.document.layouts.compact).x, 320)
|
||||
|
||||
@@ -140,7 +140,7 @@ export const PROJECTION_DISPLAY_SWITCHES = ["blindSpotBorder", "signalBorder", "
|
||||
export function validDocument(document, metadata) {
|
||||
if (!object(document)) return false
|
||||
const profiles = metadata?.projection === true ? ["large"] : PROFILES
|
||||
if (!keysEqual(document, ["version", "palette", "layouts", "widgetColors", "roadColors", ...(Object.hasOwn(document, "widgetOrder") ? ["widgetOrder"] : []), ...(Object.hasOwn(document, "speedSources") ? ["speedSources"] : []), ...(Object.hasOwn(document, "speedSourcesCard") ? ["speedSourcesCard"] : []), ...(Object.hasOwn(document, "speedAlign") ? ["speedAlign"] : []), ...(Object.hasOwn(document, "clock24Hour") ? ["clock24Hour"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "reduceMotion") ? ["reduceMotion"] : []), ...(Object.hasOwn(document, "showMaxSpeed") ? ["showMaxSpeed"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "display") ? ["display"] : []), ...(metadata?.projection === true && Object.hasOwn(document, "largeUiGammaTrial") ? ["largeUiGammaTrial"] : []), ...(metadata?.projection === true ? PROJECTION_DISPLAY_SWITCHES.filter(key => Object.hasOwn(document, key)) : []), ...(metadata?.projection === true && Object.hasOwn(document, "leadInfo") ? ["leadInfo"] : [])]) || (metadata?.projection === true ? document.version !== 8 : ![11, 12, 13].includes(document.version)) ||
|
||||
if (!keysEqual(document, ["version", "palette", "layouts", "widgetColors", "roadColors", ...(Object.hasOwn(document, "widgetOrder") ? ["widgetOrder"] : []), ...(Object.hasOwn(document, "speedSources") ? ["speedSources"] : []), ...(Object.hasOwn(document, "speedSourcesCard") ? ["speedSourcesCard"] : []), ...(Object.hasOwn(document, "speedAlign") ? ["speedAlign"] : []), ...(Object.hasOwn(document, "clock24Hour") ? ["clock24Hour"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "reduceMotion") ? ["reduceMotion"] : []), ...(Object.hasOwn(document, "showMaxSpeed") ? ["showMaxSpeed"] : []), ...(metadata?.projection !== true && Object.hasOwn(document, "display") ? ["display"] : []), ...(metadata?.projection === true && Object.hasOwn(document, "largeUiGammaTrial") ? ["largeUiGammaTrial"] : []), ...(metadata?.projection === true ? PROJECTION_DISPLAY_SWITCHES.filter(key => Object.hasOwn(document, key)) : []), ...(metadata?.projection === true && Object.hasOwn(document, "leadInfo") ? ["leadInfo"] : [])]) || (metadata?.projection === true ? document.version !== 8 : ![11, 12, 13, 14].includes(document.version)) ||
|
||||
(Object.hasOwn(document, "speedSources") && typeof document.speedSources !== "boolean") ||
|
||||
(Object.hasOwn(document, "speedSourcesCard") && typeof document.speedSourcesCard !== "boolean") ||
|
||||
(Object.hasOwn(document, "speedAlign") && !SPEED_ALIGNMENTS.includes(document.speedAlign)) ||
|
||||
|
||||
@@ -38,7 +38,7 @@ def confirmation_bounds(profile, x, y, *, opened=False):
|
||||
width = 2 * CONFIRMATION_PADDING + 2 * CONFIRMATION_BUTTON[profile] + CONFIRMATION_GAP if opened else 240
|
||||
left = x + 172 if profile == 'large' else x - 172 if opened else x - 100
|
||||
top = y - (12 if profile == 'large' else 8)
|
||||
height = CONFIRMATION_HEIGHT[profile] if opened else 152
|
||||
height = CONFIRMATION_HEIGHT[profile] if opened else (168 if profile == 'large' else 159)
|
||||
return left, top, left + width * scale, top + height * scale
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ def confirmation_buttons(profile, bounds):
|
||||
|
||||
def confirmation_footprint(profile):
|
||||
left, top, right, bottom = confirmation_bounds(profile, 0, 0, opened=True)
|
||||
# The notice is taller than the open buttons on Compact.
|
||||
bottom = max(bottom, confirmation_bounds(profile, 0, 0)[3])
|
||||
return {'x': left, 'y': top, 'width': right - left, 'height': bottom - top}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.starpilot.ui.onroad_conditional import reason as conditional_reas
|
||||
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
|
||||
from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame
|
||||
from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_caption, draw_ink, set_speed_status, speed_status
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_set_speed, draw_set_status, set_speed_status, speed_status
|
||||
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
|
||||
from openpilot.starpilot.ui.moon import draw_moon
|
||||
from openpilot.starpilot.ui.onroad_personality import PersonalityWidget
|
||||
@@ -139,24 +139,15 @@ class CompactHudRenderer:
|
||||
status = set_speed_status(state, Profile.COMPACT, shown)
|
||||
persistent = shown.slc_target_text is not None or shown.override_text is not None or speed_status(state, Profile.COMPACT, shown) is not None
|
||||
set_speed_alpha = self._set_speed_opacity(state, (state.observed_ns or time.monotonic_ns()), slc_active=persistent)
|
||||
cruise = shown.max_text
|
||||
mx, my = widget_offset(state.customization, "compact", "max_speed")
|
||||
center = widget_size(state.customization, 'compact', 'max_speed')[0] / 2 + mx
|
||||
if set_speed_alpha > 0.01 and placement(state.customization, "compact", "max_speed")["enabled"]:
|
||||
red, green, blue, alpha = rgba(state.customization, "text", "compact", "max_speed")
|
||||
color = rl.Color(red, green, blue, int(alpha * set_speed_alpha))
|
||||
draw_caption(self.fonts, 'SET', center, 20 + my, color, unit=shown.unit, size=24, centered=True)
|
||||
size = 96
|
||||
left, right = self.fonts.horizontal_ink(cruise, FontRole.SET_SPEED, size)
|
||||
size *= min(1, 128 / max(1, right - left))
|
||||
left, right = self.fonts.horizontal_ink(cruise, FontRole.SET_SPEED, size)
|
||||
draw_ink(self.fonts, cruise, FontRole.SET_SPEED, size, center - (left + right) / 2, 46 + my, color)
|
||||
draw_set_speed(self.fonts, shown, center, 16 + my, color)
|
||||
if status and placement(state.customization, 'compact', 'max_speed')['enabled']:
|
||||
color = rl.Color(241, 246, 243, 255)
|
||||
left, right = self.fonts.horizontal_ink(status, FontRole.SEMI_BOLD, 24)
|
||||
size = 24 * min(1, 128 / max(1, right - left))
|
||||
left, right = self.fonts.horizontal_ink(status, FontRole.SEMI_BOLD, size)
|
||||
draw_ink(self.fonts, status, FontRole.SEMI_BOLD, size, center - (left + right) / 2, 128 + my, color)
|
||||
draw_set_status(self.fonts, status, center, 16 + my, color)
|
||||
|
||||
def render_steering_wheel(self, state: OnroadState) -> None:
|
||||
color = self._feedback.update(state.wheel_acceleration, state.appearance.wheel_pedal_feedback, state.observed_ns, state.drive_frame)
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any, NotRequired, TypedDict
|
||||
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
|
||||
from openpilot.starpilot.ui.large_speed_geometry import LIMIT_RECTS, confirmation_bounds, confirmation_footprint
|
||||
from openpilot.starpilot.ui.large_speed_geometry import LIMIT_RECTS, confirmation_footprint
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
|
||||
from openpilot.starpilot.ui.onroad_favorites_geometry import shelf_metadata
|
||||
from openpilot.starpilot.ui.large_speed_geometry import (WIDTH, CARD_HEIGHT, ACTION_WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y,
|
||||
@@ -342,7 +342,7 @@ def customization_metadata():
|
||||
|
||||
|
||||
def default_document():
|
||||
return {"version": 13, "clock24Hour": False, "reduceMotion": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
|
||||
return {"version": 14, "clock24Hour": False, "reduceMotion": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
|
||||
"widgetColors": {profile: {} for profile in PROFILES}, "layouts": {
|
||||
profile: {key: {**widget["default"], **({"size": WIDGET_SIZES[key][profile][1]} if key in WIDGET_SIZES else {})}
|
||||
for key, widget in data["widgets"].items()}
|
||||
@@ -431,7 +431,9 @@ def confirmation_layout_valid(layout, profile, widgets, bounds, *, extra_height=
|
||||
at = layout[owner]
|
||||
if not at['enabled'] or not layout['speed_limit_actions']['enabled']:
|
||||
return True
|
||||
left, top, right, bottom = confirmation_bounds(profile, at['x'], at['y'], opened=True)
|
||||
area = confirmation_footprint(profile)
|
||||
left, top = at['x'] + area['x'], at['y'] + area['y']
|
||||
right, bottom = left + area['width'], top + area['height']
|
||||
if extra_height:
|
||||
left, right = min(left, at['x']), max(right, at['x'] + widgets[owner]['width'])
|
||||
bottom = max(bottom, at['y'] + widgets[owner]['height'] + extra_height)
|
||||
@@ -473,7 +475,7 @@ def migrate_confirmation_layout(layout, profile, widgets, bounds, *, extra_heigh
|
||||
|
||||
|
||||
def validate_document(value):
|
||||
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 14):
|
||||
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 15):
|
||||
raise ValueError("Invalid customization version")
|
||||
fields = {"version", "palette", "layouts"} | ({"widgetColors"} if value["version"] >= 2 else set())
|
||||
if value["version"] >= 3:
|
||||
@@ -745,6 +747,7 @@ def validate_document(value):
|
||||
old_gap += GAP if result.get('speedSourcesCard') is True else 0
|
||||
if actions['x'] == card['x'] + ACTION_X - 88 and actions['y'] == card['y'] + old_gap:
|
||||
actions['y'] = card['y'] + ACTION_Y - 75
|
||||
if value['version'] < 14:
|
||||
migrate_confirmation_layout(result['layouts'][profile], profile, data['widgets'], data['bounds'],
|
||||
extra_height=sources_extent(result) if profile == 'large' else 0)
|
||||
elif not confirmation_layout_valid(result['layouts'][profile], profile, data['widgets'], data['bounds'],
|
||||
|
||||
@@ -11,12 +11,12 @@ import json
|
||||
from openpilot.starpilot.ui.raylib import rl
|
||||
|
||||
from openpilot.starpilot.ui.onroad_customization import offset, rgba, widget_size
|
||||
from openpilot.starpilot.ui.onroad_state import OnroadState, manual_slc_confirmation, speed_limit_bounds, slc_action_occludes
|
||||
from openpilot.starpilot.ui.onroad_state import OnroadState, manual_slc_confirmation, slc_action_occludes
|
||||
from openpilot.starpilot.ui.onroad_widget_style import draw_control_card
|
||||
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
|
||||
from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade
|
||||
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
|
||||
from openpilot.starpilot.ui.speed_limit_widget import MUTED, SpeedLimitWidget, draw_caption, draw_ink, draw_line, draw_text, set_speed_status
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_set_speed, draw_set_status, draw_text, set_speed_status
|
||||
|
||||
|
||||
ENGAGED = rl.Color(128, 216, 166, 255)
|
||||
@@ -65,7 +65,7 @@ class UnifiedSpeedWidget:
|
||||
draw_text(self.fonts, text, role, font_size, box.x + (box.width - width) / 2, y, color)
|
||||
|
||||
def render(self, content: rl.Rectangle, state: OnroadState) -> None:
|
||||
from openpilot.starpilot.ui.large_speed_geometry import GAP, WIDTH, PADDING, SOURCE_ROW_HEIGHT, SOURCE_TOP, card_height
|
||||
from openpilot.starpilot.ui.large_speed_geometry import GAP, WIDTH, SOURCE_ROW_HEIGHT, SOURCE_TOP, card_height
|
||||
from openpilot.starpilot.ui.onroad_customization import placement
|
||||
shown = resolve_unified_speed(state)
|
||||
if shown.mode == 'hidden':
|
||||
@@ -79,31 +79,10 @@ class UnifiedSpeedWidget:
|
||||
border = rl.Color(*rgba(state.customization, 'cardBorder', 'large', 'cruise_limits'))
|
||||
text = rl.Color(*rgba(state.customization, 'text', 'large', 'cruise_limits'))
|
||||
if shown.mode != 'limit_only':
|
||||
confirming = manual_slc_confirmation(Profile.LARGE, state)
|
||||
center = rect.x + 120
|
||||
x = rect.x + PADDING if confirming else center
|
||||
draw_caption(self.fonts, 'SET', x, rect.y + 12, unit=shown.unit, centered=not confirming)
|
||||
if shown.max_text == '–':
|
||||
draw_line(Profile.LARGE, rl.Vector2(center - 28, rect.y + 90), rl.Vector2(center + 28, rect.y + 90), 8, MUTED)
|
||||
else:
|
||||
size = 100
|
||||
size *= min(1, (148 if confirming else 200) / max(1, self.fonts.measure(shown.max_text, FontRole.SET_SPEED, size).width))
|
||||
left, right = self.fonts.horizontal_ink(shown.max_text, FontRole.SET_SPEED, size)
|
||||
top, bottom = self.fonts.vertical_ink(shown.max_text, FontRole.SET_SPEED, size)
|
||||
number_x = x + self.fonts.horizontal_ink('SET', FontRole.MEDIUM, 34)[0] - left if confirming else center - (left + right) / 2
|
||||
draw_ink(self.fonts, shown.max_text, FontRole.SET_SPEED, size, number_x, rect.y + 90 - (bottom - top) / 2, text)
|
||||
if shown.mode != 'limit_only' and (label := set_speed_status(state, Profile.LARGE, shown)):
|
||||
confirming = manual_slc_confirmation(Profile.LARGE, state)
|
||||
size = 24
|
||||
left, right = self.fonts.horizontal_ink(label, FontRole.SEMI_BOLD, size)
|
||||
size *= min(1, (148 if confirming else 200) / max(1, right - left))
|
||||
left, right = self.fonts.horizontal_ink(label, FontRole.SEMI_BOLD, size)
|
||||
x = rect.x + (PADDING + self.fonts.horizontal_ink('SET', FontRole.MEDIUM, 34)[0] - left if confirming else 120 - (left + right) / 2)
|
||||
draw_ink(self.fonts, label, FontRole.SEMI_BOLD, size, x, rect.y + 136, text)
|
||||
draw_set_speed(self.fonts, shown, rect.x + 92, rect.y, text)
|
||||
if label := set_speed_status(state, Profile.LARGE, shown):
|
||||
draw_set_status(self.fonts, label, rect.x + 92, rect.y, text)
|
||||
self.sign.render(state, defer_confirmation=True)
|
||||
if (shown.mode == 'limit_only' and not manual_slc_confirmation(Profile.LARGE, state) and
|
||||
(bounds := speed_limit_bounds(Profile.LARGE, state)) is not None):
|
||||
draw_ink(self.fonts, shown.unit, FontRole.NORMAL, 24, bounds[2], bounds[3] - 32)
|
||||
if sources:
|
||||
row = rl.Rectangle(rect.x, rect.y + max(height, SOURCE_TOP) + (GAP if separate else 0), WIDTH, SOURCE_ROW_HEIGHT)
|
||||
if separate:
|
||||
|
||||
@@ -21,6 +21,8 @@ IN_USE = rl.Color(128, 216, 166, 255)
|
||||
OVERRIDE = rl.Color(214, 188, 240, 255)
|
||||
REJECT = rl.Color(242, 139, 139, 255)
|
||||
SOURCE_LABELS = {'dashboard': 'DASH', 'map': 'OSM', 'vision': 'VISION', 'mapbox': 'NAV'}
|
||||
NUMBER_SIZE, HEADING_SIZE = 70, 26
|
||||
HEADING_ROW, NUMBER_ROW, SOURCE_ROW = 25, 68, 120
|
||||
|
||||
|
||||
def draw_text(fonts, text, role, size, x, y, color=INK):
|
||||
@@ -45,24 +47,32 @@ def draw_ink(fonts, text, role, size, x, top, color=INK):
|
||||
return fonts.measure(text, role, size).width, ink_bottom - ink_top
|
||||
|
||||
|
||||
def draw_caption(fonts, label, x, top, color=MUTED, *, unit=None, size=None, centered=False):
|
||||
large = fonts.profile == Profile.LARGE
|
||||
label_size = size or (34 if large else 24)
|
||||
unit_size, gap = ((32 if centered else 28), 12) if large else (20, 8)
|
||||
if centered:
|
||||
width = fonts.measure(label, FontRole.MEDIUM, label_size).width
|
||||
if unit:
|
||||
width += gap + fonts.measure(unit, FontRole.NORMAL, unit_size).width
|
||||
x -= width / 2
|
||||
width, height = draw_ink(fonts, label, FontRole.MEDIUM, label_size, x, top, color)
|
||||
if unit:
|
||||
unit_top, _ = fonts.vertical_ink(unit, FontRole.NORMAL, unit_size)
|
||||
# Exclude the p descender when aligning mph to the caption baseline.
|
||||
_, baseline = fonts.vertical_ink('mh' if unit == 'mph' else unit, FontRole.NORMAL, unit_size)
|
||||
unit_width, _ = draw_ink(fonts, unit, FontRole.NORMAL, unit_size, x + width + gap,
|
||||
top + height - (baseline - unit_top), color)
|
||||
width += gap + unit_width
|
||||
return width, height
|
||||
def draw_speed_row(fonts, text, role, size, center, row, color, reference):
|
||||
# Shared glyph references keep SET and SLC on the same baseline as values change.
|
||||
left, right = fonts.horizontal_ink(text, role, size)
|
||||
top, bottom = fonts.vertical_ink(reference, role, size)
|
||||
draw_text(fonts, text, role, size, center - (left + right) / 2, row - (top + bottom) / 2, color)
|
||||
|
||||
|
||||
def draw_set_speed(fonts, shown, center, top, color):
|
||||
scale = LARGE_SIGN_SCALE if fonts.profile == Profile.LARGE else 1.
|
||||
text_scale = scale * Profile.COMPACT.font_scale / fonts.profile.font_scale
|
||||
draw_speed_row(fonts, 'SET', FontRole.SEMI_BOLD, HEADING_SIZE * text_scale, center,
|
||||
top + HEADING_ROW * scale, color, 'H')
|
||||
if shown.max_text == '–':
|
||||
draw_line(fonts.profile, rl.Vector2(center - 20 * scale, top + NUMBER_ROW * scale),
|
||||
rl.Vector2(center + 20 * scale, top + NUMBER_ROW * scale), 5 * scale, color)
|
||||
else:
|
||||
draw_speed_row(fonts, shown.max_text, FontRole.SET_SPEED, NUMBER_SIZE * text_scale, center,
|
||||
top + NUMBER_ROW * scale, color, '0123456789')
|
||||
|
||||
|
||||
def draw_set_status(fonts, label, center, top, color):
|
||||
scale = LARGE_SIGN_SCALE if fonts.profile == Profile.LARGE else 1.
|
||||
size = 22 * scale * Profile.COMPACT.font_scale / fonts.profile.font_scale
|
||||
left, right = fonts.horizontal_ink(label, FontRole.MEDIUM, size)
|
||||
size *= min(1, 108 * scale / max(1, right - left))
|
||||
draw_speed_row(fonts, label, FontRole.MEDIUM, size, center, top + SOURCE_ROW * scale, color, label)
|
||||
|
||||
|
||||
def speed_status(state, profile, shown):
|
||||
@@ -155,11 +165,14 @@ class SpeedLimitWidget:
|
||||
y + cy * scale - (top + bottom) / 2, color)
|
||||
|
||||
if state.slc_prompt_open:
|
||||
context = (f'Now: {current_status}' + (f' {shown.unit}' if current_status != 'PEDAL' else '')
|
||||
if current_status else 'New limit')
|
||||
context = f'Now: {current_status}' if current_status else 'NEW LIMIT'
|
||||
context_size = 26 if self.profile == Profile.LARGE else 24
|
||||
context_size *= min(1, (width - 24) / max(1, ink_width(context, context_size)))
|
||||
text(context, context_size, width / 2, 24, secondary)
|
||||
label_size, number_size, detail_size, gap = (30, 58, 26, 8) if self.profile == Profile.LARGE else (26, 50, 24, 6)
|
||||
# Reserve the full label height so capitalization does not shift either row.
|
||||
heading_height = max(ink_height(label, label_size, FontRole.SEMI_BOLD) for label in ('Accept', 'Reject'))
|
||||
number_height = ink_height(shown.proposal_text, number_size, FontRole.SET_SPEED)
|
||||
for control in slc_controls(self.profile, state):
|
||||
accent = (IN_USE if control.label == 'Accept' else REJECT) if control.enabled else MUTED
|
||||
selected = control.enabled and pressed == control.request
|
||||
@@ -169,70 +182,66 @@ class SpeedLimitWidget:
|
||||
w, h = (control.bounds[2] - control.bounds[0]) / scale, (control.bounds[3] - control.bounds[1]) / scale
|
||||
self.shapes.button(x + (left + w / 2) * scale, y + (top + h / 2) * scale, accent,
|
||||
profile=self.profile, scale=scale, pressed=selected)
|
||||
uw = ink_width(shown.unit, 20)
|
||||
size = 60 if self.profile == Profile.LARGE else 52
|
||||
padding, gap = (28, 6) if self.profile == Profile.LARGE else (24, 4)
|
||||
size *= min(1, (w - padding - gap - uw) / max(1, ink_width(shown.proposal_text, size, FontRole.BOLD)))
|
||||
nh, uh = ink_height(shown.proposal_text, size, FontRole.BOLD), ink_height(shown.unit, 20)
|
||||
caption_height = ink_height('new limit', 22)
|
||||
row_height = max(nh, uh, caption_height)
|
||||
# Center each visible text group while keeping both action headings on one row.
|
||||
label_y = top + (h - 8 - row_height) / 2
|
||||
detail_bottom = label_y + ink_height(control.label, 37) / 2 + 8 + row_height
|
||||
text(control.label, 37, left + w / 2, label_y, ink)
|
||||
start = top + (h - heading_height - gap - number_height) / 2
|
||||
text(control.label.upper(), label_size, left + w / 2, start + heading_height / 2, ink, FontRole.SEMI_BOLD)
|
||||
detail_y = start + heading_height + gap + number_height / 2
|
||||
if control.label == 'Accept':
|
||||
# Bind the adjusted proposal to its action, not to the current override or source reading.
|
||||
nw = ink_width(shown.proposal_text, size, FontRole.BOLD)
|
||||
start = left + (w - nw - gap - uw) / 2
|
||||
text(shown.proposal_text, size, start + nw / 2, detail_bottom - nh / 2, ink, FontRole.BOLD)
|
||||
text(shown.unit, 20, start + nw + gap + uw / 2, detail_bottom - uh / 2, ink)
|
||||
text(shown.proposal_text, number_size, left + w / 2, detail_y, ink, FontRole.SET_SPEED)
|
||||
else:
|
||||
text('new limit', 22, left + w / 2, detail_bottom - caption_height / 2, ink)
|
||||
text('NEW LIMIT', detail_size, left + w / 2, detail_y, ink)
|
||||
return
|
||||
|
||||
content_top = y + (12 if self.profile == Profile.LARGE else 8)
|
||||
row_origin = (content_top - y) / scale
|
||||
caption_row, hint_row = (112, 142) if self.profile == Profile.LARGE else (110, 136.75)
|
||||
source_color = rl.Color(197, 212, 208, 255)
|
||||
if current_status:
|
||||
source += f' / {current_status}'
|
||||
label_size = 22 * min(1, (width - 40) / max(1, ink_width(source, 22, FontRole.SEMI_BOLD)))
|
||||
label_size = HEADING_SIZE * min(1, (width - 40) / max(1, ink_width(source, HEADING_SIZE, FontRole.SEMI_BOLD)))
|
||||
label_width = ink_width(source, label_size, FontRole.SEMI_BOLD)
|
||||
target = state.speed_limit.pending_adjusted_limit_mps
|
||||
if not positive(target):
|
||||
target = displayed_adjusted_limit_mps(state.speed_limit)
|
||||
direction = (-1 if target > state.speed_mps else 1) if target is not None and state.speed_mps is not None and target != state.speed_mps else 0
|
||||
start = (width - label_width - (19.2 if direction else 0)) / 2
|
||||
text(source, label_size, start + label_width / 2, 22, source_color, FontRole.SEMI_BOLD)
|
||||
draw_speed_row(self.fonts, source, FontRole.SEMI_BOLD, text_size(label_size),
|
||||
x + (start + label_width / 2) * scale, content_top + HEADING_ROW * scale, source_color, 'H')
|
||||
if direction:
|
||||
self.shapes.arrow('compact', x + (start + label_width + 12.6) * scale, y + 22.5 * scale,
|
||||
self.shapes.arrow('compact', x + (start + label_width + 12.6) * scale, content_top + (HEADING_ROW + .5) * scale,
|
||||
direction, source_color, scale=1.1 * scale)
|
||||
nw = ink_width(shown.posted_text, 64, FontRole.BOLD)
|
||||
nw = ink_width(shown.posted_text, NUMBER_SIZE, FontRole.SET_SPEED)
|
||||
ow = ink_width(shown.offset_text, 32) if shown.offset_text else 0
|
||||
start = (width - nw - (10 + ow if ow else 0)) / 2
|
||||
# Align the posted value and offset at their visible lower edge.
|
||||
number_top, number_bottom = self.fonts.vertical_ink(shown.posted_text, FontRole.BOLD, text_size(64))
|
||||
text(shown.posted_text, 64, start + nw / 2, 84 - (number_bottom - number_top) / scale / 2, white, FontRole.BOLD)
|
||||
draw_speed_row(self.fonts, shown.posted_text, FontRole.SET_SPEED, text_size(NUMBER_SIZE),
|
||||
x + (start + nw / 2) * scale, content_top + NUMBER_ROW * scale, white, '0123456789')
|
||||
if ow:
|
||||
offset_text = cast(str, shown.offset_text)
|
||||
ref_top, ref_bottom = self.fonts.vertical_ink('0123456789', FontRole.SET_SPEED, text_size(NUMBER_SIZE))
|
||||
_, ink_bottom = self.fonts.vertical_ink(shown.posted_text, FontRole.SET_SPEED, text_size(NUMBER_SIZE))
|
||||
number_bottom = row_origin + NUMBER_ROW + (ink_bottom - (ref_top + ref_bottom) / 2) / scale
|
||||
top, bottom = self.fonts.vertical_ink(offset_text, FontRole.MEDIUM, text_size(32))
|
||||
text(offset_text, 32, start + nw + 10 + ow / 2, 84 - (bottom - top) / scale / 2, secondary)
|
||||
label = 'New Limit' if slc_accept_available(state) else 'Unavailable'
|
||||
text(offset_text, 32, start + nw + 10 + ow / 2, number_bottom - (bottom - top) / scale / 2, secondary)
|
||||
label = 'NEW LIMIT' if slc_accept_available(state) else 'Unavailable'
|
||||
caption = ink_width(label, 30, FontRole.SEMI_BOLD)
|
||||
cx = (width - caption - 10) / 2
|
||||
strength = self.pulse.confirmation_strength(now)
|
||||
accent = rl.Color(214, round(188 + 35 * strength), 240, 255)
|
||||
text(label, 30, cx + caption / 2, 104, accent, FontRole.SEMI_BOLD)
|
||||
ax, ay = x + (cx + caption + 8) * scale, y + 104 * scale
|
||||
text(label, 30, cx + caption / 2, row_origin + caption_row, accent, FontRole.SEMI_BOLD)
|
||||
ax, ay = x + (cx + caption + 8) * scale, content_top + caption_row * scale
|
||||
draw_line(self.profile, rl.Vector2(ax - 2 * scale, ay - 4 * scale), rl.Vector2(ax + 2 * scale, ay), 1.5 * scale, accent)
|
||||
draw_line(self.profile, rl.Vector2(ax + 2 * scale, ay), rl.Vector2(ax - 2 * scale, ay + 4 * scale), 1.5 * scale, accent)
|
||||
if state.slc_wheel_confirmation_available:
|
||||
accept, reject = ink_width('Accept', 26), ink_width('Reject', 26)
|
||||
hint_size = 23
|
||||
accept, reject = ink_width('ACCEPT', hint_size), ink_width('REJECT', hint_size)
|
||||
start = (width - 18 - 4 - accept - 10 - 18 - 4 - reject) / 2
|
||||
for left, plus in ((start, True), (start + 18 + 4 + accept + 10, False)):
|
||||
self.shapes.wheel_key(plus, x + (left + 9) * scale, y + 135.5 * scale, scale=scale)
|
||||
self.shapes.wheel_key(plus, x + (left + 9) * scale, content_top + hint_row * scale, scale=scale)
|
||||
hint_ink = rl.Color(210, 216, 216, 255)
|
||||
text('Accept', 26, start + 22 + accept / 2, 135.5, hint_ink if slc_accept_available(state) else MUTED)
|
||||
text('Reject', 26, start + 18 + 4 + accept + 10 + 22 + reject / 2, 135.5, hint_ink)
|
||||
text('ACCEPT', hint_size, start + 22 + accept / 2, row_origin + hint_row, hint_ink if slc_accept_available(state) else MUTED)
|
||||
text('REJECT', hint_size, start + 18 + 4 + accept + 10 + 22 + reject / 2, row_origin + hint_row, hint_ink)
|
||||
else:
|
||||
text('Tap to review', 22, width / 2, 135.5, secondary)
|
||||
text('Tap to review', 22, width / 2, row_origin + hint_row, secondary)
|
||||
|
||||
def _sign(self, state: OnroadState, bounds, now: float) -> None:
|
||||
shown, obs = resolve_unified_speed(state), state.speed_limit
|
||||
@@ -282,21 +291,24 @@ class SpeedLimitWidget:
|
||||
label = 'Override' if shown.override_text is not None else 'Limit'
|
||||
active_text = active_text or shown.slc_target_text or shown.posted_text
|
||||
target_rows = (('Pedal', FontRole.SEMI_BOLD, 44, 68, OVERRIDE),) if pedal else (
|
||||
(label, FontRole.SEMI_BOLD, 34, 25, OVERRIDE if label == 'Override' else white),
|
||||
(active_text, FontRole.SET_SPEED if self.profile == Profile.LARGE else FontRole.BOLD, 64, 68, white))
|
||||
(label, FontRole.SEMI_BOLD, HEADING_SIZE, HEADING_ROW, OVERRIDE if label == 'Override' else white),
|
||||
(active_text, FontRole.SET_SPEED, NUMBER_SIZE, NUMBER_ROW, white))
|
||||
posted = shown.posted_text + (f' {shown.offset_text}' if shown.offset_text else '')
|
||||
detail = f'{heading} {posted}' if pedal or shown.override_text is not None or active_text != shown.posted_text else heading
|
||||
left, right, _, _ = ink(detail, FontRole.MEDIUM, 22)
|
||||
source_rows = ((detail, FontRole.MEDIUM, 22, 120, secondary),) if detail == heading or right - left <= 108 else (
|
||||
(heading, FontRole.SEMI_BOLD, 18, 104, secondary), (posted, FontRole.MEDIUM, 20, 125, secondary))
|
||||
source_rows = ((detail, FontRole.MEDIUM, 22, SOURCE_ROW, secondary),) if detail == heading or right - left <= 108 else (
|
||||
(heading, FontRole.SEMI_BOLD, 18, 106, secondary), (posted, FontRole.MEDIUM, 20, 125, secondary))
|
||||
for value, role, size, cy, color in (*target_rows, *source_rows):
|
||||
left, right, _, _ = ink(value, role, size)
|
||||
size *= min(1, 108 / max(1, right - left))
|
||||
_, _, a, b = ink(value, role, size)
|
||||
centered(value, role, size, cy - (b - a) / 2, color)
|
||||
if role == FontRole.SET_SPEED or value in ('Limit', 'Override'):
|
||||
draw_speed_row(self.fonts, value, role, size * self.text_scale, x + center_x * scale, top + cy * scale,
|
||||
color, '0123456789' if role == FontRole.SET_SPEED else 'H')
|
||||
else:
|
||||
centered(value, role, size, cy - (b - a) / 2, color)
|
||||
return
|
||||
|
||||
large_steady = self.profile == Profile.LARGE and not vienna and not shown.pending
|
||||
color = white
|
||||
if vienna:
|
||||
x, top = x + 5 * scale, top + 5 * scale
|
||||
@@ -305,7 +317,7 @@ class SpeedLimitWidget:
|
||||
else:
|
||||
self.shapes.outline(x + center_x * scale, top + sign_center_y * scale, IN_USE if shown.limit_in_use else white)
|
||||
|
||||
heading_size = 28 if not vienna else 18
|
||||
heading_size = HEADING_SIZE if not vienna else 18
|
||||
if vienna and self.profile == Profile.LARGE and not shown.pending:
|
||||
heading_size = 21
|
||||
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
|
||||
@@ -314,7 +326,7 @@ class SpeedLimitWidget:
|
||||
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
|
||||
# Lift the Vienna stack to keep the offset clear of the bottom rim.
|
||||
lift = (12 if shown.limit_in_use else 7 if shown.offset_text else 2) if vienna else 0
|
||||
header_y = 47 - lift if vienna else 26
|
||||
header_y = 47 - lift if vienna else HEADING_ROW
|
||||
arrow_scale = 1.
|
||||
if vienna:
|
||||
# Fit the heading and arrow inside the circle at the top of their ink.
|
||||
@@ -335,10 +347,10 @@ class SpeedLimitWidget:
|
||||
self.shapes.arrow('compact', round(x + (header_left + heading_width + 6 + arrow_width / 2) * scale),
|
||||
round(top + header_y * scale), arrow_direction, color, scale=scale * arrow_scale)
|
||||
|
||||
role = FontRole.SET_SPEED if large_steady else FontRole.BOLD
|
||||
size = 58 if vienna else 64
|
||||
role = FontRole.BOLD if vienna else FontRole.SET_SPEED
|
||||
size = 58 if vienna else NUMBER_SIZE
|
||||
offset_size = 30 if vienna else 27
|
||||
number_center = (73 if shown.offset_text else 80) - lift if vienna else (68 if shown.offset_text else 80)
|
||||
number_center = (73 if shown.offset_text else 80) - lift if vienna else NUMBER_ROW
|
||||
while True:
|
||||
left, right, a, b = ink(shown.posted_text, role, size)
|
||||
width, height = right - left, b - a
|
||||
@@ -356,7 +368,11 @@ class SpeedLimitWidget:
|
||||
size *= .95
|
||||
if vienna:
|
||||
offset_size *= .95
|
||||
centered(shown.posted_text, role, size, number_top, color)
|
||||
if vienna:
|
||||
centered(shown.posted_text, role, size, number_top, color)
|
||||
else:
|
||||
draw_speed_row(self.fonts, shown.posted_text, role, size * self.text_scale, x + center_x * scale,
|
||||
top + NUMBER_ROW * scale, color, '0123456789')
|
||||
if shown.offset_text:
|
||||
if not vienna:
|
||||
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_approved_limit_offset_and_action_sizes_fit_actual_fonts():
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
|
||||
def test_large_set_and_limit_share_typography_at_the_saved_position():
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
|
||||
from openpilot.starpilot.ui.layout_preview_renderer import sample_state
|
||||
@@ -99,7 +99,7 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
|
||||
assert right-left <= 108*LARGE_SIGN_SCALE
|
||||
assert abs(posted[3] + (left + right) / 2 - (418 + SIGN_WIDTH/2*LARGE_SIGN_SCALE)) <= .5
|
||||
a, b = fonts.vertical_ink('130', FontRole.SET_SPEED, posted[2])
|
||||
assert 30 <= b - a <= 64
|
||||
assert 60 <= b - a <= 70
|
||||
assert abs(posted[4]+(a+b)/2-(95+68*LARGE_SIGN_SCALE)) <= .5
|
||||
assert {'Limit', '120 +10'} <= {call[0] for call in calls}
|
||||
set_calls = [call for call in calls if call[1] == FontRole.SET_SPEED and call[3] < 388]
|
||||
@@ -108,19 +108,20 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
|
||||
text, role, size, x, y, _ = set_calls[0]
|
||||
assert fonts.measure(text, role, size).width <= 200
|
||||
left, right = fonts.horizontal_ink(text, role, size)
|
||||
assert x + (left + right) / 2 == pytest.approx(308)
|
||||
assert x + (left + right) / 2 == pytest.approx(280)
|
||||
assert x + right + 20 <= 418
|
||||
top, bottom = fonts.vertical_ink(text, role, size)
|
||||
assert bottom - top == pytest.approx(77, abs=1)
|
||||
assert y + (top + bottom) / 2 == pytest.approx(185)
|
||||
assert next(call for call in calls if call[0] == 'SET')[2] == 34
|
||||
unit = next(call for call in calls if call[0] == ('km/h' if metric else 'mph'))
|
||||
assert unit[2] == 32
|
||||
assert unit[4] + fonts.vertical_ink(unit[0], unit[1], unit[2])[1] + 6 <= y + top
|
||||
assert (role, size, y) == (posted[1], posted[2], posted[4])
|
||||
heading = next(call for call in calls if call[0] == 'SET')
|
||||
limit = next(call for call in calls if call[0] == 'Limit')
|
||||
assert (heading[1:3], heading[4]) == (limit[1:3], limit[4])
|
||||
assert not any('mph' in call[0] or 'km/h' in call[0] for call in calls)
|
||||
if show_set and value is None:
|
||||
start, end, stroke, color = lines.call_args.args
|
||||
assert (start.x, start.y, end.x, end.y, stroke) == (280, 185, 336, 185, 8)
|
||||
assert (color.r, color.g, color.b, color.a) == (190, 208, 202, 255)
|
||||
assert (start.x, start.y, end.x, end.y, stroke) == pytest.approx(
|
||||
(280 - 20 * LARGE_SIGN_SCALE, 95 + 68 * LARGE_SIGN_SCALE,
|
||||
280 + 20 * LARGE_SIGN_SCALE, 95 + 68 * LARGE_SIGN_SCALE, 5 * LARGE_SIGN_SCALE))
|
||||
assert (color.r, color.g, color.b, color.a) == (255, 255, 255, 255)
|
||||
assert all(call[0] not in ('–', '?') for call in calls)
|
||||
else:
|
||||
lines.assert_not_called()
|
||||
@@ -129,6 +130,51 @@ def test_large_sign_is_secondary_to_set_speed_at_the_saved_position():
|
||||
rl.close_window()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pending', [False, True])
|
||||
def test_set_and_slc_match_font_and_baseline_on_both_displays(pending):
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer
|
||||
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
|
||||
from openpilot.starpilot.ui.tests.test_unified_speed_native import state
|
||||
owns_context = not rl.is_window_ready()
|
||||
if owns_context:
|
||||
rl.init_window(128, 128, 'Shared SET and SLC typography')
|
||||
try:
|
||||
for profile in Profile:
|
||||
with BitmapFonts(profile, default_font_directory()) as fonts:
|
||||
widget = UnifiedSpeedWidget(fonts) if profile == Profile.LARGE else CompactHudRenderer(fonts, Path('/unused'))
|
||||
try:
|
||||
for value in (9, 65, 128):
|
||||
current = state(cruise_kph=140)
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, speed_limit_mps=value / 3.6,
|
||||
pending_speed_limit_mps=value / 3.6 if pending else None,
|
||||
pending_adjusted_limit_mps=value / 3.6 if pending else None,
|
||||
presentation_adjusted_limit_mps=value / 3.6,
|
||||
limiting_max_set=True, effective_cluster_target_mps=value / 3.6))
|
||||
calls = []
|
||||
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)):
|
||||
if isinstance(widget, UnifiedSpeedWidget):
|
||||
widget.render(rl.Rectangle(0, 0, 2160, 1080), current)
|
||||
else:
|
||||
with patch.object(widget, '_set_speed_opacity', return_value=1.):
|
||||
widget.render_max_speed(current)
|
||||
widget._speed_limit_sign(current)
|
||||
if pending:
|
||||
sign = widget.sign if isinstance(widget, UnifiedSpeedWidget) else widget._speed_limit_widget
|
||||
sign.render_confirmation(current)
|
||||
rows = {row[0]: row for row in calls if row[-1].a == 255}
|
||||
for a, b in (('SET', 'OSM' if pending else 'Limit'), ('140', str(value))):
|
||||
assert rows[a][1] == rows[b][1]
|
||||
assert rows[a][2] == pytest.approx(rows[b][2])
|
||||
assert rows[a][4] == pytest.approx(rows[b][4])
|
||||
assert not any('mph' in text or 'km/h' in text for text in rows)
|
||||
finally:
|
||||
widget.close()
|
||||
finally:
|
||||
if owns_context:
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_pending_confirmation_keeps_three_digit_set_speed_clear():
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
|
||||
@@ -190,18 +236,17 @@ def test_status_below_set_has_real_ink_clearance_and_fits_its_widget():
|
||||
saved = placement(current.customization, profile, key)
|
||||
width, height = widget_size(current.customization, profile, key)
|
||||
assert caption[4] + fonts.vertical_ink(*caption[:3])[1] <= saved['y'] + height - 12
|
||||
assert 18 <= caption[2] <= 24
|
||||
scale = LARGE_SIGN_SCALE if profile == Profile.LARGE else 1
|
||||
assert 16 <= caption[2] * profile.font_scale / Profile.COMPACT.font_scale / scale <= 22 + 1e-9
|
||||
left, right = fonts.horizontal_ink(*caption[:3])
|
||||
number_left, number_right = fonts.horizontal_ink(*number[:3])
|
||||
if profile == Profile.COMPACT:
|
||||
assert saved['x'] + 8 <= caption[3] + left < caption[3] + right <= saved['x'] + width - 8
|
||||
assert caption[3] + (left + right) / 2 == pytest.approx(saved['x'] + width / 2)
|
||||
assert number[3] + (number_left + number_right) / 2 == pytest.approx(saved['x'] + width / 2)
|
||||
elif status != 'PEDAL':
|
||||
label = next(row for row in rows if row[0] == 'SET')
|
||||
label_left = label[3] + fonts.horizontal_ink(*label[:3])[0]
|
||||
assert caption[3] + left == pytest.approx(label_left)
|
||||
assert number[3] + number_left == pytest.approx(label_left)
|
||||
else:
|
||||
assert caption[3] + (left + right) / 2 == pytest.approx(saved['x'] + 92)
|
||||
assert number[3] + (number_left + number_right) / 2 == pytest.approx(saved['x'] + 92)
|
||||
widget.close()
|
||||
finally:
|
||||
if owns_context:
|
||||
@@ -388,7 +433,7 @@ def test_compact_preview_sprite_keeps_the_full_confirmation_and_override_sign():
|
||||
rl.init_window(128, 128, 'SLC preview crop fixture')
|
||||
canvas = _Canvas(Profile.COMPACT)
|
||||
try:
|
||||
for y, scene, vienna in product((16, 20), ('slc_pending', 'slc_retained', 'slc_display'), (False, True)):
|
||||
for y, scene, vienna in product((12, 16), ('slc_notice', 'slc_pending', 'slc_retained', 'slc_display'), (False, True)):
|
||||
document = default_document()
|
||||
document['layouts']['compact']['speed_limit']['y'] = y
|
||||
assert validate_document(document)['layouts']['compact']['speed_limit']['y'] == y
|
||||
@@ -408,8 +453,8 @@ def test_compact_preview_sprite_keeps_the_full_confirmation_and_override_sign():
|
||||
finally:
|
||||
rl.unload_image(image)
|
||||
assert np.array_equal(direct[..., 3], np.asarray(rebuilt)[..., 3])
|
||||
if scene == 'slc_pending':
|
||||
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-172, -8, 328, 152)
|
||||
if scene in ('slc_notice', 'slc_pending'):
|
||||
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-172, -8, 328, 159)
|
||||
else:
|
||||
assert (slot['offsetX'], slot['offsetY'], slot['width'], slot['height']) == (-2, 0, SIGN_WIDTH+4, SIGN_HEIGHT)
|
||||
assert np.any(direct[..., 3]), (y, scene)
|
||||
@@ -448,7 +493,7 @@ def test_large_sources_and_confirmation_text_have_clearance_below_both_signs():
|
||||
view._slc_actions(current)
|
||||
foreground = [call for call in calls if call[-1].a == 255]
|
||||
action_bottom = max(y+fonts.vertical_ink(text, role, size)[1]
|
||||
for text, role, size, _, y, _ in foreground if text in ('Accept', 'Reject'))
|
||||
for text, role, size, _, y, _ in foreground if text in ('ACCEPT', 'REJECT'))
|
||||
source_top = min(y+fonts.vertical_ink(text, role, size)[0]
|
||||
for text, role, size, _, y, _ in foreground if text in ('Dash', 'Map', 'Vision'))
|
||||
assert action_bottom+8 <= source_top
|
||||
@@ -608,7 +653,7 @@ def test_both_sign_styles_contain_text_and_arrows_at_each_profile_scale():
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_sign_profiles_share_layout_and_snap_only_final_pixels():
|
||||
def test_sign_profiles_share_layout_and_keep_aligned_numbers():
|
||||
from unittest.mock import Mock, patch
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
|
||||
from openpilot.starpilot.ui.tests.test_unified_speed_native import state
|
||||
@@ -631,7 +676,8 @@ def test_sign_profiles_share_layout_and_snap_only_final_pixels():
|
||||
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)):
|
||||
widget.render(current)
|
||||
calls = [call for call in calls if call[-1].a == 255]
|
||||
assert all(float(call[3]).is_integer() and float(call[4]).is_integer() for call in calls)
|
||||
assert all(float(call[3]).is_integer() and float(call[4]).is_integer()
|
||||
for call in calls if call[1] != FontRole.SET_SPEED)
|
||||
if not vienna:
|
||||
assert calls[0][2]/widget.text_scale <= 28
|
||||
elif fonts.profile == Profile.LARGE and not pending:
|
||||
@@ -644,10 +690,8 @@ def test_sign_profiles_share_layout_and_snap_only_final_pixels():
|
||||
small_widget, small_calls = drawn[0]
|
||||
large_widget, large_calls = drawn[1]
|
||||
assert [call[0] for call in small_calls] == [call[0] for call in large_calls]
|
||||
if not vienna and not pending:
|
||||
assert small_calls[1][1] == FontRole.BOLD
|
||||
assert large_calls[1][1] == FontRole.SET_SPEED
|
||||
continue
|
||||
if not vienna:
|
||||
assert small_calls[1][1] == large_calls[1][1] == FontRole.SET_SPEED
|
||||
start = int(vienna and not pending)
|
||||
for a, b in zip(small_calls[start:], large_calls[start:], strict=True):
|
||||
assert abs(a[2]/small_widget.text_scale-b[2]/large_widget.text_scale) < 1e-9
|
||||
@@ -673,7 +717,7 @@ def test_v5_geometry_migrates_and_keeps_custom_actions_independent():
|
||||
doc['layouts']['large']['cruise_limits'].update(x=1550, y=638)
|
||||
doc['layouts']['large']['speed_limit_actions'].update(x=1550, y=954)
|
||||
migrated = validate_document(doc)
|
||||
assert migrated['version'] == 13
|
||||
assert migrated['version'] == 14
|
||||
assert migrated['layouts']['large']['cruise_limits'] == default_document()['layouts']['large']['cruise_limits']
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1550, 'y': 882, 'enabled': True}
|
||||
assert migrated['speedSources'] is True
|
||||
@@ -720,10 +764,12 @@ def test_committed_projection_v1_right_edge_is_preserved_and_clamped():
|
||||
|
||||
|
||||
|
||||
def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_control():
|
||||
@pytest.mark.parametrize('wheel_confirmation', [False, True])
|
||||
def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_control(wheel_confirmation):
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.large_speed_geometry import CONFIRMATION_SCALE
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
|
||||
from openpilot.starpilot.ui.speed_limit_shapes import ATLAS_PATH, SPRITES, SPRITE_DENSITIES
|
||||
from openpilot.starpilot.ui.tests.test_slc_ui_actions import state
|
||||
owns_context = not rl.is_window_ready()
|
||||
if owns_context:
|
||||
@@ -746,7 +792,7 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
|
||||
pending_adjusted_limit_mps=(posted + offset) / factor,
|
||||
presentation_adjusted_limit_mps=(posted + offset) / factor)
|
||||
current = replace(current, speed_limit=obs, metric=metric, show_slc_offset=show_offset,
|
||||
slc_wheel_confirmation_available=True)
|
||||
slc_wheel_confirmation_available=wheel_confirmation)
|
||||
notice_calls = []
|
||||
with patch.object(fonts, 'draw', lambda *args, sink=notice_calls: sink.append(args)):
|
||||
widget.render_confirmation(current)
|
||||
@@ -764,8 +810,8 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
|
||||
right = boxes[f'{offset:+d}'][2] if show_offset else number[2]
|
||||
assert (number[0] + right) / 2 == pytest.approx((notice_bounds[0] + notice_bounds[2]) / 2)
|
||||
assert boxes['VISION'][3] + 6 * scale <= number[1]
|
||||
assert number[3] + 6 * scale <= boxes['New Limit'][1]
|
||||
assert boxes['New Limit'][3] + 6 * scale <= boxes['Accept'][1]
|
||||
assert number[3] + 6 * scale <= boxes['NEW LIMIT'][1]
|
||||
assert boxes['NEW LIMIT'][3] + 6 * scale <= boxes['ACCEPT' if wheel_confirmation else 'Tap to review'][1]
|
||||
if show_offset:
|
||||
assert boxes[f'{offset:+d}'][3] == pytest.approx(number[3])
|
||||
else:
|
||||
@@ -781,9 +827,8 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
|
||||
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)), patch.object(rl, 'draw_rectangle_rounded'):
|
||||
widget.render_confirmation(current)
|
||||
calls = [call for call in calls if call[-1].a == 255]
|
||||
context = 'Now: OVERRIDE 128 km/h' if posted == 110 else 'New limit'
|
||||
assert [call[0] for call in calls] == [context, 'Accept', str(posted + offset),
|
||||
'km/h' if metric else 'mph', 'Reject', 'new limit']
|
||||
context = 'Now: OVERRIDE 128' if posted == 110 else 'NEW LIMIT'
|
||||
assert [call[0] for call in calls] == [context, 'ACCEPT', str(posted + offset), 'REJECT', 'NEW LIMIT']
|
||||
bounds = speed_limit_bounds(profile, current)
|
||||
assert bounds is not None
|
||||
for text, role, size, x, y, _ in calls[:1]:
|
||||
@@ -792,31 +837,43 @@ def test_chooser_binds_the_target_to_accept_and_keeps_all_native_ink_inside_its_
|
||||
assert x + left >= bounds[0] + 10 * scale
|
||||
assert x + right <= bounds[2] - 10 * scale
|
||||
assert bounds[1] + 8 * scale <= y + top < y + bottom <= bounds[1] + 40 * scale
|
||||
heading_centers = []
|
||||
for control, content in zip(slc_controls(profile, current), (calls[1:4], calls[4:]), strict=True):
|
||||
# Measure from the painted outline, which is smaller than the touch area.
|
||||
name = f'button_{profile}'
|
||||
sx, sy, sw, sh = SPRITES[name]
|
||||
with Image.open(ATLAS_PATH) as atlas:
|
||||
alpha = np.asarray(atlas.crop((sx, sy, sx + sw, sy + sh)).getchannel('A'))
|
||||
horizontal = np.flatnonzero(alpha[sh // 2])
|
||||
vertical = np.flatnonzero(alpha[:, sw // 2])
|
||||
inner_left, inner_right = horizontal[horizontal < sw / 2][-1] + 1, horizontal[horizontal > sw / 2][0]
|
||||
inner_top, inner_bottom = vertical[vertical < sh / 2][-1] + 1, vertical[vertical > sh / 2][0]
|
||||
density = SPRITE_DENSITIES[name]
|
||||
row_centers = []
|
||||
for control, content in zip(slc_controls(profile, current), (calls[1:3], calls[3:]), strict=True):
|
||||
label_bottom = 0
|
||||
boxes = []
|
||||
cx, cy = (control.bounds[0] + control.bounds[2]) / 2, (control.bounds[1] + control.bounds[3]) / 2
|
||||
interior = (cx + (inner_left - sw / 2) * scale / density, cy + (inner_top - sh / 2) * scale / density,
|
||||
cx + (inner_right - sw / 2) * scale / density, cy + (inner_bottom - sh / 2) * scale / density)
|
||||
contour = 1.15 if profile == Profile.LARGE else 1.
|
||||
padding = (14 if profile == Profile.LARGE else 9) * scale
|
||||
for text, role, size, x, y, _ in content:
|
||||
left, right = fonts.horizontal_ink(text, role, size)
|
||||
top, bottom = fonts.vertical_ink(text, role, size)
|
||||
boxes.append((x + left, y + top, x + right, y + bottom))
|
||||
assert x + left - control.bounds[0] >= 12 * scale - 1e-6
|
||||
assert control.bounds[2] - x - right >= 12 * scale - 1e-6
|
||||
assert control.bounds[1] + 10 * scale <= y + top < y + bottom <= control.bounds[3] - 10 * scale
|
||||
if text == control.label:
|
||||
assert min(x + left - interior[0], interior[2] - x - right,
|
||||
y + top - interior[1], interior[3] - y - bottom) - contour >= padding
|
||||
if text == control.label.upper():
|
||||
label_bottom = y + bottom
|
||||
assert (bottom - top) / scale * 25.4 / 300 >= 2.8
|
||||
assert (bottom - top) / scale * 25.4 / 300 >= (1.7 if profile == Profile.LARGE else 1.5)
|
||||
else:
|
||||
assert y + top >= label_bottom + 6 * scale
|
||||
cx, cy = (control.bounds[0] + control.bounds[2]) / 2, (control.bounds[1] + control.bounds[3]) / 2
|
||||
assert (boxes[0][0] + boxes[0][2]) / 2 == pytest.approx(cx)
|
||||
assert (min(box[0] for box in boxes[1:]) + max(box[2] for box in boxes[1:])) / 2 == pytest.approx(cx)
|
||||
assert (min(box[1] for box in boxes) + max(box[3] for box in boxes)) / 2 == pytest.approx(cy)
|
||||
heading_centers.append((boxes[0][1] + boxes[0][3]) / 2)
|
||||
row_centers.append(tuple((box[1] + box[3]) / 2 for box in boxes))
|
||||
if control.label == 'Accept':
|
||||
assert boxes[1][3] == pytest.approx(boxes[2][3]), 'Speed and unit share a lower edge'
|
||||
assert abs((boxes[0][1] + boxes[1][3]) / 2 - cy) < 3 * scale
|
||||
assert (boxes[1][3] - boxes[1][1]) / scale * 25.4 / 300 >= (3.3 if profile == Profile.LARGE else 2.7)
|
||||
assert heading_centers[0] == pytest.approx(heading_centers[1])
|
||||
assert row_centers[0] == pytest.approx(row_centers[1])
|
||||
finally:
|
||||
widget.close()
|
||||
finally:
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_v12_portrait_upgrade_preserves_preferences_and_is_idempotent():
|
||||
before = copy.deepcopy(old)
|
||||
updated = validate_document(old)
|
||||
expected = copy.deepcopy(old)
|
||||
expected['version'] = 13
|
||||
expected['version'] = 14
|
||||
expected['layouts']['compact']['speed_limit'].update(x=320, y=16)
|
||||
assert old == before and updated == expected
|
||||
assert validate_document(updated) == updated
|
||||
@@ -74,6 +74,33 @@ def test_v12_edge_placements_remain_bounded_even_with_confirmation_disabled():
|
||||
assert all(not layout['speed_limit_actions']['enabled'] for layout in updated['layouts'].values())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign_y', [12, 16, 20])
|
||||
def test_v13_notice_upgrade_preserves_preferences_and_only_moves_a_conflicting_sign(sign_y):
|
||||
import copy
|
||||
old = default_document()
|
||||
old['version'] = 13
|
||||
old['layouts']['compact']['speed_limit']['y'] = sign_y
|
||||
old['layouts']['large']['current_speed'].update(x=700, enabled=False)
|
||||
old['widgetColors']['large']['current_speed'] = {'text': '#ABCDEF80'}
|
||||
old['reduceMotion'] = True
|
||||
before = copy.deepcopy(old)
|
||||
updated = validate_document(old)
|
||||
expected = copy.deepcopy(old)
|
||||
expected['version'] = 14
|
||||
expected['layouts']['compact']['speed_limit']['y'] = min(sign_y, 16)
|
||||
assert old == before and updated == expected
|
||||
assert validate_document(updated) == updated
|
||||
|
||||
|
||||
def test_compact_notice_is_reserved_even_when_the_shorter_buttons_would_fit():
|
||||
document = default_document()
|
||||
document['layouts']['compact']['speed_limit']['y'] = 20
|
||||
with pytest.raises(ValueError, match='confirmation'):
|
||||
validate_document(document)
|
||||
document['layouts']['compact']['speed_limit_actions']['enabled'] = False
|
||||
assert validate_document(document) == document
|
||||
|
||||
|
||||
@pytest.mark.parametrize('separate', [False, True])
|
||||
def test_v12_source_attachment_follows_the_new_row_without_moving_custom_actions(separate):
|
||||
old = default_document()
|
||||
@@ -165,7 +192,7 @@ def test_v7_speed_layout_migration_is_bounded_non_mutating_and_idempotent():
|
||||
original = copy.deepcopy(document)
|
||||
migrated = validate_document(document)
|
||||
assert document == original
|
||||
assert migrated['version'] == 13 and validate_document(migrated) == migrated
|
||||
assert migrated['version'] == 14 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1437
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1510, 'y': 882, 'enabled': True}
|
||||
assert migrated['layouts']['large']['current_speed'] == original['layouts']['large']['current_speed']
|
||||
@@ -195,7 +222,7 @@ def test_v8_restores_only_compact_speed_defaults_and_preserves_customizations(cu
|
||||
original = copy.deepcopy(document)
|
||||
migrated = validate_document(document)
|
||||
assert document == original
|
||||
assert migrated['version'] == 13 and validate_document(migrated) == migrated
|
||||
assert migrated['version'] == 14 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large'] == document['layouts']['large']
|
||||
assert migrated['widgetColors'] == document['widgetColors']
|
||||
assert migrated['widgetOrder'] == document['widgetOrder']
|
||||
@@ -236,7 +263,7 @@ def test_v8_actionless_layout_preserves_customizations_when_adding_compact_actio
|
||||
original = copy.deepcopy(document)
|
||||
migrated = validate_document(document)
|
||||
assert document == original
|
||||
assert migrated['version'] == 13 and validate_document(migrated) == migrated
|
||||
assert migrated['version'] == 14 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large'] == {**original['layouts']['large'], 'speed_limit_actions': large_actions}
|
||||
assert migrated['layouts']['compact']['speed_limit'] == default_document()['layouts']['compact']['speed_limit']
|
||||
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 146, 'y': 44, 'enabled': True}
|
||||
@@ -281,7 +308,7 @@ def test_protected_compact_actions_reject_actual_overlap(widget, x, y):
|
||||
validate_document(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 320, 23), ("max_speed", 0, 0), ("steering_wheel", 78, 174)])
|
||||
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 320, 16), ("max_speed", 0, 0), ("steering_wheel", 78, 174)])
|
||||
def test_protected_zone_edge_touch_is_valid(widget, x, y):
|
||||
document = default_document()
|
||||
document["layouts"]["compact"][widget].update(x=x, y=y)
|
||||
@@ -355,7 +382,7 @@ def test_v3_layout_migrates_wheel_size_without_changing_saved_placement_or_color
|
||||
for profile in ("large", "compact"):
|
||||
del document["layouts"][profile]["steering_wheel"]["size"]
|
||||
migrated = validate_document(document)
|
||||
assert migrated["version"] == 13
|
||||
assert migrated["version"] == 14
|
||||
assert migrated["palette"] == document["palette"]
|
||||
assert migrated["layouts"]["large"]["steering_wheel"] == {"x": 1300, "y": 100, "enabled": False, "size": 192}
|
||||
assert migrated["layouts"]["compact"]["steering_wheel"] == {"x": 90, "y": 130, "enabled": True, "size": 50}
|
||||
@@ -407,7 +434,7 @@ def test_malformed_pre_action_cruise_placement_is_rejected():
|
||||
def test_duplicate_key_cannot_apply_otherwise_valid_placement():
|
||||
document = default_document()
|
||||
document["layouts"]["large"]["current_speed"]["x"] = 100
|
||||
raw = json.dumps(document).replace('"version": 13', '"version": 11, "version": 11').encode()
|
||||
raw = json.dumps(document).replace('"version": 14', '"version": 11, "version": 11').encode()
|
||||
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(raw, True)):
|
||||
assert read_customization(Mock()) == default_document()
|
||||
|
||||
@@ -423,7 +450,7 @@ def test_v9_large_sign_migration_preserves_customizations_and_clamps_bottom_edge
|
||||
before = deepcopy(old)
|
||||
updated = validate_document(old)
|
||||
assert old == before
|
||||
assert updated['version'] == 13
|
||||
assert updated['version'] == 14
|
||||
assert updated['layouts']['large']['cruise_limits']['y'] == default_document()['layouts']['large']['cruise_limits']['y']
|
||||
assert updated['layouts']['large']['speed_limit_actions'] == before['layouts']['large']['speed_limit_actions']
|
||||
assert updated['layouts']['compact'] == before['layouts']['compact']
|
||||
@@ -533,7 +560,7 @@ def test_large_draw_origins_and_wheel_input_move_together():
|
||||
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card") as card, patch.object(rl, "draw_line_ex"):
|
||||
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
|
||||
card.assert_not_called()
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (292, 107)
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == pytest.approx((275, 117.14), abs=.01)
|
||||
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
|
||||
assert foreground_calls(fonts.draw)[-2].args[3:5] == (965, 112)
|
||||
wheel = SteeringWheelWidget(Path('/unused'))
|
||||
@@ -566,7 +593,7 @@ def test_frozen_default_draw_geometry_and_neutral_colors():
|
||||
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
|
||||
card.assert_not_called()
|
||||
assert fonts.draw.call_args_list[0].args[0] == 'SET'
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (192, 87)
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == pytest.approx((175, 97.14), abs=.01)
|
||||
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
|
||||
assert foreground_calls(fonts.draw)[-2].args[3:5] == (925, 72)
|
||||
assert foreground_calls(fonts.draw)[-1].args[3:5] == pytest.approx((925, 215.2))
|
||||
@@ -610,7 +637,7 @@ def test_compact_drag_keeps_animation_state_and_separate_origins():
|
||||
with patch.object(hud, "_speed_limit_sign"), patch.object(rl, "draw_circle_gradient"), patch.object(rl, "draw_texture_pro") as wheel:
|
||||
hud.render(state)
|
||||
hud.render(state)
|
||||
assert foreground_calls(fonts.draw)[-1].args[3:5] == (125, 56)
|
||||
assert foreground_calls(fonts.draw)[-1].args[3:5] == (138, 69.5)
|
||||
rect = wheel.call_args.args[2]
|
||||
assert (rect.x, rect.y) == (115, 175)
|
||||
alpha = hud._set_speed_alpha.x
|
||||
@@ -679,7 +706,7 @@ def test_v1_palette_positions_and_visibility_migrate_without_mutating_source():
|
||||
original = copy.deepcopy(old)
|
||||
result = decode_document(json.dumps(old).encode())
|
||||
assert old == original
|
||||
assert result['version'] == 13
|
||||
assert result['version'] == 14
|
||||
assert result['palette'] == old['palette']
|
||||
for profile, layout in old['layouts'].items():
|
||||
for key, position in layout.items():
|
||||
|
||||
@@ -219,7 +219,7 @@ def test_pending_confirmation_omits_redundant_speed_heading():
|
||||
with patch.object(rl, 'draw_line_ex'), patch.object(rl, 'draw_rectangle_rounded'), \
|
||||
patch.object(SpeedLimitShapes, 'button'):
|
||||
view._slc_actions(state)
|
||||
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['New limit', 'Accept', '35', 'mph', 'Reject', 'new limit']
|
||||
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['NEW LIMIT', 'ACCEPT', '35', 'REJECT', 'NEW LIMIT']
|
||||
|
||||
|
||||
def test_compact_pending_label_and_button_follow_saved_action_placement():
|
||||
@@ -242,7 +242,7 @@ def test_compact_pending_label_and_button_follow_saved_action_placement():
|
||||
with patch.object(rl, 'draw_rectangle_rounded') as button, patch.object(rl, 'draw_line_ex'), \
|
||||
patch.object(SpeedLimitShapes, 'button'):
|
||||
view._slc_actions(state)
|
||||
assert [call.args[0] for call in foreground_calls(view.fonts.draw) if call.args[0] in ('Accept', 'Reject')] == ['Accept', 'Reject']
|
||||
assert [call.args[0] for call in foreground_calls(view.fonts.draw) if call.args[0] in ('ACCEPT', 'REJECT')] == ['ACCEPT', 'REJECT']
|
||||
button.assert_not_called()
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (156., 48., 306., 160.)
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ def test_closed_and_open_card_share_anchor_and_physically_scaled_targets(profile
|
||||
opened = touch.present(current)
|
||||
after = speed_limit_bounds(profile, opened)
|
||||
assert before is not None and after is not None
|
||||
assert before[1] == after[1] and before[3] <= after[3]
|
||||
assert before[1] == after[1]
|
||||
assert before[3] - after[3] == pytest.approx(7 if profile == Profile.COMPACT else 0)
|
||||
assert after[0] <= before[0] < before[2] <= after[2]
|
||||
first, last = slc_controls(profile, opened)
|
||||
scale = CONFIRMATION_SCALE[profile]
|
||||
@@ -60,12 +61,29 @@ def test_closed_and_open_card_share_anchor_and_physically_scaled_targets(profile
|
||||
assert (first.bounds[0] - after[0]) / scale == pytest.approx(8)
|
||||
assert (after[2] - last.bounds[2]) / scale == pytest.approx(8)
|
||||
torque = current.customization['layouts'][str(profile)]['torque_bar']
|
||||
assert after[3] < torque['y']
|
||||
assert max(before[3], after[3]) <= torque['y']
|
||||
if profile == Profile.COMPACT:
|
||||
assert after == (148., 8, 476., 160.)
|
||||
assert after[0] > 146
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@pytest.mark.parametrize('wheel_confirmation', [False, True])
|
||||
def test_notice_footer_opens_choices_without_accepting_or_rejecting(profile, wheel_confirmation):
|
||||
current = replace(state(), slc_wheel_confirmation_available=wheel_confirmation)
|
||||
emitted = []
|
||||
touch = OnroadInput(emitted.append, profile)
|
||||
bounds = speed_limit_bounds(profile, current)
|
||||
assert bounds is not None
|
||||
left, _, right, bottom = bounds
|
||||
x, y = (left + right) / 2, bottom - 2 * CONFIRMATION_SCALE[profile]
|
||||
touch.press(x, y, current)
|
||||
touch.release(x, y, current)
|
||||
assert touch.present(current).slc_prompt_open
|
||||
assert len(slc_controls(profile, touch.present(current))) == 2
|
||||
assert not emitted
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@pytest.mark.parametrize('lateral', [False, True])
|
||||
def test_disengaged_and_aol_views_cancel_confirmation_but_keep_the_sign(profile, lateral):
|
||||
@@ -136,7 +154,7 @@ def test_new_layouts_reject_torque_overlap_and_legacy_migration_is_targeted(prof
|
||||
original = deepcopy(doc)
|
||||
migrated = validate_document(doc)
|
||||
assert doc == original
|
||||
assert migrated['version'] == 13
|
||||
assert migrated['version'] == 14
|
||||
assert migrated['palette'] == doc['palette']
|
||||
assert migrated['layouts']['large']['current_speed'] == doc['layouts']['large']['current_speed']
|
||||
assert migrated['layouts'][str(profile)][owner]['y'] == default_document()['layouts'][str(profile)][owner]['y']
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_hidden_set_keeps_control_context_inside_moved_confirmation(profile, mod
|
||||
document = default_document()
|
||||
document['showMaxSpeed'] = False
|
||||
document['layouts']['compact']['max_speed']['enabled'] = False
|
||||
document['layouts']['compact']['speed_limit'].update(x=200, y=20)
|
||||
document['layouts']['compact']['speed_limit'].update(x=200, y=16)
|
||||
document = validate_document(document)
|
||||
current = state(customization=document, slc_prompt_open=True)
|
||||
current = replace(current, longitudinal_overridden=mode == 'pedal', speed_limit=replace(current.speed_limit,
|
||||
@@ -79,8 +79,8 @@ def test_hidden_set_keeps_control_context_inside_moved_confirmation(profile, mod
|
||||
assert not foreground_calls(font.draw)
|
||||
SpeedLimitWidget(font, profile).render_confirmation(current)
|
||||
rows = [call.args for call in foreground_calls(font.draw)]
|
||||
expected = {'limit': 'Now: LIMIT 128 km/h', 'override': 'Now: OVERRIDE 128 km/h', 'pedal': 'Now: PEDAL'}[mode]
|
||||
assert [row[0] for row in rows] == [expected, 'Accept', '120', 'km/h', 'Reject', 'new limit']
|
||||
expected = {'limit': 'Now: LIMIT 128', 'override': 'Now: OVERRIDE 128', 'pedal': 'Now: PEDAL'}[mode]
|
||||
assert [row[0] for row in rows] == [expected, 'ACCEPT', '120', 'REJECT', 'NEW LIMIT']
|
||||
status, accept = rows[:2]
|
||||
assert status[4] + font.vertical_ink(*status[:3])[1] < accept[4]
|
||||
assert not monitor_visible(Profile.COMPACT, current, fresh=True, onroad=True, top_icons=False)
|
||||
@@ -110,7 +110,7 @@ def test_displayed_proposal_matches_the_decision_during_a_held_override(profile,
|
||||
metric=True, longitudinal_active=True, slc_prompt_open=True)
|
||||
font = fonts(profile)
|
||||
SpeedLimitWidget(font, profile).render_confirmation(current)
|
||||
assert [call.args[0] for call in foreground_calls(font.draw)] == ['Now: OVERRIDE 128 km/h', 'Accept', '120', 'km/h', 'Reject', 'new limit']
|
||||
assert [call.args[0] for call in foreground_calls(font.draw)] == ['Now: OVERRIDE 128', 'ACCEPT', '120', 'REJECT', 'NEW LIMIT']
|
||||
assert resolve_unified_speed(current).override_text == '128'
|
||||
action = Action(runtime.session_id, 1, pending.decisionId, pending.presentationId, choice)
|
||||
result = tick(runtime, sm, cp, 4, request=action).message.slcState
|
||||
@@ -142,7 +142,7 @@ def test_hidden_set_prompt_suppresses_only_overlapping_optional_text(widget, met
|
||||
monkeypatch.setattr('openpilot.starpilot.ui.onroad_weather.draw_weather_icon', Mock())
|
||||
document = default_document()
|
||||
document['layouts']['compact']['max_speed']['enabled'] = False
|
||||
document['layouts']['compact']['speed_limit'].update(x=200, y=20)
|
||||
document['layouts']['compact']['speed_limit'].update(x=200, y=16)
|
||||
document['layouts']['compact'][widget].update(enabled=True, x=330 if widget == 'weather' else 200, y=0)
|
||||
current = state(customization=validate_document(document), slc_prompt_open=True)
|
||||
view = OnroadView.__new__(OnroadView)
|
||||
|
||||
@@ -52,7 +52,7 @@ def test_v4_migration_preserves_settings_and_clamps_only_enlarged_widgets():
|
||||
doc['layouts']['large']['speed_limit_actions'].update(x=1654, y=500)
|
||||
old_speed = dict(doc['layouts']['large']['current_speed'])
|
||||
migrated = validate_document(doc)
|
||||
assert migrated['version'] == 13
|
||||
assert migrated['version'] == 14
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1437
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1678, 'y': 267, 'enabled': True}
|
||||
assert migrated['layouts']['large']['current_speed'] == old_speed
|
||||
|
||||
@@ -123,7 +123,7 @@ def test_set_and_binding_slc_stay_visible_beside_sign_or_prompt(profile, stage):
|
||||
texts = [call.args[0] for call in foreground_calls(font.draw)]
|
||||
assert 'SET' in texts and '35' in texts and not any(text.startswith('SLC ') for text in texts)
|
||||
assert ('LIMIT 30' in texts) == (stage == 'prompt')
|
||||
assert ('Now: LIMIT 30 mph' in texts) == (stage == 'expanded')
|
||||
assert ('Now: LIMIT 30' in texts) == (stage == 'expanded')
|
||||
assert ('Limit' in texts) == (stage in ('live', 'retained'))
|
||||
if pending:
|
||||
assert '25' in texts and not any(text.startswith('LAST') for text in texts)
|
||||
@@ -181,7 +181,7 @@ def test_sign_heading_identifies_the_displayed_source(profile, pending, vienna,
|
||||
font = fonts(profile)
|
||||
render_sign(SpeedLimitWidget(font, profile), current)
|
||||
expected = [heading, '50']
|
||||
assert [call.args[0] for call in foreground_calls(font.draw)] == expected + (['+5'] if offset else []) + (['New Limit', 'Tap to review'] if pending else [])
|
||||
assert [call.args[0] for call in foreground_calls(font.draw)] == expected + (['+5'] if offset else []) + (['NEW LIMIT', 'Tap to review'] if pending else [])
|
||||
if not pending:
|
||||
label, speed = foreground_calls(font.draw)[:2]
|
||||
assert rgba_tuple(label.args[-1]) == rgba_tuple(speed.args[-1])
|
||||
@@ -217,10 +217,10 @@ def test_pending_notice_pulses_without_changing_ink_and_arrow_follows_speed(prof
|
||||
assert drawing['arrow'].call_args.args[3] == -1
|
||||
render_sign(sign, replace(current, observed_ns=10_350_000_000, speed_mps=60 / 3.6))
|
||||
after = {call.args[0]: call.args for call in foreground_calls(font.draw)}
|
||||
assert set(before) == {'OSM', '50', '+5', 'New Limit', 'Tap to review'}
|
||||
assert set(before) == {'OSM', '50', '+5', 'NEW LIMIT', 'Tap to review'}
|
||||
assert all(after[text][3:5] == args[3:5] for text, args in before.items())
|
||||
assert all(rgba_tuple(after[text][-1]) == rgba_tuple(args[-1]) for text, args in before.items() if text != 'New Limit')
|
||||
assert rgba_tuple(after['New Limit'][-1]) != rgba_tuple(before['New Limit'][-1])
|
||||
assert all(rgba_tuple(after[text][-1]) == rgba_tuple(args[-1]) for text, args in before.items() if text != 'NEW LIMIT')
|
||||
assert rgba_tuple(after['NEW LIMIT'][-1]) != rgba_tuple(before['NEW LIMIT'][-1])
|
||||
drawing['circle'].assert_not_called()
|
||||
drawing['outline'].assert_not_called()
|
||||
assert drawing['arrow'].call_args.args[3] == 1
|
||||
@@ -248,12 +248,17 @@ def test_wheel_cues_follow_confirmation_availability(profile, available, drawing
|
||||
sign.render_confirmation(current)
|
||||
drawing['draw_rectangle_rounded_lines_ex'].assert_not_called()
|
||||
drawing['button'].assert_not_called()
|
||||
texts = [call.args[0] for call in foreground_calls(sign.fonts.draw)]
|
||||
assert 'NEW LIMIT' in texts
|
||||
assert ('ACCEPT' in texts) == available
|
||||
assert ('REJECT' in texts) == available
|
||||
calls = drawing['wheel_key'].call_args_list
|
||||
assert [call.args[0] for call in calls] == ([True, False] if available else [])
|
||||
if available:
|
||||
bounds = speed_limit_bounds(profile, current)
|
||||
assert bounds is not None
|
||||
assert all(call.args[2] == pytest.approx(bounds[1] + 135.5 * CONFIRMATION_SCALE[profile]) for call in calls)
|
||||
hint_y = bounds[1] + (12 if profile == Profile.LARGE else 8) + (142 if profile == Profile.LARGE else 136.75) * CONFIRMATION_SCALE[profile]
|
||||
assert all(call.args[2] == pytest.approx(hint_y) for call in calls)
|
||||
assert bounds[0] < calls[0].args[1] < calls[1].args[1] < bounds[2]
|
||||
|
||||
|
||||
@@ -398,9 +403,11 @@ def test_compact_set_keeps_dinish_number_and_has_no_backplate(drawing):
|
||||
hud.render_max_speed(current)
|
||||
hud.render_max_speed(replace(current, observed_ns=current.observed_ns + 20_000_000))
|
||||
assert [(call.args[0], call.args[1]) for call in foreground_calls(font.draw)] == [
|
||||
('SET', FontRole.MEDIUM), ('km/h', FontRole.NORMAL), ('80', FontRole.SET_SPEED)]
|
||||
('SET', FontRole.SEMI_BOLD), ('80', FontRole.SET_SPEED)]
|
||||
assert font_filename(Profile.COMPACT, FontRole.SET_SPEED) == 'DINish-Speed.fnt'
|
||||
assert foreground_calls(font.draw)[2].args[4] == 46
|
||||
number = foreground_calls(font.draw)[1].args
|
||||
top, bottom = font.vertical_ink(*number[:3])
|
||||
assert number[4] + (top + bottom) / 2 == 84
|
||||
drawing['draw_circle_gradient'].assert_not_called()
|
||||
|
||||
|
||||
@@ -416,16 +423,20 @@ def test_compact_unset_max_stays_hidden(metric, engaged, drawing):
|
||||
drawing['draw_circle_gradient'].assert_not_called()
|
||||
|
||||
|
||||
def test_large_limit_keeps_a_unit_when_set_speed_is_hidden():
|
||||
profile = Profile.LARGE
|
||||
current = state()
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=None))
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@pytest.mark.parametrize('metric', [False, True])
|
||||
def test_limit_has_no_unit_when_set_speed_is_hidden(profile, metric):
|
||||
current = state(metric=metric)
|
||||
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=None))
|
||||
current.customization['showMaxSpeed'] = False
|
||||
current.customization['layouts']['compact']['max_speed']['enabled'] = False
|
||||
font = fonts(profile)
|
||||
UnifiedSpeedWidget(font).render(rl.Rectangle(30, 30, 1800, 1020), current)
|
||||
assert [call.args[0] for call in foreground_calls(font.draw)].count('km/h') == 1
|
||||
if profile == Profile.LARGE:
|
||||
UnifiedSpeedWidget(font).render(rl.Rectangle(30, 30, 1800, 1020), current)
|
||||
else:
|
||||
CompactHudRenderer(font, Mock()).render(current)
|
||||
texts = [call.args[0] for call in foreground_calls(font.draw)]
|
||||
assert texts and not any('mph' in text or 'km/h' in text for text in texts)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
@@ -436,26 +447,26 @@ def test_transparent_chooser_keeps_touch_targets_and_pressed_feedback(profile, p
|
||||
controls = slc_controls(profile, current)
|
||||
view._slc_actions(current)
|
||||
rows = [call.args for call in foreground_calls(view.fonts.draw)]
|
||||
assert [row[0] for row in rows] == ['New limit', 'Accept', '55', 'km/h', 'Reject', 'new limit']
|
||||
assert [row[0] for row in rows] == ['NEW LIMIT', 'ACCEPT', '55', 'REJECT', 'NEW LIMIT']
|
||||
drawing['draw_rectangle_rounded'].assert_not_called()
|
||||
drawing['draw_line_ex'].assert_not_called()
|
||||
drawing['draw_rectangle_rounded_lines_ex'].assert_not_called()
|
||||
outlines = drawing['button'].call_args_list
|
||||
assert [rgba_tuple(call.args[-1]) for call in outlines] == [rgba_tuple(IN_USE), rgba_tuple(REJECT)]
|
||||
for control, content in zip(controls, (rows[1:4], rows[4:]), strict=True):
|
||||
for control, content in zip(controls, (rows[1:3], rows[3:]), strict=True):
|
||||
for row in content:
|
||||
left, top, right, bottom = control.bounds
|
||||
a, b = view.fonts.horizontal_ink(*row[:3])
|
||||
c, d = view.fonts.vertical_ink(*row[:3])
|
||||
assert left < row[3] + a < row[3] + b < right
|
||||
assert top < row[4] + c < row[4] + d < bottom
|
||||
original = [rgba_tuple(row[-1]) for row in rows if row[0] in ('Accept', 'Reject')]
|
||||
original = [rgba_tuple(row[-1]) for row in rows if row[0] in ('ACCEPT', 'REJECT')]
|
||||
view.pressed_action = lambda: controls[pressed_index].request
|
||||
view.fonts.draw.reset_mock()
|
||||
drawing['button'].reset_mock()
|
||||
view._slc_actions(current)
|
||||
assert [call.args[:5] for call in foreground_calls(view.fonts.draw)] == [row[:5] for row in rows]
|
||||
pressed = [rgba_tuple(call.args[-1]) for call in foreground_calls(view.fonts.draw) if call.args[0] in ('Accept', 'Reject')]
|
||||
pressed = [rgba_tuple(call.args[-1]) for call in foreground_calls(view.fonts.draw) if call.args[0] in ('ACCEPT', 'REJECT')]
|
||||
assert pressed[pressed_index] != original[pressed_index] and pressed[1-pressed_index] == original[1-pressed_index]
|
||||
pressed_outlines = drawing['button'].call_args_list
|
||||
assert [call.kwargs['pressed'] for call in pressed_outlines] == [pressed_index == 0, pressed_index == 1]
|
||||
|
||||
Reference in New Issue
Block a user