1use 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#[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 #[serde(default)]
25 #[validate(nested)]
26 pub settings: PerProjectSettings,
27}
28
29impl ProjectConfiguration {
30 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#[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 #[serde(default)]
49 #[validate(nested)]
50 pub meta: ProjectMetaSettings,
51
52 #[serde(default)]
54 #[validate(nested)]
55 pub app: ProjectAppSettings,
56 #[serde(default)]
58 #[validate(nested)]
59 pub modeling: ProjectModelingSettings,
60 #[serde(default)]
62 #[validate(nested)]
63 pub text_editor: ProjectTextEditorSettings,
64 #[serde(default)]
66 #[validate(nested)]
67 pub command_bar: ProjectCommandBarSettings,
68}
69
70#[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#[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 #[serde(default, skip_serializing_if = "is_default")]
88 pub onboarding_status: OnboardingStatus,
89 #[serde(default, skip_serializing_if = "is_default")]
91 pub stream_idle_mode: bool,
92 #[serde(default, skip_serializing_if = "is_default")]
94 pub allow_orbit_in_sketch_mode: bool,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub show_debug_panel: Option<bool>,
99 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
101 pub named_views: IndexMap<uuid::Uuid, NamedView>,
102}
103
104#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub base_unit: Option<UnitLength>,
112 #[serde(default, skip_serializing_if = "is_default")]
114 pub highlight_edges: DefaultTrue,
115 #[serde(default, skip_serializing_if = "is_default")]
117 pub enable_ssao: DefaultTrue,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub fixed_size_grid: Option<bool>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub snap_to_grid: Option<bool>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub major_grid_spacing: Option<f64>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub minor_grids_per_major: Option<f64>,
132 #[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 #[serde(default)]
147 pub name: String,
148 #[serde(default)]
150 pub eye_offset: f64,
151 #[serde(default)]
153 pub fov_y: f64,
154 #[serde(default)]
156 pub is_ortho: bool,
157 #[serde(default)]
159 pub ortho_scale_enabled: bool,
160 #[serde(default)]
162 pub ortho_scale_factor: f64,
163 #[serde(default)]
165 pub pivot_position: [f64; 3],
166 #[serde(default)]
168 pub pivot_rotation: [f64; 4],
169 #[serde(default)]
171 pub world_coord_system: String,
172 #[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 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 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 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}