kcl_lib/settings/types/
project.rs

1//! Types specific for modeling-app projects.
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kittycad_modeling_cmds::units::UnitLength;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use validator::Validate;
9
10use crate::settings::types::{
11    DefaultTrue, OnboardingStatus, ProjectCommandBarSettings, ProjectTextEditorSettings, is_default,
12};
13
14/// Project specific settings for the app.
15/// These live in `project.toml` in the base of the project directory.
16/// Updating the settings for the project in the app will update this file automatically.
17/// Do not edit this file manually, as it may be overwritten by the app.
18/// Manual edits can cause corruption of the settings file.
19#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
20#[ts(export)]
21#[serde(rename_all = "snake_case")]
22pub struct ProjectConfiguration {
23    /// The settings for the project.
24    #[serde(default)]
25    #[validate(nested)]
26    pub settings: PerProjectSettings,
27}
28
29impl ProjectConfiguration {
30    // TODO: remove this when we remove backwards compatibility with the old settings file.
31    pub fn parse_and_validate(toml_str: &str) -> Result<Self> {
32        let settings = toml::from_str::<Self>(toml_str)?;
33
34        settings.validate()?;
35
36        Ok(settings)
37    }
38}
39
40/// High level project settings.
41#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
42#[ts(export)]
43#[serde(rename_all = "snake_case")]
44pub struct PerProjectSettings {
45    /// Information about the project itself.
46    /// Choices about how settings are merged have prevent me (lee) from easily
47    /// moving this out of the settings structure.
48    #[serde(default)]
49    #[validate(nested)]
50    pub meta: ProjectMetaSettings,
51
52    /// The settings for the Design Studio.
53    #[serde(default)]
54    #[validate(nested)]
55    pub app: ProjectAppSettings,
56    /// Settings that affect the behavior while modeling.
57    #[serde(default)]
58    #[validate(nested)]
59    pub modeling: ProjectModelingSettings,
60    /// Settings that affect the behavior of the KCL text editor.
61    #[serde(default)]
62    #[validate(nested)]
63    pub text_editor: ProjectTextEditorSettings,
64    /// Settings that affect the behavior of the command bar.
65    #[serde(default)]
66    #[validate(nested)]
67    pub command_bar: ProjectCommandBarSettings,
68}
69
70/// Information about the project.
71#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
72#[ts(export)]
73#[serde(rename_all = "snake_case")]
74pub struct ProjectMetaSettings {
75    #[serde(default, skip_serializing_if = "is_default")]
76    pub id: uuid::Uuid,
77}
78
79/// Project specific application settings.
80// TODO: When we remove backwards compatibility with the old settings file, we can remove the
81// aliases to camelCase (and projects plural) from everywhere.
82#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
83#[ts(export)]
84#[serde(rename_all = "snake_case")]
85pub struct ProjectAppSettings {
86    /// The onboarding status of the app.
87    #[serde(default, skip_serializing_if = "is_default")]
88    pub onboarding_status: OnboardingStatus,
89    /// When the user is idle, and this is true, the stream will be torn down.
90    #[serde(default, skip_serializing_if = "is_default")]
91    pub stream_idle_mode: bool,
92    /// When the user is idle, and this is true, the stream will be torn down.
93    #[serde(default, skip_serializing_if = "is_default")]
94    pub allow_orbit_in_sketch_mode: bool,
95    /// Whether to show the debug panel, which lets you see various states
96    /// of the app to aid in development.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub show_debug_panel: Option<bool>,
99    /// Settings that affect the behavior of the command bar.
100    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
101    pub named_views: IndexMap<uuid::Uuid, NamedView>,
102}
103
104/// Project specific settings that affect the behavior while modeling.
105#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
106#[serde(rename_all = "snake_case")]
107#[ts(export)]
108pub struct ProjectModelingSettings {
109    /// The default unit to use in modeling dimensions.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub base_unit: Option<UnitLength>,
112    /// Highlight edges of 3D objects?
113    #[serde(default, skip_serializing_if = "is_default")]
114    pub highlight_edges: DefaultTrue,
115    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
116    #[serde(default, skip_serializing_if = "is_default")]
117    pub enable_ssao: DefaultTrue,
118    /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.
119    /// If true, the grid cells will be fixed-size, where the width is your default length unit.
120    /// If false, the grid will get larger as you zoom out, and smaller as you zoom in.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub fixed_size_grid: Option<bool>,
123    /// When enabled, tools like line, rectangle, etc. will snap to the grid.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub snap_to_grid: Option<bool>,
126    /// The space between major grid lines, specified in the current unit.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub major_grid_spacing: Option<f64>,
129    /// The number of minor grid lines per major grid line.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub minor_grids_per_major: Option<f64>,
132    /// The number of snaps between minor grid lines. 1 means snapping to each minor grid line.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub snaps_per_minor: Option<f64>,
135}
136
137fn named_view_point_version_one() -> f64 {
138    1.0
139}
140
141#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
142#[serde(rename_all = "snake_case")]
143#[ts(export)]
144pub struct NamedView {
145    /// User defined name to identify the named view. A label.
146    #[serde(default)]
147    pub name: String,
148    /// Engine camera eye off set
149    #[serde(default)]
150    pub eye_offset: f64,
151    /// Engine camera vertical FOV
152    #[serde(default)]
153    pub fov_y: f64,
154    // Engine camera is orthographic or perspective projection
155    #[serde(default)]
156    pub is_ortho: bool,
157    /// Engine camera is orthographic camera scaling enabled
158    #[serde(default)]
159    pub ortho_scale_enabled: bool,
160    /// Engine camera orthographic scaling factor
161    #[serde(default)]
162    pub ortho_scale_factor: f64,
163    /// Engine camera position that the camera pivots around
164    #[serde(default)]
165    pub pivot_position: [f64; 3],
166    /// Engine camera orientation in relation to the pivot position
167    #[serde(default)]
168    pub pivot_rotation: [f64; 4],
169    /// Engine camera world coordinate system orientation
170    #[serde(default)]
171    pub world_coord_system: String,
172    /// Version number of the view point if the engine camera API changes
173    #[serde(default = "named_view_point_version_one")]
174    pub version: f64,
175}
176
177#[cfg(test)]
178mod tests {
179    use indexmap::IndexMap;
180    use pretty_assertions::assert_eq;
181    use serde_json::Value;
182
183    use super::{
184        NamedView, PerProjectSettings, ProjectAppSettings, ProjectCommandBarSettings, ProjectConfiguration,
185        ProjectMetaSettings, ProjectModelingSettings, ProjectTextEditorSettings,
186    };
187    use crate::settings::types::UnitLength;
188
189    #[test]
190    fn test_project_settings_empty_file_parses() {
191        let empty_settings_file = r#""#;
192
193        let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
194        assert_eq!(parsed, ProjectConfiguration::default());
195
196        // Write the file back out.
197        let serialized = toml::to_string(&parsed).unwrap();
198        assert_eq!(
199            serialized,
200            r#"[settings.meta]
201
202[settings.app]
203
204[settings.modeling]
205
206[settings.text_editor]
207
208[settings.command_bar]
209"#
210        );
211
212        let parsed = ProjectConfiguration::parse_and_validate(empty_settings_file).unwrap();
213        assert_eq!(parsed, ProjectConfiguration::default());
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                meta: ProjectMetaSettings { id: uuid::Uuid::nil() },
270                app: ProjectAppSettings {
271                    onboarding_status: Default::default(),
272                    stream_idle_mode: false,
273                    allow_orbit_in_sketch_mode: false,
274                    show_debug_panel: Some(true),
275                    named_views: IndexMap::from([
276                        (
277                            uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
278                            NamedView {
279                                name: String::from("Hello"),
280                                eye_offset: 1236.4015,
281                                fov_y: 45.0,
282                                is_ortho: false,
283                                ortho_scale_enabled: false,
284                                ortho_scale_factor: 45.0,
285                                pivot_position: [-100.0, 100.0, 100.0],
286                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
287                                world_coord_system: String::from("RightHandedUpZ"),
288                                version: 1.0,
289                            },
290                        ),
291                        (
292                            uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
293                            NamedView {
294                                name: String::from("Goodbye"),
295                                eye_offset: 1236.4015,
296                                fov_y: 45.0,
297                                is_ortho: false,
298                                ortho_scale_enabled: false,
299                                ortho_scale_factor: 45.0,
300                                pivot_position: [-100.0, 100.0, 100.0],
301                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
302                                world_coord_system: String::from("RightHandedUpZ"),
303                                version: 1.0,
304                            },
305                        ),
306                    ]),
307                },
308                modeling: ProjectModelingSettings {
309                    base_unit: Some(UnitLength::Yards),
310                    highlight_edges: Default::default(),
311                    enable_ssao: true.into(),
312                    snap_to_grid: None,
313                    major_grid_spacing: None,
314                    minor_grids_per_major: None,
315                    snaps_per_minor: None,
316                    fixed_size_grid: None,
317                },
318                text_editor: ProjectTextEditorSettings {
319                    text_wrapping: Some(false),
320                    blinking_cursor: Some(false),
321                },
322                command_bar: ProjectCommandBarSettings {
323                    include_settings: Some(false),
324                },
325            },
326        };
327        let serialized = toml::to_string(&conf).unwrap();
328        let old_project_file = r#"[settings.meta]
329
330[settings.app]
331show_debug_panel = true
332
333[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
334name = "Hello"
335eye_offset = 1236.4015
336fov_y = 45.0
337is_ortho = false
338ortho_scale_enabled = false
339ortho_scale_factor = 45.0
340pivot_position = [-100.0, 100.0, 100.0]
341pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
342world_coord_system = "RightHandedUpZ"
343version = 1.0
344
345[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
346name = "Goodbye"
347eye_offset = 1236.4015
348fov_y = 45.0
349is_ortho = false
350ortho_scale_enabled = false
351ortho_scale_factor = 45.0
352pivot_position = [-100.0, 100.0, 100.0]
353pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
354world_coord_system = "RightHandedUpZ"
355version = 1.0
356
357[settings.modeling]
358base_unit = "yd"
359
360[settings.text_editor]
361text_wrapping = false
362blinking_cursor = false
363
364[settings.command_bar]
365include_settings = false
366"#;
367
368        assert_eq!(serialized, old_project_file)
369    }
370}