Remove sample chips and move quick select down

This commit is contained in:
dirwin31
2026-10-14 13:50:47 -07:00
parent 5c02efcc3d
commit de2989e737
10 changed files with 31 additions and 31 deletions
@@ -483,6 +483,9 @@ vm.colorAlpha("text", { target: { value: "128" } })
assert.equal(vm.selectedColors.text, "#ABCDEF80")
// The CEM gauge is marked outside CEM scenes, as an editing aid only.
assert.match(OnroadLayoutPage.template, /v-if="widget\.kind === 'cem_gauge' && state\.showEditingAids && !state\.scene\.startsWith\('cem_'\)"/)
// The Small screen shows the tap-areas chip under the layout instead of over it.
assert.match(OnroadLayoutPage.template, /state\.profile !== 'compact'" class="gx-layout__stage-chip" aria-hidden="true"><i class="bi bi-grid-3x3-gap"><\/i>Tap areas/)
assert.match(OnroadLayoutPage.template, /state\.profile === 'compact'" class="gx-layout__stage-chip gx-layout__stage-chip--below"/)
// One scene dropdown, grouped, with every scene exactly once.
assert.deepEqual(vm.sceneGroups.map(group => group.label), ["Driving", "Conditional experimental", "Speed limit"])
assert.deepEqual(vm.sceneGroups.flatMap(group => group.scenes.map(scene => scene.id)).sort(), vm.scenes.map(scene => scene.id).sort())
@@ -643,6 +643,7 @@ input[type="range"].gx-slider:active::-moz-range-thumb {
/* Tap-area overlay label sits on the preview while the grid toggle is on. */
.gx-layout__preview-card > .gx-layout__stage { position: relative; margin-inline: 0; min-width: 0; }
.gx-layout__stage-chip { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid #b799ff88; border-radius: var(--radius-full); background: rgba(22, 18, 31, .82); color: #d8c8ff; font-size: var(--fs-xs); font-weight: var(--fw-bold); white-space: nowrap; pointer-events: none; }
.gx-layout__stage-chip--below { position: static; justify-self: end; }
.gx-layout__stage-chip--start { right: auto; left: 6px; max-width: calc(100% - 12px); overflow: hidden; text-overflow: ellipsis; display: block; }
/* Covers the whole bar, buttons included, so its size never changes mid-drag. */
.gx-layout__savebar-snap { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; gap: 6px; padding: 0 14px; border-radius: inherit; background: inherit; color: var(--primary); font-weight: var(--fw-bold); pointer-events: none; }
@@ -1249,8 +1249,8 @@ export const OnroadLayoutPage = {
<p v-if="state.backdrop.status === 'unavailable'" class="gx-note" role="status">{{ state.backdrop.url ? 'The comma screen could not refresh. Showing the last one.' : 'The comma screen is unavailable. Showing a sketch.' }}</p>
<p v-if="state.art.status === 'unavailable'" class="gx-note" role="status">{{ state.art.url ? 'Widget preview could not refresh. Showing the last preview.' : 'Widget preview unavailable. Placement controls remain available.' }}</p>
<div class="gx-layout__stage">
<span v-if="state.showEditingAids && state.snapGuides.length && !snapInBar" class="gx-layout__stage-chip gx-layout__stage-chip--start" :class="{'gx-layout__stage-chip--with-tap-areas': !projection && state.showFavoriteZones}" aria-hidden="true">{{ snapSummary }}</span>
<span v-if="!projection && state.showFavoriteZones && state.showEditingAids" class="gx-layout__stage-chip" aria-hidden="true"><i class="bi bi-grid-3x3-gap"></i>Tap areas</span>
<span v-if="state.showEditingAids && state.snapGuides.length && !snapInBar" class="gx-layout__stage-chip gx-layout__stage-chip--start" :class="{'gx-layout__stage-chip--with-tap-areas': !projection && state.showFavoriteZones && state.profile !== 'compact'}" aria-hidden="true">{{ snapSummary }}</span>
<span v-if="!projection && state.showFavoriteZones && state.showEditingAids && state.profile !== 'compact'" class="gx-layout__stage-chip" aria-hidden="true"><i class="bi bi-grid-3x3-gap"></i>Tap areas</span>
<svg ref="preview" class="gx-layout__preview" :style="{'--layout-glow': state.profile === 'compact' ? '4px' : '14px'}" :viewBox="'0 0 ' + profile.width + ' ' + profile.height" :width="profile.width" :height="profile.height"
@pointermove="moveDrag" @pointerup="endDrag" @pointercancel="cancelDrag" @lostpointercapture="cancelDrag"
role="group" :aria-label="projection ? 'Android Auto layout preview' : (state.profile === 'large' ? 'Big' : 'Small') + ' layout preview'">
@@ -1275,7 +1275,6 @@ export const OnroadLayoutPage = {
opacity=".7" :stroke-width="state.profile === 'compact' ? 2 : 5" />
<rect v-if="!projection" :x="state.profile === 'large' ? 1860 : 476" y="0" :width="state.profile === 'large' ? 300 : 60" :height="profile.height" fill="#090c10" />
<g v-if="!projection && state.profile === 'large'" aria-hidden="true" pointer-events="none">
<text x="1874" y="17" font-size="14" font-weight="600" fill="#ffffffd2">SAMPLE DATA</text>
<g v-for="(sample, index) in [{label:'ACCEL',value:'1.08 ft/s²',accent:'#ffffff'}, {label:'STEER DELAY',value:'0.15000',accent:'#3b82f6'}, {label:'FRICTION',value:'0.12000',accent:'#22c55e'}, {label:'LAT ACCEL',value:'2.50000',accent:'#3b82f6'}, {label:'LATERAL %',value:'78.00%',accent:'#ffffff'}, {label:'TORQUE %',value:'42%',accent:'#ffffff'}, {label:'CHESTNUT',value:'',accent:'#ffffff'}]" :key="sample.label" :transform="'translate(1873 ' + (24 + index * 150) + ')'">
<rect width="275" height="126" rx="16" fill="#000000" stroke="#ffffff55" stroke-width="2" />
<rect x="253" y="4" width="18" height="118" rx="4" :fill="sample.accent" />
@@ -1328,6 +1327,8 @@ export const OnroadLayoutPage = {
<rect v-if="state.showEditingAids && selectionBox" class="gx-layout__outline" :x="selectionBox.x" :y="selectionBox.y" :width="selectionBox.width" :height="selectionBox.height" rx="4" fill="none" stroke="#d4baff" stroke-width="2" vector-effect="non-scaling-stroke" />
</svg>
</div>
<!-- The Small screen is too short to carry the chip over the layout; it sits underneath instead. -->
<span v-if="!projection && state.showFavoriteZones && state.showEditingAids && state.profile === 'compact'" class="gx-layout__stage-chip gx-layout__stage-chip--below" aria-hidden="true"><i class="bi bi-grid-3x3-gap"></i>Tap areas</span>
<div v-show="state.tipsOpen" id="gx-layout-tips" class="gx-layout__tips">
<p class="gx-note">Drag widgets to place them. Behind them is the comma's own screen for the chosen scene, including its border, road and alerts. Readings are samples.</p>
<p class="gx-note">Widgets snap to the screen edges, the centre and other widgets. Turn snapping off with the magnet button, or hold Option (Alt) while dragging.</p>
@@ -15,6 +15,8 @@ class OnroadVisualPreview:
cem_reason: str | None = None
curve_curvature: float | None = None
label: str = "SYNTHETIC REPLAY PREVIEW"
# The Galaxy layout editor's sample: draw exactly as the car would, without the replay labels.
layout_sample: bool = False
def parse_flags(raw: str) -> frozenset[str]:
@@ -93,7 +93,7 @@ def sample_state(scene: str, document: dict) -> OnroadState:
longitudinal_active=longitudinal, stock_cruise_active=False,
slc_system_long_available=longitudinal, torque_utilization=0.78 if lateral else 0.0,
customization=document,
visual_preview=OnroadVisualPreview(cem_reason=CEM_SCENE_REASONS[scene]) if scene in CEM_SCENE_REASONS else None)
visual_preview=OnroadVisualPreview(cem_reason=CEM_SCENE_REASONS[scene], layout_sample=True) if scene in CEM_SCENE_REASONS else None)
if scene == 'cem_lead':
state = replace(state, conditional_lead=ConditionalLead(12.0, 0.0))
@@ -281,7 +281,7 @@ class _Canvas:
finally:
rl.rl_pop_matrix()
def render(self, state: OnroadState, scene: str, *, label: bool = True) -> bytes:
def render(self, state: OnroadState, scene: str) -> bytes:
if self.view is None or self.fonts is None or self.target is None:
raise RuntimeError("Layout preview resources are closed")
self._settle(state)
@@ -299,8 +299,6 @@ class _Canvas:
self.fonts.draw('StarPilot', FontRole.BRAND, 30, width - 210, height - 90)
if self.profile == Profile.LARGE and self.viewport is None:
render_sidebar(self.fonts, rl.Rectangle(width - SIDEBAR_WIDTH, 0, SIDEBAR_WIDTH, height))
if label:
self._label(scene)
finally:
rl.end_texture_mode()
image = rl.load_image_from_texture(self.target.texture)
@@ -393,21 +391,6 @@ class _Canvas:
self.view.torque_bar._torque_filter.x = state.torque_utilization if state.lateral_active else 0.0
self.view.torque_bar._alpha_filter.x = 1.0 if state.lateral_active else 0.0
def _label(self, scene: str) -> None:
if self.fonts is None:
return
large = self.profile == Profile.LARGE
names = {"experimental": "MANUAL EXPERIMENTAL", "cem_stop_light": "CEM TRAFFIC LIGHT",
"cem_lead": "CEM STOPPED LEAD", "cem_curve": "CEM CURVE"}
label = "SAMPLE: " + names.get(scene, scene.replace("_", " ").upper())
size = 27 if large else 14
measured = self.fonts.measure(label, FontRole.SEMI_BOLD, size)
x = (1860 if large else 476) - measured.width - (38 if large else 8)
y = 1000 if large else 213
rl.draw_rectangle_rounded(rl.Rectangle(x - 8, y - 3, measured.width + 16, measured.height + 6),
0.2, 4, rl.Color(0, 0, 0, 220))
self.fonts.draw(label, FontRole.SEMI_BOLD, size, x, y, rl.Color(255, 255, 255, 255))
def close(self) -> None:
if self.map is not None:
self.map.close()
@@ -470,7 +453,7 @@ class LayoutPreviewRenderer:
# Everything the car draws except the movable widgets, which the editor places over it.
# A non-empty order that names no widget keeps the ordered path and draws none of them.
order = {**state.customization.get('widgetOrder', {}), str(profile): ['backdrop']}
return self._canvas.render(replace(state, customization={**state.customization, 'widgetOrder': order}), scene, label=False)
return self._canvas.render(replace(state, customization={**state.customization, 'widgetOrder': order}), scene)
return self._canvas.render(state, scene)
def expire(self) -> None:
@@ -65,5 +65,4 @@ def render_sidebar(fonts: BitmapFonts, frame: rl.Rectangle,
fonts.draw(line, FontRole.SEMI_BOLD, font_size,
rect.x + (rect.width - measured.width) / 2, y, rl.WHITE)
y += measured.height
fonts.draw("SAMPLE DATA", FontRole.SEMI_BOLD, 14, frame.x + 14, frame.y + 3,
rl.Color(255, 255, 255, 210))
+4 -2
View File
@@ -198,7 +198,7 @@ class OnroadView:
def _preview_badge(self, state: OnroadState) -> None:
"""Keep synthetic replay visibly identified even over a full alert."""
preview = state.visual_preview
if preview is None:
if preview is None or preview.layout_sample:
return
flags = []
if preview.cem_reason is not None:
@@ -422,7 +422,9 @@ class OnroadView:
self.fonts.draw(label, FontRole.SEMI_BOLD, 29, 1390 + right_shift, 56,
curve_color)
mode = conditional_status(state)
if mode is not None and state.alert.size != AlertSize.FULL:
# The layout editor leaves out the mode caption; its CEM scenes are named in the editor itself.
sample = state.visual_preview is not None and state.visual_preview.layout_sample
if mode is not None and state.alert.size != AlertSize.FULL and not sample:
family, reason, mode_color = mode
mode_y = 101 if label is not None else 56
self.fonts.draw(f'{family} {reason}', FontRole.SEMI_BOLD, 25, 1390 + right_shift, mode_y, mode_color)
@@ -48,6 +48,10 @@ def status(state: OnroadState) -> tuple[str, str, rl.Color] | None:
if preview is not None and preview.cem_reason in ('CURVE', 'LEAD', 'STOP LIGHT', 'SPEED'):
if state.alert.size == AlertSize.FULL:
return None
if preview.layout_sample:
# The layout editor's sample keeps the car's own reason (the gauge draws from it), not replay wording.
detail = _REASONS.get(reason(state))
return ('CEM', detail, rl.Color(112, 192, 216, 255)) if detail else None
return 'CEM', f'PREVIEW {preview.cem_reason}', rl.Color(112, 192, 216, 255)
value = (state.conditional_effective if state.longitudinal_active else None) or getattr(state, 'conditional_perception', None)
if value is None or value.choice not in (ModeChoice.CEM, ModeChoice.CCM):
@@ -32,7 +32,7 @@ class DeveloperPreviewTests(unittest.TestCase):
value = preview_at(frozenset(("cem", "csc")), 3.0)
with self.assertRaises(FrozenInstanceError):
value.__setattr__("cem_reason", "ACTUAL")
self.assertEqual(set(vars(value)), {"cem_reason", "curve_curvature", "label"})
self.assertEqual(set(vars(value)), {"cem_reason", "curve_curvature", "label", "layout_sample"})
def test_invalid_or_duplicate_flags_and_time_rejected(self):
for raw in ("cem,cem", "csc,bogus", "CEM", ",cem"):
@@ -80,7 +80,12 @@ class LayoutPreviewRendererTests(unittest.TestCase):
self.assertTrue(sample.experimental_enabled)
self.assertEqual(sample.visual_preview.cem_reason, reason)
self.assertIsNone(sample.conditional_effective)
self.assertEqual(sample.visual_preview.label, "SYNTHETIC REPLAY PREVIEW")
self.assertTrue(sample.visual_preview.layout_sample) # Drawn as the car would, without replay labels.
from openpilot.starpilot.ui.onroad_conditional import status
# The gauge still reads the real reason, but the mode caption itself is not drawn in the editor.
family, detail, _ = status(sample)
self.assertEqual(family, "CEM")
self.assertFalse(detail.startswith("PREVIEW"))
self.assertEqual(sample_state("cem_lead", document).speed_mps, 0.0)
with self.assertRaises(ValueError):
sample_state("live", document)
@@ -120,7 +125,7 @@ class LayoutPreviewRendererTests(unittest.TestCase):
self.assertEqual(renderer({"document": document, "profile": "compact", "scene": "braking", "art": "backdrop"}), b"png")
state, scene = canvas.render.call_args.args
self.assertEqual(scene, "braking")
self.assertEqual(canvas.render.call_args.kwargs, {"label": False})
self.assertEqual(canvas.render.call_args.kwargs, {})
# An order naming no widget keeps the ordered path, so no movable widget is drawn.
self.assertEqual(state.customization["widgetOrder"]["compact"], ["backdrop"])
self.assertEqual(state.customization["layouts"], document["layouts"])
@@ -200,7 +205,7 @@ class LayoutPreviewRendererTests(unittest.TestCase):
patch("openpilot.starpilot.ui.layout_preview_sidebar.rl.draw_rectangle_rounded_lines_ex"):
render_sidebar(fonts, rl.Rectangle(1860, 0, 300, 1080), metric)
labels = [call.args[0] for call in fonts.draw.call_args_list]
self.assertEqual(labels, ["ACCEL", "1.08 ft/s²", "SAMPLE DATA"])
self.assertEqual(labels, ["ACCEL", "1.08 ft/s²"])
if __name__ == "__main__":