mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-08-05 16:26:14 +08:00
theme
This commit is contained in:
@@ -137,6 +137,13 @@ def get_param_color(params, key: str, fallback_alpha: int = 255) -> rl.Color | N
|
||||
return rl.Color(red, green, blue, alpha)
|
||||
|
||||
|
||||
def is_stock_color_scheme(params) -> bool:
|
||||
if params is None:
|
||||
return True
|
||||
scheme = params.get("ColorScheme", encoding="utf-8", default="stock")
|
||||
return (scheme or "stock").lower() == "stock"
|
||||
|
||||
|
||||
def get_visual_color(params, param_key: str, theme_key: str, fallback: rl.Color | None = None) -> rl.Color:
|
||||
base = _as_color(fallback if fallback is not None else rl.WHITE)
|
||||
override = get_param_color(params, param_key, base.a)
|
||||
|
||||
@@ -7,11 +7,11 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, with_alpha
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha
|
||||
from openpilot.selfdrive.ui.lib.starpilot_visuals import lead_indicator_enabled
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.mici.onroad import blend_colors
|
||||
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color, get_path_edge_color
|
||||
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -21,6 +21,7 @@ MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
RAINBOW_GRADIENT_COLOR_COUNT = 19
|
||||
RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0
|
||||
STOCK_LINE_GREEN = rl.Color(0, 255, 0, 255)
|
||||
|
||||
THROTTLE_COLORS = [
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
@@ -370,8 +371,8 @@ class ModelRenderer(Widget):
|
||||
if override is not None:
|
||||
color = with_alpha(override, int(alpha * override.a))
|
||||
else:
|
||||
_base_color = get_path_edge_color(ui_state)
|
||||
color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255))
|
||||
base_color = STOCK_LINE_GREEN if is_stock_color_scheme(self._params) else get_theme_color("PathEdge", STOCK_LINE_GREEN)
|
||||
color = with_alpha(base_color, int(alpha * base_color.a))
|
||||
|
||||
# turn adjacent lls orange if torque is high
|
||||
torque = self._torque_filter.x
|
||||
@@ -383,7 +384,13 @@ class ModelRenderer(Widget):
|
||||
np.interp(abs(torque), [0.6, 0.8], [0.0, 1.0])
|
||||
)
|
||||
else:
|
||||
lane_lines_color = get_visual_color(self._params, "LaneLinesColor", "LaneLines", rl.WHITE)
|
||||
lane_lines_override = get_param_color(self._params, "LaneLinesColor", STOCK_LINE_GREEN.a)
|
||||
if lane_lines_override is not None:
|
||||
lane_lines_color = lane_lines_override
|
||||
elif is_stock_color_scheme(self._params):
|
||||
lane_lines_color = STOCK_LINE_GREEN
|
||||
else:
|
||||
lane_lines_color = get_theme_color("LaneLines", STOCK_LINE_GREEN)
|
||||
color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a))
|
||||
|
||||
return color
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, with_alpha
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha
|
||||
from openpilot.selfdrive.ui.lib.starpilot_visuals import lead_indicator_enabled
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -19,6 +19,7 @@ MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
RAINBOW_GRADIENT_COLOR_COUNT = 19
|
||||
RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0
|
||||
STOCK_LINE_GREEN = rl.Color(0, 255, 0, 255)
|
||||
|
||||
THROTTLE_COLORS = [
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
@@ -363,7 +364,13 @@ class ModelRenderer(Widget):
|
||||
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
lane_lines_color = get_visual_color(self._params, "LaneLinesColor", "LaneLines", rl.WHITE)
|
||||
lane_lines_override = get_param_color(self._params, "LaneLinesColor", STOCK_LINE_GREEN.a)
|
||||
if lane_lines_override is not None:
|
||||
lane_lines_color = lane_lines_override
|
||||
elif is_stock_color_scheme(self._params):
|
||||
lane_lines_color = STOCK_LINE_GREEN
|
||||
else:
|
||||
lane_lines_color = get_theme_color("LaneLines", STOCK_LINE_GREEN)
|
||||
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
if lane_line.projected_points.size == 0:
|
||||
|
||||
@@ -6,12 +6,13 @@ import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, with_alpha
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, is_stock_color_scheme, with_alpha
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
|
||||
_METRICS_FONT = None
|
||||
_METRICS_FONT_SIZE = 45
|
||||
_STOCK_LINE_GREEN = rl.Color(0, 255, 0, 241)
|
||||
|
||||
|
||||
def _get_metrics_font():
|
||||
@@ -147,14 +148,10 @@ def render_path_edges(renderer) -> None:
|
||||
Qt reference: paintPathEdges in starpilot_annotated_camera.cc:732-769
|
||||
|
||||
Path edges are the area between track_edge_vertices (outer) and track_vertices (inner).
|
||||
Color is based on current mode:
|
||||
- If switchback_mode_enabled: use STATUS_SWITCHBACK_MODE_ENABLED color
|
||||
- If always_on_lateral_active: use STATUS_ALWAYS_ON_LATERAL_ACTIVE color
|
||||
- If conditional_status == 1: use STATUS_CEM_DISABLED color
|
||||
- If experimental_mode: use STATUS_EXPERIMENTAL_MODE_ENABLED color
|
||||
- If traffic_mode_enabled: use STATUS_TRAFFIC_MODE_ENABLED color
|
||||
- If color_scheme != "stock" and path_edges_color is set: use that color
|
||||
- Else: stock green gradient HSL(148/360, 0.94, 0.41, 0.4) → HSL(112/360, 1.0, 0.54, 0.35) → transparent
|
||||
Color selection on Python UIs:
|
||||
- If path_edges_color is set: use that color
|
||||
- Else if color_scheme != "stock": use the active theme color
|
||||
- Else: use a fixed stock-style green so border color carries engagement status
|
||||
|
||||
The path_edges_color param is a hex string like "#178644".
|
||||
"""
|
||||
@@ -166,48 +163,24 @@ def render_path_edges(renderer) -> None:
|
||||
|
||||
edge_strip = np.vstack([outer, inner[::-1]])
|
||||
|
||||
if ui_state.switchback_mode_enabled:
|
||||
base_color = rl.Color(139, 108, 197, 241)
|
||||
elif ui_state.always_on_lateral_active:
|
||||
base_color = rl.Color(10, 186, 181, 241)
|
||||
elif ui_state.conditional_status == 1:
|
||||
base_color = rl.Color(255, 255, 0, 241)
|
||||
elif renderer._experimental_mode:
|
||||
base_color = rl.Color(218, 111, 37, 241)
|
||||
elif ui_state.traffic_mode_enabled:
|
||||
base_color = rl.Color(201, 34, 49, 241)
|
||||
override = get_param_color(renderer._params, "PathEdgesColor", 241)
|
||||
if override is not None:
|
||||
base_color = rl.Color(override.r, override.g, override.b, 241)
|
||||
elif is_stock_color_scheme(renderer._params):
|
||||
base_color = _STOCK_LINE_GREEN
|
||||
else:
|
||||
override = get_param_color(renderer._params, "PathEdgesColor", 241)
|
||||
color_scheme = renderer._params.get("ColorScheme", encoding="utf-8", default="stock")
|
||||
if override is not None:
|
||||
base_color = rl.Color(override.r, override.g, override.b, 241)
|
||||
elif color_scheme != "stock":
|
||||
theme_color = get_theme_color("PathEdge", rl.Color(23, 134, 68, 241))
|
||||
base_color = rl.Color(theme_color.r, theme_color.g, theme_color.b, 241)
|
||||
else:
|
||||
base_color = None
|
||||
theme_color = get_theme_color("PathEdge", _STOCK_LINE_GREEN)
|
||||
base_color = rl.Color(theme_color.r, theme_color.g, theme_color.b, 241)
|
||||
|
||||
if base_color is not None:
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0),
|
||||
end=(0.0, 0.0),
|
||||
colors=[
|
||||
with_alpha(base_color, int(255 * 0.4)),
|
||||
with_alpha(base_color, int(255 * 0.35)),
|
||||
with_alpha(base_color, 0),
|
||||
],
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
else:
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0),
|
||||
end=(0.0, 0.0),
|
||||
colors=[
|
||||
_hsla_to_color(148.0 / 360.0, 0.94, 0.41, 0.4),
|
||||
_hsla_to_color(112.0 / 360.0, 1.0, 0.54, 0.35),
|
||||
_hsla_to_color(112.0 / 360.0, 1.0, 0.54, 0.0),
|
||||
],
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0),
|
||||
end=(0.0, 0.0),
|
||||
colors=[
|
||||
with_alpha(base_color, int(255 * 0.4)),
|
||||
with_alpha(base_color, int(255 * 0.35)),
|
||||
with_alpha(base_color, 0),
|
||||
],
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
|
||||
draw_polygon(renderer._rect, edge_strip, gradient=gradient)
|
||||
|
||||
@@ -2,6 +2,11 @@ import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
|
||||
const endpointOptionsCache = {}
|
||||
const endpointOptionsInflight = {}
|
||||
const COLOR_UI_DEFAULTS = {
|
||||
LaneLinesColor: "#00ff00",
|
||||
PathEdgesColor: "#00ff00",
|
||||
PathColor: "#30ff9c",
|
||||
}
|
||||
|
||||
// Plain variables — scheduling/routing flags that must NOT be reactive
|
||||
let syncScheduled = false
|
||||
@@ -42,6 +47,34 @@ function toSelectValue(value) {
|
||||
return value === null || value === undefined ? "" : String(value)
|
||||
}
|
||||
|
||||
function normalizeHexColor(rawValue) {
|
||||
const value = String(rawValue || "").trim()
|
||||
if (!value || value.toLowerCase() === "stock") return ""
|
||||
|
||||
const stripped = value.startsWith("#") ? value.slice(1) : value
|
||||
if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(stripped)) return ""
|
||||
return `#${stripped.slice(0, 6).toLowerCase()}`
|
||||
}
|
||||
|
||||
function getColorDefault(param) {
|
||||
const candidate = normalizeHexColor(param?.default_color)
|
||||
if (candidate) return candidate
|
||||
return COLOR_UI_DEFAULTS[param?.key] || "#ffffff"
|
||||
}
|
||||
|
||||
function resolveColorInputValue(param, rawValue = undefined) {
|
||||
return normalizeHexColor(rawValue ?? state.values[param?.key]) || getColorDefault(param)
|
||||
}
|
||||
|
||||
function formatColorDisplayValue(param, rawValue = undefined) {
|
||||
const value = normalizeHexColor(rawValue ?? state.values[param?.key])
|
||||
return value ? value.toUpperCase() : "Stock"
|
||||
}
|
||||
|
||||
function isStockColorValue(rawValue) {
|
||||
return normalizeHexColor(rawValue) === ""
|
||||
}
|
||||
|
||||
function resolveEndpointTemplate(template) {
|
||||
if (!template) return ""
|
||||
return String(template).replace(/\{([A-Za-z0-9_]+)\}/g, (_, key) => {
|
||||
@@ -126,6 +159,14 @@ function syncInputs() {
|
||||
el.checked = !!state.values[el.id.slice(3)]
|
||||
}
|
||||
|
||||
// Sync color inputs — map unset/"stock" values to the picker fallback color.
|
||||
for (const el of document.querySelectorAll("input[type='color'].ds-color[id^='ds-']")) {
|
||||
const key = el.id.slice(3)
|
||||
const param = state.paramMetaByKey[key]
|
||||
if (!param) continue
|
||||
el.value = resolveColorInputValue(param)
|
||||
}
|
||||
|
||||
// Sync selects — hydrate options + set value
|
||||
for (const el of document.querySelectorAll("select.ds-select[id^='ds-']")) {
|
||||
const key = el.id.slice(3)
|
||||
@@ -544,6 +585,8 @@ async function updateParam(key, elType) {
|
||||
} else if (elType === "dropdown") {
|
||||
formattedVal = coerceValueByType(el.value, param.data_type)
|
||||
selectedLabel = el.options?.[el.selectedIndex]?.textContent || ""
|
||||
} else if (elType === "color") {
|
||||
formattedVal = normalizeHexColor(el.value) || getColorDefault(param)
|
||||
} else {
|
||||
formattedVal = coerceValueByType(el.value, param.data_type)
|
||||
}
|
||||
@@ -589,9 +632,46 @@ function revertInput(key, current, elType) {
|
||||
return
|
||||
}
|
||||
|
||||
if (elType === "color") {
|
||||
const param = state.paramMetaByKey[key]
|
||||
if (!param) return
|
||||
el.value = resolveColorInputValue(param, current)
|
||||
return
|
||||
}
|
||||
|
||||
el.value = current
|
||||
}
|
||||
|
||||
async function resetColorParam(param) {
|
||||
const key = param?.key
|
||||
if (!key) return
|
||||
|
||||
const current = state.values[key]
|
||||
if (isStockColorValue(current)) return
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/params", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, value: "stock" }),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok) {
|
||||
const updated = (data.updated && typeof data.updated === "object") ? data.updated : {}
|
||||
state.values = { ...state.values, [key]: "stock", ...updated }
|
||||
showParamSnackbar(data.message || `Parameter '${key}' reset to stock.`)
|
||||
scheduleSyncInputs()
|
||||
} else {
|
||||
showParamSnackbar(data.error || "Failed to reset parameter", "error")
|
||||
revertInput(key, current, "color")
|
||||
}
|
||||
} catch (e) {
|
||||
showParamSnackbar("Network error — is the device reachable?", "error")
|
||||
revertInput(key, current, "color")
|
||||
}
|
||||
}
|
||||
|
||||
function toggleManage(key) {
|
||||
state.expanded = { ...state.expanded, [key]: !state.expanded[key] }
|
||||
scheduleSyncInputs()
|
||||
@@ -640,6 +720,7 @@ function renderSettingRow(p) {
|
||||
}
|
||||
|
||||
const isNumeric = p.ui_type === "numeric"
|
||||
const isColor = p.ui_type === "color"
|
||||
const isChild = p.parent_key ? "ds-child-modifier" : ""
|
||||
const lockReason = getSettingLockReason(p)
|
||||
const isLocked = lockReason !== ""
|
||||
@@ -659,7 +740,8 @@ function renderSettingRow(p) {
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
${isNumeric ? html`<span class="ds-row-value" id="ds-display-${p.key}">${() => {
|
||||
${(isNumeric || isColor) ? html`<span class="ds-row-value" id="ds-display-${p.key}">${() => {
|
||||
if (isColor) return formatColorDisplayValue(p)
|
||||
const currentValue = state.values[p.key]
|
||||
const bounds = numericBounds(p)
|
||||
return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".."
|
||||
@@ -733,6 +815,20 @@ function renderSettingRow(p) {
|
||||
@change="${() => updateParam(p.key, "dropdown")}">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
` : p.ui_type === "color" ? html`
|
||||
<div style="display:flex; align-items:center; gap:0.75rem;">
|
||||
<input
|
||||
type="color"
|
||||
class="ds-color"
|
||||
id="ds-${p.key}"
|
||||
?disabled="${isLocked}"
|
||||
value="${() => resolveColorInputValue(p)}"
|
||||
@change="${() => updateParam(p.key, "color")}" />
|
||||
<button
|
||||
class="ds-reset-btn"
|
||||
?disabled="${() => isLocked || isStockColorValue(state.values[p.key])}"
|
||||
@click="${() => resetColorParam(p)}">Stock</button>
|
||||
</div>
|
||||
` : html`
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -1698,6 +1698,15 @@
|
||||
"max": 24.0,
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "LaneLinesColor",
|
||||
"label": "Lane Line Color",
|
||||
"description": "Set a custom lane-line color for the Python UIs.\n\nUse Stock to follow the default line color behavior.",
|
||||
"data_type": "string",
|
||||
"ui_type": "color",
|
||||
"default_color": "#00FF00",
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "BorderWidth",
|
||||
"label": "Border Width",
|
||||
@@ -1719,6 +1728,15 @@
|
||||
"max": 100.0,
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "PathEdgesColor",
|
||||
"label": "Path Edge Color",
|
||||
"description": "Set a custom path-edge color for the Python UIs.\n\nUse Stock to follow the default line color behavior.",
|
||||
"data_type": "string",
|
||||
"ui_type": "color",
|
||||
"default_color": "#00FF00",
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "PathWidth",
|
||||
"label": "Path Width",
|
||||
@@ -1730,6 +1748,15 @@
|
||||
"step": 0.1,
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "PathColor",
|
||||
"label": "Path Color",
|
||||
"description": "Set a custom filled-path color for the Python UIs.\n\nUse Stock to follow the default path color behavior.",
|
||||
"data_type": "string",
|
||||
"ui_type": "color",
|
||||
"default_color": "#30FF9C",
|
||||
"parent_key": "ModelUI"
|
||||
},
|
||||
{
|
||||
"key": "RoadEdgesWidth",
|
||||
"label": "Road Edges Width",
|
||||
|
||||
Reference in New Issue
Block a user