1use std::collections::HashSet;
8
9pub type Rgba = [f32; 4];
11
12fn hex(hex: u32, alpha: f32) -> Rgba {
13 [
14 ((hex >> 16) & 0xff) as f32 / 255.0,
15 ((hex >> 8) & 0xff) as f32 / 255.0,
16 (hex & 0xff) as f32 / 255.0,
17 alpha,
18 ]
19}
20
21pub fn parse_css_hex(value: &str) -> Option<[f32; 3]> {
23 let v = value.trim();
24 let digits = v
25 .strip_prefix('#')
26 .or_else(|| v.strip_prefix("0x"))
27 .or_else(|| v.strip_prefix("0X"))?;
28 let expand = |c: char| c.to_digit(16).map(|d| (d * 17) as f32 / 255.0);
29 match digits.len() {
30 3 => {
31 let mut chars = digits.chars();
32 Some([
33 expand(chars.next()?)?,
34 expand(chars.next()?)?,
35 expand(chars.next()?)?,
36 ])
37 }
38 6 => {
39 let n = u32::from_str_radix(digits, 16).ok()?;
40 Some([
41 ((n >> 16) & 0xff) as f32 / 255.0,
42 ((n >> 8) & 0xff) as f32 / 255.0,
43 (n & 0xff) as f32 / 255.0,
44 ])
45 }
46 _ => None,
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct SketchColors {
62 pub movable: u32,
64 pub locked: u32,
66 pub geometry: u32,
68 pub point: u32,
70 pub construction_point: u32,
72 pub under_constrained_point: u32,
74 pub selected: u32,
76 pub hovered: u32,
78 pub preview: u32,
80 pub constraint: u32,
82}
83
84impl Default for SketchColors {
85 fn default() -> Self {
86 Self {
88 movable: 0x4aa3ff,
89 locked: 0xe6ebf2,
90 geometry: 0xffff88,
91 point: 0x9ec9ff,
92 construction_point: 0xffa86a,
93 under_constrained_point: 0xffb347,
94 selected: 0xffa500,
95 hovered: 0x7fd0ff,
96 preview: 0x8fa0b8,
97 constraint: 0x4ade80,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FaceColorMode {
105 Uniform,
108 HashedBySolid,
110}
111
112#[derive(Debug, Clone, PartialEq)]
115pub struct RenderSettings {
116 pub background: [f32; 3],
117 pub face_color_mode: FaceColorMode,
118 pub face_color: Rgba,
119 pub face_selected_color: Rgba,
120 pub hover_color: Rgba,
121 pub edge_color: Rgba,
122 pub edge_selected_color: Rgba,
123 pub edge_width_px: f32,
124 pub hidden_edge_alpha: f32,
127 pub vertex_color: Rgba,
128 pub vertex_selected_color: Rgba,
129 pub vertex_size_px: f32,
130 pub flat_shading: bool,
131 pub wireframe: bool,
135 pub axis_length_px: f32,
137 pub pick_double_sided: bool,
138 pub lod_factor: f64,
140 pub sketch_movable_color: Rgba,
145 pub sketch_locked_color: Rgba,
146 pub sketch_geometry_color: Rgba,
147 pub sketch_point_color: Rgba,
148 pub sketch_construction_point_color: Rgba,
149 pub sketch_under_constrained_point_color: Rgba,
150 pub sketch_selected_color: Rgba,
151 pub sketch_hovered_color: Rgba,
152 pub sketch_preview_color: Rgba,
153 pub sketch_constraint_color: Rgba,
154}
155
156impl Default for RenderSettings {
157 fn default() -> Self {
158 let sk = SketchColors::default();
159 Self {
160 background: [
161 ((0x0b) as f32) / 255.0,
162 ((0x0d) as f32) / 255.0,
163 ((0x10) as f32) / 255.0,
164 ],
165 face_color_mode: FaceColorMode::Uniform,
166 face_color: hex(0x00009e, 1.0),
167 face_selected_color: hex(0xffc400, 1.0),
168 hover_color: hex(0xfbff00, 1.0),
169 edge_color: hex(0x009dff, 1.0),
170 edge_selected_color: hex(0xff00ff, 1.0),
171 edge_width_px: 2.0,
172 hidden_edge_alpha: 0.22,
173 vertex_color: hex(0x4aff03, 1.0),
174 vertex_selected_color: hex(0x00ffff, 1.0),
175 vertex_size_px: 6.0,
176 flat_shading: false,
177 wireframe: false,
178 axis_length_px: 46.0,
179 pick_double_sided: true,
180 lod_factor: 1.0,
181 sketch_movable_color: hex(sk.movable, 1.0),
184 sketch_locked_color: hex(sk.locked, 1.0),
185 sketch_geometry_color: hex(sk.geometry, 1.0),
186 sketch_point_color: hex(sk.point, 1.0),
187 sketch_construction_point_color: hex(sk.construction_point, 1.0),
188 sketch_under_constrained_point_color: hex(sk.under_constrained_point, 1.0),
189 sketch_selected_color: hex(sk.selected, 1.0),
190 sketch_hovered_color: hex(sk.hovered, 1.0),
191 sketch_preview_color: hex(sk.preview, 1.0),
192 sketch_constraint_color: hex(sk.constraint, 1.0),
193 }
194 }
195}
196
197impl RenderSettings {
198 pub fn artifact() -> Self {
202 Self {
203 background: [
204 ((0x10) as f32) / 255.0,
205 ((0x14) as f32) / 255.0,
206 ((0x18) as f32) / 255.0,
207 ],
208 face_color_mode: FaceColorMode::HashedBySolid,
209 edge_color: hex(0x0d1030, 1.0),
210 edge_width_px: 1.6,
211 hidden_edge_alpha: 0.0,
212 vertex_size_px: 0.0,
213 axis_length_px: 0.0,
214 ..Self::default()
215 }
216 }
217
218 pub fn apply_json(&mut self, json: &str) -> Result<(), String> {
221 let value: serde_json::Value =
222 serde_json::from_str(json).map_err(|error| format!("settings parse: {error}"))?;
223 let color = |key: &str, target: &mut Rgba| {
224 if let Some(v) = value.get(key).and_then(|v| v.as_str()) {
225 if let Some(rgb) = parse_css_hex(v) {
226 target[0] = rgb[0];
227 target[1] = rgb[1];
228 target[2] = rgb[2];
229 }
230 }
231 };
232 color("faceColor", &mut self.face_color);
233 color("faceSelectedColor", &mut self.face_selected_color);
234 color("hoverColor", &mut self.hover_color);
235 color("edgeColor", &mut self.edge_color);
236 color("edgeSelectedColor", &mut self.edge_selected_color);
237 color("vertexColor", &mut self.vertex_color);
238 color("vertexSelectedColor", &mut self.vertex_selected_color);
239 color("sketchMovableColor", &mut self.sketch_movable_color);
241 color("sketchLockedColor", &mut self.sketch_locked_color);
242 color("sketchGeometryColor", &mut self.sketch_geometry_color);
243 color("sketchPointColor", &mut self.sketch_point_color);
244 color("sketchConstructionPointColor", &mut self.sketch_construction_point_color);
245 color("sketchUnderConstrainedPointColor", &mut self.sketch_under_constrained_point_color);
246 color("sketchSelectedColor", &mut self.sketch_selected_color);
247 color("sketchHoveredColor", &mut self.sketch_hovered_color);
248 color("sketchPreviewColor", &mut self.sketch_preview_color);
249 color("sketchConstraintColor", &mut self.sketch_constraint_color);
250 if let Some(v) = value.get("background").and_then(|v| v.as_str()) {
251 if let Some(rgb) = parse_css_hex(v) {
252 self.background = rgb;
253 }
254 }
255 if let Some(v) = value.get("edgeWidthPx").and_then(|v| v.as_f64()) {
256 self.edge_width_px = (v as f32).clamp(0.0, 32.0);
257 }
258 if let Some(v) = value.get("vertexSizePx").and_then(|v| v.as_f64()) {
259 self.vertex_size_px = (v as f32).clamp(0.0, 64.0);
260 }
261 if let Some(v) = value.get("hiddenEdgeAlpha").and_then(|v| v.as_f64()) {
262 self.hidden_edge_alpha = (v as f32).clamp(0.0, 1.0);
263 }
264 if let Some(v) = value.get("faceColorMode").and_then(|v| v.as_str()) {
265 match v.trim().to_ascii_lowercase().as_str() {
266 "hashedbysolid" | "hashed" => self.face_color_mode = FaceColorMode::HashedBySolid,
267 "uniform" => self.face_color_mode = FaceColorMode::Uniform,
268 _ => {}
269 }
270 }
271 if let Some(v) = value.get("flatShading").and_then(|v| v.as_bool()) {
272 self.flat_shading = v;
273 }
274 if let Some(v) = value.get("wireframe").and_then(|v| v.as_bool()) {
275 self.wireframe = v;
276 }
277 if let Some(v) = value.get("axisLengthPx").and_then(|v| v.as_f64()) {
278 self.axis_length_px = (v as f32).clamp(0.0, 512.0);
279 }
280 if let Some(v) = value.get("pickDoubleSided").and_then(|v| v.as_bool()) {
281 self.pick_double_sided = v;
282 }
283 if let Some(label) = value.get("renderQuality").and_then(|v| v.as_str()) {
287 if let Some(lod) = lod_from_quality(label) {
288 self.lod_factor = lod;
289 }
290 }
291 Ok(())
292 }
293
294 pub fn to_json(&self) -> String {
299 serde_json::json!({
300 "background": rgb_to_css_hex(self.background),
301 "faceColorMode": match self.face_color_mode {
302 FaceColorMode::Uniform => "uniform",
303 FaceColorMode::HashedBySolid => "hashedBySolid",
304 },
305 "faceColor": rgba_to_css_hex(self.face_color),
306 "faceSelectedColor": rgba_to_css_hex(self.face_selected_color),
307 "hoverColor": rgba_to_css_hex(self.hover_color),
308 "edgeColor": rgba_to_css_hex(self.edge_color),
309 "edgeSelectedColor": rgba_to_css_hex(self.edge_selected_color),
310 "edgeWidthPx": self.edge_width_px as f64,
311 "hiddenEdgeAlpha": self.hidden_edge_alpha as f64,
312 "vertexColor": rgba_to_css_hex(self.vertex_color),
313 "vertexSelectedColor": rgba_to_css_hex(self.vertex_selected_color),
314 "vertexSizePx": self.vertex_size_px as f64,
315 "flatShading": self.flat_shading,
316 "wireframe": self.wireframe,
317 "axisLengthPx": self.axis_length_px as f64,
318 "pickDoubleSided": self.pick_double_sided,
319 "renderQuality": quality_from_lod(self.lod_factor),
320 "sketchMovableColor": rgba_to_css_hex(self.sketch_movable_color),
321 "sketchLockedColor": rgba_to_css_hex(self.sketch_locked_color),
322 "sketchGeometryColor": rgba_to_css_hex(self.sketch_geometry_color),
323 "sketchPointColor": rgba_to_css_hex(self.sketch_point_color),
324 "sketchConstructionPointColor": rgba_to_css_hex(self.sketch_construction_point_color),
325 "sketchUnderConstrainedPointColor": rgba_to_css_hex(self.sketch_under_constrained_point_color),
326 "sketchSelectedColor": rgba_to_css_hex(self.sketch_selected_color),
327 "sketchHoveredColor": rgba_to_css_hex(self.sketch_hovered_color),
328 "sketchPreviewColor": rgba_to_css_hex(self.sketch_preview_color),
329 "sketchConstraintColor": rgba_to_css_hex(self.sketch_constraint_color),
330 })
331 .to_string()
332 }
333
334 pub fn sketch_colors(&self) -> SketchColors {
341 SketchColors {
342 movable: rgba_to_u32(self.sketch_movable_color),
343 locked: rgba_to_u32(self.sketch_locked_color),
344 geometry: rgba_to_u32(self.sketch_geometry_color),
345 point: rgba_to_u32(self.sketch_point_color),
346 construction_point: rgba_to_u32(self.sketch_construction_point_color),
347 under_constrained_point: rgba_to_u32(self.sketch_under_constrained_point_color),
348 selected: rgba_to_u32(self.sketch_selected_color),
349 hovered: rgba_to_u32(self.sketch_hovered_color),
350 preview: rgba_to_u32(self.sketch_preview_color),
351 constraint: rgba_to_u32(self.sketch_constraint_color),
352 }
353 }
354
355 pub fn settings_schema_json(&self) -> String {
361 let current: serde_json::Value =
362 serde_json::from_str(&self.to_json()).unwrap_or(serde_json::Value::Null);
363 let fields: Vec<serde_json::Value> = settings_schema()
364 .iter()
365 .map(|field| {
366 let kind = match &field.kind {
367 FieldKind::Color => serde_json::json!({ "type": "color" }),
368 FieldKind::Bool => serde_json::json!({ "type": "bool" }),
369 FieldKind::Enum { variants } => {
370 serde_json::json!({ "type": "enum", "variants": variants })
371 }
372 FieldKind::Number { min, max, step } => serde_json::json!({
373 "type": "number", "min": min, "max": max, "step": step
374 }),
375 FieldKind::Range { min, max, step } => serde_json::json!({
376 "type": "range", "min": min, "max": max, "step": step
377 }),
378 FieldKind::Scalar { step } => {
381 serde_json::json!({ "type": "scalar", "step": step })
382 }
383 FieldKind::Text { read_only } => {
384 serde_json::json!({ "type": "text", "readOnly": read_only })
385 }
386 FieldKind::Vec3 { step } => serde_json::json!({ "type": "vec3", "step": step }),
387 FieldKind::Reference { filter, multiple } => serde_json::json!({
388 "type": "reference", "filter": filter, "multiple": multiple
389 }),
390 FieldKind::Button { label } => {
391 serde_json::json!({ "type": "button", "label": label })
392 }
393 };
394 serde_json::json!({
395 "key": field.key,
396 "label": field.label,
397 "group": field.group,
398 "kind": kind,
399 "value": current.get(field.key),
400 })
401 })
402 .collect();
403 serde_json::json!({ "fields": fields }).to_string()
404 }
405}
406
407fn rgb_to_css_hex(rgb: [f32; 3]) -> String {
410 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u32;
411 format!("#{:02x}{:02x}{:02x}", q(rgb[0]), q(rgb[1]), q(rgb[2]))
412}
413
414fn rgba_to_css_hex(rgba: Rgba) -> String {
417 rgb_to_css_hex([rgba[0], rgba[1], rgba[2]])
418}
419
420fn rgba_to_u32(rgba: Rgba) -> u32 {
425 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u32;
426 (q(rgba[0]) << 16) | (q(rgba[1]) << 8) | q(rgba[2])
427}
428
429#[derive(Debug, Clone, PartialEq)]
433pub enum FieldKind {
434 Color,
436 Bool,
438 Enum { variants: Vec<&'static str> },
441 Number { min: f64, max: f64, step: f64 },
443 Range { min: f64, max: f64, step: f64 },
445 Scalar { step: f64 },
448 Text { read_only: bool },
452 Vec3 { step: f64 },
454 Reference { filter: Vec<String>, multiple: bool },
459 Button { label: String },
463}
464
465#[derive(Debug, Clone)]
470pub struct SettingsField {
471 pub key: &'static str,
473 pub label: &'static str,
474 pub group: &'static str,
475 pub kind: FieldKind,
476}
477
478#[derive(Debug, Clone)]
487pub struct FormField {
488 pub path: Vec<String>,
490 pub label: String,
491 pub group: String,
492 pub kind: FieldKind,
493}
494
495impl FormField {
496 pub fn key(&self) -> &str {
498 self.path.last().map(String::as_str).unwrap_or("")
499 }
500}
501
502pub const RENDER_QUALITY: &[(&str, f64)] = &[
508 ("Draft", 4.0),
509 ("Low", 2.0),
510 ("Medium", 1.0),
511 ("High", 0.5),
512 ("Ultra", 0.25),
513];
514
515fn lod_from_quality(label: &str) -> Option<f64> {
517 RENDER_QUALITY
518 .iter()
519 .find(|(name, _)| *name == label)
520 .map(|(_, factor)| *factor)
521}
522
523fn quality_from_lod(lod: f64) -> &'static str {
527 RENDER_QUALITY
528 .iter()
529 .min_by(|(_, a), (_, b)| {
530 (a - lod).abs().total_cmp(&(b - lod).abs())
531 })
532 .map(|(name, _)| *name)
533 .unwrap_or("Medium")
534}
535
536pub fn settings_form_fields() -> Vec<FormField> {
540 settings_schema()
541 .into_iter()
542 .map(|field| FormField {
543 path: vec![field.key.to_string()],
544 label: field.label.to_string(),
545 group: field.group.to_string(),
546 kind: field.kind,
547 })
548 .collect()
549}
550
551pub fn settings_schema() -> Vec<SettingsField> {
556 let f = |key, label, group, kind| SettingsField { key, label, group, kind };
557 vec![
558 f("background", "Background", "Scene", FieldKind::Color),
560 f(
561 "axisLengthPx",
562 "Axis length (px)",
563 "Scene",
564 FieldKind::Number { min: 0.0, max: 512.0, step: 1.0 },
565 ),
566 f(
567 "renderQuality",
568 "Render Quality",
569 "Scene",
570 FieldKind::Enum { variants: RENDER_QUALITY.iter().map(|(label, _)| *label).collect() },
571 ),
572 f(
574 "faceColorMode",
575 "Face color mode",
576 "Faces",
577 FieldKind::Enum { variants: vec!["uniform", "hashedBySolid"] },
578 ),
579 f("faceColor", "Face color", "Faces", FieldKind::Color),
580 f("faceSelectedColor", "Selected face", "Faces", FieldKind::Color),
581 f("hoverColor", "Hover", "Faces", FieldKind::Color),
582 f("flatShading", "Flat shading", "Faces", FieldKind::Bool),
583 f("wireframe", "Wireframe", "Faces", FieldKind::Bool),
584 f("edgeColor", "Edge color", "Edges", FieldKind::Color),
586 f("edgeSelectedColor", "Selected edge", "Edges", FieldKind::Color),
587 f(
588 "edgeWidthPx",
589 "Edge width (px)",
590 "Edges",
591 FieldKind::Number { min: 0.0, max: 32.0, step: 0.1 },
592 ),
593 f(
594 "hiddenEdgeAlpha",
595 "Hidden-edge alpha",
596 "Edges",
597 FieldKind::Range { min: 0.0, max: 1.0, step: 0.01 },
598 ),
599 f("vertexColor", "Vertex color", "Vertices", FieldKind::Color),
601 f("vertexSelectedColor", "Selected vertex", "Vertices", FieldKind::Color),
602 f(
603 "vertexSizePx",
604 "Vertex size (px)",
605 "Vertices",
606 FieldKind::Number { min: 0.0, max: 64.0, step: 0.5 },
607 ),
608 f("pickDoubleSided", "Pick double-sided", "Picking", FieldKind::Bool),
610 f("sketchMovableColor", "Movable geometry", "Sketch", FieldKind::Color),
614 f("sketchLockedColor", "Locked geometry", "Sketch", FieldKind::Color),
615 f("sketchGeometryColor", "Geometry (no mobility)", "Sketch", FieldKind::Color),
616 f("sketchPointColor", "Point", "Sketch", FieldKind::Color),
617 f("sketchConstructionPointColor", "Construction point", "Sketch", FieldKind::Color),
618 f("sketchUnderConstrainedPointColor", "Under-constrained point", "Sketch", FieldKind::Color),
619 f("sketchSelectedColor", "Selected", "Sketch", FieldKind::Color),
620 f("sketchHoveredColor", "Hovered", "Sketch", FieldKind::Color),
621 f("sketchPreviewColor", "Draw preview", "Sketch", FieldKind::Color),
622 f("sketchConstraintColor", "Constraint / dimension", "Sketch", FieldKind::Color),
623 ]
624}
625
626#[derive(Debug, Clone)]
629pub struct VertexRef {
630 pub solid: String,
631 pub position: [f64; 3],
632}
633
634#[derive(Debug, Default)]
638pub struct Emphasis {
639 pub selected_solids: HashSet<String>,
640 pub selected_faces: HashSet<String>,
641 pub selected_edges: HashSet<String>,
642 pub selected_vertices: Vec<VertexRef>,
643 pub selected_datums: HashSet<String>,
649 pub hovered_solids: HashSet<String>,
650 pub hovered_faces: HashSet<String>,
651 pub hovered_edges: HashSet<String>,
652 pub hovered_vertices: Vec<VertexRef>,
653 pub generation: u64,
655}
656
657impl Emphasis {
658 pub fn is_empty(&self) -> bool {
659 self.selected_solids.is_empty()
660 && self.selected_faces.is_empty()
661 && self.selected_edges.is_empty()
662 && self.selected_vertices.is_empty()
663 && self.selected_datums.is_empty()
664 && self.hovered_solids.is_empty()
665 && self.hovered_faces.is_empty()
666 && self.hovered_edges.is_empty()
667 && self.hovered_vertices.is_empty()
668 }
669
670 pub fn apply_json(&mut self, json: &str) -> Result<(), String> {
673 let value: serde_json::Value =
674 serde_json::from_str(json).map_err(|error| format!("emphasis parse: {error}"))?;
675 let names = |group: &serde_json::Value, key: &str| -> HashSet<String> {
676 group
677 .get(key)
678 .and_then(|v| v.as_array())
679 .map(|list| {
680 list.iter()
681 .filter_map(|v| v.as_str().map(str::to_string))
682 .collect()
683 })
684 .unwrap_or_default()
685 };
686 let vertices = |group: &serde_json::Value| -> Vec<VertexRef> {
687 group
688 .get("vertices")
689 .and_then(|v| v.as_array())
690 .map(|list| {
691 list.iter()
692 .filter_map(|v| {
693 let solid = v.get("solid")?.as_str()?.to_string();
694 let p = v.get("position")?.as_array()?;
695 Some(VertexRef {
696 solid,
697 position: [
698 p.first()?.as_f64()?,
699 p.get(1)?.as_f64()?,
700 p.get(2)?.as_f64()?,
701 ],
702 })
703 })
704 .collect()
705 })
706 .unwrap_or_default()
707 };
708 let empty = serde_json::json!({});
709 let selected = value.get("selected").unwrap_or(&empty);
710 let hovered = value.get("hovered").unwrap_or(&empty);
711 self.selected_solids = names(selected, "solids");
712 self.selected_faces = names(selected, "faces");
713 self.selected_edges = names(selected, "edges");
714 self.selected_datums = names(selected, "datums");
715 self.selected_vertices = vertices(selected);
716 self.hovered_solids = names(hovered, "solids");
717 self.hovered_faces = names(hovered, "faces");
718 self.hovered_edges = names(hovered, "edges");
719 self.hovered_vertices = vertices(hovered);
720 self.generation = self.generation.wrapping_add(1);
721 Ok(())
722 }
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub enum EmphasisState {
729 Base,
730 Selected,
731 Hovered,
732}
733
734impl Emphasis {
735 pub fn face_state(&self, solid: &str, face: &str) -> EmphasisState {
736 if self.hovered_solids.contains(solid) || (!face.is_empty() && self.hovered_faces.contains(face)) {
737 EmphasisState::Hovered
738 } else if self.selected_solids.contains(solid)
739 || (!face.is_empty() && self.selected_faces.contains(face))
740 {
741 EmphasisState::Selected
742 } else {
743 EmphasisState::Base
744 }
745 }
746
747 pub fn edge_state(&self, solid: &str, edge: &str) -> EmphasisState {
748 if self.hovered_solids.contains(solid) || (!edge.is_empty() && self.hovered_edges.contains(edge)) {
749 EmphasisState::Hovered
750 } else if self.selected_solids.contains(solid)
751 || (!edge.is_empty() && self.selected_edges.contains(edge))
752 {
753 EmphasisState::Selected
754 } else {
755 EmphasisState::Base
756 }
757 }
758
759 pub fn vertex_state(&self, solid: &str, position: [f64; 3], tol: f64) -> EmphasisState {
760 let matches = |refs: &[VertexRef]| {
761 refs.iter().any(|r| {
762 r.solid == solid
763 && (r.position[0] - position[0]).abs() <= tol
764 && (r.position[1] - position[1]).abs() <= tol
765 && (r.position[2] - position[2]).abs() <= tol
766 })
767 };
768 if matches(&self.hovered_vertices) {
769 EmphasisState::Hovered
770 } else if matches(&self.selected_vertices) {
771 EmphasisState::Selected
772 } else {
773 EmphasisState::Base
774 }
775 }
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781
782 #[test]
783 fn settings_json_overrides() {
784 let mut settings = RenderSettings::default();
785 settings
786 .apply_json(
787 r##"{"faceColor": "#ff0000", "edgeWidthPx": 4.5, "flatShading": true,
788 "hoverColor": "#0f0", "unknownKey": 1}"##,
789 )
790 .unwrap();
791 assert_eq!(settings.face_color[0], 1.0);
792 assert_eq!(settings.face_color[1], 0.0);
793 assert_eq!(settings.edge_width_px, 4.5);
794 assert!(settings.flat_shading);
795 assert_eq!(settings.hover_color[1], 1.0);
796 }
797
798 #[test]
799 fn settings_json_face_color_mode_and_wireframe() {
800 let mut settings = RenderSettings::default();
801 assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
803 assert!(!settings.wireframe);
804 settings
805 .apply_json(r##"{"faceColorMode": "hashedBySolid", "wireframe": true}"##)
806 .unwrap();
807 assert_eq!(settings.face_color_mode, FaceColorMode::HashedBySolid);
808 assert!(settings.wireframe);
809 settings
811 .apply_json(r##"{"faceColorMode": "uniform", "wireframe": false}"##)
812 .unwrap();
813 assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
814 assert!(!settings.wireframe);
815 settings.apply_json(r##"{"faceColorMode": "bogus"}"##).unwrap();
817 assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
818 }
819
820 #[test]
821 fn emphasis_states_cascade_from_solid() {
822 let mut emphasis = Emphasis::default();
823 emphasis
824 .apply_json(
825 r#"{"selected": {"solids": ["A"], "faces": ["F1"]},
826 "hovered": {"edges": ["E1"], "vertices": [{"solid": "A", "position": [1, 2, 3]}]}}"#,
827 )
828 .unwrap();
829 assert_eq!(emphasis.face_state("A", "anything"), EmphasisState::Selected);
830 assert_eq!(emphasis.face_state("B", "F1"), EmphasisState::Selected);
831 assert_eq!(emphasis.face_state("B", "F2"), EmphasisState::Base);
832 assert_eq!(emphasis.edge_state("B", "E1"), EmphasisState::Hovered);
833 assert_eq!(
834 emphasis.vertex_state("A", [1.0, 2.0, 3.0], 1e-9),
835 EmphasisState::Hovered
836 );
837 assert_eq!(
838 emphasis.vertex_state("B", [1.0, 2.0, 3.0], 1e-9),
839 EmphasisState::Base
840 );
841 let gen0 = emphasis.generation;
842 emphasis.apply_json(r#"{}"#).unwrap();
843 assert!(emphasis.is_empty());
844 assert_ne!(emphasis.generation, gen0);
845 }
846
847 #[test]
848 fn settings_to_json_roundtrip_is_identity() {
849 let mut s = RenderSettings::default();
852 s.face_color = hex(0x123456, 1.0);
853 s.edge_color = hex(0xabcdef, 1.0);
854 s.background = [0.0, 0.0, 0.0];
855 s.face_color_mode = FaceColorMode::HashedBySolid;
856 s.edge_width_px = 3.5;
857 s.hidden_edge_alpha = 0.5;
858 s.vertex_size_px = 8.0;
859 s.axis_length_px = 30.0;
860 s.lod_factor = 2.0;
864 s.flat_shading = true;
865 s.wireframe = true;
866 s.pick_double_sided = false;
867 s.sketch_movable_color = hex(0x112233, 1.0);
869 s.sketch_constraint_color = hex(0x00ff00, 1.0);
870 s.sketch_selected_color = hex(0xfedcba, 1.0);
871
872 let mut identity = s.clone();
875 identity.apply_json(&s.to_json()).unwrap();
876 assert_eq!(identity, s);
877
878 let mut rebuilt = RenderSettings::default();
881 rebuilt.apply_json(&s.to_json()).unwrap();
882 assert_eq!(rebuilt, s);
883 }
884
885 #[test]
886 fn settings_schema_covers_every_json_key() {
887 let json: serde_json::Value =
890 serde_json::from_str(&RenderSettings::default().to_json()).unwrap();
891 for field in settings_schema() {
892 assert!(
893 json.get(field.key).is_some(),
894 "schema field {} has no to_json value",
895 field.key
896 );
897 }
898 let mut s = RenderSettings::default();
900 s.wireframe = true;
901 let export: serde_json::Value = serde_json::from_str(&s.settings_schema_json()).unwrap();
902 let wf = export["fields"]
903 .as_array()
904 .unwrap()
905 .iter()
906 .find(|f| f["key"] == "wireframe")
907 .unwrap();
908 assert_eq!(wf["value"], serde_json::json!(true));
909 }
910
911 #[test]
912 fn sketch_colors_default_matches_the_hex_constants() {
913 assert_eq!(RenderSettings::default().sketch_colors(), SketchColors::default());
917 }
918
919 #[test]
920 fn sketch_colors_follow_apply_json() {
921 let mut s = RenderSettings::default();
924 s.apply_json(r##"{"sketchConstraintColor": "#ff0000", "sketchMovableColor": "#00ff00"}"##)
925 .unwrap();
926 let c = s.sketch_colors();
927 assert_eq!(c.constraint, 0xff0000);
928 assert_eq!(c.movable, 0x00ff00);
929 assert_eq!(c.locked, SketchColors::default().locked);
931 }
932
933 #[test]
934 fn css_hex_parsing() {
935 assert_eq!(parse_css_hex("#ffffff"), Some([1.0, 1.0, 1.0]));
936 assert_eq!(parse_css_hex("#f00"), Some([1.0, 0.0, 0.0]));
937 assert!(parse_css_hex("red").is_none());
938 assert_eq!(parse_css_hex("0x00009e").map(|c| c[2]), Some(0x9e as f32 / 255.0));
939 }
940}