use anyhow::Result;
use indexmap::IndexMap;
use kittycad_modeling_cmds::units::UnitLength;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use validator::Validate;
use crate::settings::types::DefaultTrue;
use crate::settings::types::OnboardingStatus;
use crate::settings::types::ProjectCommandBarSettings;
use crate::settings::types::ProjectTextEditorSettings;
use crate::settings::types::is_default;
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectConfiguration {
#[serde(default)]
#[validate(nested)]
pub settings: PerProjectSettings,
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub cloud: ProjectCloudSettings,
}
impl ProjectConfiguration {
pub fn parse_and_validate(toml_str: &str) -> Result<Self> {
let settings = toml::from_str::<Self>(toml_str)?;
settings.validate()?;
Ok(settings)
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct PerProjectSettings {
#[serde(default)]
#[validate(nested)]
pub meta: ProjectMetaSettings,
#[serde(default)]
#[validate(nested)]
pub app: ProjectAppSettings,
#[serde(default)]
#[validate(nested)]
pub modeling: ProjectModelingSettings,
#[serde(default)]
#[validate(nested)]
pub text_editor: ProjectTextEditorSettings,
#[serde(default)]
#[validate(nested)]
pub command_bar: ProjectCommandBarSettings,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectMetaSettings {
#[serde(default, skip_serializing_if = "is_default")]
pub id: uuid::Uuid,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectCloudSettings {
#[serde(flatten, default, skip_serializing_if = "IndexMap::is_empty")]
pub environments: IndexMap<String, ProjectCloudEnvironmentSettings>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectCloudEnvironmentSettings {
#[serde(default, skip_serializing_if = "is_default")]
pub project_id: uuid::Uuid,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectAppSettings {
#[serde(default, skip_serializing_if = "is_default")]
pub onboarding_status: OnboardingStatus,
#[serde(default, skip_serializing_if = "is_default")]
pub stream_idle_mode: bool,
#[serde(default, skip_serializing_if = "is_default")]
pub allow_orbit_in_sketch_mode: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_debug_panel: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub zookeeper_mode: Option<String>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub named_views: IndexMap<uuid::Uuid, NamedView>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct ProjectModelingSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_unit: Option<UnitLength>,
#[serde(default, skip_serializing_if = "is_default")]
pub highlight_edges: DefaultTrue,
#[serde(default, skip_serializing_if = "is_default")]
pub enable_ssao: DefaultTrue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fixed_size_grid: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snap_to_grid: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub major_grid_spacing: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minor_grids_per_major: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snaps_per_minor: Option<f64>,
}
fn named_view_point_version_one() -> f64 {
1.0
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct NamedView {
#[serde(default)]
pub name: String,
#[serde(default)]
pub eye_offset: f64,
#[serde(default)]
pub fov_y: f64,
#[serde(default)]
pub is_ortho: bool,
#[serde(default)]
pub ortho_scale_enabled: bool,
#[serde(default)]
pub ortho_scale_factor: f64,
#[serde(default)]
pub pivot_position: [f64; 3],
#[serde(default)]
pub pivot_rotation: [f64; 4],
#[serde(default)]
pub world_coord_system: String,
#[serde(default = "named_view_point_version_one")]
pub version: f64,
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use pretty_assertions::assert_eq;
use serde_json::Value;
use super::NamedView;
use super::PerProjectSettings;
use super::ProjectAppSettings;
use super::ProjectCloudEnvironmentSettings;
use super::ProjectCloudSettings;
use super::ProjectCommandBarSettings;
use super::ProjectConfiguration;
use super::ProjectMetaSettings;
use super::ProjectModelingSettings;
use super::ProjectTextEditorSettings;
use crate::settings::types::UnitLength;
#[test]
fn test_project_settings_empty_file_parses() {
let empty_settings_file = r#""#;
let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
assert_eq!(parsed, ProjectConfiguration::default());
let serialized = toml::to_string(&parsed).unwrap();
assert_eq!(
serialized,
r#"[settings.meta]
[settings.app]
[settings.modeling]
[settings.text_editor]
[settings.command_bar]
"#
);
let parsed = ProjectConfiguration::parse_and_validate(empty_settings_file).unwrap();
assert_eq!(parsed, ProjectConfiguration::default());
}
#[test]
fn named_view_serde_json() {
let json = r#"
[
{
"name":"dog",
"pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
"pivot_position":[0.5,0,0.5],
"eye_offset":231.52048,
"fov_y":45,
"ortho_scale_factor":1.574129,
"is_ortho":true,
"ortho_scale_enabled":true,
"world_coord_system":"RightHandedUpZ"
}
]
"#;
let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
let version = named_views[0].version;
assert_eq!(version, 1.0);
}
#[test]
fn named_view_serde_json_string() {
let json = r#"
[
{
"name":"dog",
"pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
"pivot_position":[0.5,0,0.5],
"eye_offset":231.52048,
"fov_y":45,
"ortho_scale_factor":1.574129,
"is_ortho":true,
"ortho_scale_enabled":true,
"world_coord_system":"RightHandedUpZ"
}
]
"#;
let named_views: Value = match serde_json::from_str(json) {
Ok(x) => x,
Err(_) => return,
};
println!("{}", named_views);
}
#[test]
fn test_project_settings_named_views() {
let conf = ProjectConfiguration {
settings: PerProjectSettings {
meta: ProjectMetaSettings { id: uuid::Uuid::nil() },
app: ProjectAppSettings {
onboarding_status: Default::default(),
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
show_debug_panel: Some(true),
zookeeper_mode: None,
named_views: IndexMap::from([
(
uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
NamedView {
name: String::from("Hello"),
eye_offset: 1236.4015,
fov_y: 45.0,
is_ortho: false,
ortho_scale_enabled: false,
ortho_scale_factor: 45.0,
pivot_position: [-100.0, 100.0, 100.0],
pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
world_coord_system: String::from("RightHandedUpZ"),
version: 1.0,
},
),
(
uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
NamedView {
name: String::from("Goodbye"),
eye_offset: 1236.4015,
fov_y: 45.0,
is_ortho: false,
ortho_scale_enabled: false,
ortho_scale_factor: 45.0,
pivot_position: [-100.0, 100.0, 100.0],
pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
world_coord_system: String::from("RightHandedUpZ"),
version: 1.0,
},
),
]),
},
modeling: ProjectModelingSettings {
base_unit: Some(UnitLength::Yards),
highlight_edges: Default::default(),
enable_ssao: true.into(),
snap_to_grid: None,
major_grid_spacing: None,
minor_grids_per_major: None,
snaps_per_minor: None,
fixed_size_grid: None,
},
text_editor: ProjectTextEditorSettings {
text_wrapping: Some(false),
blinking_cursor: Some(false),
},
command_bar: ProjectCommandBarSettings {
include_settings: Some(false),
},
},
cloud: ProjectCloudSettings::default(),
};
let serialized = toml::to_string(&conf).unwrap();
let old_project_file = r#"[settings.meta]
[settings.app]
show_debug_panel = true
[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
name = "Hello"
eye_offset = 1236.4015
fov_y = 45.0
is_ortho = false
ortho_scale_enabled = false
ortho_scale_factor = 45.0
pivot_position = [-100.0, 100.0, 100.0]
pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
world_coord_system = "RightHandedUpZ"
version = 1.0
[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
name = "Goodbye"
eye_offset = 1236.4015
fov_y = 45.0
is_ortho = false
ortho_scale_enabled = false
ortho_scale_factor = 45.0
pivot_position = [-100.0, 100.0, 100.0]
pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
world_coord_system = "RightHandedUpZ"
version = 1.0
[settings.modeling]
base_unit = "yd"
[settings.text_editor]
text_wrapping = false
blinking_cursor = false
[settings.command_bar]
include_settings = false
"#;
assert_eq!(serialized, old_project_file)
}
#[test]
fn test_project_settings_cloud_metadata_round_trip() {
let local_project_id = uuid::uuid!("e8f5178c-5227-4567-bb5a-f52b3caef5ea");
let zoo_cloud_project_id = uuid::uuid!("04c988e3-ec37-48a4-b491-45c3668934f1");
let dev_cloud_project_id = uuid::uuid!("e9632dae-19ca-49ea-bcc1-ee8e34ff9de3");
let conf = ProjectConfiguration {
settings: PerProjectSettings {
meta: ProjectMetaSettings { id: local_project_id },
..Default::default()
},
cloud: ProjectCloudSettings {
environments: IndexMap::from([
(
"zoo.dev".to_owned(),
ProjectCloudEnvironmentSettings {
project_id: zoo_cloud_project_id,
},
),
(
"dev.zoo.dev".to_owned(),
ProjectCloudEnvironmentSettings {
project_id: dev_cloud_project_id,
},
),
]),
},
};
let serialized = toml::to_string(&conf).unwrap();
assert!(serialized.contains(&format!(
"[cloud.\"zoo.dev\"]\nproject_id = \"{zoo_cloud_project_id}\"\n"
)));
assert!(serialized.contains(&format!(
"[cloud.\"dev.zoo.dev\"]\nproject_id = \"{dev_cloud_project_id}\"\n"
)));
assert!(serialized.contains(&format!("[settings.meta]\nid = \"{local_project_id}\"\n")));
let parsed = ProjectConfiguration::parse_and_validate(&serialized).unwrap();
assert_eq!(parsed, conf);
}
}