haply 1.2.0

Haply Robotics Client Library for the Inverse Service
Documentation
//! Navigation module types - bubble navigation settings, state, status, and configure.

use serde::de;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use ts_rs::TS;

use super::base_types::Linear3D;
use super::primitives::{CenterMode, EasingType, SdfPrimitive};

// ============================================================
// Incoming state/status (from service -> client)
// ============================================================

/// Navigation state contributed to Inverse3 device state when navigation is active.
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct NavigationState {
    #[serde(default)]
    pub bubble: Option<BubbleNavigationState>,
}

/// Bubble navigation runtime state.
#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct BubbleNavigationState {
    #[serde(default)]
    pub center: Option<Linear3D>,
    #[serde(default)]
    pub velocity_zone_width: Option<f32>,
    #[serde(default)]
    pub collision_extra_inflate: Option<f32>,
}

/// Navigation status contributed to Inverse3 device status.
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct NavigationStatusInfo {
    #[serde(default)]
    pub mode: Option<String>,
}

/// Navigation config contributed in full snapshots at `inverse3[].config.navigation`.
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct NavigationConfigSnapshot {
    #[serde(default)]
    pub mode: Option<String>,
    #[serde(default)]
    pub bubble: Option<BubbleNavigationSettings>,
}

// ============================================================
// Full HTTP GET response (config + state + status combined)
// ============================================================

/// Combined response from `GET /{type}/{id}/config/navigation`.
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct NavigationFullResponse {
    #[serde(default)]
    pub config: Option<BubbleNavigationSettings>,
    #[serde(default)]
    pub state: Option<BubbleNavigationState>,
    #[serde(default)]
    pub status: Option<NavigationStatusInfo>,
}

// ============================================================
// Outgoing configure (client -> service)
// ============================================================

/// Outgoing navigation configure command.
///
/// Set `mode` to `"bubble"` to start/update, or `"disabled"` to stop.
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct NavigationConfigure {
    pub mode: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bubble: Option<BubbleNavigationSettings>,
}

// ============================================================
// Full bubble navigation settings
// ============================================================

/// Bubble center settings (3.5 schema).
#[derive(Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct BubbleCenterSettings {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position: Option<Linear3D>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub relative: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub follow: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub speed: Option<f32>,
}

/// Collision detection settings (3.5 schema).
#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize, Serialize, TS)]
pub struct CollisionDetectionSettings {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub force_threshold: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub inflate_ratio: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exit_ratio: Option<f32>,
}

/// Complete bubble navigation settings.
/// All fields are `Option` - only set fields are serialized/applied; absent fields keep defaults.
#[derive(Clone, Debug, PartialEq, Default, Serialize, TS)]
pub struct BubbleNavigationSettings {
    /// Bubble center behavior and placement.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub center: Option<BubbleCenterSettings>,

    /// SDF shape defining the dead zone (default: sphere r=0.05).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shape: Option<SdfPrimitive>,

    // --- Velocity ---

    /// Width of the rate-control shell in meters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub velocity_zone_width: Option<f32>,

    /// Maximum navigation velocity in m/s.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_velocity: Option<f32>,

    /// Easing curve for velocity ramp.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub velocity_ease: Option<EasingType>,

    /// Zero entry speed when entering velocity zone.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reset_velocity_on_entry: Option<bool>,

    // --- Bump ---

    /// Tactile bump width at surface in meters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bump_width: Option<f32>,

    /// Bump spring stiffness.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bump_stiffness: Option<f32>,

    // --- Spring gains ---

    /// Spring gain at bubble center.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub spring_inner: Option<f32>,

    /// Spring gain at bubble surface d=0.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub spring_surface: Option<f32>,

    /// Spring gain at outer boundary d=W.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub spring_outer: Option<f32>,

    /// Hard wall spring stiffness.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub wall_stiffness: Option<f32>,

    // --- Damping gains ---

    /// Damping at bubble center.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub damping_inner: Option<f32>,

    /// Damping at bubble surface.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub damping_surface: Option<f32>,

    /// Damping at outer boundary.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub damping_outer: Option<f32>,

    // --- Rotation / Scale ---

    /// Apply workspace rotation to navigation direction.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rotation_enabled: Option<bool>,

    /// Apply workspace scale to navigation velocity.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scale_enabled: Option<bool>,

    // --- Collision ---

    /// Collision behavior for bubble inflation and blocking.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub collision_detection: Option<CollisionDetectionSettings>,
}

#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
struct BubbleNavigationSettingsSerde {
    #[serde(default)]
    center: Option<Value>,
    #[serde(default)]
    shape: Option<SdfPrimitive>,
    #[serde(default)]
    velocity_zone_width: Option<f32>,
    #[serde(default)]
    max_velocity: Option<f32>,
    #[serde(default)]
    velocity_ease: Option<EasingType>,
    #[serde(default)]
    reset_velocity_on_entry: Option<bool>,
    #[serde(default)]
    bump_width: Option<f32>,
    #[serde(default)]
    bump_stiffness: Option<f32>,
    #[serde(default)]
    spring_inner: Option<f32>,
    #[serde(default)]
    spring_surface: Option<f32>,
    #[serde(default)]
    spring_outer: Option<f32>,
    #[serde(default)]
    wall_stiffness: Option<f32>,
    #[serde(default)]
    damping_inner: Option<f32>,
    #[serde(default)]
    damping_surface: Option<f32>,
    #[serde(default)]
    damping_outer: Option<f32>,
    #[serde(default)]
    rotation_enabled: Option<bool>,
    #[serde(default)]
    scale_enabled: Option<bool>,
    #[serde(default)]
    collision_detection: Option<CollisionDetectionSettings>,

    // Legacy compatibility inputs (deserialize-only).
    #[serde(default)]
    center_mode: Option<CenterMode>,
    #[serde(default)]
    center_drift_speed: Option<f32>,
    #[serde(default)]
    stop_at_collision: Option<bool>,
    #[serde(default)]
    collision_threshold: Option<f32>,
    #[serde(default)]
    collision_inflate_scale: Option<f32>,
}

fn parse_center_compat(value: Option<Value>) -> Result<Option<BubbleCenterSettings>, serde_json::Error> {
    let Some(center_value) = value else {
        return Ok(None);
    };

    if center_value.is_null() {
        return Ok(None);
    }

    match center_value {
        Value::Object(map) => {
            let has_new_center_shape =
                map.contains_key("position") ||
                map.contains_key("relative") ||
                map.contains_key("follow") ||
                map.contains_key("speed");
            let has_legacy_linear_shape =
                map.contains_key("x") &&
                map.contains_key("y") &&
                map.contains_key("z");

            if has_new_center_shape {
                let parsed: BubbleCenterSettings = serde_json::from_value(Value::Object(map))?;
                Ok(Some(parsed))
            } else if has_legacy_linear_shape {
                let pos: Linear3D = serde_json::from_value(Value::Object(map))?;
                Ok(Some(BubbleCenterSettings {
                    position: Some(pos),
                    ..Default::default()
                }))
            } else {
                let parsed: BubbleCenterSettings = serde_json::from_value(Value::Object(map))?;
                Ok(Some(parsed))
            }
        }
        other => {
            let pos: Linear3D = serde_json::from_value(other)?;
            Ok(Some(BubbleCenterSettings {
                position: Some(pos),
                ..Default::default()
            }))
        }
    }
}

impl<'de> Deserialize<'de> for BubbleNavigationSettings {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let helper = BubbleNavigationSettingsSerde::deserialize(deserializer)?;

        let mut center = parse_center_compat(helper.center).map_err(de::Error::custom)?;

        if let Some(legacy_mode) = helper.center_mode {
            let center_cfg = center.get_or_insert_with(Default::default);
            match legacy_mode {
                CenterMode::AutoFollow => {
                    if center_cfg.follow.is_none() {
                        center_cfg.follow = Some(true);
                    }
                    if center_cfg.speed.is_none() {
                        center_cfg.speed = helper.center_drift_speed;
                    }
                }
                CenterMode::Fixed => {
                    if center_cfg.follow.is_none() {
                        center_cfg.follow = Some(false);
                    }
                }
                CenterMode::TrackCursor => {
                    if center_cfg.follow.is_none() {
                        center_cfg.follow = Some(true);
                    }
                    if center_cfg.speed.is_none() {
                        center_cfg.speed = Some(0.0);
                    }
                }
            }
        }

        if helper.center_drift_speed.is_some() {
            let center_cfg = center.get_or_insert_with(Default::default);
            if center_cfg.speed.is_none() {
                center_cfg.speed = helper.center_drift_speed;
            }
        }

        let mut collision_detection = helper.collision_detection;
        if
            helper.stop_at_collision.is_some() ||
            helper.collision_threshold.is_some() ||
            helper.collision_inflate_scale.is_some()
        {
            let collision_cfg = collision_detection.get_or_insert_with(Default::default);
            if collision_cfg.enabled.is_none() {
                collision_cfg.enabled = helper.stop_at_collision;
            }
            if collision_cfg.force_threshold.is_none() {
                collision_cfg.force_threshold = helper.collision_threshold;
            }
            if collision_cfg.inflate_ratio.is_none() {
                collision_cfg.inflate_ratio = helper.collision_inflate_scale;
            }
        }

        Ok(BubbleNavigationSettings {
            center,
            shape: helper.shape,
            velocity_zone_width: helper.velocity_zone_width,
            max_velocity: helper.max_velocity,
            velocity_ease: helper.velocity_ease,
            reset_velocity_on_entry: helper.reset_velocity_on_entry,
            bump_width: helper.bump_width,
            bump_stiffness: helper.bump_stiffness,
            spring_inner: helper.spring_inner,
            spring_surface: helper.spring_surface,
            spring_outer: helper.spring_outer,
            wall_stiffness: helper.wall_stiffness,
            damping_inner: helper.damping_inner,
            damping_surface: helper.damping_surface,
            damping_outer: helper.damping_outer,
            rotation_enabled: helper.rotation_enabled,
            scale_enabled: helper.scale_enabled,
            collision_detection,
        })
    }
}