"""Dataclass schema for full GUI config profiles.""" from __future__ import annotations from copy import deepcopy from dataclasses import dataclass, field import json from pathlib import Path from typing import Any from python_app.models.run_config_model import RunConfigModel @dataclass(slots=True) class GuiSwitchStateModel: """UI-only state for switch selection controls.""" combo_mode: str = "text" combos_text: str = "" single_input: str = "0" single_output: str = "0" @dataclass(slots=True) class GuiPassThroughStateModel: """UI-only defaults for pass-through rendering.""" show_magnitude: bool = True show_phase: bool = True combo_filter: str = "" fixed_y_enabled: bool = False y_min_db: float = -100.0 y_max_db: float = 0.0 @dataclass(slots=True) class GuiBscanStateModel: """UI-only defaults for B-scan live settings.""" axis: str = "abs" cut_m: float = 0.824 max_depth_m: float = 1.0 gain: float = 1.0 start_freq_mhz: float = 100.0 stop_freq_mhz: float = 8800.0 subtract_mean_ascan: bool = False @dataclass(slots=True) class GuiGprStateModel: """UI-only defaults for coherent backprojection GPR live settings.""" input_positions: str = "" output_positions: str = "" min_depth_m: float = 2.0 max_depth_m: float = 14.0 range_comp_power: float = 0.28 angle_comp_power: float = 0.10 score_mode: str = "combined" max_detected_objects_to_draw: int = 5 draw_top_m_objects: int = 2 start_freq_mhz: float = 3000.0 stop_freq_mhz: float = 6000.0 background_subtract_enabled: bool = True background_mean_count: int = 10 remove_sidelobe_objects_enabled: bool = True imaging_plane_y_m: float = 0.0 render_mode: str = "heatmap" min_visible_score: float = 0.0 visible_x_min_m: float = -2.0 visible_x_max_m: float = 2.0 visible_z_min_m: float = 0.0 visible_z_max_m: float = 14.0 @dataclass(slots=True) class GuiLegacyGprStateModel: """UI-only defaults for legacy point/extended GPR live settings.""" mode: str = "point" input_positions: str = "" output_positions: str = "" min_depth_m: float = 2.0 max_depth_m: float = 14.0 comp_power: float = 0.2 start_freq_mhz: float = 3000.0 stop_freq_mhz: float = 6000.0 speed_m_s: float = 0.0 ignore_socket_speed_enabled: bool = False look_angle_deg: float = 0.0 apply_freq_phase_correction: bool = True reference_mode: str = "frame_center" snr_thresh: float = 4.5 snr_comp_max: float = 25.0 background_subtract_enabled: bool = True background_mean_count: int = 10 render_mode: str = "heatmap" min_visible_pair_count: int = 1 visible_x_min_m: float = -2.0 visible_x_max_m: float = 2.0 visible_z_min_m: float = 0.0 visible_z_max_m: float = 14.0 @dataclass(slots=True) class GuiProcessingStateModel: """UI-only processing-section defaults.""" selected_mode: str = "pass_through" pass_through: GuiPassThroughStateModel = field(default_factory=GuiPassThroughStateModel) bscan: GuiBscanStateModel = field(default_factory=GuiBscanStateModel) gpr: GuiGprStateModel = field(default_factory=GuiGprStateModel) legacy_gpr: GuiLegacyGprStateModel = field(default_factory=GuiLegacyGprStateModel) @dataclass(slots=True) class GuiDataActionsStateModel: """UI-only defaults for snapshot/export controls.""" save_count: int = 10 save_path: str = "" save_name: str = "snapshot_manual" @dataclass(slots=True) class GuiPreprocessDialogStateModel: """UI-only defaults for preprocessing dialog controls.""" set_name: str = "set_001" radar_config_dir: str = "" use_all_radar_configs: bool = False median_sweep_count: int = 5 @dataclass(slots=True) class GuiStateModel: """GUI-only profile payload stored under top-level `gui`.""" version: int = 1 switches: GuiSwitchStateModel = field(default_factory=GuiSwitchStateModel) processing: GuiProcessingStateModel = field(default_factory=GuiProcessingStateModel) data_actions: GuiDataActionsStateModel = field(default_factory=GuiDataActionsStateModel) preprocess_dialog: GuiPreprocessDialogStateModel = field(default_factory=GuiPreprocessDialogStateModel) @dataclass(slots=True) class GuiProfileModel: """Full GUI profile with pipeline-compatible root config and optional UI state.""" run_config: RunConfigModel = field(default_factory=RunConfigModel) gui: GuiStateModel | None = None @staticmethod def from_dict(payload: dict[str, Any]) -> GuiProfileModel: """Build profile from JSON-like payload using codec layer.""" from python_app.models.gui_profile_codec import gui_profile_from_dict return gui_profile_from_dict(payload) @classmethod def load_from_path(cls, path: Path) -> GuiProfileModel: """Load JSON file from disk and decode into profile model.""" payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError(f"Config profile root must be JSON object: {path}") return cls.from_dict(payload) def clone(self) -> GuiProfileModel: """Create deep copy while preserving whether `gui` is absent.""" return deepcopy(self) def to_dict(self) -> dict[str, Any]: """Encode profile into JSON-serializable dictionary.""" from python_app.models.gui_profile_codec import gui_profile_to_dict return gui_profile_to_dict(self)