Skip to main content

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;
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/// Project specific settings for the app.
18/// These live in `project.toml` in the base of the project directory.
19/// Updating the settings for the project in the app will update this file automatically.
20/// Do not edit this file manually, as it may be overwritten by the app.
21/// Manual edits can cause corruption of the settings file.
22#[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    /// The settings for the project.
27    #[serde(default)]
28    #[validate(nested)]
29    pub settings: PerProjectSettings,
30}
31
32impl ProjectConfiguration {
33    // TODO: remove this when we remove backwards compatibility with the old settings file.
34    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/// High level project settings.
44#[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    /// Information about the project itself.
49    /// Choices about how settings are merged have prevent me (lee) from easily
50    /// moving this out of the settings structure.
51    #[serde(default)]
52    #[validate(nested)]
53    pub meta: ProjectMetaSettings,
54
55    /// The settings for the Design Studio.
56    #[serde(default)]
57    #[validate(nested)]
58    pub app: ProjectAppSettings,
59    /// Settings that affect the behavior while modeling.
60    #[serde(default)]
61    #[validate(nested)]
62    pub modeling: ProjectModelingSettings,
63    /// Settings that affect the behavior of the KCL text editor.
64    #[serde(default)]
65    #[validate(nested)]
66    pub text_editor: ProjectTextEditorSettings,
67    /// Settings that affect the behavior of the command bar.
68    #[serde(default)]
69    #[validate(nested)]
70    pub command_bar: ProjectCommandBarSettings,
71}
72
73/// Information about the project.
74#[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/// Project specific application settings.
83// TODO: When we remove backwards compatibility with the old settings file, we can remove the
84// aliases to camelCase (and projects plural) from everywhere.
85#[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    /// The onboarding status of the app.
90    #[serde(default, skip_serializing_if = "is_default")]
91    pub onboarding_status: OnboardingStatus,
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 stream_idle_mode: bool,
95    /// When the user is idle, and this is true, the stream will be torn down.
96    #[serde(default, skip_serializing_if = "is_default")]
97    pub allow_orbit_in_sketch_mode: bool,
98    /// Whether to show the debug panel, which lets you see various states
99    /// of the app to aid in development.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub show_debug_panel: Option<bool>,
102    /// Zookeeper reasoning mode. Uses the app default if not set.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub zookeeper_mode: Option<String>,
105    /// Settings that affect the behavior of the command bar.
106    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
107    pub named_views: IndexMap<uuid::Uuid, NamedView>,
108}
109
110/// Project specific settings that affect the behavior while modeling.
111#[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    /// The default unit to use in modeling dimensions.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub base_unit: Option<UnitLength>,
118    /// Highlight edges of 3D objects?
119    #[serde(default, skip_serializing_if = "is_default")]
120    pub highlight_edges: DefaultTrue,
121    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
122    #[serde(default, skip_serializing_if = "is_default")]
123    pub enable_ssao: DefaultTrue,
124    /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.
125    /// If true, the grid cells will be fixed-size, where the width is your default length unit.
126    /// If false, the grid will get larger as you zoom out, and smaller as you zoom in.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub fixed_size_grid: Option<bool>,
129    /// When enabled, tools like line, rectangle, etc. will snap to the grid.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub snap_to_grid: Option<bool>,
132    /// The space between major grid lines, specified in the current unit.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub major_grid_spacing: Option<f64>,
135    /// The number of minor grid lines per major grid line.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub minor_grids_per_major: Option<f64>,
138    /// The number of snaps between minor grid lines. 1 means snapping to each minor grid line.
139    #[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    /// User defined name to identify the named view. A label.
152    #[serde(default)]
153    pub name: String,
154    /// Engine camera eye off set
155    #[serde(default)]
156    pub eye_offset: f64,
157    /// Engine camera vertical FOV
158    #[serde(default)]
159    pub fov_y: f64,
160    // Engine camera is orthographic or perspective projection
161    #[serde(default)]
162    pub is_ortho: bool,
163    /// Engine camera is orthographic camera scaling enabled
164    #[serde(default)]
165    pub ortho_scale_enabled: bool,
166    /// Engine camera orthographic scaling factor
167    #[serde(default)]
168    pub ortho_scale_factor: f64,
169    /// Engine camera position that the camera pivots around
170    #[serde(default)]
171    pub pivot_position: [f64; 3],
172    /// Engine camera orientation in relation to the pivot position
173    #[serde(default)]
174    pub pivot_rotation: [f64; 4],
175    /// Engine camera world coordinate system orientation
176    #[serde(default)]
177    pub world_coord_system: String,
178    /// Version number of the view point if the engine camera API changes
179    #[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        // Write the file back out.
207        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        // serde_json to a NamedView will produce default values
244        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        // serde_json to string does not produce default values
268        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}