1use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds::units::UnitLength;
6use schemars::JsonSchema;
7use serde::Deserialize;
8use serde::Serialize;
9use validator::Validate;
10
11use crate::settings::types::DefaultTrue;
12use crate::settings::types::OnboardingStatus;
13use crate::settings::types::ProjectCommandBarSettings;
14use crate::settings::types::ProjectTextEditorSettings;
15use crate::settings::types::is_default;
16
17#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
23#[ts(export)]
24#[serde(rename_all = "snake_case")]
25pub struct ProjectConfiguration {
26 #[serde(default)]
28 #[validate(nested)]
29 pub settings: PerProjectSettings,
30}
31
32impl ProjectConfiguration {
33 pub fn parse_and_validate(toml_str: &str) -> Result<Self> {
35 let settings = toml::from_str::<Self>(toml_str)?;
36
37 settings.validate()?;
38
39 Ok(settings)
40 }
41}
42
43#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
45#[ts(export)]
46#[serde(rename_all = "snake_case")]
47pub struct PerProjectSettings {
48 #[serde(default)]
52 #[validate(nested)]
53 pub meta: ProjectMetaSettings,
54
55 #[serde(default)]
57 #[validate(nested)]
58 pub app: ProjectAppSettings,
59 #[serde(default)]
61 #[validate(nested)]
62 pub modeling: ProjectModelingSettings,
63 #[serde(default)]
65 #[validate(nested)]
66 pub text_editor: ProjectTextEditorSettings,
67 #[serde(default)]
69 #[validate(nested)]
70 pub command_bar: ProjectCommandBarSettings,
71}
72
73#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
75#[ts(export)]
76#[serde(rename_all = "snake_case")]
77pub struct ProjectMetaSettings {
78 #[serde(default, skip_serializing_if = "is_default")]
79 pub id: uuid::Uuid,
80}
81
82#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
86#[ts(export)]
87#[serde(rename_all = "snake_case")]
88pub struct ProjectAppSettings {
89 #[serde(default, skip_serializing_if = "is_default")]
91 pub onboarding_status: OnboardingStatus,
92 #[serde(default, skip_serializing_if = "is_default")]
94 pub stream_idle_mode: bool,
95 #[serde(default, skip_serializing_if = "is_default")]
97 pub allow_orbit_in_sketch_mode: bool,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub show_debug_panel: Option<bool>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub zookeeper_mode: Option<String>,
105 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
107 pub named_views: IndexMap<uuid::Uuid, NamedView>,
108}
109
110#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
112#[serde(rename_all = "snake_case")]
113#[ts(export)]
114pub struct ProjectModelingSettings {
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub base_unit: Option<UnitLength>,
118 #[serde(default, skip_serializing_if = "is_default")]
120 pub highlight_edges: DefaultTrue,
121 #[serde(default, skip_serializing_if = "is_default")]
123 pub enable_ssao: DefaultTrue,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub fixed_size_grid: Option<bool>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub snap_to_grid: Option<bool>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub major_grid_spacing: Option<f64>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub minor_grids_per_major: Option<f64>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub snaps_per_minor: Option<f64>,
141}
142
143fn named_view_point_version_one() -> f64 {
144 1.0
145}
146
147#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
148#[serde(rename_all = "snake_case")]
149#[ts(export)]
150pub struct NamedView {
151 #[serde(default)]
153 pub name: String,
154 #[serde(default)]
156 pub eye_offset: f64,
157 #[serde(default)]
159 pub fov_y: f64,
160 #[serde(default)]
162 pub is_ortho: bool,
163 #[serde(default)]
165 pub ortho_scale_enabled: bool,
166 #[serde(default)]
168 pub ortho_scale_factor: f64,
169 #[serde(default)]
171 pub pivot_position: [f64; 3],
172 #[serde(default)]
174 pub pivot_rotation: [f64; 4],
175 #[serde(default)]
177 pub world_coord_system: String,
178 #[serde(default = "named_view_point_version_one")]
180 pub version: f64,
181}
182
183#[cfg(test)]
184mod tests {
185 use indexmap::IndexMap;
186 use pretty_assertions::assert_eq;
187 use serde_json::Value;
188
189 use super::NamedView;
190 use super::PerProjectSettings;
191 use super::ProjectAppSettings;
192 use super::ProjectCommandBarSettings;
193 use super::ProjectConfiguration;
194 use super::ProjectMetaSettings;
195 use super::ProjectModelingSettings;
196 use super::ProjectTextEditorSettings;
197 use crate::settings::types::UnitLength;
198
199 #[test]
200 fn test_project_settings_empty_file_parses() {
201 let empty_settings_file = r#""#;
202
203 let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
204 assert_eq!(parsed, ProjectConfiguration::default());
205
206 let serialized = toml::to_string(&parsed).unwrap();
208 assert_eq!(
209 serialized,
210 r#"[settings.meta]
211
212[settings.app]
213
214[settings.modeling]
215
216[settings.text_editor]
217
218[settings.command_bar]
219"#
220 );
221
222 let parsed = ProjectConfiguration::parse_and_validate(empty_settings_file).unwrap();
223 assert_eq!(parsed, ProjectConfiguration::default());
224 }
225
226 #[test]
227 fn named_view_serde_json() {
228 let json = r#"
229 [
230 {
231 "name":"dog",
232 "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
233 "pivot_position":[0.5,0,0.5],
234 "eye_offset":231.52048,
235 "fov_y":45,
236 "ortho_scale_factor":1.574129,
237 "is_ortho":true,
238 "ortho_scale_enabled":true,
239 "world_coord_system":"RightHandedUpZ"
240 }
241 ]
242 "#;
243 let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
245 let version = named_views[0].version;
246 assert_eq!(version, 1.0);
247 }
248
249 #[test]
250 fn named_view_serde_json_string() {
251 let json = r#"
252 [
253 {
254 "name":"dog",
255 "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
256 "pivot_position":[0.5,0,0.5],
257 "eye_offset":231.52048,
258 "fov_y":45,
259 "ortho_scale_factor":1.574129,
260 "is_ortho":true,
261 "ortho_scale_enabled":true,
262 "world_coord_system":"RightHandedUpZ"
263 }
264 ]
265 "#;
266
267 let named_views: Value = match serde_json::from_str(json) {
269 Ok(x) => x,
270 Err(_) => return,
271 };
272 println!("{}", named_views);
273 }
274
275 #[test]
276 fn test_project_settings_named_views() {
277 let conf = ProjectConfiguration {
278 settings: PerProjectSettings {
279 meta: ProjectMetaSettings { id: uuid::Uuid::nil() },
280 app: ProjectAppSettings {
281 onboarding_status: Default::default(),
282 stream_idle_mode: false,
283 allow_orbit_in_sketch_mode: false,
284 show_debug_panel: Some(true),
285 zookeeper_mode: None,
286 named_views: IndexMap::from([
287 (
288 uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
289 NamedView {
290 name: String::from("Hello"),
291 eye_offset: 1236.4015,
292 fov_y: 45.0,
293 is_ortho: false,
294 ortho_scale_enabled: false,
295 ortho_scale_factor: 45.0,
296 pivot_position: [-100.0, 100.0, 100.0],
297 pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
298 world_coord_system: String::from("RightHandedUpZ"),
299 version: 1.0,
300 },
301 ),
302 (
303 uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
304 NamedView {
305 name: String::from("Goodbye"),
306 eye_offset: 1236.4015,
307 fov_y: 45.0,
308 is_ortho: false,
309 ortho_scale_enabled: false,
310 ortho_scale_factor: 45.0,
311 pivot_position: [-100.0, 100.0, 100.0],
312 pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
313 world_coord_system: String::from("RightHandedUpZ"),
314 version: 1.0,
315 },
316 ),
317 ]),
318 },
319 modeling: ProjectModelingSettings {
320 base_unit: Some(UnitLength::Yards),
321 highlight_edges: Default::default(),
322 enable_ssao: true.into(),
323 snap_to_grid: None,
324 major_grid_spacing: None,
325 minor_grids_per_major: None,
326 snaps_per_minor: None,
327 fixed_size_grid: None,
328 },
329 text_editor: ProjectTextEditorSettings {
330 text_wrapping: Some(false),
331 blinking_cursor: Some(false),
332 },
333 command_bar: ProjectCommandBarSettings {
334 include_settings: Some(false),
335 },
336 },
337 };
338 let serialized = toml::to_string(&conf).unwrap();
339 let old_project_file = r#"[settings.meta]
340
341[settings.app]
342show_debug_panel = true
343
344[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
345name = "Hello"
346eye_offset = 1236.4015
347fov_y = 45.0
348is_ortho = false
349ortho_scale_enabled = false
350ortho_scale_factor = 45.0
351pivot_position = [-100.0, 100.0, 100.0]
352pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
353world_coord_system = "RightHandedUpZ"
354version = 1.0
355
356[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
357name = "Goodbye"
358eye_offset = 1236.4015
359fov_y = 45.0
360is_ortho = false
361ortho_scale_enabled = false
362ortho_scale_factor = 45.0
363pivot_position = [-100.0, 100.0, 100.0]
364pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
365world_coord_system = "RightHandedUpZ"
366version = 1.0
367
368[settings.modeling]
369base_unit = "yd"
370
371[settings.text_editor]
372text_wrapping = false
373blinking_cursor = false
374
375[settings.command_bar]
376include_settings = false
377"#;
378
379 assert_eq!(serialized, old_project_file)
380 }
381}