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    /// Disable the new Copilot in Text-to-CAD for this project, only available in Zoo Design Studio (Staging).
78    #[serde(default, skip_serializing_if = "is_default")]
79    pub disable_copilot: bool,
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    /// Settings that affect the behavior of the command bar.
103    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
104    pub named_views: IndexMap<uuid::Uuid, NamedView>,
105}
106
107/// Project specific settings that affect the behavior while modeling.
108#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
109#[serde(rename_all = "snake_case")]
110#[ts(export)]
111pub struct ProjectModelingSettings {
112    /// The default unit to use in modeling dimensions.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub base_unit: Option<UnitLength>,
115    /// Highlight edges of 3D objects?
116    #[serde(default, skip_serializing_if = "is_default")]
117    pub highlight_edges: DefaultTrue,
118    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
119    #[serde(default, skip_serializing_if = "is_default")]
120    pub enable_ssao: DefaultTrue,
121    /// When enabled, the grid will use a fixed size based on your selected units rather than automatically scaling with zoom level.
122    /// If true, the grid cells will be fixed-size, where the width is your default length unit.
123    /// If false, the grid will get larger as you zoom out, and smaller as you zoom in.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub fixed_size_grid: Option<bool>,
126    /// When enabled, tools like line, rectangle, etc. will snap to the grid.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub snap_to_grid: Option<bool>,
129    /// The space between major grid lines, specified in the current unit.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub major_grid_spacing: Option<f64>,
132    /// The number of minor grid lines per major grid line.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub minor_grids_per_major: Option<f64>,
135    /// The number of snaps between minor grid lines. 1 means snapping to each minor grid line.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub snaps_per_minor: Option<f64>,
138}
139
140fn named_view_point_version_one() -> f64 {
141    1.0
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
145#[serde(rename_all = "snake_case")]
146#[ts(export)]
147pub struct NamedView {
148    /// User defined name to identify the named view. A label.
149    #[serde(default)]
150    pub name: String,
151    /// Engine camera eye off set
152    #[serde(default)]
153    pub eye_offset: f64,
154    /// Engine camera vertical FOV
155    #[serde(default)]
156    pub fov_y: f64,
157    // Engine camera is orthographic or perspective projection
158    #[serde(default)]
159    pub is_ortho: bool,
160    /// Engine camera is orthographic camera scaling enabled
161    #[serde(default)]
162    pub ortho_scale_enabled: bool,
163    /// Engine camera orthographic scaling factor
164    #[serde(default)]
165    pub ortho_scale_factor: f64,
166    /// Engine camera position that the camera pivots around
167    #[serde(default)]
168    pub pivot_position: [f64; 3],
169    /// Engine camera orientation in relation to the pivot position
170    #[serde(default)]
171    pub pivot_rotation: [f64; 4],
172    /// Engine camera world coordinate system orientation
173    #[serde(default)]
174    pub world_coord_system: String,
175    /// Version number of the view point if the engine camera API changes
176    #[serde(default = "named_view_point_version_one")]
177    pub version: f64,
178}
179
180#[cfg(test)]
181mod tests {
182    use indexmap::IndexMap;
183    use pretty_assertions::assert_eq;
184    use serde_json::Value;
185
186    use super::{
187        NamedView, PerProjectSettings, ProjectAppSettings, ProjectCommandBarSettings, ProjectConfiguration,
188        ProjectMetaSettings, ProjectModelingSettings, ProjectTextEditorSettings,
189    };
190    use crate::settings::types::UnitLength;
191
192    #[test]
193    fn test_project_settings_empty_file_parses() {
194        let empty_settings_file = r#""#;
195
196        let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
197        assert_eq!(parsed, ProjectConfiguration::default());
198
199        // Write the file back out.
200        let serialized = toml::to_string(&parsed).unwrap();
201        assert_eq!(
202            serialized,
203            r#"[settings.meta]
204
205[settings.app]
206
207[settings.modeling]
208
209[settings.text_editor]
210
211[settings.command_bar]
212"#
213        );
214
215        let parsed = ProjectConfiguration::parse_and_validate(empty_settings_file).unwrap();
216        assert_eq!(parsed, ProjectConfiguration::default());
217    }
218
219    #[test]
220    fn named_view_serde_json() {
221        let json = r#"
222        [
223          {
224            "name":"dog",
225            "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
226            "pivot_position":[0.5,0,0.5],
227            "eye_offset":231.52048,
228            "fov_y":45,
229            "ortho_scale_factor":1.574129,
230            "is_ortho":true,
231            "ortho_scale_enabled":true,
232            "world_coord_system":"RightHandedUpZ"
233          }
234    ]
235    "#;
236        // serde_json to a NamedView will produce default values
237        let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
238        let version = named_views[0].version;
239        assert_eq!(version, 1.0);
240    }
241
242    #[test]
243    fn named_view_serde_json_string() {
244        let json = r#"
245        [
246          {
247            "name":"dog",
248            "pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
249            "pivot_position":[0.5,0,0.5],
250            "eye_offset":231.52048,
251            "fov_y":45,
252            "ortho_scale_factor":1.574129,
253            "is_ortho":true,
254            "ortho_scale_enabled":true,
255            "world_coord_system":"RightHandedUpZ"
256          }
257    ]
258    "#;
259
260        // serde_json to string does not produce default values
261        let named_views: Value = match serde_json::from_str(json) {
262            Ok(x) => x,
263            Err(_) => return,
264        };
265        println!("{}", named_views);
266    }
267
268    #[test]
269    fn test_project_settings_named_views() {
270        let conf = ProjectConfiguration {
271            settings: PerProjectSettings {
272                meta: ProjectMetaSettings {
273                    id: uuid::Uuid::nil(),
274                    disable_copilot: Default::default(),
275                },
276                app: ProjectAppSettings {
277                    onboarding_status: Default::default(),
278                    stream_idle_mode: false,
279                    allow_orbit_in_sketch_mode: false,
280                    show_debug_panel: Some(true),
281                    named_views: IndexMap::from([
282                        (
283                            uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
284                            NamedView {
285                                name: String::from("Hello"),
286                                eye_offset: 1236.4015,
287                                fov_y: 45.0,
288                                is_ortho: false,
289                                ortho_scale_enabled: false,
290                                ortho_scale_factor: 45.0,
291                                pivot_position: [-100.0, 100.0, 100.0],
292                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
293                                world_coord_system: String::from("RightHandedUpZ"),
294                                version: 1.0,
295                            },
296                        ),
297                        (
298                            uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
299                            NamedView {
300                                name: String::from("Goodbye"),
301                                eye_offset: 1236.4015,
302                                fov_y: 45.0,
303                                is_ortho: false,
304                                ortho_scale_enabled: false,
305                                ortho_scale_factor: 45.0,
306                                pivot_position: [-100.0, 100.0, 100.0],
307                                pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
308                                world_coord_system: String::from("RightHandedUpZ"),
309                                version: 1.0,
310                            },
311                        ),
312                    ]),
313                },
314                modeling: ProjectModelingSettings {
315                    base_unit: Some(UnitLength::Yards),
316                    highlight_edges: Default::default(),
317                    enable_ssao: true.into(),
318                    snap_to_grid: None,
319                    major_grid_spacing: None,
320                    minor_grids_per_major: None,
321                    snaps_per_minor: None,
322                    fixed_size_grid: None,
323                },
324                text_editor: ProjectTextEditorSettings {
325                    text_wrapping: Some(false),
326                    blinking_cursor: Some(false),
327                },
328                command_bar: ProjectCommandBarSettings {
329                    include_settings: Some(false),
330                },
331            },
332        };
333        let serialized = toml::to_string(&conf).unwrap();
334        let old_project_file = r#"[settings.meta]
335
336[settings.app]
337show_debug_panel = true
338
339[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
340name = "Hello"
341eye_offset = 1236.4015
342fov_y = 45.0
343is_ortho = false
344ortho_scale_enabled = false
345ortho_scale_factor = 45.0
346pivot_position = [-100.0, 100.0, 100.0]
347pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
348world_coord_system = "RightHandedUpZ"
349version = 1.0
350
351[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
352name = "Goodbye"
353eye_offset = 1236.4015
354fov_y = 45.0
355is_ortho = false
356ortho_scale_enabled = false
357ortho_scale_factor = 45.0
358pivot_position = [-100.0, 100.0, 100.0]
359pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
360world_coord_system = "RightHandedUpZ"
361version = 1.0
362
363[settings.modeling]
364base_unit = "yd"
365
366[settings.text_editor]
367text_wrapping = false
368blinking_cursor = false
369
370[settings.command_bar]
371include_settings = false
372"#;
373
374        assert_eq!(serialized, old_project_file)
375    }
376}