mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Restore C4 MAX fade; contain SLC offsets for readability
This commit is contained in:
@@ -10,6 +10,12 @@ assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', signLayou
|
||||
assert.equal(sceneHidesWidget('engaged', 'compact', 'speed_limit', signLayout, signWidgets), false)
|
||||
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', signLayout,
|
||||
{ ...signWidgets, speed_limit: { ...signWidgets.speed_limit, visualInsetTop: 0 } }), false)
|
||||
const restoredLayout = { speed_limit: {x:330, y:20, enabled:true}, speed_limit_actions: {x:128, y:186, enabled:true} }
|
||||
const restoredWidgets = { speed_limit: {width:118, height:132}, speed_limit_actions: {width:348, height:54} }
|
||||
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', restoredLayout, restoredWidgets), false)
|
||||
assert.equal(sceneHidesWidget('slc_pending', 'compact', 'speed_limit', {
|
||||
...restoredLayout, speed_limit: {...restoredLayout.speed_limit, y:60}
|
||||
}, restoredWidgets), true)
|
||||
|
||||
function fixture() {
|
||||
const requests = [], updates = [], timers = new Map(), revoked = []
|
||||
|
||||
@@ -57,7 +57,7 @@ export function snapshot() {
|
||||
metadata.roadColorFields = [{ id: 'path', label: 'Path', default: '#30FF9CFF' },
|
||||
{ id: 'pathEdge', label: 'Path edges', default: '#00FF40FF' }, { id: 'laneLines', label: 'Lane lines', default: '#FFFFFFFF' }]
|
||||
metadata.profiles.compact.widgets.speed_limit_actions.visualInsetTop = 32
|
||||
const document = { version: 8, roadColors: { large: {}, compact: {} }, widgetColors: { large: {}, compact: {} }, palette: Object.fromEntries(metadata.paletteFields.map((field) => [field.id, field.default])),
|
||||
const document = { version: 9, roadColors: { large: {}, compact: {} }, widgetColors: { large: {}, compact: {} }, palette: Object.fromEntries(metadata.paletteFields.map((field) => [field.id, field.default])),
|
||||
layouts: Object.fromEntries(Object.entries(metadata.profiles).map(([id, profile]) => [id,
|
||||
Object.fromEntries(Object.entries(profile.widgets).map(([key, definition]) => [key,
|
||||
{ ...definition.default, ...(definition.resizable ? { size: definition.resizable.default } : {}) }]))])) }
|
||||
@@ -69,6 +69,17 @@ for (const name of ["layoutDocument", "previewDocument", "withDisplay", "ordered
|
||||
assert.equal(layoutModule[name], documentHelpers[name], name)
|
||||
|
||||
const data = snapshot()
|
||||
const restoredCompact = copy(data)
|
||||
for (const [id, width, height, x, y] of [["max_speed", 162, 162, 0, 0], ["speed_limit", 118, 132, 330, 20], ["speed_limit_actions", 348, 54, 128, 186]]) {
|
||||
Object.assign(restoredCompact.metadata.profiles.compact.widgets[id], { width, height, default: {x, y, enabled:true} })
|
||||
restoredCompact.document.layouts.compact[id] = {x, y, enabled:true}
|
||||
}
|
||||
delete restoredCompact.metadata.profiles.compact.widgets.speed_limit_actions.visualInsetTop
|
||||
restoredCompact.defaults = copy(restoredCompact.document)
|
||||
assert.equal(validSnapshot(restoredCompact), true, "Restored small-UI defaults are editable")
|
||||
assert.equal(clampPlacement(restoredCompact.metadata.profiles.compact, "max_speed", 174, 25, restoredCompact.document.layouts.compact), null)
|
||||
assert.deepEqual(clampPlacement(restoredCompact.metadata.profiles.compact, "speed_limit", 330, 20, restoredCompact.document.layouts.compact), {x:330, y:20})
|
||||
assert.deepEqual(restoredCompact.document.layouts.large, data.document.layouts.large)
|
||||
const withMode = copy(data)
|
||||
const modeId = "driving_mode_descriptions"
|
||||
withMode.metadata.profiles.compact.widgets[modeId] = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { validDocument } from "../web/js/onroad-layout.js"
|
||||
const metadata = { profiles: Object.fromEntries(["large", "compact"].map(name => [name, {
|
||||
bounds: {x:0,y:0,width:100,height:100}, widgets:{}, reservedZones:[]
|
||||
}])) }
|
||||
const document = { version:8, palette:{cardFill:"#000000A6",cardBorder:"#C4CDD0B4",text:"#FFFFFFFF"},
|
||||
const document = { version:9, palette:{cardFill:"#000000A6",cardBorder:"#C4CDD0B4",text:"#FFFFFFFF"},
|
||||
layouts:{large:{},compact:{}},widgetColors:{large:{},compact:{}},roadColors:{large:{},compact:{}} }
|
||||
assert.equal(validDocument(document, metadata), true)
|
||||
for (const flag of [true, false]) assert.equal(validDocument({...document,speedSources:flag},metadata),true)
|
||||
|
||||
@@ -113,7 +113,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"] : []), ...(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"] : [])]) || document.version !== 8 ||
|
||||
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"] : []), ...(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"] : [])]) || document.version !== (metadata?.projection === true ? 8 : 9) ||
|
||||
(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)) ||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from openpilot.starpilot.system.android_auto.identity import DATA_DIR
|
||||
from openpilot.starpilot.system.android_auto.display_profile import screen_geometry
|
||||
from openpilot.starpilot.ui.onroad_customization import (CEM_GAUGE_WIDGET, CLOCK_WIDGET, JETLINK_WIDGET, MODE_WIDGET,
|
||||
STOPPED_TIMER_WIDGET, CEM_GAUGE_FRAME, CEM_GAUGE_INSET, MAP_MIN_SIZE, MAP_OPACITY, NAV_CARD_SIZE,
|
||||
STOPPED_TIMER_WIDGET, CEM_GAUGE_FRAME, CEM_GAUGE_INSET, MAP_MIN_SIZE, NAV_CARD_SIZE,
|
||||
SPEED_ALIGNMENTS, customization_metadata, validate_map_box, validate_widget_order)
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
|
||||
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
|
||||
@@ -204,7 +204,7 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
result = validate_layout_for_viewport(value, viewport, _legacy_geometry=True)
|
||||
card, actions = result['widgets']['cruise_limits'], result['widgets']['speed_limit_actions']
|
||||
gap = actions['y'] - card['y']
|
||||
migrate_speed_layout(result['widgets'], 'large', metadata['bounds'], gap if gap in (372, 425) else 232)
|
||||
migrate_speed_layout(result['widgets'], 'large', metadata['bounds'], gap if gap in (372, 425) else 232, widgets=metadata['widgets'])
|
||||
result['version'] = 2
|
||||
return validate_layout_for_viewport(result, viewport)
|
||||
fields = {'version', 'canvas', 'widgets'}
|
||||
@@ -313,7 +313,7 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
if value['version'] < 4:
|
||||
card, actions = result['widgets']['cruise_limits'], result['widgets']['speed_limit_actions']
|
||||
gap = actions['y'] - card['y']
|
||||
migrate_speed_layout(result['widgets'], 'large', bounds, gap if gap in (232, 492) else 232)
|
||||
migrate_speed_layout(result['widgets'], 'large', bounds, gap if gap in (232, 492) else 232, widgets=metadata['widgets'])
|
||||
result['version'] = 4
|
||||
if len(json.dumps(result, separators=(',', ':'), allow_nan=False).encode()) > MAX_BYTES:
|
||||
raise ValueError('Projection layout too large')
|
||||
|
||||
@@ -12,12 +12,12 @@ GAP = 8
|
||||
SOURCE_ROW_HEIGHT = 76
|
||||
PADDING = 24
|
||||
|
||||
LIMIT_RECTS = {'large': (272, 94, 292, 124), 'compact': (158, 10, 264, 82)}
|
||||
LIMIT_RECTS = {'large': (272, 94, 292, 124), 'compact': (330, 20, 118, 132)}
|
||||
ACTION_BOUNDS = {
|
||||
'large': ((100, 248, 372, 408), (404, 248, 676, 408)),
|
||||
'compact': ((16, 118, 224, 222), (248, 118, 456, 222)),
|
||||
'compact': ((128, 182, 296, 240), (308, 182, 476, 240)),
|
||||
}
|
||||
ACTION_TEXT = {'large': (64, 320), 'compact': (50, 146)}
|
||||
ACTION_TEXT = {'large': (64, 320), 'compact': (48, 213)}
|
||||
|
||||
|
||||
def card_height(mode):
|
||||
|
||||
@@ -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 MUTED, SpeedLimitWidget, draw_caption, draw_ink
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
|
||||
from openpilot.starpilot.ui.moon import draw_moon
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def draw_lead_icon(rect: rl.Rectangle) -> None:
|
||||
|
||||
|
||||
class CompactHudRenderer:
|
||||
"""Compact speed group and the native steering-wheel placement."""
|
||||
"""Transient compact MAX, detached speed-limit sign and native steering wheel."""
|
||||
|
||||
def __init__(self, fonts: BitmapFonts, asset_directory: Path):
|
||||
self.fonts = fonts
|
||||
@@ -137,14 +137,18 @@ class CompactHudRenderer:
|
||||
cruise = "–" if state.cruise_kph is None else str(round(state.cruise_kph if state.metric else state.cruise_kph * 0.621371))
|
||||
mx, my = widget_offset(state.customization, "compact", "max_speed")
|
||||
if set_speed_alpha > 0.01 and placement(state.customization, "compact", "max_speed")["enabled"]:
|
||||
shadow = rl.Color(0, 0, 0, int(255 / 2 * set_speed_alpha))
|
||||
try:
|
||||
rl.draw_circle_gradient(rl.Vector2(81 + mx, 81 + my), 81, shadow, rl.BLANK)
|
||||
except (RuntimeError, TypeError):
|
||||
rl.draw_circle_gradient(int(81 + mx), int(81 + my), 81, shadow, rl.BLANK)
|
||||
red, green, blue, alpha = rgba(state.customization, "text", "compact", "max_speed")
|
||||
color = rl.Color(red, green, blue, int(alpha * set_speed_alpha))
|
||||
muted = rl.Color(MUTED.r, MUTED.g, MUTED.b, int(255 * set_speed_alpha))
|
||||
speed_size = 108
|
||||
while speed_size > 72 and self.fonts.measure(cruise, FontRole.SET_SPEED, speed_size).width > 124:
|
||||
color = rl.Color(red, green, blue, int(alpha * 0.9 * set_speed_alpha))
|
||||
speed_size = 112
|
||||
while speed_size > 72 and self.fonts.measure(cruise, FontRole.SET_SPEED, speed_size).width > 128:
|
||||
speed_size -= 1
|
||||
draw_caption(self.fonts, 'SET', 23 + mx, 10 + my, muted, unit='km/h' if state.metric else 'mph')
|
||||
draw_ink(self.fonts, cruise, FontRole.SET_SPEED, speed_size, 17 + mx, 34 + my, color)
|
||||
self.fonts.draw(cruise, FontRole.SET_SPEED, speed_size, 17 + mx, -4 + my, color)
|
||||
self.fonts.draw("MAX", FontRole.SEMI_BOLD, 36, 25 + mx, 109 + 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)
|
||||
@@ -186,7 +190,8 @@ class CompactHudRenderer:
|
||||
self._was_cruise_active = True
|
||||
self._last_cruise_display = display
|
||||
self._last_cruise_ns = now_ns
|
||||
return self._set_speed_alpha.update(1.0)
|
||||
elapsed = now_ns - self._set_speed_changed_ns if self._set_speed_changed_ns is not None else 0
|
||||
return self._set_speed_alpha.update(float(0 < elapsed < 2_500_000_000))
|
||||
if (self._last_cruise_ns is None or now_ns < self._last_cruise_ns or
|
||||
now_ns - self._last_cruise_ns > 150_000_000):
|
||||
self._set_speed_alpha.x = 0.0
|
||||
|
||||
@@ -59,6 +59,8 @@ class Widget(TypedDict):
|
||||
sourcesExtent: NotRequired[dict[str, float]]
|
||||
box: NotRequired[dict[str, int]]
|
||||
opacity: NotRequired[dict[str, int]]
|
||||
anchorShift: NotRequired[float]
|
||||
expandable: NotRequired[bool]
|
||||
|
||||
|
||||
class LayoutProfile(TypedDict):
|
||||
@@ -95,23 +97,23 @@ PROFILES: dict[str, LayoutProfile] = {
|
||||
"protectedWidget": "speed_limit_actions",
|
||||
"reservedZones": [],
|
||||
"widgets": {
|
||||
"max_speed": _widget("Set speed", "max_speed", 146, 114, 0, 0),
|
||||
"speed_limit": _widget("Speed limit", "speed_limit", 264, 82, 158, 18),
|
||||
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", 440, 52, 16, 122),
|
||||
"max_speed": _widget("Set speed", "max_speed", 162, 162, 0, 0),
|
||||
"speed_limit": _widget("Speed limit", "speed_limit", 118, 132, 330, 20),
|
||||
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", 348, 54, 128, 186),
|
||||
"steering_wheel": _widget("Steering wheel", "steering_wheel", 50, 50, 21, 176),
|
||||
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 60, 60, 16, 10)}}}
|
||||
|
||||
# The editor protects the visible words; padded touch targets extend to y=222.
|
||||
PROFILES['compact']['widgets']['speed_limit']['visualInsetTop'] = 12
|
||||
PROFILES['compact']['widgets']['speed_limit']['visualInsetLeft'] = 8
|
||||
PROFILES['compact']['widgets']['speed_limit_actions']['note'] = 'Accept / Decline appear after tapping a pending limit; touch areas extend below the labels.'
|
||||
PROFILES['compact']['widgets']['speed_limit_actions']['bounds'] = {'x': 0, 'y': 4, 'width': 476, 'height': 188}
|
||||
PROFILES['compact']['widgets']['speed_limit_actions']['note'] = 'Accept / Decline appear after tapping a pending limit.'
|
||||
|
||||
SPEED_LAYOUT_V7 = {
|
||||
'large': {'cruise_limits': (320, 210, 88, 75), 'speed_limit_actions': (320, 108, 88, 307)},
|
||||
'compact': {'max_speed': (162, 162, 0, 0), 'speed_limit': (118, 132, 174, 12),
|
||||
'speed_limit_actions': (282, 54, 174, 180)},
|
||||
}
|
||||
COMPACT_SPEED_LAYOUT_V8 = {
|
||||
'max_speed': (146, 114, 0, 0), 'speed_limit': (264, 82, 158, 18),
|
||||
'speed_limit_actions': (440, 52, 16, 122),
|
||||
}
|
||||
|
||||
for profile, sizes in DM_SIZES.items():
|
||||
PROFILES[profile]['widgets']['driver_monitor']['default']['size'] = sizes[1]
|
||||
@@ -177,9 +179,9 @@ PROFILES["large"]["widgets"][NAV_CARD_WIDGET] = _nav_card
|
||||
_nav_map = _widget("Map overlay", NAV_MAP_WIDGET, 560, 420, 1230, 625)
|
||||
_nav_map["default"].update(enabled=False, width=560, height=420, opacity=MAP_OPACITY[2])
|
||||
_nav_map["box"] = {"minWidth": MAP_MIN_SIZE[0], "maxWidth": 1800, "minHeight": MAP_MIN_SIZE[1], "maxHeight": 1020}
|
||||
_nav_map["opacity"] = dict(zip(("min", "max", "default"), MAP_OPACITY))
|
||||
_nav_map["opacity"] = dict(zip(("min", "max", "default"), MAP_OPACITY, strict=True))
|
||||
_nav_map["note"] = ("Road lines and your route over the camera. Drag the corner to resize; opacity sets how much shows. "
|
||||
"Turning it on here turns it off in the other screen's layout.")
|
||||
+ "Turning it on here turns it off in the other screen's layout.")
|
||||
PROFILES["large"]["widgets"][NAV_MAP_WIDGET] = _nav_map
|
||||
|
||||
JETLINK_WIDGET = "jetlink_status"
|
||||
@@ -187,7 +189,7 @@ JETLINK_SIZE = (420, 84)
|
||||
JETLINK_OPACITY = (0, 100, 85) # percent: min, max, default
|
||||
_jetlink = _widget("Jetlink status", JETLINK_WIDGET, *JETLINK_SIZE, 30, 650)
|
||||
_jetlink["default"].update(enabled=False, opacity=JETLINK_OPACITY[2])
|
||||
_jetlink["opacity"] = dict(zip(("min", "max", "default"), JETLINK_OPACITY))
|
||||
_jetlink["opacity"] = dict(zip(("min", "max", "default"), JETLINK_OPACITY, strict=True))
|
||||
_jetlink["note"] = "Shown while Jetlink is on: connection, device and host processing time. Opacity sets the card background."
|
||||
PROFILES["large"]["widgets"][JETLINK_WIDGET] = _jetlink
|
||||
# Restore the Dom road graphic in the gap between the speed card and main speed.
|
||||
@@ -301,7 +303,7 @@ def customization_metadata():
|
||||
|
||||
|
||||
def default_document():
|
||||
return {"version": 8, "clock24Hour": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
|
||||
return {"version": 9, "clock24Hour": 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()}
|
||||
@@ -316,17 +318,18 @@ def validate_map_box(placement, widget):
|
||||
raise ValueError("Invalid map overlay size or opacity")
|
||||
|
||||
|
||||
def migrate_speed_layout(layout, profile, bounds, old_gap=232):
|
||||
def migrate_speed_layout(layout, profile, bounds, old_gap=232, *, previous=None, widgets=None):
|
||||
widgets = PROFILES[profile]['widgets'] if widgets is None else widgets
|
||||
if profile == 'large':
|
||||
card, actions = layout['cruise_limits'], layout['speed_limit_actions']
|
||||
if actions['x'] == card['x'] and actions['y'] - card['y'] == old_gap:
|
||||
actions.update(x=card['x'] + ACTION_X - 88, y=card['y'] + ACTION_Y - 75)
|
||||
gauge = layout[CEM_GAUGE_WIDGET]
|
||||
if (gauge['x'], gauge['y']) == (480, 99):
|
||||
default = PROFILES['large']['widgets'][CEM_GAUGE_WIDGET]['default']
|
||||
default = widgets[CEM_GAUGE_WIDGET]['default']
|
||||
gauge.update(x=default['x'], y=default['y'])
|
||||
for key, (_, _, old_x, old_y) in SPEED_LAYOUT_V7[profile].items():
|
||||
saved, widget = layout[key], PROFILES[profile]['widgets'][key]
|
||||
for key, (_, _, old_x, old_y) in (previous or SPEED_LAYOUT_V7[profile]).items():
|
||||
saved, widget = layout[key], widgets[key]
|
||||
area = widget.get('bounds', bounds)
|
||||
if (saved['x'], saved['y']) == (old_x, old_y):
|
||||
saved.update(x=widget['default']['x'], y=widget['default']['y'])
|
||||
@@ -340,7 +343,7 @@ def migrate_speed_layout(layout, profile, bounds, old_gap=232):
|
||||
if above < 0 and below + height > bounds['height']:
|
||||
actions['y'] = PROFILES[profile]['widgets']['speed_limit_actions']['default']['y']
|
||||
break
|
||||
# The words now span the screen. Move only widgets intersecting that new row.
|
||||
# Keep custom placements unless they intersect the confirmation words.
|
||||
for key, height in _compact_action_collisions(layout):
|
||||
candidates = (actions['y'] - height, actions['y'] + action_height)
|
||||
layout[key]['y'] = min((y for y in candidates if 0 <= y <= bounds['height'] - height),
|
||||
@@ -362,7 +365,7 @@ def _compact_action_collisions(layout):
|
||||
|
||||
|
||||
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, 9):
|
||||
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 10):
|
||||
raise ValueError("Invalid customization version")
|
||||
fields = {"version", "palette", "layouts"} | ({"widgetColors"} if value["version"] >= 2 else set())
|
||||
if value["version"] >= 3:
|
||||
@@ -416,6 +419,9 @@ def validate_document(value):
|
||||
"y": actions_default["y"] + cruise["y"] - cruise_default["y"],
|
||||
"enabled": cruise["enabled"],
|
||||
}
|
||||
if value['version'] == 8 and 'speed_limit_actions' not in layouts['compact']:
|
||||
_, _, x, y = COMPACT_SPEED_LAYOUT_V8['speed_limit_actions']
|
||||
layouts['compact']['speed_limit_actions'] = {'x': x, 'y': y, 'enabled': True}
|
||||
for profile in PROFILES:
|
||||
for key in ("driver_monitor", "torque_bar", "speed_limit_actions"):
|
||||
layouts[profile].setdefault(key, dict(PROFILES[profile]["widgets"][key]["default"]))
|
||||
@@ -530,6 +536,8 @@ def validate_document(value):
|
||||
dimensions = legacy.get(key) if value['version'] < 6 and profile == 'large' else None
|
||||
if dimensions is None and value['version'] < 8:
|
||||
dimensions = SPEED_LAYOUT_V7[profile].get(key)
|
||||
if value['version'] == 8 and profile == 'compact':
|
||||
dimensions = COMPACT_SPEED_LAYOUT_V8.get(key)
|
||||
dimension = (dimensions[0 if axis == 'x' else 1] if dimensions else
|
||||
size if key in WIDGET_SIZES else placement[extent] if widget.get("box") else widget[extent])
|
||||
if old_gauge:
|
||||
@@ -539,7 +547,7 @@ def validate_document(value):
|
||||
result["layouts"][profile][key] = {**placement, **({"size": size} if key in WIDGET_SIZES else {})}
|
||||
if old_gauge:
|
||||
result["layouts"][profile][key].update(x=placement["x"] + CEM_GAUGE_INSET[0], y=placement["y"] + CEM_GAUGE_INSET[1])
|
||||
if profile == "compact" and value['version'] >= 8 and any(_compact_action_collisions(result['layouts'][profile])):
|
||||
if profile == "compact" and value['version'] >= 9 and any(_compact_action_collisions(result['layouts'][profile])):
|
||||
raise ValueError("Widget overlaps protected speed limit actions")
|
||||
if 'widgetOrder' in value:
|
||||
orders = value['widgetOrder']
|
||||
@@ -569,6 +577,9 @@ def validate_document(value):
|
||||
for profile in PROFILES:
|
||||
migrate_speed_layout(result['layouts'][profile], profile, PROFILES[profile]['bounds'], old_gap)
|
||||
return validate_document(result)
|
||||
if value['version'] == 8:
|
||||
migrate_speed_layout(result['layouts']['compact'], 'compact', PROFILES['compact']['bounds'], previous=COMPACT_SPEED_LAYOUT_V8)
|
||||
return validate_document(result)
|
||||
if len(json.dumps(result, separators=(",", ":")).encode()) > MAX_BYTES:
|
||||
raise ValueError("Customization too large")
|
||||
return result
|
||||
|
||||
@@ -67,6 +67,9 @@ class SpeedLimitWidget:
|
||||
self.pulse.update(state, now, visible=bounds is not None)
|
||||
if bounds is None:
|
||||
return
|
||||
if self.profile == Profile.COMPACT:
|
||||
self._compact_sign(state, bounds, now)
|
||||
return
|
||||
shown, obs = resolve_unified_speed(state), state.speed_limit
|
||||
pending = shown.pending and not obs.automatic_adoption_pending
|
||||
elapsed = max(0., now - obs.pending_since_ns / 1e9)
|
||||
@@ -101,3 +104,55 @@ class SpeedLimitWidget:
|
||||
left, top, right, bottom = x - 4, number_top - 3, x + width + 4, number_top + height + 3
|
||||
draw_line(self.profile, rl.Vector2(left, top), rl.Vector2(right, bottom), 3 if large else 2.5, color)
|
||||
draw_line(self.profile, rl.Vector2(right, top), rl.Vector2(left, bottom), 3 if large else 2.5, color)
|
||||
|
||||
def _compact_sign(self, state: OnroadState, bounds, now: float) -> None:
|
||||
shown, obs = resolve_unified_speed(state), state.speed_limit
|
||||
x, top = bounds[0] + 12, bounds[1] + 12
|
||||
pending = shown.pending and not obs.automatic_adoption_pending
|
||||
elapsed = max(0., now - obs.pending_since_ns / 1e9)
|
||||
hop = -6 * math.sin(math.pi * (elapsed % 1.5) / 1.5) ** 2 if pending else 0
|
||||
white = self.pulse.color(rl.Color(244, 247, 245, 255), now)
|
||||
secondary = self.pulse.color(rl.Color(223, 232, 227, 255), now)
|
||||
|
||||
def centered(text, role, size, ink_top, color):
|
||||
left, right = self.fonts.horizontal_ink(text, role, size)
|
||||
a, b = self.fonts.vertical_ink(text, role, size)
|
||||
origin = x + 59 - (left + right) / 2
|
||||
self.fonts.draw(text, role, size, origin, ink_top - a, color)
|
||||
return origin + left, ink_top, origin + right, ink_top + b - a
|
||||
|
||||
if state.appearance.use_vienna_sign:
|
||||
cx, cy = x + 59, top + 67 + hop
|
||||
rl.draw_circle(int(cx), int(cy), 59, rl.WHITE)
|
||||
rl.draw_ring(rl.Vector2(cx, cy), 51, 59, 0, 360, 64, self.pulse.color(rl.Color(201, 34, 49, 255), now))
|
||||
color = self.pulse.color(rl.Color(0, 0, 0, 255), now)
|
||||
size = 58 if len(shown.posted_text) <= 2 else 48
|
||||
a, b = self.fonts.vertical_ink(shown.posted_text, FontRole.BOLD, size)
|
||||
number_top = top + 36 + hop if shown.offset_text else cy - (b - a) / 2
|
||||
number = centered(shown.posted_text, FontRole.BOLD, size, number_top, color)
|
||||
if shown.offset_text:
|
||||
centered(shown.offset_text, FontRole.MEDIUM, 36, top + 81 + hop, color)
|
||||
else:
|
||||
rect = rl.Rectangle(x + 2, top + 2, 114, 128)
|
||||
rl.draw_rectangle_rounded(rect, .1, 16, rl.Color(10, 17, 19, 230))
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, .1, 16, 2, white)
|
||||
for label, y in (('SPEED', top + 11), ('LIMIT', top + 27)):
|
||||
centered(label, FontRole.SEMI_BOLD, 20, y, white)
|
||||
number = centered(shown.posted_text, FontRole.BOLD, 58, top + (50 if shown.offset_text else 61) + hop, white)
|
||||
if shown.offset_text:
|
||||
centered(shown.offset_text, FontRole.MEDIUM, 36, top + 98 + hop, secondary)
|
||||
if pending:
|
||||
target = displayed_adjusted_limit_mps(obs)
|
||||
if target is not None and state.speed_mps is not None and target != state.speed_mps:
|
||||
ax, ay = ((x + 105, top + 49 + hop) if state.appearance.use_vienna_sign else (x + 108, top + 23))
|
||||
arrow_color = color if state.appearance.use_vienna_sign else white
|
||||
direction = -1 if target > state.speed_mps else 1
|
||||
tip = rl.Vector2(ax, ay + direction * 6)
|
||||
draw_line(self.profile, rl.Vector2(ax, ay - direction * 6), tip, 2, arrow_color)
|
||||
for dx in (-3, 3):
|
||||
draw_line(self.profile, tip, rl.Vector2(ax + dx, tip.y - direction * 4), 2, arrow_color)
|
||||
if obs.driver_override_active:
|
||||
color = rl.Color(197, 163, 255, 240)
|
||||
left, top, right, bottom = number
|
||||
draw_line(self.profile, rl.Vector2(left - 3, top - 2), rl.Vector2(right + 3, bottom + 2), 2.5, color)
|
||||
draw_line(self.profile, rl.Vector2(right + 3, top - 2), rl.Vector2(left - 3, bottom + 2), 2.5, color)
|
||||
|
||||
@@ -45,9 +45,15 @@ def test_approved_limit_offset_and_action_sizes_fit_actual_fonts():
|
||||
large = profile == Profile.LARGE
|
||||
limit_size, offset_size = (70, 46) if large else (58, 36)
|
||||
for value, offset in (('65', '+5'), ('120', '+10'), ('130', '-10'), ('145', '-137')):
|
||||
width = (fonts.measure(value, FontRole.BOLD, limit_size).width +
|
||||
fonts.measure(offset, FontRole.MEDIUM, offset_size).width + (40 if large else 31))
|
||||
a, b = fonts.horizontal_ink(value, FontRole.BOLD, limit_size)
|
||||
value_width = b - a
|
||||
a, b = fonts.horizontal_ink(offset, FontRole.MEDIUM, offset_size)
|
||||
offset_width = b - a
|
||||
width = value_width + offset_width + 40 if large else max(value_width, offset_width) + 20
|
||||
assert width <= LIMIT_RECTS[profile][2]
|
||||
action_width = 272 if large else 168
|
||||
assert all(fonts.measure(label, FontRole.MEDIUM, ACTION_TEXT[profile][0]).width + 4 <= action_width
|
||||
for label in ('Accept', 'Decline'))
|
||||
ppi = (2160**2 + 1080**2)**.5 / 6 if large else 300
|
||||
for text, role, size, minimum in (('65', FontRole.BOLD, limit_size, 2.8),
|
||||
('+5', FontRole.MEDIUM, offset_size, 2.0),
|
||||
@@ -191,13 +197,17 @@ def test_native_captions_offsets_and_bounce_fit_their_touch_targets():
|
||||
hud._set_speed_alpha.x = 1.
|
||||
hud.render_max_speed(current)
|
||||
hud._speed_limit_sign(current)
|
||||
ink = {call[0]: (call[3], call[4]+fonts.vertical_ink(call[0], call[1], call[2])[0],
|
||||
call[3]+fonts.measure(call[0], call[1], call[2]).width,
|
||||
ink = {call[0]: (call[3]+fonts.horizontal_ink(call[0], call[1], call[2])[0], call[4]+fonts.vertical_ink(call[0], call[1], call[2])[0],
|
||||
call[3]+fonts.horizontal_ink(call[0], call[1], call[2])[1],
|
||||
call[4]+fonts.vertical_ink(call[0], call[1], call[2])[1])
|
||||
for call in calls if call[-1].a == 255}
|
||||
assert ink['SET'][1] == ink['LIMIT'][1]
|
||||
assert ink['120'][3] == ink['+10'][3]
|
||||
assert ink['120'][2] < ink['+10'][0]
|
||||
if profile == Profile.LARGE:
|
||||
assert ink['SET'][1] == ink['LIMIT'][1]
|
||||
assert ink['120'][3] == ink['+10'][3]
|
||||
assert ink['120'][2] < ink['+10'][0]
|
||||
else:
|
||||
assert 'SET' not in ink and 'SPEED' in ink
|
||||
assert ink['120'][3] < ink['+10'][1]
|
||||
bounds = speed_limit_bounds(profile, current)
|
||||
assert bounds is not None
|
||||
left, top, right, bottom = bounds
|
||||
@@ -211,7 +221,7 @@ def test_native_captions_offsets_and_bounce_fit_their_touch_targets():
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_compact_preview_sprite_keeps_the_caption_arrow_and_override_cross():
|
||||
def test_compact_preview_sprite_keeps_the_sign_offset_arrow_and_override_cross():
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.layout_preview_renderer import _Canvas, _straight_rgba, sample_state
|
||||
@@ -220,7 +230,7 @@ def test_compact_preview_sprite_keeps_the_caption_arrow_and_override_cross():
|
||||
rl.init_window(128, 128, 'SLC preview crop fixture')
|
||||
canvas = _Canvas(Profile.COMPACT)
|
||||
try:
|
||||
for y, scene in product((18, 0), ('slc_pending', 'slc_retained')):
|
||||
for y, scene in product((20, 0), ('slc_pending', 'slc_retained')):
|
||||
document = default_document()
|
||||
document['layouts']['compact']['speed_limit']['y'] = y
|
||||
with patch.dict('openpilot.starpilot.ui.layout_preview_renderer.RUNTIME_PREVIEW_WIDGETS', compact=('speed_limit',)):
|
||||
@@ -229,7 +239,7 @@ def test_compact_preview_sprite_keeps_the_caption_arrow_and_override_cross():
|
||||
slot = json.loads(sheet.info['starpilot-widget-art'])['sprites']['speed_limit']
|
||||
sprite = sheet.crop((slot['x'], slot['y'], slot['x']+slot['width'], slot['y']+slot['height']))
|
||||
rebuilt = Image.new('RGBA', Profile.COMPACT.size)
|
||||
rebuilt.paste(sprite, (round(158+slot['offsetX']), round(y+slot['offsetY'])))
|
||||
rebuilt.paste(sprite, (round(330+slot['offsetX']), round(y+slot['offsetY'])))
|
||||
image = rl.load_image_from_texture(canvas.target.texture)
|
||||
try:
|
||||
rl.image_flip_vertical(image)
|
||||
@@ -237,16 +247,60 @@ def test_compact_preview_sprite_keeps_the_caption_arrow_and_override_cross():
|
||||
finally:
|
||||
rl.unload_image(image)
|
||||
assert np.array_equal(direct[..., 3], np.asarray(rebuilt)[..., 3])
|
||||
if y:
|
||||
assert slot['offsetY'] < 0 and np.asarray(sprite)[:round(-slot['offsetY']), :, 3].any()
|
||||
if scene == 'slc_retained':
|
||||
assert slot['offsetX'] < 0 and np.asarray(sprite)[:, :round(-slot['offsetX']), 3].any()
|
||||
assert slot['offsetX'] == slot['offsetY'] == 0
|
||||
assert (slot['width'], slot['height']) == (118, 132)
|
||||
finally:
|
||||
canvas.close()
|
||||
if owns_context:
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_compact_balanced_stack_uses_mock_sizes_and_contains_all_ink():
|
||||
from unittest.mock import patch
|
||||
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget
|
||||
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, 'C4 contained sign fixture')
|
||||
try:
|
||||
with BitmapFonts(Profile.COMPACT, default_font_directory()) as fonts:
|
||||
widget = SpeedLimitWidget(fonts, Profile.COMPACT)
|
||||
for vienna, (posted, offset), elapsed, (x, y) in product(
|
||||
(False, True), ((65, 5), (120, 10), (130, -10), (145, -137), (55, 0)),
|
||||
(0, 750_000_000, 1_500_000_000), ((330, 20), (100, 0), (358, 108))):
|
||||
current = state(observed_ns=10_000_000_000+elapsed)
|
||||
current.customization['layouts']['compact']['speed_limit'].update(x=x, y=y)
|
||||
current = replace(current, appearance=replace(current.appearance, use_vienna_sign=vienna),
|
||||
speed_limit=replace(current.speed_limit, speed_limit_mps=posted/3.6,
|
||||
pending_speed_limit_mps=posted/3.6, presentation_adjusted_limit_mps=(posted+offset)/3.6))
|
||||
calls = []
|
||||
with patch.object(fonts, 'draw', lambda *args, sink=calls: sink.append(args)):
|
||||
widget.render(current)
|
||||
ink = []
|
||||
for text, role, size, left, top, _ in calls:
|
||||
a, b = fonts.horizontal_ink(text, role, size)
|
||||
c, d = fonts.vertical_ink(text, role, size)
|
||||
box = left+a, top+c, left+b, top+d
|
||||
if vienna:
|
||||
hop = -6 if elapsed == 750_000_000 else 0
|
||||
cx, cy = x+59, y+67+hop
|
||||
assert all((px-cx)**2+(py-cy)**2 < 59**2 for px, py in product((box[0], box[2]), (box[1], box[3])))
|
||||
else:
|
||||
assert x+7 <= box[0] < box[2] <= x+111
|
||||
assert y+7 <= box[1] < box[3] <= y+125
|
||||
ink.append((text, box))
|
||||
if role == FontRole.BOLD:
|
||||
assert size == (48 if vienna and len(text) > 2 else 58)
|
||||
elif role == FontRole.MEDIUM:
|
||||
assert size == 36
|
||||
for index, (_, a) in enumerate(ink):
|
||||
for _, b in ink[index+1:]:
|
||||
assert not (a[0] < b[2] and a[2] > b[0] and a[1] < b[3] and a[3] > b[1])
|
||||
finally:
|
||||
if owns_context:
|
||||
rl.close_window()
|
||||
|
||||
|
||||
def test_v5_geometry_migrates_and_keeps_custom_actions_independent():
|
||||
doc = default_document()
|
||||
doc['version'] = 5
|
||||
@@ -254,9 +308,9 @@ 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'] == 8
|
||||
assert migrated['layouts']['large']['cruise_limits'] == {'x': 1342, 'y': 638, 'enabled': True}
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1254, 'y': 890, 'enabled': True}
|
||||
assert migrated['version'] == 9
|
||||
assert migrated['layouts']['large']['cruise_limits'] == {'x': 1550, 'y': 638, 'enabled': True}
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1550, 'y': 890, 'enabled': True}
|
||||
assert migrated['speedSources'] is True
|
||||
assert doc['layouts']['large']['cruise_limits']['x'] == 1550
|
||||
assert validate_document(migrated) == migrated
|
||||
|
||||
@@ -65,19 +65,85 @@ 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'] == 8 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1342
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1254, 'y': 890, 'enabled': True}
|
||||
assert migrated['version'] == 9 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1510
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1510, 'y': 890, 'enabled': True}
|
||||
assert migrated['layouts']['large']['current_speed'] == original['layouts']['large']['current_speed']
|
||||
assert migrated['layouts']['large']['cem_gauge'] == {'x': 56, 'y': 452, 'enabled': True}
|
||||
assert migrated['layouts']['compact']['speed_limit']['x'] == 212
|
||||
assert migrated['layouts']['compact']['max_speed']['y'] == 8
|
||||
assert migrated['layouts']['compact']['speed_limit']['x'] == 330
|
||||
assert migrated['layouts']['compact']['max_speed']['y'] == 24
|
||||
assert migrated['widgetColors'] == original['widgetColors'] and migrated['speedSources']
|
||||
document['layouts']['large']['cruise_limits']['x'] = 2000
|
||||
with pytest.raises(ValueError, match='outside'):
|
||||
validate_document(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('custom', (False, True))
|
||||
def test_v8_restores_only_compact_speed_defaults_and_preserves_customizations(custom):
|
||||
import copy
|
||||
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8
|
||||
document = default_document()
|
||||
document['version'] = 8
|
||||
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
|
||||
document['layouts']['compact'][key].update(x=x, y=y)
|
||||
if custom:
|
||||
document['layouts']['compact']['speed_limit'].update(x=100, y=40, enabled=False)
|
||||
document['layouts']['compact']['max_speed'].update(x=50, y=0)
|
||||
document['widgetColors']['compact']['max_speed'] = {'text': '#12345678'}
|
||||
document['layouts']['large']['cruise_limits'].update(x=1100, y=180)
|
||||
document['widgetOrder'] = {profile: list(order) for profile, order in DEFAULT_WIDGET_ORDER.items()}
|
||||
original = copy.deepcopy(document)
|
||||
migrated = validate_document(document)
|
||||
assert document == original
|
||||
assert migrated['version'] == 9 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large'] == document['layouts']['large']
|
||||
assert migrated['widgetColors'] == document['widgetColors']
|
||||
assert migrated['widgetOrder'] == document['widgetOrder']
|
||||
expected = document['layouts']['compact']['speed_limit'] if custom else {'x': 330, 'y': 20, 'enabled': True}
|
||||
assert migrated['layouts']['compact']['speed_limit'] == expected
|
||||
assert migrated['layouts']['compact']['max_speed']['x'] == (50 if custom else 0)
|
||||
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 128, 'y': 186, 'enabled': True}
|
||||
|
||||
|
||||
def test_v8_compact_speed_widgets_at_old_edges_stay_in_bounds_after_upgrade():
|
||||
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8
|
||||
document = default_document()
|
||||
document['version'] = 8
|
||||
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
|
||||
document['layouts']['compact'][key].update(x=x, y=y)
|
||||
document['layouts']['compact']['max_speed'].update(x=330, y=126)
|
||||
document['layouts']['compact']['speed_limit'].update(x=212, y=158)
|
||||
migrated = validate_document(document)
|
||||
assert migrated['layouts']['compact']['max_speed'] == {'x': 314, 'y': 24, 'enabled': True}
|
||||
assert migrated['layouts']['compact']['speed_limit'] == {'x': 212, 'y': 108, 'enabled': True}
|
||||
assert validate_document(migrated) == migrated
|
||||
|
||||
|
||||
def test_v8_actionless_layout_preserves_customizations_when_adding_compact_actions():
|
||||
import copy
|
||||
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8, RAIL_WIDGETS
|
||||
document = default_document()
|
||||
document['version'] = 8
|
||||
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
|
||||
document['layouts']['compact'][key].update(x=x, y=y)
|
||||
large_actions = document['layouts']['large'].pop('speed_limit_actions')
|
||||
for key in ('speed_limit_actions', *RAIL_WIDGETS):
|
||||
del document['layouts']['compact'][key]
|
||||
document['layouts']['large']['current_speed']['x'] = 700
|
||||
document['layouts']['compact']['speed_limit'].update(x=100, y=40)
|
||||
document['palette']['text'] = '#ABCD1234'
|
||||
document['widgetColors']['compact']['max_speed'] = {'text': '#12345678'}
|
||||
original = copy.deepcopy(document)
|
||||
migrated = validate_document(document)
|
||||
assert document == original
|
||||
assert migrated['version'] == 9 and validate_document(migrated) == migrated
|
||||
assert migrated['layouts']['large'] == {**original['layouts']['large'], 'speed_limit_actions': large_actions}
|
||||
assert migrated['layouts']['compact']['speed_limit'] == original['layouts']['compact']['speed_limit']
|
||||
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 128, 'y': 186, 'enabled': True}
|
||||
assert migrated['palette'] == original['palette']
|
||||
assert migrated['widgetColors'] == original['widgetColors']
|
||||
|
||||
|
||||
def test_clock_is_an_opt_in_widget_and_migrates_saved_layout_and_layer_order():
|
||||
document = default_document()
|
||||
metadata = customization_metadata()['profiles']
|
||||
@@ -107,7 +173,7 @@ def test_clock_format_preference_defaults_migrates_and_validates():
|
||||
validate_document(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("widget,x,y", [("max_speed", 174, 19), ("steering_wheel", 174, 131)])
|
||||
@pytest.mark.parametrize("widget,x,y", [("max_speed", 174, 25), ("steering_wheel", 174, 137)])
|
||||
def test_protected_compact_actions_reject_actual_overlap(widget, x, y):
|
||||
document = default_document()
|
||||
document["layouts"]["compact"][widget].update(x=x, y=y)
|
||||
@@ -115,7 +181,7 @@ def test_protected_compact_actions_reject_actual_overlap(widget, x, y):
|
||||
validate_document(document)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 200, 48), ("max_speed", 174, 0), ("steering_wheel", 124, 174)])
|
||||
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 200, 48), ("max_speed", 174, 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)
|
||||
@@ -155,7 +221,7 @@ def test_mode_widget_defaults_off_without_resetting_saved_layouts():
|
||||
key = 'driving_mode_descriptions'
|
||||
document = default_document()
|
||||
document['layouts']['large']['current_speed'].update(x=700, y=100, enabled=False)
|
||||
document['layouts']['compact']['speed_limit_actions']['y'] = 114
|
||||
document['layouts']['compact']['speed_limit_actions']['y'] = 180
|
||||
for layout in document['layouts'].values():
|
||||
assert layout.pop(key)['enabled'] is False
|
||||
migrated = validate_document(document)
|
||||
@@ -166,7 +232,7 @@ def test_mode_widget_defaults_off_without_resetting_saved_layouts():
|
||||
migrated['layouts']['large'][key].update(enabled=True, x=300, y=600)
|
||||
assert validate_document(migrated) == migrated
|
||||
migrated['layouts']['compact'][key]['enabled'] = True
|
||||
migrated['layouts']['compact'][key]['y'] = 90
|
||||
migrated['layouts']['compact'][key]['y'] = 130
|
||||
with pytest.raises(ValueError, match='protected speed limit actions'):
|
||||
validate_document(migrated)
|
||||
|
||||
@@ -185,14 +251,14 @@ def test_v3_layout_migrates_wheel_size_without_changing_saved_placement_or_color
|
||||
del document["layouts"]["large"]["cem_gauge"] # Version 3 predates the CEM gauge.
|
||||
document["palette"]["text"] = "#ABCDEF80"
|
||||
document["layouts"]["large"]["steering_wheel"].update(x=1300, y=100, enabled=False)
|
||||
document["layouts"]["compact"]["steering_wheel"].update(x=90, y=150)
|
||||
document["layouts"]["compact"]["steering_wheel"].update(x=90, y=130)
|
||||
for profile in ("large", "compact"):
|
||||
del document["layouts"][profile]["steering_wheel"]["size"]
|
||||
migrated = validate_document(document)
|
||||
assert migrated["version"] == 8
|
||||
assert migrated["version"] == 9
|
||||
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": 174, "enabled": True, "size": 50}
|
||||
assert migrated["layouts"]["compact"]["steering_wheel"] == {"x": 90, "y": 130, "enabled": True, "size": 50}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("removed", [("driver_monitor", "torque_bar"), ("torque_bar",), ()])
|
||||
@@ -238,7 +304,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": 8', '"version": 8, "version": 8').encode()
|
||||
raw = json.dumps(document).replace('"version": 9', '"version": 9, "version": 9').encode()
|
||||
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(raw, True)):
|
||||
assert read_customization(Mock()) == default_document()
|
||||
|
||||
@@ -370,7 +436,7 @@ def test_speed_limit_actions_move_independently_of_cruise_card_and_keep_touch_pr
|
||||
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
|
||||
controls = slc_controls(Profile.LARGE, state)
|
||||
assert controls[0].bounds == (388, 600, 660, 760)
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (16, 118, 224, 222)
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (128, 182, 296, 240)
|
||||
emit = Mock()
|
||||
inputs = OnroadInput(emit)
|
||||
from openpilot.starpilot.ui.tests.test_slc_ui_actions import tap_sign
|
||||
@@ -392,24 +458,24 @@ def test_speed_limit_actions_move_independently_of_cruise_card_and_keep_touch_pr
|
||||
|
||||
def test_moved_compact_actions_relocate_hitboxes_without_weakening_state_gate():
|
||||
document = default_document()
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=30, y=114)
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=120, y=180)
|
||||
document["layouts"]["compact"]["speed_limit"]["enabled"] = False
|
||||
assert validate_document(document) == document
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=36, y=114)
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=128, y=180)
|
||||
document = validate_document(document)
|
||||
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
|
||||
pending_speed_limit_mps=15, session_id="drive", decision_id=1,
|
||||
presentation_id=2, action_enabled=True)
|
||||
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (36, 110, 244, 214)
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (128, 176, 296, 234)
|
||||
emit = Mock()
|
||||
inputs = OnroadInput(emit, Profile.COMPACT)
|
||||
from openpilot.starpilot.ui.tests.test_slc_ui_actions import tap_sign
|
||||
document["layouts"]["compact"]["speed_limit"]["enabled"] = True
|
||||
tap_sign(inputs, state)
|
||||
document["layouts"]["compact"]["speed_limit"]["enabled"] = False
|
||||
inputs.press(110, 200, state)
|
||||
inputs.release(110, 200, state)
|
||||
inputs.press(200, 200, state)
|
||||
inputs.release(200, 200, state)
|
||||
assert emit.call_count == 1
|
||||
inputs.press(480, 200, state)
|
||||
inputs.release(480, 200, state)
|
||||
@@ -450,10 +516,12 @@ def test_compact_us_sign_keeps_camera_visible_at_saved_position(speed, size):
|
||||
with patch.object(rl, "draw_rectangle_rounded") as fill, \
|
||||
patch.object(rl, "draw_rectangle_rounded_lines_ex") as outline:
|
||||
hud._speed_limit_sign(state)
|
||||
fill.assert_not_called()
|
||||
outline.assert_not_called()
|
||||
assert [(call.args[0], call.args[2]) for call in foreground_calls(fonts.draw)] == [("LIMIT", 24), ('km/h', 20), (str(speed), size)]
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (200, 22)
|
||||
fill.assert_called_once()
|
||||
outline.assert_called_once()
|
||||
rect = outline.call_args.args[0]
|
||||
assert (rect.x, rect.y, rect.width, rect.height) == (202, 32, 114, 128)
|
||||
assert [(call.args[0], call.args[2]) for call in foreground_calls(fonts.draw)] == [("SPEED", 20), ('LIMIT', 20), (str(speed), size)]
|
||||
assert foreground_calls(fonts.draw)[0].args[3:5] == (254, 41)
|
||||
|
||||
|
||||
def test_compact_drag_keeps_animation_state_and_separate_origins():
|
||||
@@ -468,7 +536,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] == (117, 44)
|
||||
assert foreground_calls(fonts.draw)[-1].args[3:5] == (125, 119)
|
||||
rect = wheel.call_args.args[2]
|
||||
assert (rect.x, rect.y) == (115, 175)
|
||||
alpha = hud._set_speed_alpha.x
|
||||
@@ -486,7 +554,7 @@ def test_saved_six_widget_layout_adds_only_default_rail_positions():
|
||||
document = default_document()
|
||||
for key in ("model_confidence", "conditional_mode", "following_distance"):
|
||||
del document["layouts"]["compact"][key]
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=30, enabled=False)
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=120, enabled=False)
|
||||
document["palette"]["text"] = "#ABCD1234"
|
||||
migrated = validate_document(document)
|
||||
assert migrated["palette"] == document["palette"]
|
||||
@@ -513,7 +581,7 @@ def test_movable_rail_keeps_limits_and_does_not_cover_active_alerts():
|
||||
widget._conditional.reset_mock()
|
||||
widget.render(rl.Rectangle(0, 0, 536, 240), road(document, alert=OnroadAlert(size=AlertSize.FULL)))
|
||||
widget._conditional.assert_not_called()
|
||||
document["layouts"]["compact"]["conditional_mode"].update(x=200, y=101)
|
||||
document["layouts"]["compact"]["conditional_mode"].update(x=200, y=107)
|
||||
with pytest.raises(ValueError, match="protected"):
|
||||
validate_document(document)
|
||||
document["layouts"]["compact"]["conditional_mode"].update(x=477, y=0)
|
||||
@@ -537,7 +605,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'] == 8
|
||||
assert result['version'] == 9
|
||||
assert result['palette'] == old['palette']
|
||||
for profile, layout in old['layouts'].items():
|
||||
for key, position in layout.items():
|
||||
@@ -682,9 +750,9 @@ def test_intentional_small_sign_action_overlap_roundtrips_and_large_is_independe
|
||||
from openpilot.starpilot.ui.onroad_state import compact_sign_obscured_by_actions
|
||||
document = default_document()
|
||||
large = json.loads(json.dumps(document["layouts"]["large"]))
|
||||
document["layouts"]["compact"]["speed_limit"].update(x=200, y=108, enabled=enabled)
|
||||
document["layouts"]["compact"]["speed_limit"].update(x=200, y=70, enabled=enabled)
|
||||
assert decode_document(json.dumps(validate_document(document))) == document
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=16, y=114)
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=120, y=180)
|
||||
assert validate_document(document) == document
|
||||
assert document["layouts"]["large"] == large
|
||||
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
|
||||
@@ -704,7 +772,7 @@ def test_saved_small_actions_near_top_have_no_heading_and_keep_labels_visible(mo
|
||||
from openpilot.starpilot.ui.onroad import OnroadView
|
||||
document = default_document()
|
||||
document["layouts"]["compact"]["speed_limit_actions"].update(x=16, y=4)
|
||||
document["layouts"]["compact"]["max_speed"].update(y=100)
|
||||
document["layouts"]["compact"]["max_speed"].update(x=314, y=74)
|
||||
document["layouts"]["compact"]["driver_monitor"].update(y=60)
|
||||
assert decode_document(json.dumps(document)) == document
|
||||
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
|
||||
@@ -720,13 +788,13 @@ def test_saved_small_actions_near_top_have_no_heading_and_keep_labels_visible(mo
|
||||
view._slc_actions(state)
|
||||
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['Accept', 'Decline']
|
||||
assert foreground_calls(view.fonts.draw)[0].args[4] >= 0
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (16, 0, 224, 104)
|
||||
assert slc_controls(Profile.COMPACT, state)[0].bounds == (16, 0, 184, 58)
|
||||
|
||||
|
||||
def test_overlapping_small_sign_restores_after_pending_and_actions_keep_exact_input():
|
||||
from openpilot.starpilot.ui.onroad_state import SlcActionKind
|
||||
document = default_document()
|
||||
document["layouts"]["compact"]["speed_limit"].update(y=108)
|
||||
document["layouts"]["compact"]["speed_limit"].update(y=70)
|
||||
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
|
||||
pending_speed_limit_mps=15, session_id="drive", decision_id=1,
|
||||
presentation_id=2, action_enabled=True)
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_slc_hold_rejects_pending_foreign_session_and_new_drive():
|
||||
def test_compact_max_hides_during_gap_without_restarting_same_value_fade():
|
||||
renderer = CompactHudRenderer(Mock(), Path('/unused'))
|
||||
active = OnroadState(True, True, 20.0, 80.0, SpeedLimitObservation(), longitudinal_active=True)
|
||||
assert renderer._set_speed_opacity(active, 1_000_000_000) > 0.0
|
||||
assert renderer._set_speed_opacity(active, 1_000_000_000) == 0.0
|
||||
assert renderer._set_speed_opacity(active, 1_020_000_000) > 0.0
|
||||
before = renderer._set_speed_alpha.x
|
||||
unavailable = replace(active, cruise_kph=None)
|
||||
@@ -131,9 +131,9 @@ def test_compact_max_expires_once_and_short_gap_does_not_rearm_it():
|
||||
active = OnroadState(True, True, 20.0, 80.0, SpeedLimitObservation(), longitudinal_active=True)
|
||||
for frame in range(200):
|
||||
renderer._set_speed_opacity(active, 1_000_000_000 + frame * 20_000_000)
|
||||
assert renderer._set_speed_alpha.x > .99
|
||||
assert renderer._set_speed_alpha.x < .01
|
||||
renderer._set_speed_opacity(replace(active, cruise_kph=None), 5_000_000_000)
|
||||
assert renderer._set_speed_opacity(active, 5_040_000_000) > .99
|
||||
assert renderer._set_speed_opacity(active, 5_040_000_000) < .01
|
||||
assert renderer._set_speed_changed_ns == 1_000_000_000
|
||||
renderer._set_speed_opacity(replace(active, cruise_kph=90.0), 5_060_000_000)
|
||||
assert renderer._set_speed_opacity(replace(active, cruise_kph=90.0), 5_080_000_000) > .01
|
||||
@@ -145,7 +145,7 @@ def test_new_drive_rearms_max_without_an_intervening_render():
|
||||
renderer._set_speed_opacity(active, 1_000_000_000)
|
||||
renderer._set_speed_opacity(active, 1_020_000_000)
|
||||
new_drive = replace(active, drive_frame=200)
|
||||
assert renderer._set_speed_opacity(new_drive, 9_000_000_000) > 0
|
||||
assert renderer._set_speed_opacity(new_drive, 9_000_000_000) == 0
|
||||
assert renderer._set_speed_changed_ns == 9_000_000_000
|
||||
assert renderer._set_speed_opacity(new_drive, 9_020_000_000) > 0
|
||||
|
||||
|
||||
@@ -117,9 +117,12 @@ def test_slc_decision_keeps_its_touch_priority_only_where_it_overlaps():
|
||||
card = NavigationCard(Mock(profile=Profile.COMPACT))
|
||||
pending = SpeedLimitObservation(kind=ObservationKind.VALID, source='map', speed_limit_mps=22., pending_speed_limit_mps=22.,
|
||||
session_id='s', decision_id=1, presentation_id=1, action_enabled=True)
|
||||
# Open confirmation controls cover the card's lower edge.
|
||||
assert card.bounds(replace(state(), slc_prompt_open=True, longitudinal_active=True, speed_limit=pending)) is None
|
||||
assert card.bounds(replace(state(), slc_prompt_open=True, longitudinal_active=False, speed_limit=pending)) is None
|
||||
observed = replace(state(), slc_prompt_open=True, longitudinal_active=True, speed_limit=pending)
|
||||
assert card.bounds(observed) is not None
|
||||
observed.customization['layouts']['compact']['speed_limit_actions']['y'] = 4
|
||||
observed.customization['layouts']['compact']['max_speed']['y'] = 58
|
||||
assert card.bounds(observed) is None
|
||||
assert card.bounds(replace(observed, longitudinal_active=False)) is None
|
||||
# SLC being on, with no decision on screen, leaves the directions up.
|
||||
assert card.bounds(replace(state(), longitudinal_active=True, speed_limit=SpeedLimitObservation(action_enabled=True))) is not None
|
||||
large = NavigationCard(Mock(profile=Profile.LARGE))
|
||||
|
||||
@@ -180,14 +180,15 @@ def test_valid_vision_limit_has_persistent_sign_without_action_receipt():
|
||||
fonts = Mock(profile=Profile.COMPACT)
|
||||
fonts.measure.return_value = NS(width=30, height=20)
|
||||
fonts.vertical_ink.return_value = (0, 20)
|
||||
fonts.horizontal_ink.return_value = (0, 30)
|
||||
renderer = CompactHudRenderer(fonts, Mock())
|
||||
valid = SpeedLimitObservation(kind=ObservationKind.VALID, source='vision', speed_limit_mps=24.6)
|
||||
shown = replace(road(), appearance=replace(road().appearance, show_speed_limit_sign=True), speed_limit=valid)
|
||||
with patch.object(rl, 'draw_rectangle_rounded') as fill, patch.object(rl, 'draw_rectangle_rounded_lines_ex') as outline:
|
||||
renderer._speed_limit_sign(shown)
|
||||
fill.assert_not_called()
|
||||
outline.assert_not_called()
|
||||
assert [call.args[0] for call in foreground_calls(fonts.draw)] == ['LIMIT', 'mph', '55']
|
||||
fill.assert_called_once()
|
||||
outline.assert_called_once()
|
||||
assert [call.args[0] for call in foreground_calls(fonts.draw)] == ['SPEED', 'LIMIT', '55']
|
||||
outline.reset_mock()
|
||||
renderer._speed_limit_sign(replace(shown, speed_limit=SpeedLimitObservation(kind=ObservationKind.STALE)))
|
||||
outline.assert_not_called()
|
||||
@@ -195,7 +196,7 @@ def test_valid_vision_limit_has_persistent_sign_without_action_receipt():
|
||||
outline.assert_not_called()
|
||||
with patch.object(rl, 'draw_circle') as circle, patch.object(rl, 'draw_ring'):
|
||||
renderer._speed_limit_sign(replace(shown, appearance=replace(shown.appearance, use_vienna_sign=True)))
|
||||
circle.assert_not_called()
|
||||
circle.assert_called_once()
|
||||
|
||||
|
||||
def test_pending_confirmation_omits_redundant_speed_heading():
|
||||
@@ -214,7 +215,7 @@ def test_pending_confirmation_omits_redundant_speed_heading():
|
||||
|
||||
def test_compact_pending_label_and_button_follow_saved_action_placement():
|
||||
document = default_document()
|
||||
document['layouts']['compact']['speed_limit_actions']['x'] = 36
|
||||
document['layouts']['compact']['speed_limit_actions']['x'] = 120
|
||||
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
|
||||
pending_speed_limit_mps=15, session_id='drive', decision_id=4,
|
||||
presentation_id=5, action_enabled=True)
|
||||
@@ -226,7 +227,7 @@ def test_compact_pending_label_and_button_follow_saved_action_placement():
|
||||
view.fonts.vertical_ink.return_value = (0, 20)
|
||||
with patch.object(rl, 'draw_rectangle_rounded') as button, patch.object(rl, 'draw_line_ex'):
|
||||
view._slc_actions(state)
|
||||
assert foreground_calls(view.fonts.draw)[0].args[3:5] == (115, 136)
|
||||
assert foreground_calls(view.fonts.draw)[0].args[3:5] == (179, 203)
|
||||
button.assert_not_called()
|
||||
|
||||
|
||||
|
||||
@@ -814,7 +814,7 @@ class TestRuntimeSnapshot(unittest.TestCase):
|
||||
began = hud._set_speed_changed_ns
|
||||
self.assertEqual(hud._set_speed_changed_ns, began)
|
||||
if frame > 100:
|
||||
self.assertGreater(opacity, .99)
|
||||
self.assertLess(opacity, .01)
|
||||
ui.started = False
|
||||
ended = adapter.build(ShellMode.HOME, now_ns=now).onroad
|
||||
self.assertFalse(ended.camera_available or ended.engaged)
|
||||
|
||||
@@ -52,9 +52,9 @@ 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'] == 8
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1342
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1254, 'y': 248, 'enabled': True}
|
||||
assert migrated['version'] == 9
|
||||
assert migrated['layouts']['large']['cruise_limits']['x'] == 1642
|
||||
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1554, 'y': 248, 'enabled': True}
|
||||
assert migrated['layouts']['large']['current_speed'] == old_speed
|
||||
assert migrated['speedSources'] is True
|
||||
assert rgba(migrated, 'cardFill', 'large', 'cruise_limits') == (18, 52, 86, 120)
|
||||
|
||||
@@ -19,7 +19,7 @@ from openpilot.starpilot.ui.unified_speed_presentation import displayed_limit_mp
|
||||
@pytest.fixture(autouse=True)
|
||||
def drawing(monkeypatch):
|
||||
calls = {}
|
||||
for name in ('draw_rectangle_rounded_lines_ex', 'draw_rectangle_rounded', 'draw_line_ex', 'draw_circle', 'draw_ring'):
|
||||
for name in ('draw_rectangle_rounded_lines_ex', 'draw_rectangle_rounded', 'draw_line_ex', 'draw_circle', 'draw_ring', 'draw_circle_gradient'):
|
||||
calls[name] = Mock()
|
||||
monkeypatch.setattr(rl, name, calls[name])
|
||||
monkeypatch.setattr('openpilot.starpilot.ui.onroad_large_widgets.draw_control_card', Mock())
|
||||
@@ -38,6 +38,7 @@ def state(**changes):
|
||||
|
||||
def fonts(profile=Profile.LARGE):
|
||||
return SimpleNamespace(profile=profile, draw=Mock(), vertical_ink=lambda text, role, size: (0, size * .75),
|
||||
horizontal_ink=lambda text, role, size: (0, len(text) * size * .5),
|
||||
measure=lambda text, role, size: SimpleNamespace(width=len(text) * size * .5, height=size))
|
||||
|
||||
|
||||
@@ -67,6 +68,21 @@ def test_both_renderers_show_small_latest_sign_and_offset(profile):
|
||||
assert '50' in texts and '+5' in texts and '40' not in texts
|
||||
|
||||
|
||||
@pytest.mark.parametrize('metric', [False, True])
|
||||
@pytest.mark.parametrize('vienna', [False, True])
|
||||
@pytest.mark.parametrize('offset', [None, 0., .01, -.01])
|
||||
def test_compact_sign_omits_missing_zero_and_rounded_zero_offsets(metric, vienna, offset):
|
||||
current = state(metric=metric)
|
||||
factor = 3.6 if metric else 2.2369362921
|
||||
current = replace(current, appearance=replace(current.appearance, use_vienna_sign=vienna),
|
||||
speed_limit=SpeedLimitObservation(ObservationKind.VALID, 'map', 65 / factor, offset_mps=offset,
|
||||
action_enabled=True))
|
||||
assert resolve_unified_speed(current).offset_text is None
|
||||
font = fonts(Profile.COMPACT)
|
||||
CompactHudRenderer(font, Mock())._speed_limit_sign(current)
|
||||
assert [call.args[0] for call in font.draw.call_args_list] == (['65'] if vienna else ['SPEED', 'LIMIT', '65'])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
def test_pending_sign_hops_and_arrow_follows_vehicle_speed(profile, drawing):
|
||||
font = fonts(profile)
|
||||
@@ -96,17 +112,35 @@ def test_only_set_speed_override_draws_cross(profile, drawing):
|
||||
assert len(foreground_calls(drawing['draw_line_ex'])) == 2
|
||||
|
||||
|
||||
def test_compact_set_speed_stays_visible_while_cruise_is_active():
|
||||
def test_compact_set_speed_fades_after_adjustment_and_reappears_on_change():
|
||||
renderer = CompactHudRenderer(fonts(), Mock())
|
||||
current = state(drive_frame=1)
|
||||
for now in range(10_000_000_000, 20_000_000_000, 100_000_000):
|
||||
alpha = renderer._set_speed_opacity(current, now)
|
||||
assert alpha > .99
|
||||
assert alpha < .01
|
||||
changed = replace(current, cruise_kph=current.cruise_kph + 10)
|
||||
assert renderer._set_speed_opacity(changed, 20_000_000_000) < .01
|
||||
assert renderer._set_speed_opacity(changed, 20_020_000_000) > .01
|
||||
assert renderer._set_speed_opacity(replace(current, longitudinal_active=False), 20_000_000_000) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('profile', list(Profile))
|
||||
def test_limit_keeps_a_unit_when_set_speed_is_hidden(profile):
|
||||
def test_compact_max_restores_dinish_number_label_and_shadow(drawing):
|
||||
from openpilot.starpilot.ui.presentation import FontRole, font_filename
|
||||
font = fonts(Profile.COMPACT)
|
||||
hud = CompactHudRenderer(font, Mock())
|
||||
current = state(drive_frame=1, cruise_kph=80)
|
||||
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 font.draw.call_args_list] == [
|
||||
('80', FontRole.SET_SPEED), ('MAX', FontRole.SEMI_BOLD)]
|
||||
assert font_filename(Profile.COMPACT, FontRole.SET_SPEED) == 'DINish-Speed.fnt'
|
||||
assert font.draw.call_args_list[0].args[3:5] == (17, -4)
|
||||
assert font.draw.call_args_list[1].args[2:5] == (36, 25, 109)
|
||||
drawing['draw_circle_gradient'].assert_called_once()
|
||||
|
||||
|
||||
def test_large_limit_keeps_a_unit_when_set_speed_is_hidden():
|
||||
profile = Profile.LARGE
|
||||
current = state()
|
||||
current.customization['showMaxSpeed'] = False
|
||||
current.customization['layouts']['compact']['max_speed']['enabled'] = False
|
||||
|
||||
Reference in New Issue
Block a user