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 #[serde(default, skip_serializing_if = "is_default")]
79 pub disable_copilot: bool,
80}
81
82#[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 #[serde(default, skip_serializing_if = "is_default")]
91 pub onboarding_status: OnboardingStatus,
92 #[serde(default, skip_serializing_if = "is_default")]
94 pub stream_idle_mode: bool,
95 #[serde(default, skip_serializing_if = "is_default")]
97 pub allow_orbit_in_sketch_mode: bool,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub show_debug_panel: Option<bool>,
102 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
104 pub named_views: IndexMap<uuid::Uuid, NamedView>,
105}
106
107#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub base_unit: Option<UnitLength>,
115 #[serde(default, skip_serializing_if = "is_default")]
117 pub highlight_edges: DefaultTrue,
118 #[serde(default, skip_serializing_if = "is_default")]
120 pub enable_ssao: DefaultTrue,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub fixed_size_grid: Option<bool>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub snap_to_grid: Option<bool>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub major_grid_spacing: Option<f64>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub minor_grids_per_major: Option<f64>,
135 #[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 #[serde(default)]
150 pub name: String,
151 #[serde(default)]
153 pub eye_offset: f64,
154 #[serde(default)]
156 pub fov_y: f64,
157 #[serde(default)]
159 pub is_ortho: bool,
160 #[serde(default)]
162 pub ortho_scale_enabled: bool,
163 #[serde(default)]
165 pub ortho_scale_factor: f64,
166 #[serde(default)]
168 pub pivot_position: [f64; 3],
169 #[serde(default)]
171 pub pivot_rotation: [f64; 4],
172 #[serde(default)]
174 pub world_coord_system: String,
175 #[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 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 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 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}