kcl_lib/settings/types/
project.rs

1//! Types specific for modeling-app projects.
2
3use 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, OnboardingStatus, TextEditorSettings, UnitLength,
11};
12
13/// Project specific settings for the app.
14/// These live in `project.toml` in the base of the project directory.
15/// Updating the settings for the project in the app will update this file automatically.
16/// Do not edit this file manually, as it may be overwritten by the app.
17/// Manual edits can cause corruption of the settings file.
18#[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    /// The settings for the project.
23    #[serde(default)]
24    #[validate(nested)]
25    pub settings: PerProjectSettings,
26}
27
28impl ProjectConfiguration {
29    // TODO: remove this when we remove backwards compatibility with the old settings file.
30    pub fn parse_and_validate(toml_str: &str) -> Result<Self> {
31        let settings = toml::from_str::<Self>(toml_str)?;
32
33        settings.validate()?;
34
35        Ok(settings)
36    }
37}
38
39/// High level project settings.
40#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
41#[ts(export)]
42#[serde(rename_all = "snake_case")]
43pub struct PerProjectSettings {
44    /// The settings for the Design Studio.
45    #[serde(default)]
46    #[validate(nested)]
47    pub app: ProjectAppSettings,
48    /// Settings that affect the behavior while modeling.
49    #[serde(default)]
50    #[validate(nested)]
51    pub modeling: ProjectModelingSettings,
52    /// Settings that affect the behavior of the KCL text editor.
53    #[serde(default)]
54    #[validate(nested)]
55    pub text_editor: TextEditorSettings,
56    /// Settings that affect the behavior of the command bar.
57    #[serde(default)]
58    #[validate(nested)]
59    pub command_bar: CommandBarSettings,
60}
61
62/// Project specific application settings.
63// TODO: When we remove backwards compatibility with the old settings file, we can remove the
64// aliases to camelCase (and projects plural) from everywhere.
65#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
66#[ts(export)]
67#[serde(rename_all = "snake_case")]
68pub struct ProjectAppSettings {
69    /// The settings for the appearance of the app.
70    #[serde(default, skip_serializing_if = "is_default")]
71    #[validate(nested)]
72    pub appearance: ProjectAppearanceSettings,
73    /// The onboarding status of the app.
74    #[serde(default, skip_serializing_if = "is_default")]
75    pub onboarding_status: OnboardingStatus,
76    /// Permanently dismiss the banner warning to download the desktop app.
77    /// This setting only applies to the web app. And is temporary until we have Linux support.
78    #[serde(default, skip_serializing_if = "is_default")]
79    pub dismiss_web_banner: bool,
80    /// When the user is idle, and this is true, the stream will be torn down.
81    #[serde(default, skip_serializing_if = "is_default")]
82    pub stream_idle_mode: bool,
83    /// When the user is idle, and this is true, the stream will be torn down.
84    #[serde(default, skip_serializing_if = "is_default")]
85    pub allow_orbit_in_sketch_mode: bool,
86    /// Whether to show the debug panel, which lets you see various states
87    /// of the app to aid in development.
88    #[serde(default, skip_serializing_if = "is_default")]
89    pub show_debug_panel: bool,
90    /// Settings that affect the behavior of the command bar.
91    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
92    pub named_views: IndexMap<uuid::Uuid, NamedView>,
93}
94
95/// Project specific appearance settings.
96#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
97#[ts(export)]
98#[serde(rename_all = "snake_case")]
99pub struct ProjectAppearanceSettings {
100    /// The hue of the primary theme color for the app.
101    #[serde(default, skip_serializing_if = "is_default")]
102    #[validate(nested)]
103    pub color: AppColor,
104}
105
106/// Project specific settings that affect the behavior while modeling.
107#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
108#[serde(rename_all = "snake_case")]
109#[ts(export)]
110pub struct ProjectModelingSettings {
111    /// The default unit to use in modeling dimensions.
112    #[serde(default, skip_serializing_if = "is_default")]
113    pub base_unit: UnitLength,
114    /// Highlight edges of 3D objects?
115    #[serde(default, skip_serializing_if = "is_default")]
116    pub highlight_edges: DefaultTrue,
117    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
118    #[serde(default, skip_serializing_if = "is_default")]
119    pub enable_ssao: DefaultTrue,
120}
121
122fn named_view_point_version_one() -> f64 {
123    1.0
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
127#[serde(rename_all = "snake_case")]
128#[ts(export)]
129pub struct NamedView {
130    /// User defined name to identify the named view. A label.
131    #[serde(default)]
132    pub name: String,
133    /// Engine camera eye off set
134    #[serde(default)]
135    pub eye_offset: f64,
136    /// Engine camera vertical FOV
137    #[serde(default)]
138    pub fov_y: f64,
139    // Engine camera is orthographic or perspective projection
140    #[serde(default)]
141    pub is_ortho: bool,
142    /// Engine camera is orthographic camera scaling enabled
143    #[serde(default)]
144    pub ortho_scale_enabled: bool,
145    /// Engine camera orthographic scaling factor
146    #[serde(default)]
147    pub ortho_scale_factor: f64,
148    /// Engine camera position that the camera pivots around
149    #[serde(default)]
150    pub pivot_position: [f64; 3],
151    /// Engine camera orientation in relation to the pivot position
152    #[serde(default)]
153    pub pivot_rotation: [f64; 4],
154    /// Engine camera world coordinate system orientation
155    #[serde(default)]
156    pub world_coord_system: String,
157    /// Version number of the view point if the engine camera API changes
158    #[serde(default = "named_view_point_version_one")]
159    pub version: f64,
160}
161
162#[cfg(test)]
163mod tests {
164    use indexmap::IndexMap;
165    use pretty_assertions::assert_eq;
166    use serde_json::Value;
167
168    use super::{
169        CommandBarSettings, NamedView, PerProjectSettings, ProjectAppSettings, ProjectAppearanceSettings,
170        ProjectConfiguration, ProjectModelingSettings, TextEditorSettings,
171    };
172    use crate::settings::types::UnitLength;
173
174    #[test]
175    fn test_project_settings_empty_file_parses() {
176        let empty_settings_file = r#""#;
177
178        let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
179        assert_eq!(parsed, ProjectConfiguration::default());
180
181        // Write the file back out.
182        let serialized = toml::to_string(&parsed).unwrap();
183        assert_eq!(
184            serialized,
185            r#"[settings.app]
186
187[settings.modeling]
188
189[settings.text_editor]
190
191[settings.command_bar]
192"#
193        );
194
195        let parsed = ProjectConfiguration::parse_and_validate(empty_settings_file).unwrap();
196        assert_eq!(parsed, ProjectConfiguration::default());
197    }
198
199    #[test]
200    fn test_project_settings_color_validation_error() {
201        let settings_file = r#"[settings.app.appearance]
202color = 1567.4"#;
203
204        let result = ProjectConfiguration::parse_and_validate(settings_file);
205        if let Ok(r) = result {
206            panic!("Expected an error, but got success: {:?}", r);
207        }
208        assert!(result.is_err());
209
210        assert!(result
211            .unwrap_err()
212            .to_string()
213            .contains("color: Validation error: color"));
214    }
215
216    #[test]
217    fn named_view_serde_json() {
218        let json = r#"
219        [
220          {
221            "name":"dog",
222            "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
223            "pivot_position":[0.5,0,0.5],
224            "eye_offset":231.52048,
225            "fov_y":45,
226            "ortho_scale_factor":1.574129,
227            "is_ortho":true,
228            "ortho_scale_enabled":true,
229            "world_coord_system":"RightHandedUpZ"
230          }
231    ]
232    "#;
233        // serde_json to a NamedView will produce default values
234        let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
235        let version = named_views[0].version;
236        assert_eq!(version, 1.0);
237    }
238
239    #[test]
240    fn named_view_serde_json_string() {
241        let json = r#"
242        [
243          {
244            "name":"dog",
245            "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
246            "pivot_position":[0.5,0,0.5],
247            "eye_offset":231.52048,
248            "fov_y":45,
249            "ortho_scale_factor":1.574129,
250            "is_ortho":true,
251            "ortho_scale_enabled":true,
252            "world_coord_system":"RightHandedUpZ"
253          }
254    ]
255    "#;
256
257        // serde_json to string does not produce default values
258        let named_views: Value = match serde_json::from_str(json) {
259            Ok(x) => x,
260            Err(_) => return,
261        };
262        println!("{}", named_views);
263    }
264
265    #[test]
266    fn test_project_settings_named_views() {
267        let conf = ProjectConfiguration {
268            settings: PerProjectSettings {
269                app: ProjectAppSettings {
270                    appearance: ProjectAppearanceSettings { color: 138.0.into() },
271                    onboarding_status: Default::default(),
272                    dismiss_web_banner: false,
273                    stream_idle_mode: false,
274                    allow_orbit_in_sketch_mode: false,
275                    show_debug_panel: true,
276                    named_views: IndexMap::from([
277                        (
278                            uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
279                            NamedView {
280                                name: String::from("Hello"),
281                                eye_offset: 1236.4015,
282                                fov_y: 45.0,
283                                is_ortho: false,
284                                ortho_scale_enabled: false,
285                                ortho_scale_factor: 45.0,
286                                pivot_position: [-100.0, 100.0, 100.0],
287                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
288                                world_coord_system: String::from("RightHandedUpZ"),
289                                version: 1.0,
290                            },
291                        ),
292                        (
293                            uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
294                            NamedView {
295                                name: String::from("Goodbye"),
296                                eye_offset: 1236.4015,
297                                fov_y: 45.0,
298                                is_ortho: false,
299                                ortho_scale_enabled: false,
300                                ortho_scale_factor: 45.0,
301                                pivot_position: [-100.0, 100.0, 100.0],
302                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
303                                world_coord_system: String::from("RightHandedUpZ"),
304                                version: 1.0,
305                            },
306                        ),
307                    ]),
308                },
309                modeling: ProjectModelingSettings {
310                    base_unit: UnitLength::Yd,
311                    highlight_edges: Default::default(),
312                    enable_ssao: true.into(),
313                },
314                text_editor: TextEditorSettings {
315                    text_wrapping: false.into(),
316                    blinking_cursor: false.into(),
317                },
318                command_bar: CommandBarSettings {
319                    include_settings: false.into(),
320                },
321            },
322        };
323        let serialized = toml::to_string(&conf).unwrap();
324        let old_project_file = r#"[settings.app]
325show_debug_panel = true
326
327[settings.app.appearance]
328color = 138.0
329
330[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
331name = "Hello"
332eye_offset = 1236.4015
333fov_y = 45.0
334is_ortho = false
335ortho_scale_enabled = false
336ortho_scale_factor = 45.0
337pivot_position = [-100.0, 100.0, 100.0]
338pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
339world_coord_system = "RightHandedUpZ"
340version = 1.0
341
342[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
343name = "Goodbye"
344eye_offset = 1236.4015
345fov_y = 45.0
346is_ortho = false
347ortho_scale_enabled = false
348ortho_scale_factor = 45.0
349pivot_position = [-100.0, 100.0, 100.0]
350pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
351world_coord_system = "RightHandedUpZ"
352version = 1.0
353
354[settings.modeling]
355base_unit = "yd"
356
357[settings.text_editor]
358text_wrapping = false
359blinking_cursor = false
360
361[settings.command_bar]
362include_settings = false
363"#;
364
365        assert_eq!(serialized, old_project_file)
366    }
367}