Polish GUI design

This commit is contained in:
Ayzen
2026-10-06 14:16:14 +03:00
parent 5bb86b7165
commit 4947d5a14e
25 changed files with 222 additions and 235 deletions
+22 -15
View File
@@ -64,8 +64,13 @@ controls, processing settings and a copy of the live plot through an embedded we
(default 2 cm). The radar motion is corrected from each trace's capture (default 2 cm). The radar motion is corrected from each trace's capture
timestamps, so the sweep timing needs no parameters. timestamps, so the sweep timing needs no parameters.
The desktop uses a light theme with dark plot canvases. **Mode** stays below the
**Actions** buttons while the settings sections scroll; the settings panel and
runtime log can be collapsed with their arrow buttons. SAR3D status, peak
coordinates and level appear above the projections.
For B-scans on every radar model, **B-scan pair** selects a pair from the displayed For B-scans on every radar model, **B-scan pair** selects a pair from the displayed
history independently of the run combinations. **Next combination** advances output history independently of the run combinations. **Next Combination** advances output
first, then input, and wraps to the first recorded pair; it is enabled with at least first, then input, and wraps to the first recorded pair; it is enabled with at least
two pairs. Desktop and browser selectors redraw the cached B-scan immediately. two pairs. Desktop and browser selectors redraw the cached B-scan immediately.
@@ -186,7 +191,7 @@ The preprocessor still requires S21 calibration/reference sets. Start the GUI:
./start.sh --profile run_config.json ./start.sh --profile run_config.json
``` ```
Open **Preprocessing**, use **Create Neutral S21 Sets**, then **Start**. This makes Open **Preprocessing…**, use **Create Neutral S21 Sets**, then **Start**. This makes
identity calibration + zero reference, preserving the simulated S21 samples. identity calibration + zero reference, preserving the simulated S21 samples.
**Dataset replay (recorded scene).** When **Dataset replay (recorded scene).** When
@@ -220,18 +225,18 @@ The embedded server listens at `http://<host>:8080`; set
`RADAR_SYSTEM_WEBUI_PORT` to use another port. It is available in desktop and `RADAR_SYSTEM_WEBUI_PORT` to use another port. It is available in desktop and
headless mode. Browser actions are: headless mode. Browser actions are:
- **Start**, **Single Capture**, **Stop**, **Tmp Reference** and **Remove Last Measurement**; - **Start**, **Single Capture**, **Stop**, **Capture Temporary Reference** and **Remove Last Measurement**;
- load a profile from `run_configs/*.json` using **Load**; - load a profile from `run_configs/*.json` using **Load**;
- **Save Last 1000** and **Start + Record** with a path, optional name and recording count; - **Save Last 1000** and **Start + Record to Disk** with a path, optional name and recording count;
- edit processor settings and antenna tables with **Apply**, or clear history with **Reset history**; - edit processor settings and antenna tables with **Apply**, or clear history with **Clear Runtime History**;
- select a **B-scan pair** or advance with **Next combination**. - select a **B-scan pair** or advance with **Next Combination**.
Copy profiles from `run_config_examples/` into `run_configs/` to offer them in the Copy profiles from `run_config_examples/` into `run_configs/` to offer them in the
browser. JSON export and calibration dialogs are desktop actions. PNG capture browser. JSON export and calibration dialogs are desktop actions. PNG capture
runs only while a browser is connected and the plot has changed, including zoom runs only while a browser is connected and the plot has changed, including zoom
and resize, at most five times per second. Each client retains the latest frame, and resize, at most five times per second. Each client retains the latest frame,
status and settings independently; reconnecting refreshes settings, and **Apply** status and settings independently; reconnecting refreshes settings, and **Apply**
sends only edited fields. sends only edited fields. Field labels, units and checkboxes match the desktop form.
## Data and preprocessing ## Data and preprocessing
@@ -243,7 +248,7 @@ The limit is fixed; legacy profile `save_count` values are ignored. Saving
keeps an active disk recording running. keeps an active disk recording running.
Without raw/preprocessed taps, result snapshots remain available, but S21 JSON Without raw/preprocessed taps, result snapshots remain available, but S21 JSON
requires retained traces. requires retained traces.
**Start + Record to Disk** (**Start + Record** in the browser) has a separate count **Start + Record to Disk** has a separate count
for future results. It keeps collections received after arming until that count, for future results. It keeps collections received after arming until that count,
Stop, or exhaustion of the bounded writer queue. If storage falls behind, Stop, or exhaustion of the bounded writer queue. If storage falls behind,
recording stops accepting data, drains the queue and final chunk, and reports the limit. recording stops accepting data, drains the queue and final chunk, and reports the limit.
@@ -264,7 +269,7 @@ and actual `speed_m_s` (including speed supplied by the locator). Settings
changed while acquisition is stopped are recorded for the reprocessed results as well. changed while acquisition is stopped are recorded for the reprocessed results as well.
**Remove Last Measurement** uses capture order and removes matching collections **Remove Last Measurement** uses capture order and removes matching collections
from every stage; the processor suppresses that sweep if it is still pending. from every stage; the processor suppresses that sweep if it is still pending.
**Clear Runtime History** (**Reset history** in the browser) clears all retained **Clear Runtime History** clears all retained
stages and rejects late results from before the cutoff. These actions leave saved stages and rejects late results from before the cutoff. These actions leave saved
files intact. A sweep is identified by `(collection_id, capture_start_ns)`; all files intact. A sweep is identified by `(collection_id, capture_start_ns)`; all
stages preserve the producer's capture window. `monotonic_ns` is diagnostic only. stages preserve the producer's capture window. `monotonic_ns` is diagnostic only.
@@ -287,15 +292,17 @@ are retained; new files and directories respect the process umask. Earlier NPZ/J
remain supported. Invalid trace shapes, duplicate combinations and non-finite remain supported. Invalid trace shapes, duplicate combinations and non-finite
calibration/reference values are rejected before a saved set is replaced. calibration/reference values are rejected before a saved set is replaced.
Median captures also require identical frequency axes and channel coverage. Median captures also require identical frequency axes and channel coverage.
Open **Preprocessing** to create or select the ordinary S21 calibration and Open **Preprocessing…** to create or select the ordinary S21 calibration and
empty-scene reference. The processing order is through calibration → calibrated empty-scene reference. The processing order is through calibration → calibrated
reference subtraction → notch → object correction → the selected processor. reference subtraction → notch → object correction → the selected processor.
### Object calibration ### Object calibration
**Create…** in the **Object Calibration** row opens a separate dialog. Use **Create…** in the **Object calibration** row opens a separate dialog. Use
**Capture data** to take fresh preprocessed sweeps, then enter the measured **Capture Data** to take fresh preprocessed sweeps, then enter the measured
reflector centre X/Y/Z in metres and use **Calculate and save**. reflector centre X/Y/Z in metres and use **Calculate and Save**. The optional
**Calculation Settings** section expands inside the scrollable dialog body;
the calculation, cancellation and close buttons remain visible below it.
Coordinates follow the notebook: X across the array, Y above the ground, Z along Coordinates follow the notebook: X across the array, Y above the ground, Z along
the ground. the ground.
Height above ground, radar pitch and sphere radius follow `MIMO_calibration.ipynb`; Height above ground, radar pitch and sphere radius follow `MIMO_calibration.ipynb`;
@@ -490,11 +497,11 @@ filtering before object correction.
Object capture checks both acquisition processes and releases its pause only after Object capture checks both acquisition processes and releases its pause only after
completion or cancellation; losing the producer reports a failure immediately. completion or cancellation; losing the producer reports a failure immediately.
`SnapshotController` coordinates manual exports, paused Clear/Remove operations `SnapshotController` coordinates manual exports, paused Clear/Remove operations
and Start + Record. and Start + Record to Disk.
History deletion is retained before publishing it to the processor, so a failed History deletion is retained before publishing it to the processor, so a failed
settings write can be retried without restoring deleted sweeps. `RecordingController` settings write can be retried without restoring deleted sweeps. `RecordingController`
owns a bounded writer session; its disk thread receives data and storage only. owns a bounded writer session; its disk thread receives data and storage only.
A processor restart during Start + Record does not finish the newly armed session. A processor restart during Start + Record to Disk does not finish the newly armed session.
Temporary references and neutral sets clear retained processing history without Temporary references and neutral sets clear retained processing history without
restarting the locator while the pipeline is paused. restarting the locator while the pipeline is paused.
+3 -3
View File
@@ -53,8 +53,8 @@ class AntennaGeometryEditor(QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._table) layout.addWidget(self._table)
buttons = QHBoxLayout() buttons = QHBoxLayout()
self._add_button = QPushButton("Add antenna", self) self._add_button = QPushButton("Add Antenna", self)
self._remove_button = QPushButton("Remove selected", self) self._remove_button = QPushButton("Remove Selected", self)
buttons.addWidget(self._add_button) buttons.addWidget(self._add_button)
buttons.addWidget(self._remove_button) buttons.addWidget(self._remove_button)
layout.addLayout(buttons) layout.addLayout(buttons)
@@ -104,7 +104,7 @@ class AntennaGeometryEditor(QWidget):
except (ValueError, RuntimeError) as exc: except (ValueError, RuntimeError) as exc:
with QSignalBlocker(self): with QSignalBlocker(self):
self.set_records(self._valid_records) self.set_records(self._valid_records)
QMessageBox.warning(self, "Invalid antenna geometry", str(exc)) QMessageBox.warning(self, "Invalid Antenna Geometry", str(exc))
return return
self._pending_row = None self._pending_row = None
self._valid_records = records self._valid_records = records
@@ -10,7 +10,7 @@ import math
from pathlib import Path from pathlib import Path
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox
from python_app.storage.npz.paths import radar_config_filename_prefix from python_app.storage.npz.paths import radar_config_filename_prefix
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
@@ -300,7 +300,18 @@ class ConfigController:
def web_schema(self) -> list[dict[str, object]]: def web_schema(self) -> list[dict[str, object]]:
schema = [] schema = []
for name, (group, widget) in self._web_widgets(str(self._scalar("_processing_mode"))).items(): for name, (group, widget) in self._web_widgets(str(self._scalar("_processing_mode"))).items():
entry: dict[str, object] = {"name": name, "group": group, "enabled": widget.isEnabled(), "value": _read_widget(widget)} if isinstance(widget, QCheckBox):
label = widget.text()
else:
parent = widget.parentWidget()
assert parent is not None
form = parent.layout()
assert isinstance(form, QFormLayout)
caption = form.labelForField(widget)
assert isinstance(caption, QLabel)
label = caption.text()
entry: dict[str, object] = {"name": name, "label": label, "group": group,
"enabled": widget.isEnabled(), "value": _read_widget(widget)}
if isinstance(widget, QComboBox): if isinstance(widget, QComboBox):
entry.update(kind="select", options=[widget.itemText(i) for i in range(widget.count())]) entry.update(kind="select", options=[widget.itemText(i) for i in range(widget.count())])
elif isinstance(widget, QCheckBox): elif isinstance(widget, QCheckBox):
@@ -370,6 +381,5 @@ class ConfigController:
raise raise
def mode_summary(self, mode: str) -> str: def mode_summary(self, mode: str) -> str:
values = (f"{binding.spec.web}={self._scalar(binding.spec.widget)}" return ", ".join(f"{binding.spec.web}={self._scalar(binding.spec.widget)}"
for binding in active_web_fields(mode) if binding.spec.web != "processor_mode") for binding in active_web_fields(mode) if binding.spec.web != "processor_mode")
return "(" + ", ".join(values) + ")"
@@ -200,4 +200,5 @@ class LiveSettingsController:
self.active_mode = mode self.active_mode = mode
self._pipeline.plots.set_mode(mode) self._pipeline.plots.set_mode(mode)
self.set_mode_page(mode) self.set_mode_page(mode)
self._log.info(f"Processing mode selected: {mode} {self.config.mode_summary(mode)}") self._log.info(f"Processing mode selected: {mode}")
self._log.debug(f"Processing settings ({mode}): {self.config.mode_summary(mode)}")
+8 -6
View File
@@ -12,9 +12,9 @@ from pathlib import Path
import threading import threading
import traceback import traceback
from PyQt6.QtCore import QSignalBlocker, QTimer from PyQt6.QtCore import QSignalBlocker, QTimer, Qt
from PyQt6.QtGui import QTextCursor from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QTextEdit, QVBoxLayout, QWidget from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QMessageBox, QToolButton, QTextEdit, QVBoxLayout, QWidget
from python_app.gui.settings_defaults import env_enabled from python_app.gui.settings_defaults import env_enabled
from python_app.logging_setup import LOG_LEVELS, add_handler, configure_logging, get_logger, package_logger, set_log_level from python_app.logging_setup import LOG_LEVELS, add_handler, configure_logging, get_logger, package_logger, set_log_level
@@ -102,8 +102,9 @@ class LogController:
self.level.setToolTip("Logging verbosity — applied live and saved to run_config") self.level.setToolTip("Logging verbosity — applied live and saved to run_config")
self.level.currentTextChanged.connect(self.set_level) self.level.currentTextChanged.connect(self.set_level)
header.addWidget(self.level) header.addWidget(self.level)
self._toggle = QPushButton("Collapse") self._toggle = QToolButton()
self._toggle.setObjectName("sectionToggleButton") self._toggle.setAutoRaise(True)
self._toggle.setArrowType(Qt.ArrowType.DownArrow)
self._toggle.setToolTip("Hide runtime log entries") self._toggle.setToolTip("Hide runtime log entries")
self._toggle.clicked.connect(self.toggle) self._toggle.clicked.connect(self.toggle)
header.addWidget(self._toggle) header.addWidget(self._toggle)
@@ -112,7 +113,8 @@ class LogController:
self._box.setObjectName("runtimeLogBox") self._box.setObjectName("runtimeLogBox")
self._box.setReadOnly(True) self._box.setReadOnly(True)
self._box.setUndoRedoEnabled(False) self._box.setUndoRedoEnabled(False)
self._box.setMinimumHeight(170) self._box.setMinimumHeight(130)
self._box.setMaximumHeight(140)
document = self._box.document() document = self._box.document()
assert document is not None assert document is not None
document.setMaximumBlockCount(1200) document.setMaximumBlockCount(1200)
@@ -133,7 +135,7 @@ class LogController:
def toggle(self) -> None: def toggle(self) -> None:
visible = self._box.isHidden() visible = self._box.isHidden()
self._box.setVisible(visible) self._box.setVisible(visible)
self._toggle.setText("Collapse" if visible else "Expand") self._toggle.setArrowType(Qt.ArrowType.DownArrow if visible else Qt.ArrowType.RightArrow)
self._toggle.setToolTip("Hide runtime log entries" if visible else "Show runtime log entries") self._toggle.setToolTip("Hide runtime log entries" if visible else "Show runtime log entries")
def flush(self) -> None: def flush(self) -> None:
@@ -87,7 +87,7 @@ class RadarSettingsController:
def apply_limits(self, limits: dict[str, float | int] | None) -> bool: def apply_limits(self, limits: dict[str, float | int] | None) -> bool:
previous, self.limits = self.limits, limits previous, self.limits = self.limits, limits
labels = ("Start Hz", "Stop Hz", "Points", "IF BW Hz", "Stimulus Power dBm") labels = ("Start (Hz)", "Stop (Hz)", "Points", "IF bandwidth (Hz)", "Stimulus power (dBm)")
edits = self._radar.sweep_edits edits = self._radar.sweep_edits
hint = self._radar.limits_hint hint = self._radar.limits_hint
if limits is None: if limits is None:
@@ -3,9 +3,11 @@
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from python_app.gui.theme import PLOT_BACKGROUND
def create_plot(title: str = "") -> pg.PlotWidget: def create_plot(title: str = "") -> pg.PlotWidget:
plot = pg.PlotWidget(background="#0f141c", enableMenu=False, title=title) plot = pg.PlotWidget(background=PLOT_BACKGROUND, enableMenu=False, title=title)
plot.showGrid(x=True, y=True, alpha=0.2) plot.showGrid(x=True, y=True, alpha=0.2)
return plot return plot
@@ -20,4 +22,3 @@ def build_lut(stops: list[str], *, size: int = 256) -> np.ndarray:
for channel in range(3): for channel in range(3):
lut[:, channel] = np.interp(sample_positions, stop_positions, stop_colors[:, channel]).astype(np.uint8) lut[:, channel] = np.interp(sample_positions, stop_positions, stop_colors[:, channel]).astype(np.uint8)
return lut return lut
@@ -64,7 +64,6 @@ class GprRenderer:
def __init__(self) -> None: def __init__(self) -> None:
self.widget = create_plot("GPR coherent BP") self.widget = create_plot("GPR coherent BP")
self.widget.setBackground("#0f141c")
plot = self.widget.getPlotItem() plot = self.widget.getPlotItem()
assert plot is not None assert plot is not None
self._plot = plot self._plot = plot
@@ -39,7 +39,7 @@ SAR3D_VIEWS: dict[str, tuple[str, str, str, tuple[int, int] | None]] = {
"sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)), "sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)),
"sar3d_side": ("Side (Z-Y)", "Z", "Y", (2, 1)), "sar3d_side": ("Side (Z-Y)", "Z", "Y", (2, 1)),
"sar3d_top": ("Top (X-Z)", "X", "Z", (0, 2)), "sar3d_top": ("Top (X-Z)", "X", "Z", (0, 2)),
"sar3d_oblique": ("3D projection", "", "", None), "sar3d_oblique": ("3D Projection", "", "", None),
} }
# Corners of the scene box indexed by bits (x, y, z); an edge joins corners one bit apart. # Corners of the scene box indexed by bits (x, y, z); an edge joins corners one bit apart.
@@ -209,10 +209,9 @@ class Sar3dRenderer:
(x0, x1), _, (z0, z1) = bounds (x0, x1), _, (z0, z1) = bounds
ground = (np.array([[x0, 0.0, z0], [x1, 0.0, z0], [x1, 0.0, z1], [x0, 0.0, z1], [x0, 0.0, z0]]) @ basis.table.T).T ground = (np.array([[x0, 0.0, z0], [x1, 0.0, z0], [x1, 0.0, z1], [x0, 0.0, z1], [x0, 0.0, z0]]) @ basis.table.T).T
self.ground.setData(ground[0], ground[1]) self.ground.setData(ground[0], ground[1])
summary = f"SAR3D: ready — {status[1]}" if status else "SAR3D: ready"
if level is not None: if level is not None:
x, y, z = peak_xyz x, y, z = peak_xyz
self.plots["sar3d_oblique"].setTitle( summary += f" | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}"
f"3D projection | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}" self.status_label.setText(summary)
)
self.status_label.setText(f"SAR3D: ready — {status[1]}" if status else "SAR3D: ready")
return True return True
@@ -34,8 +34,9 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
) )
save_vna_json_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) save_vna_json_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
remove_last_button = QPushButton("Remove Last Measurement") remove_last_button = QPushButton("Remove Last Measurement")
remove_last_button.setProperty("role", "destructive")
remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
capture_tmp_reference_button = QPushButton("Capture Tmp Reference") capture_tmp_reference_button = QPushButton("Capture Temporary Reference")
capture_tmp_reference_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) capture_tmp_reference_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
record_button = QPushButton("Start + Record to Disk") record_button = QPushButton("Start + Record to Disk")
record_button.setToolTip( record_button.setToolTip(
@@ -61,7 +62,7 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
record_count_row = QHBoxLayout() record_count_row = QHBoxLayout()
record_count_row.setSpacing(8) record_count_row.setSpacing(8)
record_count_row.addWidget(QLabel("Number of Sweeps to Record")) record_count_row.addWidget(QLabel("Sweeps to record"))
record_count_row.addWidget(record_count) record_count_row.addWidget(record_count)
record_count_row.addStretch(1) record_count_row.addStretch(1)
layout.addLayout(record_count_row) layout.addLayout(record_count_row)
@@ -69,7 +70,7 @@ def build_data_actions_group(gui: GuiStateModel) -> DataView:
path_row = QHBoxLayout() path_row = QHBoxLayout()
path_row.setSpacing(8) path_row.setSpacing(8)
save_path_input = view.add("_save_path_input", QLineEdit(str(data_defaults.save_path))) save_path_input = view.add("_save_path_input", QLineEdit(str(data_defaults.save_path)))
browse_button = QPushButton("Browse") browse_button = QPushButton("Browse…")
path_row.addWidget(QLabel("Path")) path_row.addWidget(QLabel("Path"))
path_row.addWidget(save_path_input, stretch=1) path_row.addWidget(save_path_input, stretch=1)
path_row.addWidget(browse_button) path_row.addWidget(browse_button)
@@ -22,6 +22,7 @@ def build_two_column_form_widget(
form.setContentsMargins(0, 0, 0, 0) form.setContentsMargins(0, 0, 0, 0)
form.setHorizontalSpacing(10) form.setHorizontalSpacing(10)
form.setVerticalSpacing(6) form.setVerticalSpacing(6)
form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
for row in column_rows: for row in column_rows:
if isinstance(row, tuple): if isinstance(row, tuple):
@@ -20,7 +20,7 @@ def build_preprocess_summary_group() -> PreprocessView:
for key in PREPROCESS_ASSET_NAMES: for key in PREPROCESS_ASSET_NAMES:
label = QLabel("<not selected>") label = QLabel("<not selected>")
view.labels[key] = label view.labels[key] = label
rows.append((preprocess_asset_display_name(key), label)) rows.append((preprocess_asset_display_name(key).capitalize(), label))
layout.addWidget(build_two_column_form_widget(group, rows, split_index=(len(rows) + 1) // 2)) layout.addWidget(build_two_column_form_widget(group, rows, split_index=(len(rows) + 1) // 2))
return view return view
@@ -1,16 +1,18 @@
"""Pinned primary actions; controllers connect their own buttons.""" """Pinned primary actions; controllers connect their own buttons."""
from PyQt6.QtWidgets import QGridLayout, QGroupBox, QPushButton, QSizePolicy from PyQt6.QtWidgets import QComboBox, QFormLayout, QGridLayout, QGroupBox, QPushButton, QSizePolicy, QWidget
from python_app.gui.controllers.sections.views import PrimaryView from python_app.gui.controllers.sections.views import PrimaryView
def build_primary_actions_group() -> PrimaryView: def build_primary_actions_group(mode: QComboBox) -> PrimaryView:
view = PrimaryView(group=QGroupBox("Actions"), start=QPushButton("Start"), view = PrimaryView(group=QGroupBox("Actions"), start=QPushButton("Start"),
single_capture=QPushButton("Single Capture"), stop=QPushButton("Stop"), single_capture=QPushButton("Single Capture"), stop=QPushButton("Stop"),
apply_radar=QPushButton("Apply Radar"), load=QPushButton("Load Config"), apply_radar=QPushButton("Apply Radar"), load=QPushButton("Load Config…"),
save=QPushButton("Save Config"), preprocess=QPushButton("Preprocessing"), save=QPushButton("Save Config…"), preprocess=QPushButton("Preprocessing…"),
clear=QPushButton("Clear Runtime History")) clear=QPushButton("Clear Runtime History"))
view.start.setProperty("role", "primary")
view.clear.setProperty("role", "destructive")
layout = QGridLayout(view.group) layout = QGridLayout(view.group)
layout.setHorizontalSpacing(8) layout.setHorizontalSpacing(8)
layout.setVerticalSpacing(8) layout.setVerticalSpacing(8)
@@ -20,4 +22,9 @@ def build_primary_actions_group() -> PrimaryView:
layout.addWidget(button, index // 3, index % 3, 1, 2 if button is view.clear else 1) layout.addWidget(button, index // 3, index % 3, 1, 2 if button is view.clear else 1)
for column in range(3): for column in range(3):
layout.setColumnStretch(column, 1) layout.setColumnStretch(column, 1)
mode_row = QWidget(view.group)
mode_form = QFormLayout(mode_row)
mode_form.setContentsMargins(0, 0, 0, 0)
mode_form.addRow("Mode", mode)
layout.addWidget(mode_row, 3, 0, 1, 3)
return view return view
@@ -75,8 +75,8 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
unwrap_phase_checkbox, unwrap_phase_checkbox,
("Switch combos", pass_through_combo_filter_input), ("Switch combos", pass_through_combo_filter_input),
pass_through_fixed_y_enabled, pass_through_fixed_y_enabled,
("Y min dB", pass_through_y_min_db), ("Y min (dB)", pass_through_y_min_db),
("Y max dB", pass_through_y_max_db), ("Y max (dB)", pass_through_y_max_db),
], ],
split_index=5, split_index=5,
) )
@@ -98,11 +98,11 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
processing_mode_pages, processing_mode_pages,
[ [
("Axis", bscan_axis), ("Axis", bscan_axis),
("Cut m", bscan_cut_m), ("Cut (m)", bscan_cut_m),
("Max depth m", bscan_max_depth_m), ("Max depth (m)", bscan_max_depth_m),
("Gain", bscan_gain), ("Gain", bscan_gain),
("Start MHz", bscan_start_freq_mhz), ("Start (MHz)", bscan_start_freq_mhz),
("Stop MHz", bscan_stop_freq_mhz), ("Stop (MHz)", bscan_stop_freq_mhz),
("Scans to show (stopped)", bscan_history_window), ("Scans to show (stopped)", bscan_history_window),
bscan_subtract_mean_ascan, bscan_subtract_mean_ascan,
], ],
@@ -188,27 +188,27 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
[ [
("Input positions", gpr_input_positions_input), ("Input positions", gpr_input_positions_input),
("Output positions", gpr_output_positions_input), ("Output positions", gpr_output_positions_input),
("Min depth m", gpr_min_depth_m), ("Min depth (m)", gpr_min_depth_m),
("Max depth m", gpr_max_depth_m), ("Max depth (m)", gpr_max_depth_m),
("Range comp power", gpr_range_comp_power), ("Range compensation power", gpr_range_comp_power),
("Object min frac", gpr_object_min_frac), ("Object threshold fraction", gpr_object_min_frac),
("Score mode", gpr_score_mode), ("Score mode", gpr_score_mode),
("Motion mode", gpr_motion_mode), ("Motion mode", gpr_motion_mode),
("Look angle deg", gpr_look_angle_deg), ("Look angle (°)", gpr_look_angle_deg),
("Direction sign", gpr_direction_sign), ("Direction sign", gpr_direction_sign),
gpr_ignore_socket_speed_enabled, gpr_ignore_socket_speed_enabled,
("Speed m/s", gpr_speed_m_s), ("Speed (m/s)", gpr_speed_m_s),
("Render mode", gpr_render_mode), ("Render mode", gpr_render_mode),
("Max detected objects", gpr_max_detected_objects_to_draw), ("Max detected objects", gpr_max_detected_objects_to_draw),
("Draw top M objects", gpr_draw_top_m_objects), ("Draw top M objects", gpr_draw_top_m_objects),
("Approach min frames", gpr_object_approach_min_frames), ("Approach min frames", gpr_object_approach_min_frames),
("Start MHz", gpr_start_freq_mhz), ("Start (MHz)", gpr_start_freq_mhz),
("Stop MHz", gpr_stop_freq_mhz), ("Stop (MHz)", gpr_stop_freq_mhz),
("Imaging plane Y m", gpr_imaging_plane_y_m), ("Imaging plane Y (m)", gpr_imaging_plane_y_m),
("Visible X min m", gpr_visible_x_min_m), ("Visible X min (m)", gpr_visible_x_min_m),
("Visible X max m", gpr_visible_x_max_m), ("Visible X max (m)", gpr_visible_x_max_m),
("Visible Z min m", gpr_visible_z_min_m), ("Visible Z min (m)", gpr_visible_z_min_m),
("Visible Z max m", gpr_visible_z_max_m), ("Visible Z max (m)", gpr_visible_z_max_m),
gpr_background_subtract_enabled, gpr_background_subtract_enabled,
("Mean count", gpr_background_mean_count), ("Mean count", gpr_background_mean_count),
gpr_remove_sidelobe_objects_enabled, gpr_remove_sidelobe_objects_enabled,
@@ -259,24 +259,24 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
("Relative permittivity", gpr_relative_permittivity), ("Relative permittivity", gpr_relative_permittivity),
("Input positions", legacy_gpr_input_positions_input), ("Input positions", legacy_gpr_input_positions_input),
("Output positions", legacy_gpr_output_positions_input), ("Output positions", legacy_gpr_output_positions_input),
("Min depth m", legacy_gpr_min_depth_m), ("Min depth (m)", legacy_gpr_min_depth_m),
("Max depth m", legacy_gpr_max_depth_m), ("Max depth (m)", legacy_gpr_max_depth_m),
("Comp power", legacy_gpr_comp_power), ("Compensation power", legacy_gpr_comp_power),
("SNR thresh", legacy_gpr_snr_thresh), ("SNR threshold", legacy_gpr_snr_thresh),
("SNR comp max", legacy_gpr_snr_comp_max), ("Max SNR compensation", legacy_gpr_snr_comp_max),
("Speed m/s", legacy_gpr_speed_m_s), ("Speed (m/s)", legacy_gpr_speed_m_s),
legacy_gpr_ignore_socket_speed_enabled, legacy_gpr_ignore_socket_speed_enabled,
("Render mode", legacy_gpr_render_mode), ("Render mode", legacy_gpr_render_mode),
("Min visible pairs", legacy_gpr_min_visible_pair_count), ("Min visible pairs", legacy_gpr_min_visible_pair_count),
("Start MHz", legacy_gpr_start_freq_mhz), ("Start (MHz)", legacy_gpr_start_freq_mhz),
("Stop MHz", legacy_gpr_stop_freq_mhz), ("Stop (MHz)", legacy_gpr_stop_freq_mhz),
("Look angle deg", legacy_gpr_look_angle_deg), ("Look angle (°)", legacy_gpr_look_angle_deg),
("Reference mode", legacy_gpr_reference_mode), ("Reference mode", legacy_gpr_reference_mode),
legacy_gpr_apply_freq_phase_correction, legacy_gpr_apply_freq_phase_correction,
("Visible X min m", legacy_gpr_visible_x_min_m), ("Visible X min (m)", legacy_gpr_visible_x_min_m),
("Visible X max m", legacy_gpr_visible_x_max_m), ("Visible X max (m)", legacy_gpr_visible_x_max_m),
("Visible Z min m", legacy_gpr_visible_z_min_m), ("Visible Z min (m)", legacy_gpr_visible_z_min_m),
("Visible Z max m", legacy_gpr_visible_z_max_m), ("Visible Z max (m)", legacy_gpr_visible_z_max_m),
legacy_gpr_background_subtract_enabled, legacy_gpr_background_subtract_enabled,
("Mean count", legacy_gpr_background_mean_count), ("Mean count", legacy_gpr_background_mean_count),
], ],
@@ -345,7 +345,6 @@ def build_processing_group(config: RunConfigModel, gui: GuiStateModel) -> Proces
) )
processing_mode_pages.addWidget(sar3d_page) processing_mode_pages.addWidget(sar3d_page)
form.addRow("Mode", processing_mode)
form.addRow(processing_mode_pages) form.addRow(processing_mode_pages)
form.addRow(gpr_common_page) form.addRow(gpr_common_page)
@@ -39,25 +39,20 @@ def build_radar_group(config: RunConfigModel) -> RadarView:
power_input = view.add("_power_input", QLineEdit(f"{defaults.sweep.power_dbm:g}")) power_input = view.add("_power_input", QLineEdit(f"{defaults.sweep.power_dbm:g}"))
power_input.setToolTip("Stimulus power configured in the active profile.") power_input.setToolTip("Stimulus power configured in the active profile.")
radar_start_label = QLabel("Start Hz") radar_labels = tuple(QLabel(text) for text in (
radar_stop_label = QLabel("Stop Hz") "Start (Hz)", "Stop (Hz)", "Points", "IF bandwidth (Hz)", "Stimulus power (dBm)",
radar_points_label = QLabel("Points") ))
radar_ifbw_label = QLabel("IF BW Hz") for label in radar_labels:
radar_power_label = QLabel("Stimulus Power dBm") label.setWordWrap(True)
radar_limits_hint = QLabel("Device limits are not available.") radar_limits_hint = QLabel("Device limits are not available.")
radar_limits_hint.setObjectName("hintLabel") radar_limits_hint.setObjectName("hintLabel")
radar_limits_hint.setWordWrap(True)
vna_layout.addWidget( vna_layout.addWidget(
build_two_column_form_widget( build_two_column_form_widget(
group, group,
[ list(zip(radar_labels, (start_hz_input, stop_hz_input, points_input, ifbw_input, power_input))),
(radar_start_label, start_hz_input),
(radar_stop_label, stop_hz_input),
(radar_points_label, points_input),
(radar_ifbw_label, ifbw_input),
(radar_power_label, power_input),
],
) )
) )
vna_layout.addWidget(radar_limits_hint) vna_layout.addWidget(radar_limits_hint)
@@ -71,7 +66,7 @@ def build_radar_group(config: RunConfigModel) -> RadarView:
adc_radar_settings_panel.setVisible(config.is_kamil_adc) adc_radar_settings_panel.setVisible(config.is_kamil_adc)
return RadarView(group=group, fields=view.fields, return RadarView(group=group, fields=view.fields,
sweep_edits=(start_hz_input, stop_hz_input, points_input, ifbw_input, power_input), sweep_edits=(start_hz_input, stop_hz_input, points_input, ifbw_input, power_input),
sweep_labels=(radar_start_label, radar_stop_label, radar_points_label, radar_ifbw_label, radar_power_label), sweep_labels=radar_labels,
limits_hint=radar_limits_hint, vna_panel=vna_radar_settings_panel, adc_panel=adc_radar_settings_panel, limits_hint=radar_limits_hint, vna_panel=vna_radar_settings_panel, adc_panel=adc_radar_settings_panel,
optical_enabled=optical_enabled, optical_mode=optical_mode, optical_common=optical_common, optical_enabled=optical_enabled, optical_mode=optical_mode, optical_common=optical_common,
optical_manual=optical_manual, optical_variation=optical_variation, optical_manual=optical_manual, optical_variation=optical_variation,
@@ -144,9 +139,9 @@ def _build_adc_settings_panel(view: SectionView, config: RunConfigModel):
("TTY path", adc_tty_path_input), ("TTY path", adc_tty_path_input),
("Arguments", adc_args_input), ("Arguments", adc_args_input),
("Environment JSON", adc_env_input), ("Environment JSON", adc_env_input),
("Startup timeout s", adc_startup_timeout_s_input), ("Startup timeout (s)", adc_startup_timeout_s_input),
("Read timeout s", adc_sweep_timeout_s_input), ("Read timeout (s)", adc_sweep_timeout_s_input),
("Stop timeout s", adc_stop_timeout_s_input), ("Stop timeout (s)", adc_stop_timeout_s_input),
("Board enabled", optical_enabled_checkbox), ("Board enabled", optical_enabled_checkbox),
("Board port", optical_port_input), ("Board port", optical_port_input),
("Board mode", optical_mode_combo), ("Board mode", optical_mode_combo),
@@ -29,7 +29,7 @@ def build_switch_group(config: RunConfigModel, gui: GuiStateModel) -> SwitchView
global_form = QFormLayout() global_form = QFormLayout()
global_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) global_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
settling_ms = view.add("_settling_ms", QLineEdit(str(config.runtime.settling_ms))) settling_ms = view.add("_settling_ms", QLineEdit(str(config.runtime.settling_ms)))
global_form.addRow("Settling ms", settling_ms) global_form.addRow("Settling (ms)", settling_ms)
layout.addLayout(global_form) layout.addLayout(global_form)
combos_text = view.add("_combos_text", QLineEdit(str(switch_defaults.combos_text))) combos_text = view.add("_combos_text", QLineEdit(str(switch_defaults.combos_text)))
@@ -71,7 +71,7 @@ def build_switch_group(config: RunConfigModel, gui: GuiStateModel) -> SwitchView
display_combo = view.add("_display_combo", QComboBox()) display_combo = view.add("_display_combo", QComboBox())
display_combo.setEnabled(False) display_combo.setEnabled(False)
display_row.addWidget(display_combo, stretch=1) display_row.addWidget(display_combo, stretch=1)
next_combo_button = QPushButton("Next combination") next_combo_button = QPushButton("Next Combination")
next_combo_button.setEnabled(False) next_combo_button.setEnabled(False)
display_row.addWidget(next_combo_button) display_row.addWidget(next_combo_button)
layout.addLayout(display_row) layout.addLayout(display_row)
+29 -19
View File
@@ -10,11 +10,12 @@ import pyqtgraph as pg
from PyQt6.QtCore import QSignalBlocker, QRectF, Qt, pyqtSignal from PyQt6.QtCore import QSignalBlocker, QRectF, Qt, pyqtSignal
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFormLayout, QGroupBox, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QScrollArea, QTableWidget, QTableWidgetItem,
QToolButton, QVBoxLayout, QWidget, QToolButton, QVBoxLayout, QWidget,
) )
from python_app.models.dataset_model import SweepCollection from python_app.models.dataset_model import SweepCollection
from python_app.gui.theme import PLOT_BACKGROUND
class ObjectCalibrationDialog(QDialog): class ObjectCalibrationDialog(QDialog):
@@ -33,7 +34,13 @@ class ObjectCalibrationDialog(QDialog):
self._provenance: dict | None = None self._provenance: dict | None = None
self._capture_pending = self._calculating = False self._capture_pending = self._calculating = False
self._calibration: dict | None = None self._calibration: dict | None = None
layout = QVBoxLayout(self) root_layout = QVBoxLayout(self)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
content = QWidget(scroll)
layout = QVBoxLayout(content)
scroll.setWidget(content)
root_layout.addWidget(scroll)
name_row = QHBoxLayout() name_row = QHBoxLayout()
name_row.addWidget(QLabel("Set name", self)) name_row.addWidget(QLabel("Set name", self))
self._set_name_input = QLineEdit(set_name, self) self._set_name_input = QLineEdit(set_name, self)
@@ -49,6 +56,7 @@ class ObjectCalibrationDialog(QDialog):
explanation.setWordWrap(True) explanation.setWordWrap(True)
layout.addWidget(explanation) layout.addWidget(explanation)
self._table = QTableWidget(0, 5, self) self._table = QTableWidget(0, 5, self)
self._table.setMaximumHeight(140)
self._table.setHorizontalHeaderLabels(["Target", "Sweeps", "X (m)", "Y (m)", "Z (m)"]) self._table.setHorizontalHeaderLabels(["Target", "Sweeps", "X (m)", "Y (m)", "Z (m)"])
horizontal, vertical = self._table.horizontalHeader(), self._table.verticalHeader() horizontal, vertical = self._table.horizontalHeader(), self._table.verticalHeader()
assert horizontal is not None and vertical is not None assert horizontal is not None and vertical is not None
@@ -57,9 +65,10 @@ class ObjectCalibrationDialog(QDialog):
self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
layout.addWidget(self._table) layout.addWidget(self._table)
actions = QHBoxLayout() actions = QHBoxLayout()
self._capture_button = QPushButton("Capture data", self) self._capture_button = QPushButton("Capture Data", self)
self._capture_button.clicked.connect(self.capture_requested.emit) self._capture_button.clicked.connect(self.capture_requested.emit)
self._remove_button = QPushButton("Remove selected target", self) self._remove_button = QPushButton("Remove Selected Target", self)
self._remove_button.setProperty("role", "destructive")
self._remove_button.clicked.connect(self._remove_capture) self._remove_button.clicked.connect(self._remove_capture)
actions.addWidget(self._capture_button) actions.addWidget(self._capture_button)
actions.addWidget(self._remove_button) actions.addWidget(self._remove_button)
@@ -68,27 +77,27 @@ class ObjectCalibrationDialog(QDialog):
self._settings: dict[str, QDoubleSpinBox | QCheckBox | QComboBox] = {} self._settings: dict[str, QDoubleSpinBox | QCheckBox | QComboBox] = {}
form = QFormLayout() form = QFormLayout()
for key, title, default, low, high in ( for key, title, default, low, high in (
("sphere_radius_m", "Sphere radius, m (0 = point)", .16, 0, 100), ("sphere_radius_m", "Sphere radius (m, 0 = point)", .16, 0, 100),
("antenna_height_m", "Antenna origin height above ground, m", .92, 0, 100), ("antenna_height_m", "Antenna origin height above ground (m)", .92, 0, 100),
("pitch_deg", "Radar downward tilt, °", 10, -90, 90), ("pitch_deg", "Radar downward tilt (°)", 10, -90, 90),
): ):
form.addRow(title, self._number(key, default, low, high)) form.addRow(title, self._number(key, default, low, high))
layout.addLayout(form) layout.addLayout(form)
# A disclosure header: only the header shows until the operator opens it. # A disclosure header: only the header shows until the operator opens it.
advanced_toggle = QToolButton(self) advanced_toggle = QToolButton(self)
advanced_toggle.setText("Calculation settings") advanced_toggle.setText("Calculation Settings")
advanced_toggle.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) advanced_toggle.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
advanced_toggle.setArrowType(Qt.ArrowType.RightArrow) advanced_toggle.setArrowType(Qt.ArrowType.RightArrow)
advanced_toggle.setAutoRaise(True) advanced_toggle.setAutoRaise(True)
advanced = QGroupBox(self) advanced = QGroupBox(self)
advanced_form = QFormLayout(advanced) advanced_form = QFormLayout(advanced)
for key, title, default, low, high in ( for key, title, default, low, high in (
("f_min", "Estimation band start, MHz", 2000, 0, 100000), ("f_min", "Estimation band start (MHz)", 2000, 0, 100000),
("f_max", "Estimation band stop, MHz", 6000, 0, 100000), ("f_max", "Estimation band stop (MHz)", 6000, 0, 100000),
("gate_half_m", "Echo path gate half-width, m", .15, .000001, 100), ("gate_half_m", "Echo path gate half-width (m)", .15, .001, 100),
("gate_taper_m", "Gate taper width, m", .03, .000001, 100), ("gate_taper_m", "Gate taper width (m)", .03, .001, 100),
("coarse_range_search_m", "Coarse path search, ±m (0 = off)", .6, 0, 100), ("coarse_range_search_m", "Coarse path search (±m, 0 = off)", .6, 0, 100),
("refine_range_m", "Coordinate refinement, ±m", .1, 0, 100), ("refine_range_m", "Coordinate refinement (±m)", .1, 0, 100),
): ):
advanced_form.addRow(title, self._number(key, default, low, high)) advanced_form.addRow(title, self._number(key, default, low, high))
axes = QWidget(advanced) axes = QWidget(advanced)
@@ -123,7 +132,7 @@ class ObjectCalibrationDialog(QDialog):
self._target_combo = QComboBox(self) self._target_combo = QComboBox(self)
layout.addWidget(self._target_combo) layout.addWidget(self._target_combo)
self._phase_view = pg.GraphicsLayoutWidget(parent=self) self._phase_view = pg.GraphicsLayoutWidget(parent=self)
self._phase_view.setBackground("#0f141c") self._phase_view.setBackground(PLOT_BACKGROUND)
self._phase_view.setMinimumHeight(270) self._phase_view.setMinimumHeight(270)
self._phase_plots = [] self._phase_plots = []
self._phase_images = [] self._phase_images = []
@@ -154,9 +163,10 @@ class ObjectCalibrationDialog(QDialog):
self._status_label.setWordWrap(True) self._status_label.setWordWrap(True)
layout.addWidget(self._status_label) layout.addWidget(self._status_label)
footer = QHBoxLayout() footer = QHBoxLayout()
self._calculate_button = QPushButton("Calculate and save", self) self._calculate_button = QPushButton("Calculate and Save", self)
self._calculate_button.setProperty("role", "primary")
self._calculate_button.clicked.connect(self.calculate_requested.emit) self._calculate_button.clicked.connect(self.calculate_requested.emit)
self._cancel_button = QPushButton("Cancel operation", self) self._cancel_button = QPushButton("Cancel Operation", self)
self._cancel_button.clicked.connect(self.cancel_requested.emit) self._cancel_button.clicked.connect(self.cancel_requested.emit)
close_button = QPushButton("Close", self) close_button = QPushButton("Close", self)
close_button.clicked.connect(self.reject) close_button.clicked.connect(self.reject)
@@ -164,7 +174,7 @@ class ObjectCalibrationDialog(QDialog):
footer.addWidget(self._cancel_button) footer.addWidget(self._cancel_button)
footer.addStretch(1) footer.addStretch(1)
footer.addWidget(close_button) footer.addWidget(close_button)
layout.addLayout(footer) root_layout.addLayout(footer)
self._table.itemChanged.connect(self._measurements_changed) self._table.itemChanged.connect(self._measurements_changed)
self._target_combo.currentIndexChanged.connect(self._draw_phase_effect) self._target_combo.currentIndexChanged.connect(self._draw_phase_effect)
for field in [*self._settings.values(), *self._refine_axes.values()]: for field in [*self._settings.values(), *self._refine_axes.values()]:
@@ -175,7 +185,7 @@ class ObjectCalibrationDialog(QDialog):
def _number(self, key: str, value: float, low: float, high: float) -> QDoubleSpinBox: def _number(self, key: str, value: float, low: float, high: float) -> QDoubleSpinBox:
field = QDoubleSpinBox(self) field = QDoubleSpinBox(self)
field.setDecimals(6) field.setDecimals(0 if key in {"f_min", "f_max"} else 1 if key == "pitch_deg" else 3)
field.setRange(low, high) field.setRange(low, high)
field.setSingleStep(100 if key in {"f_min", "f_max"} else .1 if key == "pitch_deg" else .01) field.setSingleStep(100 if key in {"f_min", "f_max"} else .1 if key == "pitch_deg" else .01)
field.setValue(value) field.setValue(value)
+14 -20
View File
@@ -26,6 +26,7 @@ from PyQt6.QtWidgets import (
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from python_app.gui.controllers.renderers.common import create_plot
from python_app.models.gui_profile_schema import GuiPreprocessDialogStateModel from python_app.models.gui_profile_schema import GuiPreprocessDialogStateModel
from python_app.models.dataset_model import TraceData from python_app.models.dataset_model import TraceData
from python_app.orchestration.preprocess_assets import ( from python_app.orchestration.preprocess_assets import (
@@ -113,7 +114,7 @@ class PreprocessDialog(QDialog):
layout.addLayout(self._build_median_sweep_row(group)) layout.addLayout(self._build_median_sweep_row(group))
layout.addWidget(self._build_radar_config_group(group)) layout.addWidget(self._build_radar_config_group(group))
layout.addWidget(self._build_selector_group("S21", tuple(PREPROCESS_ASSET_NAMES), group)) layout.addLayout(self._build_selectors(group))
return group return group
def _build_median_sweep_row(self, parent: QGroupBox) -> QHBoxLayout: def _build_median_sweep_row(self, parent: QGroupBox) -> QHBoxLayout:
@@ -141,7 +142,7 @@ class PreprocessDialog(QDialog):
self._radar_config_dir_input = QLineEdit(group) self._radar_config_dir_input = QLineEdit(group)
self._radar_config_dir_input.setPlaceholderText("Directory with radar JSON configs") self._radar_config_dir_input.setPlaceholderText("Directory with radar JSON configs")
self._radar_config_dir_input.editingFinished.connect(self.radar_config_dir_changed.emit) self._radar_config_dir_input.editingFinished.connect(self.radar_config_dir_changed.emit)
browse_button = QPushButton("Browse...", group) browse_button = QPushButton("Browse…", group)
browse_button.clicked.connect(self._choose_radar_config_dir) browse_button.clicked.connect(self._choose_radar_config_dir)
dir_row.addWidget(QLabel("Directory")) dir_row.addWidget(QLabel("Directory"))
dir_row.addWidget(self._radar_config_dir_input, stretch=1) dir_row.addWidget(self._radar_config_dir_input, stretch=1)
@@ -160,14 +161,13 @@ class PreprocessDialog(QDialog):
layout.addWidget(self._use_all_radar_configs_checkbox) layout.addWidget(self._use_all_radar_configs_checkbox)
return group return group
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox: def _build_selectors(self, parent: QGroupBox) -> QFormLayout:
"""Build one selector subgroup for a channel family.""" """Build the S21 set selectors in capture order."""
group = QGroupBox(title, parent) form = QFormLayout()
form = QFormLayout(group)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
for key in keys: for key in PREPROCESS_ASSET_NAMES:
combo = QComboBox(group) combo = QComboBox(parent)
combo.setPlaceholderText("<not selected>") combo.setPlaceholderText("<not selected>")
combo.setProperty("optional", key not in REQUIRED_PREPROCESS_ASSET_KEYS) combo.setProperty("optional", key not in REQUIRED_PREPROCESS_ASSET_KEYS)
self._set_combo_items(combo, [], "") self._set_combo_items(combo, [], "")
@@ -175,7 +175,7 @@ class PreprocessDialog(QDialog):
self._set_combos[key] = combo self._set_combos[key] = combo
field = combo field = combo
if key == "s21_object_calibration": if key == "s21_object_calibration":
field = QWidget(group) field = QWidget(parent)
row = QHBoxLayout(field) row = QHBoxLayout(field)
row.setContentsMargins(0, 0, 0, 0) row.setContentsMargins(0, 0, 0, 0)
row.addWidget(combo, stretch=1) row.addWidget(combo, stretch=1)
@@ -183,12 +183,12 @@ class PreprocessDialog(QDialog):
self._create_object_calibration_button.setToolTip("Calculate a MIMO calibration from reflector measurements.") self._create_object_calibration_button.setToolTip("Calculate a MIMO calibration from reflector measurements.")
self._create_object_calibration_button.clicked.connect(self.create_object_calibration_requested.emit) self._create_object_calibration_button.clicked.connect(self.create_object_calibration_requested.emit)
row.addWidget(self._create_object_calibration_button) row.addWidget(self._create_object_calibration_button)
form.addRow(self._asset_row_label(key), field) form.addRow(preprocess_asset_display_name(key).capitalize(), field)
return group return form
def _build_sequence_group(self) -> QGroupBox: def _build_sequence_group(self) -> QGroupBox:
"""Build sequential-capture controls and capture log.""" """Build sequential-capture controls and capture log."""
group = QGroupBox("Sequential Capture (Fill Full N*M)", self) group = QGroupBox("Capture All Antenna Pairs", self)
layout = QGridLayout(group) layout = QGridLayout(group)
self._active_kind_label = QLabel("<none>", group) self._active_kind_label = QLabel("<none>", group)
@@ -239,6 +239,7 @@ class PreprocessDialog(QDialog):
self._save_sequence_button.setEnabled(False) self._save_sequence_button.setEnabled(False)
self._abort_button = QPushButton("Abort Sequence", parent) self._abort_button = QPushButton("Abort Sequence", parent)
self._abort_button.setProperty("role", "destructive")
self._abort_button.clicked.connect(self.abort_sequence_requested.emit) self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
self._abort_button.setEnabled(False) self._abort_button.setEnabled(False)
@@ -270,7 +271,6 @@ class PreprocessDialog(QDialog):
placeholder = QLabel("Preview will appear after the first successful capture.", host) placeholder = QLabel("Preview will appear after the first successful capture.", host)
placeholder.setWordWrap(True) placeholder.setWordWrap(True)
placeholder.setMinimumHeight(320)
layout.addWidget(placeholder) layout.addWidget(placeholder)
self._preview_host_layout = layout self._preview_host_layout = layout
@@ -531,8 +531,7 @@ class PreprocessDialog(QDialog):
@staticmethod @staticmethod
def _build_preview_axis(left_label: str, left_units: str) -> "pg.PlotWidget": def _build_preview_axis(left_label: str, left_units: str) -> "pg.PlotWidget":
"""Return one PlotWidget configured with the project's preview style.""" """Return one PlotWidget configured with the project's preview style."""
plot = pg.PlotWidget(background="#101418", enableMenu=False) plot = create_plot()
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", "Frequency", units="Hz") plot.setLabel("bottom", "Frequency", units="Hz")
plot.setLabel("left", left_label, units=left_units) plot.setLabel("left", left_label, units=left_units)
plot.setMinimumHeight(320) plot.setMinimumHeight(320)
@@ -554,11 +553,6 @@ class PreprocessDialog(QDialog):
self._radar_config_dir_input.setText(selected_dir) self._radar_config_dir_input.setText(selected_dir)
self.radar_config_dir_changed.emit() self.radar_config_dir_changed.emit()
@staticmethod
def _asset_row_label(key: str) -> str:
"""Return short selector label for one preprocess asset."""
return preprocess_asset_display_name(key)
@staticmethod @staticmethod
def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None: def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None:
"""Replace combo contents and keep previous value when still available.""" """Replace combo contents and keep previous value when still available."""
+20 -63
View File
@@ -5,23 +5,15 @@ from __future__ import annotations
from PyQt6.QtGui import QColor, QPalette from PyQt6.QtGui import QColor, QPalette
from PyQt6.QtWidgets import QApplication, QStyleFactory from PyQt6.QtWidgets import QApplication, QStyleFactory
PLOT_BACKGROUND = "#0f141c"
_LIGHT_STYLESHEET = """ _LIGHT_STYLESHEET = """
QMainWindow, QDialog {
background-color: #f3f6fb;
}
QWidget {
color: #1f2937;
font-size: 13px;
}
QGroupBox { QGroupBox {
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #d7dee8; border: 1px solid #d7dee8;
border-radius: 10px; border-radius: 6px;
margin-top: 14px; margin-top: 12px;
padding: 10px; padding: 6px;
} }
QGroupBox::title { QGroupBox::title {
@@ -35,11 +27,11 @@ QGroupBox::title {
QPushButton { QPushButton {
background-color: #f8fafc; background-color: #f8fafc;
border: 1px solid #cad4e1; border: 1px solid #cad4e1;
border-radius: 8px; border-radius: 4px;
padding: 7px 12px; padding: 3px 10px;
} }
QPushButton:hover { QPushButton:hover:enabled {
background-color: #eef3f9; background-color: #eef3f9;
} }
@@ -47,70 +39,30 @@ QPushButton:pressed {
background-color: #e4ebf4; background-color: #e4ebf4;
} }
QPushButton:checked { QPushButton:checked, QPushButton[role="primary"] {
background-color: #2f7ee6; background-color: #2f7ee6;
border-color: #2f7ee6; border-color: #2f7ee6;
color: #ffffff; color: #ffffff;
} }
QPushButton:checked:hover { QPushButton:checked:hover:enabled, QPushButton[role="primary"]:hover:enabled {
background-color: #3b8bf4; background-color: #3b8bf4;
} }
QPushButton[role="destructive"] {
color: #a73545;
border-color: #cf9ba4;
}
QPushButton:disabled { QPushButton:disabled {
color: #98a4b3; color: #98a4b3;
background-color: #f3f5f8; background-color: #f3f5f8;
border-color: #dde4ec; border-color: #dde4ec;
} }
QPushButton#settingsToggleButton {
min-width: 26px;
max-width: 26px;
padding: 6px 0px;
border-radius: 7px;
font-weight: 700;
}
QPushButton#sectionToggleButton {
min-width: 84px;
padding: 5px 10px;
border-radius: 7px;
}
QLineEdit,
QPlainTextEdit,
QTextEdit,
QComboBox,
QSpinBox,
QDoubleSpinBox {
background-color: #ffffff;
border: 1px solid #c9d4e1;
border-radius: 7px;
padding: 5px 8px;
selection-background-color: #cfe3ff;
}
QLineEdit:focus,
QPlainTextEdit:focus,
QTextEdit:focus,
QComboBox:focus,
QSpinBox:focus,
QDoubleSpinBox:focus {
border: 1px solid #2f7ee6;
}
QTextEdit#runtimeLogBox { QTextEdit#runtimeLogBox {
font-family: "DejaVu Sans Mono"; font-family: "DejaVu Sans Mono";
font-size: 12px; font-size: 12px;
padding: 6px 8px;
color: #1f2937;
background-color: #fbfdff;
border-color: #c9d4e1;
}
QComboBox::drop-down {
border: none;
width: 18px;
} }
QScrollArea { QScrollArea {
@@ -121,7 +73,6 @@ QScrollArea {
QLabel#statusLabel { QLabel#statusLabel {
color: #35507a; color: #35507a;
font-weight: 600; font-weight: 600;
padding: 2px 1px;
} }
QLabel#hintLabel { QLabel#hintLabel {
@@ -133,6 +84,9 @@ QLabel#hintLabel {
def apply_light_theme(app: QApplication) -> None: def apply_light_theme(app: QApplication) -> None:
"""Apply a minimal light theme shared by all windows.""" """Apply a minimal light theme shared by all windows."""
app.setStyle(QStyleFactory.create("Fusion")) app.setStyle(QStyleFactory.create("Fusion"))
font = app.font()
font.setPointSize(10)
app.setFont(font)
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor("#f3f6fb")) palette.setColor(QPalette.ColorRole.Window, QColor("#f3f6fb"))
@@ -148,5 +102,8 @@ def apply_light_theme(app: QApplication) -> None:
palette.setColor(QPalette.ColorRole.Link, QColor("#2f7ee6")) palette.setColor(QPalette.ColorRole.Link, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.Highlight, QColor("#2f7ee6")) palette.setColor(QPalette.ColorRole.Highlight, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff")) palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
for role in (QPalette.ColorRole.Text, QPalette.ColorRole.ButtonText, QPalette.ColorRole.WindowText):
palette.setColor(QPalette.ColorGroup.Disabled, role, QColor("#98a4b3"))
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor("#f3f5f8"))
app.setPalette(palette) app.setPalette(palette)
app.setStyleSheet(_LIGHT_STYLESHEET) app.setStyleSheet(_LIGHT_STYLESHEET)
+2 -4
View File
@@ -17,12 +17,10 @@ from PyQt6.QtCore import QRectF
from pyqtgraph.exporters import ImageExporter from pyqtgraph.exporters import ImageExporter
from python_app.models.dataset_model import TraceData from python_app.models.dataset_model import TraceData
from python_app.gui.theme import PLOT_BACKGROUND
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Matches the preview pane styling in preprocess_dialog so saved PNGs and the
# on-screen preview look the same.
_BACKGROUND = "#101418"
_MAGNITUDE_PEN = pg.mkPen("#4cc9f0", width=1.8) _MAGNITUDE_PEN = pg.mkPen("#4cc9f0", width=1.8)
_PHASE_PEN = pg.mkPen("#f48c06", width=1.8) _PHASE_PEN = pg.mkPen("#f48c06", width=1.8)
_EXPORT_SIZE = (1400, 520) _EXPORT_SIZE = (1400, 520)
@@ -45,7 +43,7 @@ def export_trace_png(
phase_deg = np.degrees(np.angle(samples)) phase_deg = np.degrees(np.angle(samples))
layout = pg.GraphicsLayoutWidget(show=False, size=_EXPORT_SIZE) layout = pg.GraphicsLayoutWidget(show=False, size=_EXPORT_SIZE)
layout.setBackground(_BACKGROUND) layout.setBackground(PLOT_BACKGROUND)
try: try:
layout.ci.addLabel(title, row=0, col=0, colspan=2) layout.ci.addLabel(title, row=0, col=0, colspan=2)
+14 -10
View File
@@ -2,9 +2,10 @@
from pathlib import Path from pathlib import Path
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow, QPushButton, QFileDialog, QFrame, QHBoxLayout, QLabel, QMainWindow,
QScrollArea, QVBoxLayout, QWidget, QScrollArea, QToolButton, QVBoxLayout, QWidget,
) )
from python_app.gui.controllers.history_controller import HistoryController from python_app.gui.controllers.history_controller import HistoryController
@@ -29,9 +30,9 @@ class WindowView:
layout.setContentsMargins(12, 12, 12, 12) layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(14) layout.setSpacing(14)
layout.addWidget(plot_stack, stretch=11) layout.addWidget(plot_stack, stretch=11)
self._toggle = QPushButton(">") self._toggle = QToolButton()
self._toggle.setObjectName("settingsToggleButton") self._toggle.setAutoRaise(True)
self._toggle.setFixedWidth(26) self._toggle.setArrowType(Qt.ArrowType.RightArrow)
self._toggle.setToolTip("Hide settings panel") self._toggle.setToolTip("Hide settings panel")
self._toggle.clicked.connect(self.toggle_settings) self._toggle.clicked.connect(self.toggle_settings)
layout.addWidget(self._toggle) layout.addWidget(self._toggle)
@@ -40,12 +41,12 @@ class WindowView:
settings_layout = QVBoxLayout(self.settings_panel) settings_layout = QVBoxLayout(self.settings_panel)
settings_layout.setContentsMargins(0, 0, 0, 0) settings_layout.setContentsMargins(0, 0, 0, 0)
settings_layout.setSpacing(10) settings_layout.setSpacing(10)
self.primary = build_primary_actions_group() self.processing = build_processing_group(config, gui)
self.primary = build_primary_actions_group(self.processing.mode)
settings_layout.addWidget(self.primary.group) settings_layout.addWidget(self.primary.group)
self.data = build_data_actions_group(gui) self.data = build_data_actions_group(gui)
self.switches = build_switch_group(config, gui) self.switches = build_switch_group(config, gui)
self.preprocess = build_preprocess_summary_group() self.preprocess = build_preprocess_summary_group()
self.processing = build_processing_group(config, gui)
self.radar = build_radar_group(config) self.radar = build_radar_group(config)
self.data.browse.clicked.connect(lambda: self.browse_save_path(project_root)) self.data.browse.clicked.connect(lambda: self.browse_save_path(project_root))
self.sections = (self.data, self.switches, self.preprocess, self.processing, self.radar) self.sections = (self.data, self.switches, self.preprocess, self.processing, self.radar)
@@ -63,10 +64,13 @@ class WindowView:
settings_layout.addWidget(scroll, stretch=1) settings_layout.addWidget(scroll, stretch=1)
self.status = QLabel("Status: idle") self.status = QLabel("Status: idle")
self.status.setObjectName("statusLabel") self.status.setObjectName("statusLabel")
self.status.setWordWrap(True)
self._history = QLabel("History: raw=0, preprocessed=0, results=0") self._history = QLabel("History: raw=0, preprocessed=0, results=0")
self._history.setObjectName("hintLabel") self._history.setObjectName("hintLabel")
settings_layout.addWidget(self.status) status_row = QHBoxLayout()
settings_layout.addWidget(self._history) status_row.addWidget(self.status, stretch=1)
status_row.addWidget(self._history, alignment=Qt.AlignmentFlag.AlignRight)
settings_layout.addLayout(status_row)
settings_layout.addWidget(log_panel) settings_layout.addWidget(log_panel)
layout.addWidget(self.settings_panel, stretch=7) layout.addWidget(self.settings_panel, stretch=7)
parent.resize(1650, 940) parent.resize(1650, 940)
@@ -74,7 +78,7 @@ class WindowView:
def toggle_settings(self) -> None: def toggle_settings(self) -> None:
visible = self.settings_panel.isHidden() visible = self.settings_panel.isHidden()
self.settings_panel.setVisible(visible) self.settings_panel.setVisible(visible)
self._toggle.setText(">" if visible else "<") self._toggle.setArrowType(Qt.ArrowType.RightArrow if visible else Qt.ArrowType.LeftArrow)
self._toggle.setToolTip("Hide settings panel" if visible else "Show settings panel") self._toggle.setToolTip("Hide settings panel" if visible else "Show settings panel")
def update_history(self, history: HistoryController) -> None: def update_history(self, history: HistoryController) -> None:
+2 -1
View File
@@ -51,7 +51,8 @@ class Sar3dPlotTest(unittest.TestCase):
def test_all_four_projections_have_visible_images_and_correct_peak_axes(self) -> None: def test_all_four_projections_have_visible_images_and_correct_peak_axes(self) -> None:
self.assertTrue(self.renderer.draw(_projections())) self.assertTrue(self.renderer.draw(_projections()))
self.assertEqual(self.renderer.status_label.text(), "SAR3D: ready — CUDA") self.assertEqual(self.renderer.status_label.text(),
"SAR3D: ready — CUDA | max X +0.50, Y -0.20, Z 2.00 m | level 2")
positions = {"sar3d_front": (0.5, -0.2), "sar3d_side": (2, -0.2), positions = {"sar3d_front": (0.5, -0.2), "sar3d_side": (2, -0.2),
"sar3d_top": (0.5, 2), "sar3d_oblique": (0.5, -0.2)} "sar3d_top": (0.5, 2), "sar3d_oblique": (0.5, -0.2)}
for name, position in positions.items(): for name, position in positions.items():
+4 -4
View File
@@ -117,7 +117,7 @@ btnNextCombo.addEventListener("click", () => changeDisplayCombo("/api/next_combo
settingsToggle.addEventListener("click", () => sidePanel.classList.toggle("collapsed")); settingsToggle.addEventListener("click", () => sidePanel.classList.toggle("collapsed"));
// The form is built ENTIRELY from the schema the server derives from the Qt widgets // The form is built ENTIRELY from the schema the server derives from the Qt widgets
// (field, group, kind, options, ranges, value, enabled). The web hardcodes nothing // (field, label, group, kind, options, ranges, value, enabled). The web hardcodes nothing
// and shows only the active mode's fields, so it always mirrors the desktop. // and shows only the active mode's fields, so it always mirrors the desktop.
const fieldInputs = {}; // field name -> { el, kind, dirty } const fieldInputs = {}; // field name -> { el, kind, dirty }
let formSignature = ""; // field names currently in the form (detect mode/structure change) let formSignature = ""; // field names currently in the form (detect mode/structure change)
@@ -146,7 +146,7 @@ function makeGeometryEditor(entry) {
input.type = "number"; input.type = "number";
input.step = index === 0 ? "1" : "any"; input.step = index === 0 ? "1" : "any";
input.required = true; input.required = true;
input.setAttribute("aria-label", `${entry.name} ${key}`); input.setAttribute("aria-label", `${entry.label} ${key}`);
if (index === 0) { input.min = "0"; input.max = entry.position_count - 1; } if (index === 0) { input.min = "0"; input.max = entry.position_count - 1; }
input.value = record[key] ?? 0; input.value = record[key] ?? 0;
input.disabled = entry.enabled === false; input.disabled = entry.enabled === false;
@@ -177,7 +177,7 @@ function makeGeometryEditor(entry) {
}; };
el.setRecords(entry.value); el.setRecords(entry.value);
add.type = "button"; add.type = "button";
add.textContent = "Add antenna"; add.textContent = "Add Antenna";
add.addEventListener("click", () => { add.addEventListener("click", () => {
if (body.rows.length >= entry.position_count) return; if (body.rows.length >= entry.position_count) return;
const positions = new Set(Array.from(body.rows, (row) => Number(row.querySelector("input").value))); const positions = new Set(Array.from(body.rows, (row) => Number(row.querySelector("input").value)));
@@ -196,7 +196,7 @@ function makeFieldRow(entry) {
row.className = "field"; row.className = "field";
if (entry.kind === "geometry") row.classList.add("geometry-field"); if (entry.kind === "geometry") row.classList.add("geometry-field");
const label = document.createElement("label"); const label = document.createElement("label");
label.textContent = entry.kind === "geometry" ? (entry.name === "tx_geometry" ? "Tx antenna positions" : "Rx antenna positions") : entry.name; label.textContent = entry.label;
label.htmlFor = "f_" + entry.name; label.htmlFor = "f_" + entry.name;
row.appendChild(label); row.appendChild(label);
+10 -10
View File
@@ -10,21 +10,21 @@
<header class="topbar"> <header class="topbar">
<div class="brand">Radar System</div> <div class="brand">Radar System</div>
<div class="controls"> <div class="controls">
<button id="btn-start" class="btn">Start</button> <button id="btn-start" class="btn primary">Start</button>
<button id="btn-single" class="btn">Single Capture</button> <button id="btn-single" class="btn">Single Capture</button>
<button id="btn-stop" class="btn">Stop</button> <button id="btn-stop" class="btn">Stop</button>
<button id="btn-tmp-ref" class="btn">Tmp Reference</button> <button id="btn-tmp-ref" class="btn">Capture Temporary Reference</button>
<button id="btn-remove-last" class="btn">Remove Last Measurement</button> <button id="btn-remove-last" class="btn destructive">Remove Last Measurement</button>
</div> </div>
</header> </header>
<main class="layout"> <main class="layout">
<section class="plot-panel"> <section class="plot-panel">
<div class="plot-head"> <div class="plot-head">
<span class="plot-title">Processor output</span> <span class="plot-title">Processor Output</span>
<div id="display-combo-controls" class="config-row" hidden> <div id="display-combo-controls" class="config-row" hidden>
<select id="display-combo" class="config-control" aria-label="B-scan channel combination"></select> <select id="display-combo" class="config-control" aria-label="B-scan channel combination"></select>
<button id="btn-next-combo" class="btn">Next combination</button> <button id="btn-next-combo" class="btn">Next Combination</button>
</div> </div>
</div> </div>
<div id="sar3d-status" class="sar3d-status" role="status" hidden></div> <div id="sar3d-status" class="sar3d-status" role="status" hidden></div>
@@ -35,7 +35,7 @@
<aside class="side-panel"> <aside class="side-panel">
<div class="config-section"> <div class="config-section">
<div class="config-title">Config profile</div> <div class="config-title">Config Profile</div>
<div class="config-row"> <div class="config-row">
<select id="config-select" class="config-control" aria-label="Run config"></select> <select id="config-select" class="config-control" aria-label="Run config"></select>
<button id="btn-load-config" class="btn">Load</button> <button id="btn-load-config" class="btn">Load</button>
@@ -44,7 +44,7 @@
</div> </div>
<div class="config-section"> <div class="config-section">
<div class="config-title">Save / record dataset</div> <div class="config-title">Save / Record Dataset</div>
<div class="config-row"> <div class="config-row">
<input id="save-path" class="config-control" type="text" placeholder="Save path" aria-label="Save path" /> <input id="save-path" class="config-control" type="text" placeholder="Save path" aria-label="Save path" />
</div> </div>
@@ -55,20 +55,20 @@
<div class="config-row"> <div class="config-row">
<input id="record-count" class="config-control" type="number" min="1" step="1" <input id="record-count" class="config-control" type="number" min="1" step="1"
placeholder="Sweeps to record" aria-label="Number of sweeps to record" /> placeholder="Sweeps to record" aria-label="Number of sweeps to record" />
<button id="btn-start-recording" class="btn">Start + Record</button> <button id="btn-start-recording" class="btn">Start + Record to Disk</button>
</div> </div>
<div class="config-active" id="recording-status"></div> <div class="config-active" id="recording-status"></div>
</div> </div>
<div class="panel-head" id="settings-toggle"> <div class="panel-head" id="settings-toggle">
<span class="panel-title">Processor settings</span> <span class="panel-title">Processor Settings</span>
<span class="chevron" id="settings-chevron">▾</span> <span class="chevron" id="settings-chevron">▾</span>
</div> </div>
<div class="panel-body" id="settings-body"> <div class="panel-body" id="settings-body">
<div id="settings-fields" class="settings-fields"></div> <div id="settings-fields" class="settings-fields"></div>
<div class="settings-actions"> <div class="settings-actions">
<button id="btn-apply" class="btn primary">Apply</button> <button id="btn-apply" class="btn primary">Apply</button>
<button id="btn-reset-history" class="btn">Reset history</button> <button id="btn-reset-history" class="btn destructive">Clear Runtime History</button>
</div> </div>
<div id="settings-note" class="note"></div> <div id="settings-note" class="note"></div>
</div> </div>
+7 -6
View File
@@ -78,6 +78,7 @@ body {
color: #ffffff; color: #ffffff;
} }
.btn.primary:hover:not(:disabled) { background: var(--accent-hover); } .btn.primary:hover:not(:disabled) { background: var(--accent-hover); }
.btn.destructive { color: #a73545; border-color: #cf9ba4; }
.btn:disabled { .btn:disabled {
color: var(--disabled-text); color: var(--disabled-text);
background: var(--disabled-bg); background: var(--disabled-bg);
@@ -125,6 +126,7 @@ body {
overflow: hidden; overflow: hidden;
} }
#plot { display: block; width: 100%; height: 100%; object-fit: contain; } #plot { display: block; width: 100%; height: 100%; object-fit: contain; }
#plot:not([src]) { display: none; }
/* Side panel */ /* Side panel */
.side-panel { .side-panel {
@@ -154,7 +156,7 @@ body {
border-radius: 7px; border-radius: 7px;
padding: 5px 7px; padding: 5px 7px;
color: var(--text); color: var(--text);
font-family: var(--mono); font-family: inherit;
font-size: 12px; font-size: 12px;
} }
.config-control:focus { outline: none; border-color: var(--accent); } .config-control:focus { outline: none; border-color: var(--accent); }
@@ -197,8 +199,7 @@ body {
.group-title { .group-title {
margin: 12px 0 6px; margin: 12px 0 6px;
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: capitalize;
letter-spacing: 0.6px;
color: var(--muted); color: var(--muted);
font-weight: 700; font-weight: 700;
} }
@@ -227,7 +228,7 @@ body {
border-radius: 7px; border-radius: 7px;
padding: 4px 7px; padding: 4px 7px;
color: var(--text); color: var(--text);
font-family: var(--mono); font-family: inherit;
font-size: 12px; font-size: 12px;
} }
.field input:focus, .field input:focus,
@@ -251,7 +252,7 @@ body {
border-radius: 7px; border-radius: 7px;
padding: 4px 7px; padding: 4px 7px;
color: var(--text); color: var(--text);
font-family: var(--mono); font-family: inherit;
font-size: 12px; font-size: 12px;
} }
.field textarea:focus { outline: none; border-color: var(--accent); } .field textarea:focus { outline: none; border-color: var(--accent); }
@@ -262,7 +263,7 @@ body {
padding: 10px 12px; padding: 10px 12px;
border-top: 1px solid var(--border-soft); border-top: 1px solid var(--border-soft);
} }
.settings-actions .btn { flex: 1; } .settings-actions .btn { flex: auto; white-space: nowrap; }
.note { .note {
padding: 0 12px 10px; padding: 0 12px 10px;
color: var(--muted); color: var(--muted);