1use anyhow::Result;
4use indexmap::IndexMap;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::settings::types::{
10 is_default, AppColor, CommandBarSettings, DefaultTrue, FloatOrInt, OnboardingStatus, TextEditorSettings, UnitLength,
11};
12
13#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
19#[ts(export)]
20#[serde(rename_all = "snake_case")]
21pub struct ProjectConfiguration {
22 #[serde(default)]
24 #[validate(nested)]
25 pub settings: PerProjectSettings,
26}
27
28impl ProjectConfiguration {
29 pub fn backwards_compatible_toml_parse(toml_str: &str) -> Result<Self> {
31 let mut settings = toml::from_str::<Self>(toml_str)?;
32
33 if let Some(theme_color) = &settings.settings.app.theme_color {
34 if settings.settings.app.appearance.color == AppColor::default() {
35 settings.settings.app.appearance.color = theme_color.clone().into();
36 settings.settings.app.theme_color = None;
37 }
38 }
39
40 if let Some(enable_ssao) = settings.settings.app.enable_ssao {
41 if settings.settings.modeling.enable_ssao.into() {
42 settings.settings.modeling.enable_ssao = enable_ssao.into();
43 settings.settings.app.enable_ssao = None;
44 }
45 }
46
47 if settings.settings.modeling.show_debug_panel && !settings.settings.app.show_debug_panel {
48 settings.settings.app.show_debug_panel = settings.settings.modeling.show_debug_panel;
49 settings.settings.modeling.show_debug_panel = Default::default();
50 }
51
52 settings.validate()?;
53
54 Ok(settings)
55 }
56}
57
58#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
60#[ts(export)]
61#[serde(rename_all = "snake_case")]
62pub struct PerProjectSettings {
63 #[serde(default)]
65 #[validate(nested)]
66 pub app: ProjectAppSettings,
67 #[serde(default)]
69 #[validate(nested)]
70 pub modeling: ProjectModelingSettings,
71 #[serde(default, alias = "textEditor")]
73 #[validate(nested)]
74 pub text_editor: TextEditorSettings,
75 #[serde(default, alias = "commandBar")]
77 #[validate(nested)]
78 pub command_bar: CommandBarSettings,
79}
80
81#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
85#[ts(export)]
86#[serde(rename_all = "snake_case")]
87pub struct ProjectAppSettings {
88 #[serde(default, skip_serializing_if = "is_default")]
90 #[validate(nested)]
91 pub appearance: ProjectAppearanceSettings,
92 #[serde(default, alias = "onboardingStatus", skip_serializing_if = "is_default")]
94 pub onboarding_status: OnboardingStatus,
95 #[serde(default, skip_serializing_if = "Option::is_none", alias = "themeColor")]
97 #[ts(skip)]
98 pub theme_color: Option<FloatOrInt>,
99 #[serde(default, alias = "enableSSAO", skip_serializing_if = "Option::is_none")]
101 #[ts(skip)]
102 pub enable_ssao: Option<bool>,
103 #[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
106 pub dismiss_web_banner: bool,
107 #[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
109 pub stream_idle_mode: bool,
110 #[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
112 pub allow_orbit_in_sketch_mode: bool,
113 #[serde(default, alias = "showDebugPanel", skip_serializing_if = "is_default")]
116 pub show_debug_panel: bool,
117 #[serde(default, alias = "namedViews", skip_serializing_if = "IndexMap::is_empty")]
119 pub named_views: IndexMap<uuid::Uuid, NamedView>,
120}
121
122#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
124#[ts(export)]
125#[serde(rename_all = "snake_case")]
126pub struct ProjectAppearanceSettings {
127 #[serde(default, skip_serializing_if = "is_default")]
129 #[validate(nested)]
130 pub color: AppColor,
131}
132
133#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
135#[serde(rename_all = "snake_case")]
136#[ts(export)]
137pub struct ProjectModelingSettings {
138 #[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
140 pub base_unit: UnitLength,
141 #[serde(default, alias = "highlightEdges", skip_serializing_if = "is_default")]
143 pub highlight_edges: DefaultTrue,
144 #[serde(default, alias = "showDebugPanel", skip_serializing_if = "is_default")]
148 #[ts(skip)]
149 pub show_debug_panel: bool,
150 #[serde(default, skip_serializing_if = "is_default")]
152 pub enable_ssao: DefaultTrue,
153}
154
155fn named_view_point_version_one() -> f64 {
156 1.0
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
160#[serde(rename_all = "snake_case")]
161#[ts(export)]
162pub struct NamedView {
163 #[serde(default, alias = "name")]
165 pub name: String,
166 #[serde(default, alias = "eyeOffset")]
168 pub eye_offset: f64,
169 #[serde(default, alias = "fovY")]
171 pub fov_y: f64,
172 #[serde(default, alias = "isOrtho")]
174 pub is_ortho: bool,
175 #[serde(default, alias = "orthoScaleEnabled")]
177 pub ortho_scale_enabled: bool,
178 #[serde(default, alias = "orthoScaleFactor")]
180 pub ortho_scale_factor: f64,
181 #[serde(default, alias = "pivotPosition")]
183 pub pivot_position: [f64; 3],
184 #[serde(default, alias = "pivotRotation")]
186 pub pivot_rotation: [f64; 4],
187 #[serde(default, alias = "worldCoordSystem")]
189 pub world_coord_system: String,
190 #[serde(default = "named_view_point_version_one")]
192 pub version: f64,
193}
194
195#[cfg(test)]
196mod tests {
197 use indexmap::IndexMap;
198 use pretty_assertions::assert_eq;
199 use serde_json::Value;
200
201 use super::{
202 CommandBarSettings, NamedView, PerProjectSettings, ProjectAppSettings, ProjectAppearanceSettings,
203 ProjectConfiguration, ProjectModelingSettings, TextEditorSettings,
204 };
205 use crate::settings::types::UnitLength;
206
207 #[test]
208 fn test_backwards_compatible_project_settings_file() {
211 let old_project_file = r#"[settings.app]
212themeColor = "138"
213
214[settings.textEditor]
215textWrapping = false
216blinkingCursor = false
217
218[settings.modeling]
219showDebugPanel = true
220
221[settings.commandBar]
222includeSettings = false
223#"#;
224
225 let parsed = ProjectConfiguration::backwards_compatible_toml_parse(old_project_file).unwrap();
227 assert_eq!(
228 parsed,
229 ProjectConfiguration {
230 settings: PerProjectSettings {
231 app: ProjectAppSettings {
232 appearance: ProjectAppearanceSettings { color: 138.0.into() },
233 onboarding_status: Default::default(),
234 theme_color: None,
235 dismiss_web_banner: false,
236 enable_ssao: None,
237 stream_idle_mode: false,
238 allow_orbit_in_sketch_mode: false,
239 show_debug_panel: true,
240 named_views: IndexMap::default()
241 },
242 modeling: ProjectModelingSettings {
243 base_unit: UnitLength::Mm,
244 highlight_edges: Default::default(),
245 show_debug_panel: Default::default(),
246 enable_ssao: true.into(),
247 },
248 text_editor: TextEditorSettings {
249 text_wrapping: false.into(),
250 blinking_cursor: false.into()
251 },
252 command_bar: CommandBarSettings {
253 include_settings: false.into()
254 },
255 }
256 }
257 );
258
259 let serialized = toml::to_string(&parsed).unwrap();
261 assert_eq!(
262 serialized,
263 r#"[settings.app]
264show_debug_panel = true
265
266[settings.app.appearance]
267color = 138.0
268
269[settings.modeling]
270
271[settings.text_editor]
272text_wrapping = false
273blinking_cursor = false
274
275[settings.command_bar]
276include_settings = false
277"#
278 );
279 }
280
281 #[test]
282 fn test_project_settings_empty_file_parses() {
283 let empty_settings_file = r#""#;
284
285 let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
286 assert_eq!(parsed, ProjectConfiguration::default());
287
288 let serialized = toml::to_string(&parsed).unwrap();
290 assert_eq!(
291 serialized,
292 r#"[settings.app]
293
294[settings.modeling]
295
296[settings.text_editor]
297
298[settings.command_bar]
299"#
300 );
301
302 let parsed = ProjectConfiguration::backwards_compatible_toml_parse(empty_settings_file).unwrap();
303 assert_eq!(parsed, ProjectConfiguration::default());
304 }
305
306 #[test]
307 fn test_project_settings_color_validation_error() {
308 let settings_file = r#"[settings.app.appearance]
309color = 1567.4"#;
310
311 let result = ProjectConfiguration::backwards_compatible_toml_parse(settings_file);
312 if let Ok(r) = result {
313 panic!("Expected an error, but got success: {:?}", r);
314 }
315 assert!(result.is_err());
316
317 assert!(result
318 .unwrap_err()
319 .to_string()
320 .contains("color: Validation error: color"));
321 }
322
323 #[test]
324 fn named_view_serde_json() {
325 let json = r#"
326 [
327 {
328 "name":"dog",
329 "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
330 "pivot_position":[0.5,0,0.5],
331 "eye_offset":231.52048,
332 "fov_y":45,
333 "ortho_scale_factor":1.574129,
334 "is_ortho":true,
335 "ortho_scale_enabled":true,
336 "world_coord_system":"RightHandedUpZ"
337 }
338 ]
339 "#;
340 let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
342 let version = named_views[0].version;
343 assert_eq!(version, 1.0);
344 }
345
346 #[test]
347 fn named_view_serde_json_string() {
348 let json = r#"
349 [
350 {
351 "name":"dog",
352 "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
353 "pivot_position":[0.5,0,0.5],
354 "eye_offset":231.52048,
355 "fov_y":45,
356 "ortho_scale_factor":1.574129,
357 "is_ortho":true,
358 "ortho_scale_enabled":true,
359 "world_coord_system":"RightHandedUpZ"
360 }
361 ]
362 "#;
363
364 let named_views: Value = match serde_json::from_str(json) {
366 Ok(x) => x,
367 Err(_) => return,
368 };
369 println!("{}", named_views);
370 }
371
372 #[test]
373 fn test_project_settings_named_views() {
374 let conf = ProjectConfiguration {
375 settings: PerProjectSettings {
376 app: ProjectAppSettings {
377 appearance: ProjectAppearanceSettings { color: 138.0.into() },
378 onboarding_status: Default::default(),
379 theme_color: None,
380 dismiss_web_banner: false,
381 enable_ssao: None,
382 stream_idle_mode: false,
383 allow_orbit_in_sketch_mode: false,
384 show_debug_panel: true,
385 named_views: IndexMap::from([
386 (
387 uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
388 NamedView {
389 name: String::from("Hello"),
390 eye_offset: 1236.4015,
391 fov_y: 45.0,
392 is_ortho: false,
393 ortho_scale_enabled: false,
394 ortho_scale_factor: 45.0,
395 pivot_position: [-100.0, 100.0, 100.0],
396 pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
397 world_coord_system: String::from("RightHandedUpZ"),
398 version: 1.0,
399 },
400 ),
401 (
402 uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
403 NamedView {
404 name: String::from("Goodbye"),
405 eye_offset: 1236.4015,
406 fov_y: 45.0,
407 is_ortho: false,
408 ortho_scale_enabled: false,
409 ortho_scale_factor: 45.0,
410 pivot_position: [-100.0, 100.0, 100.0],
411 pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
412 world_coord_system: String::from("RightHandedUpZ"),
413 version: 1.0,
414 },
415 ),
416 ]),
417 },
418 modeling: ProjectModelingSettings {
419 base_unit: UnitLength::Yd,
420 highlight_edges: Default::default(),
421 show_debug_panel: Default::default(),
422 enable_ssao: true.into(),
423 },
424 text_editor: TextEditorSettings {
425 text_wrapping: false.into(),
426 blinking_cursor: false.into(),
427 },
428 command_bar: CommandBarSettings {
429 include_settings: false.into(),
430 },
431 },
432 };
433 let serialized = toml::to_string(&conf).unwrap();
434 let old_project_file = r#"[settings.app]
435show_debug_panel = true
436
437[settings.app.appearance]
438color = 138.0
439
440[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
441name = "Hello"
442eye_offset = 1236.4015
443fov_y = 45.0
444is_ortho = false
445ortho_scale_enabled = false
446ortho_scale_factor = 45.0
447pivot_position = [-100.0, 100.0, 100.0]
448pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
449world_coord_system = "RightHandedUpZ"
450version = 1.0
451
452[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
453name = "Goodbye"
454eye_offset = 1236.4015
455fov_y = 45.0
456is_ortho = false
457ortho_scale_enabled = false
458ortho_scale_factor = 45.0
459pivot_position = [-100.0, 100.0, 100.0]
460pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
461world_coord_system = "RightHandedUpZ"
462version = 1.0
463
464[settings.modeling]
465base_unit = "yd"
466
467[settings.text_editor]
468text_wrapping = false
469blinking_cursor = false
470
471[settings.command_bar]
472include_settings = false
473"#;
474
475 assert_eq!(serialized, old_project_file)
476 }
477}