use std::collections::HashSet;
pub type Rgba = [f32; 4];
fn hex(hex: u32, alpha: f32) -> Rgba {
[
((hex >> 16) & 0xff) as f32 / 255.0,
((hex >> 8) & 0xff) as f32 / 255.0,
(hex & 0xff) as f32 / 255.0,
alpha,
]
}
pub fn parse_css_hex(value: &str) -> Option<[f32; 3]> {
let v = value.trim();
let digits = v
.strip_prefix('#')
.or_else(|| v.strip_prefix("0x"))
.or_else(|| v.strip_prefix("0X"))?;
let expand = |c: char| c.to_digit(16).map(|d| (d * 17) as f32 / 255.0);
match digits.len() {
3 => {
let mut chars = digits.chars();
Some([
expand(chars.next()?)?,
expand(chars.next()?)?,
expand(chars.next()?)?,
])
}
6 => {
let n = u32::from_str_radix(digits, 16).ok()?;
Some([
((n >> 16) & 0xff) as f32 / 255.0,
((n >> 8) & 0xff) as f32 / 255.0,
(n & 0xff) as f32 / 255.0,
])
}
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SketchColors {
pub movable: u32,
pub locked: u32,
pub geometry: u32,
pub point: u32,
pub construction_point: u32,
pub under_constrained_point: u32,
pub selected: u32,
pub hovered: u32,
pub preview: u32,
pub constraint: u32,
}
impl Default for SketchColors {
fn default() -> Self {
Self {
movable: 0x4aa3ff,
locked: 0xe6ebf2,
geometry: 0xffff88,
point: 0x9ec9ff,
construction_point: 0xffa86a,
under_constrained_point: 0xffb347,
selected: 0xffa500,
hovered: 0x7fd0ff,
preview: 0x8fa0b8,
constraint: 0x4ade80,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaceColorMode {
Uniform,
HashedBySolid,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderSettings {
pub background: [f32; 3],
pub face_color_mode: FaceColorMode,
pub face_color: Rgba,
pub face_selected_color: Rgba,
pub hover_color: Rgba,
pub edge_color: Rgba,
pub edge_selected_color: Rgba,
pub edge_width_px: f32,
pub hidden_edge_alpha: f32,
pub vertex_color: Rgba,
pub vertex_selected_color: Rgba,
pub vertex_size_px: f32,
pub flat_shading: bool,
pub wireframe: bool,
pub axis_length_px: f32,
pub pick_double_sided: bool,
pub lod_factor: f64,
pub sketch_movable_color: Rgba,
pub sketch_locked_color: Rgba,
pub sketch_geometry_color: Rgba,
pub sketch_point_color: Rgba,
pub sketch_construction_point_color: Rgba,
pub sketch_under_constrained_point_color: Rgba,
pub sketch_selected_color: Rgba,
pub sketch_hovered_color: Rgba,
pub sketch_preview_color: Rgba,
pub sketch_constraint_color: Rgba,
}
impl Default for RenderSettings {
fn default() -> Self {
let sk = SketchColors::default();
Self {
background: [
((0x0b) as f32) / 255.0,
((0x0d) as f32) / 255.0,
((0x10) as f32) / 255.0,
],
face_color_mode: FaceColorMode::Uniform,
face_color: hex(0x00009e, 1.0),
face_selected_color: hex(0xffc400, 1.0),
hover_color: hex(0xfbff00, 1.0),
edge_color: hex(0x009dff, 1.0),
edge_selected_color: hex(0xff00ff, 1.0),
edge_width_px: 2.0,
hidden_edge_alpha: 0.22,
vertex_color: hex(0x4aff03, 1.0),
vertex_selected_color: hex(0x00ffff, 1.0),
vertex_size_px: 6.0,
flat_shading: false,
wireframe: false,
axis_length_px: 46.0,
pick_double_sided: true,
lod_factor: 1.0,
sketch_movable_color: hex(sk.movable, 1.0),
sketch_locked_color: hex(sk.locked, 1.0),
sketch_geometry_color: hex(sk.geometry, 1.0),
sketch_point_color: hex(sk.point, 1.0),
sketch_construction_point_color: hex(sk.construction_point, 1.0),
sketch_under_constrained_point_color: hex(sk.under_constrained_point, 1.0),
sketch_selected_color: hex(sk.selected, 1.0),
sketch_hovered_color: hex(sk.hovered, 1.0),
sketch_preview_color: hex(sk.preview, 1.0),
sketch_constraint_color: hex(sk.constraint, 1.0),
}
}
}
impl RenderSettings {
pub fn artifact() -> Self {
Self {
background: [
((0x10) as f32) / 255.0,
((0x14) as f32) / 255.0,
((0x18) as f32) / 255.0,
],
face_color_mode: FaceColorMode::HashedBySolid,
edge_color: hex(0x0d1030, 1.0),
edge_width_px: 1.6,
hidden_edge_alpha: 0.0,
vertex_size_px: 0.0,
axis_length_px: 0.0,
..Self::default()
}
}
pub fn apply_json(&mut self, json: &str) -> Result<(), String> {
let value: serde_json::Value =
serde_json::from_str(json).map_err(|error| format!("settings parse: {error}"))?;
let color = |key: &str, target: &mut Rgba| {
if let Some(v) = value.get(key).and_then(|v| v.as_str()) {
if let Some(rgb) = parse_css_hex(v) {
target[0] = rgb[0];
target[1] = rgb[1];
target[2] = rgb[2];
}
}
};
color("faceColor", &mut self.face_color);
color("faceSelectedColor", &mut self.face_selected_color);
color("hoverColor", &mut self.hover_color);
color("edgeColor", &mut self.edge_color);
color("edgeSelectedColor", &mut self.edge_selected_color);
color("vertexColor", &mut self.vertex_color);
color("vertexSelectedColor", &mut self.vertex_selected_color);
color("sketchMovableColor", &mut self.sketch_movable_color);
color("sketchLockedColor", &mut self.sketch_locked_color);
color("sketchGeometryColor", &mut self.sketch_geometry_color);
color("sketchPointColor", &mut self.sketch_point_color);
color("sketchConstructionPointColor", &mut self.sketch_construction_point_color);
color("sketchUnderConstrainedPointColor", &mut self.sketch_under_constrained_point_color);
color("sketchSelectedColor", &mut self.sketch_selected_color);
color("sketchHoveredColor", &mut self.sketch_hovered_color);
color("sketchPreviewColor", &mut self.sketch_preview_color);
color("sketchConstraintColor", &mut self.sketch_constraint_color);
if let Some(v) = value.get("background").and_then(|v| v.as_str()) {
if let Some(rgb) = parse_css_hex(v) {
self.background = rgb;
}
}
if let Some(v) = value.get("edgeWidthPx").and_then(|v| v.as_f64()) {
self.edge_width_px = (v as f32).clamp(0.0, 32.0);
}
if let Some(v) = value.get("vertexSizePx").and_then(|v| v.as_f64()) {
self.vertex_size_px = (v as f32).clamp(0.0, 64.0);
}
if let Some(v) = value.get("hiddenEdgeAlpha").and_then(|v| v.as_f64()) {
self.hidden_edge_alpha = (v as f32).clamp(0.0, 1.0);
}
if let Some(v) = value.get("faceColorMode").and_then(|v| v.as_str()) {
match v.trim().to_ascii_lowercase().as_str() {
"hashedbysolid" | "hashed" => self.face_color_mode = FaceColorMode::HashedBySolid,
"uniform" => self.face_color_mode = FaceColorMode::Uniform,
_ => {}
}
}
if let Some(v) = value.get("flatShading").and_then(|v| v.as_bool()) {
self.flat_shading = v;
}
if let Some(v) = value.get("wireframe").and_then(|v| v.as_bool()) {
self.wireframe = v;
}
if let Some(v) = value.get("axisLengthPx").and_then(|v| v.as_f64()) {
self.axis_length_px = (v as f32).clamp(0.0, 512.0);
}
if let Some(v) = value.get("pickDoubleSided").and_then(|v| v.as_bool()) {
self.pick_double_sided = v;
}
if let Some(label) = value.get("renderQuality").and_then(|v| v.as_str()) {
if let Some(lod) = lod_from_quality(label) {
self.lod_factor = lod;
}
}
Ok(())
}
pub fn to_json(&self) -> String {
serde_json::json!({
"background": rgb_to_css_hex(self.background),
"faceColorMode": match self.face_color_mode {
FaceColorMode::Uniform => "uniform",
FaceColorMode::HashedBySolid => "hashedBySolid",
},
"faceColor": rgba_to_css_hex(self.face_color),
"faceSelectedColor": rgba_to_css_hex(self.face_selected_color),
"hoverColor": rgba_to_css_hex(self.hover_color),
"edgeColor": rgba_to_css_hex(self.edge_color),
"edgeSelectedColor": rgba_to_css_hex(self.edge_selected_color),
"edgeWidthPx": self.edge_width_px as f64,
"hiddenEdgeAlpha": self.hidden_edge_alpha as f64,
"vertexColor": rgba_to_css_hex(self.vertex_color),
"vertexSelectedColor": rgba_to_css_hex(self.vertex_selected_color),
"vertexSizePx": self.vertex_size_px as f64,
"flatShading": self.flat_shading,
"wireframe": self.wireframe,
"axisLengthPx": self.axis_length_px as f64,
"pickDoubleSided": self.pick_double_sided,
"renderQuality": quality_from_lod(self.lod_factor),
"sketchMovableColor": rgba_to_css_hex(self.sketch_movable_color),
"sketchLockedColor": rgba_to_css_hex(self.sketch_locked_color),
"sketchGeometryColor": rgba_to_css_hex(self.sketch_geometry_color),
"sketchPointColor": rgba_to_css_hex(self.sketch_point_color),
"sketchConstructionPointColor": rgba_to_css_hex(self.sketch_construction_point_color),
"sketchUnderConstrainedPointColor": rgba_to_css_hex(self.sketch_under_constrained_point_color),
"sketchSelectedColor": rgba_to_css_hex(self.sketch_selected_color),
"sketchHoveredColor": rgba_to_css_hex(self.sketch_hovered_color),
"sketchPreviewColor": rgba_to_css_hex(self.sketch_preview_color),
"sketchConstraintColor": rgba_to_css_hex(self.sketch_constraint_color),
})
.to_string()
}
pub fn sketch_colors(&self) -> SketchColors {
SketchColors {
movable: rgba_to_u32(self.sketch_movable_color),
locked: rgba_to_u32(self.sketch_locked_color),
geometry: rgba_to_u32(self.sketch_geometry_color),
point: rgba_to_u32(self.sketch_point_color),
construction_point: rgba_to_u32(self.sketch_construction_point_color),
under_constrained_point: rgba_to_u32(self.sketch_under_constrained_point_color),
selected: rgba_to_u32(self.sketch_selected_color),
hovered: rgba_to_u32(self.sketch_hovered_color),
preview: rgba_to_u32(self.sketch_preview_color),
constraint: rgba_to_u32(self.sketch_constraint_color),
}
}
pub fn settings_schema_json(&self) -> String {
let current: serde_json::Value =
serde_json::from_str(&self.to_json()).unwrap_or(serde_json::Value::Null);
let fields: Vec<serde_json::Value> = settings_schema()
.iter()
.map(|field| {
let kind = match &field.kind {
FieldKind::Color => serde_json::json!({ "type": "color" }),
FieldKind::Bool => serde_json::json!({ "type": "bool" }),
FieldKind::Enum { variants } => {
serde_json::json!({ "type": "enum", "variants": variants })
}
FieldKind::Number { min, max, step } => serde_json::json!({
"type": "number", "min": min, "max": max, "step": step
}),
FieldKind::Range { min, max, step } => serde_json::json!({
"type": "range", "min": min, "max": max, "step": step
}),
FieldKind::Scalar { step } => {
serde_json::json!({ "type": "scalar", "step": step })
}
FieldKind::Text { read_only } => {
serde_json::json!({ "type": "text", "readOnly": read_only })
}
FieldKind::Vec3 { step } => serde_json::json!({ "type": "vec3", "step": step }),
FieldKind::Reference { filter, multiple } => serde_json::json!({
"type": "reference", "filter": filter, "multiple": multiple
}),
FieldKind::Button { label } => {
serde_json::json!({ "type": "button", "label": label })
}
};
serde_json::json!({
"key": field.key,
"label": field.label,
"group": field.group,
"kind": kind,
"value": current.get(field.key),
})
})
.collect();
serde_json::json!({ "fields": fields }).to_string()
}
}
fn rgb_to_css_hex(rgb: [f32; 3]) -> String {
let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u32;
format!("#{:02x}{:02x}{:02x}", q(rgb[0]), q(rgb[1]), q(rgb[2]))
}
fn rgba_to_css_hex(rgba: Rgba) -> String {
rgb_to_css_hex([rgba[0], rgba[1], rgba[2]])
}
fn rgba_to_u32(rgba: Rgba) -> u32 {
let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u32;
(q(rgba[0]) << 16) | (q(rgba[1]) << 8) | q(rgba[2])
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldKind {
Color,
Bool,
Enum { variants: Vec<&'static str> },
Number { min: f64, max: f64, step: f64 },
Range { min: f64, max: f64, step: f64 },
Scalar { step: f64 },
Text { read_only: bool },
Vec3 { step: f64 },
Reference { filter: Vec<String>, multiple: bool },
Button { label: String },
}
#[derive(Debug, Clone)]
pub struct SettingsField {
pub key: &'static str,
pub label: &'static str,
pub group: &'static str,
pub kind: FieldKind,
}
#[derive(Debug, Clone)]
pub struct FormField {
pub path: Vec<String>,
pub label: String,
pub group: String,
pub kind: FieldKind,
}
impl FormField {
pub fn key(&self) -> &str {
self.path.last().map(String::as_str).unwrap_or("")
}
}
pub const RENDER_QUALITY: &[(&str, f64)] = &[
("Draft", 4.0),
("Low", 2.0),
("Medium", 1.0),
("High", 0.5),
("Ultra", 0.25),
];
fn lod_from_quality(label: &str) -> Option<f64> {
RENDER_QUALITY
.iter()
.find(|(name, _)| *name == label)
.map(|(_, factor)| *factor)
}
fn quality_from_lod(lod: f64) -> &'static str {
RENDER_QUALITY
.iter()
.min_by(|(_, a), (_, b)| {
(a - lod).abs().total_cmp(&(b - lod).abs())
})
.map(|(name, _)| *name)
.unwrap_or("Medium")
}
pub fn settings_form_fields() -> Vec<FormField> {
settings_schema()
.into_iter()
.map(|field| FormField {
path: vec![field.key.to_string()],
label: field.label.to_string(),
group: field.group.to_string(),
kind: field.kind,
})
.collect()
}
pub fn settings_schema() -> Vec<SettingsField> {
let f = |key, label, group, kind| SettingsField { key, label, group, kind };
vec![
f("background", "Background", "Scene", FieldKind::Color),
f(
"axisLengthPx",
"Axis length (px)",
"Scene",
FieldKind::Number { min: 0.0, max: 512.0, step: 1.0 },
),
f(
"renderQuality",
"Render Quality",
"Scene",
FieldKind::Enum { variants: RENDER_QUALITY.iter().map(|(label, _)| *label).collect() },
),
f(
"faceColorMode",
"Face color mode",
"Faces",
FieldKind::Enum { variants: vec!["uniform", "hashedBySolid"] },
),
f("faceColor", "Face color", "Faces", FieldKind::Color),
f("faceSelectedColor", "Selected face", "Faces", FieldKind::Color),
f("hoverColor", "Hover", "Faces", FieldKind::Color),
f("flatShading", "Flat shading", "Faces", FieldKind::Bool),
f("wireframe", "Wireframe", "Faces", FieldKind::Bool),
f("edgeColor", "Edge color", "Edges", FieldKind::Color),
f("edgeSelectedColor", "Selected edge", "Edges", FieldKind::Color),
f(
"edgeWidthPx",
"Edge width (px)",
"Edges",
FieldKind::Number { min: 0.0, max: 32.0, step: 0.1 },
),
f(
"hiddenEdgeAlpha",
"Hidden-edge alpha",
"Edges",
FieldKind::Range { min: 0.0, max: 1.0, step: 0.01 },
),
f("vertexColor", "Vertex color", "Vertices", FieldKind::Color),
f("vertexSelectedColor", "Selected vertex", "Vertices", FieldKind::Color),
f(
"vertexSizePx",
"Vertex size (px)",
"Vertices",
FieldKind::Number { min: 0.0, max: 64.0, step: 0.5 },
),
f("pickDoubleSided", "Pick double-sided", "Picking", FieldKind::Bool),
f("sketchMovableColor", "Movable geometry", "Sketch", FieldKind::Color),
f("sketchLockedColor", "Locked geometry", "Sketch", FieldKind::Color),
f("sketchGeometryColor", "Geometry (no mobility)", "Sketch", FieldKind::Color),
f("sketchPointColor", "Point", "Sketch", FieldKind::Color),
f("sketchConstructionPointColor", "Construction point", "Sketch", FieldKind::Color),
f("sketchUnderConstrainedPointColor", "Under-constrained point", "Sketch", FieldKind::Color),
f("sketchSelectedColor", "Selected", "Sketch", FieldKind::Color),
f("sketchHoveredColor", "Hovered", "Sketch", FieldKind::Color),
f("sketchPreviewColor", "Draw preview", "Sketch", FieldKind::Color),
f("sketchConstraintColor", "Constraint / dimension", "Sketch", FieldKind::Color),
]
}
#[derive(Debug, Clone)]
pub struct VertexRef {
pub solid: String,
pub position: [f64; 3],
}
#[derive(Debug, Default)]
pub struct Emphasis {
pub selected_solids: HashSet<String>,
pub selected_faces: HashSet<String>,
pub selected_edges: HashSet<String>,
pub selected_vertices: Vec<VertexRef>,
pub selected_datums: HashSet<String>,
pub hovered_solids: HashSet<String>,
pub hovered_faces: HashSet<String>,
pub hovered_edges: HashSet<String>,
pub hovered_vertices: Vec<VertexRef>,
pub generation: u64,
}
impl Emphasis {
pub fn is_empty(&self) -> bool {
self.selected_solids.is_empty()
&& self.selected_faces.is_empty()
&& self.selected_edges.is_empty()
&& self.selected_vertices.is_empty()
&& self.selected_datums.is_empty()
&& self.hovered_solids.is_empty()
&& self.hovered_faces.is_empty()
&& self.hovered_edges.is_empty()
&& self.hovered_vertices.is_empty()
}
pub fn apply_json(&mut self, json: &str) -> Result<(), String> {
let value: serde_json::Value =
serde_json::from_str(json).map_err(|error| format!("emphasis parse: {error}"))?;
let names = |group: &serde_json::Value, key: &str| -> HashSet<String> {
group
.get(key)
.and_then(|v| v.as_array())
.map(|list| {
list.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
};
let vertices = |group: &serde_json::Value| -> Vec<VertexRef> {
group
.get("vertices")
.and_then(|v| v.as_array())
.map(|list| {
list.iter()
.filter_map(|v| {
let solid = v.get("solid")?.as_str()?.to_string();
let p = v.get("position")?.as_array()?;
Some(VertexRef {
solid,
position: [
p.first()?.as_f64()?,
p.get(1)?.as_f64()?,
p.get(2)?.as_f64()?,
],
})
})
.collect()
})
.unwrap_or_default()
};
let empty = serde_json::json!({});
let selected = value.get("selected").unwrap_or(&empty);
let hovered = value.get("hovered").unwrap_or(&empty);
self.selected_solids = names(selected, "solids");
self.selected_faces = names(selected, "faces");
self.selected_edges = names(selected, "edges");
self.selected_datums = names(selected, "datums");
self.selected_vertices = vertices(selected);
self.hovered_solids = names(hovered, "solids");
self.hovered_faces = names(hovered, "faces");
self.hovered_edges = names(hovered, "edges");
self.hovered_vertices = vertices(hovered);
self.generation = self.generation.wrapping_add(1);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmphasisState {
Base,
Selected,
Hovered,
}
impl Emphasis {
pub fn face_state(&self, solid: &str, face: &str) -> EmphasisState {
if self.hovered_solids.contains(solid) || (!face.is_empty() && self.hovered_faces.contains(face)) {
EmphasisState::Hovered
} else if self.selected_solids.contains(solid)
|| (!face.is_empty() && self.selected_faces.contains(face))
{
EmphasisState::Selected
} else {
EmphasisState::Base
}
}
pub fn edge_state(&self, solid: &str, edge: &str) -> EmphasisState {
if self.hovered_solids.contains(solid) || (!edge.is_empty() && self.hovered_edges.contains(edge)) {
EmphasisState::Hovered
} else if self.selected_solids.contains(solid)
|| (!edge.is_empty() && self.selected_edges.contains(edge))
{
EmphasisState::Selected
} else {
EmphasisState::Base
}
}
pub fn vertex_state(&self, solid: &str, position: [f64; 3], tol: f64) -> EmphasisState {
let matches = |refs: &[VertexRef]| {
refs.iter().any(|r| {
r.solid == solid
&& (r.position[0] - position[0]).abs() <= tol
&& (r.position[1] - position[1]).abs() <= tol
&& (r.position[2] - position[2]).abs() <= tol
})
};
if matches(&self.hovered_vertices) {
EmphasisState::Hovered
} else if matches(&self.selected_vertices) {
EmphasisState::Selected
} else {
EmphasisState::Base
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_json_overrides() {
let mut settings = RenderSettings::default();
settings
.apply_json(
r##"{"faceColor": "#ff0000", "edgeWidthPx": 4.5, "flatShading": true,
"hoverColor": "#0f0", "unknownKey": 1}"##,
)
.unwrap();
assert_eq!(settings.face_color[0], 1.0);
assert_eq!(settings.face_color[1], 0.0);
assert_eq!(settings.edge_width_px, 4.5);
assert!(settings.flat_shading);
assert_eq!(settings.hover_color[1], 1.0);
}
#[test]
fn settings_json_face_color_mode_and_wireframe() {
let mut settings = RenderSettings::default();
assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
assert!(!settings.wireframe);
settings
.apply_json(r##"{"faceColorMode": "hashedBySolid", "wireframe": true}"##)
.unwrap();
assert_eq!(settings.face_color_mode, FaceColorMode::HashedBySolid);
assert!(settings.wireframe);
settings
.apply_json(r##"{"faceColorMode": "uniform", "wireframe": false}"##)
.unwrap();
assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
assert!(!settings.wireframe);
settings.apply_json(r##"{"faceColorMode": "bogus"}"##).unwrap();
assert_eq!(settings.face_color_mode, FaceColorMode::Uniform);
}
#[test]
fn emphasis_states_cascade_from_solid() {
let mut emphasis = Emphasis::default();
emphasis
.apply_json(
r#"{"selected": {"solids": ["A"], "faces": ["F1"]},
"hovered": {"edges": ["E1"], "vertices": [{"solid": "A", "position": [1, 2, 3]}]}}"#,
)
.unwrap();
assert_eq!(emphasis.face_state("A", "anything"), EmphasisState::Selected);
assert_eq!(emphasis.face_state("B", "F1"), EmphasisState::Selected);
assert_eq!(emphasis.face_state("B", "F2"), EmphasisState::Base);
assert_eq!(emphasis.edge_state("B", "E1"), EmphasisState::Hovered);
assert_eq!(
emphasis.vertex_state("A", [1.0, 2.0, 3.0], 1e-9),
EmphasisState::Hovered
);
assert_eq!(
emphasis.vertex_state("B", [1.0, 2.0, 3.0], 1e-9),
EmphasisState::Base
);
let gen0 = emphasis.generation;
emphasis.apply_json(r#"{}"#).unwrap();
assert!(emphasis.is_empty());
assert_ne!(emphasis.generation, gen0);
}
#[test]
fn settings_to_json_roundtrip_is_identity() {
let mut s = RenderSettings::default();
s.face_color = hex(0x123456, 1.0);
s.edge_color = hex(0xabcdef, 1.0);
s.background = [0.0, 0.0, 0.0];
s.face_color_mode = FaceColorMode::HashedBySolid;
s.edge_width_px = 3.5;
s.hidden_edge_alpha = 0.5;
s.vertex_size_px = 8.0;
s.axis_length_px = 30.0;
s.lod_factor = 2.0;
s.flat_shading = true;
s.wireframe = true;
s.pick_double_sided = false;
s.sketch_movable_color = hex(0x112233, 1.0);
s.sketch_constraint_color = hex(0x00ff00, 1.0);
s.sketch_selected_color = hex(0xfedcba, 1.0);
let mut identity = s.clone();
identity.apply_json(&s.to_json()).unwrap();
assert_eq!(identity, s);
let mut rebuilt = RenderSettings::default();
rebuilt.apply_json(&s.to_json()).unwrap();
assert_eq!(rebuilt, s);
}
#[test]
fn settings_schema_covers_every_json_key() {
let json: serde_json::Value =
serde_json::from_str(&RenderSettings::default().to_json()).unwrap();
for field in settings_schema() {
assert!(
json.get(field.key).is_some(),
"schema field {} has no to_json value",
field.key
);
}
let mut s = RenderSettings::default();
s.wireframe = true;
let export: serde_json::Value = serde_json::from_str(&s.settings_schema_json()).unwrap();
let wf = export["fields"]
.as_array()
.unwrap()
.iter()
.find(|f| f["key"] == "wireframe")
.unwrap();
assert_eq!(wf["value"], serde_json::json!(true));
}
#[test]
fn sketch_colors_default_matches_the_hex_constants() {
assert_eq!(RenderSettings::default().sketch_colors(), SketchColors::default());
}
#[test]
fn sketch_colors_follow_apply_json() {
let mut s = RenderSettings::default();
s.apply_json(r##"{"sketchConstraintColor": "#ff0000", "sketchMovableColor": "#00ff00"}"##)
.unwrap();
let c = s.sketch_colors();
assert_eq!(c.constraint, 0xff0000);
assert_eq!(c.movable, 0x00ff00);
assert_eq!(c.locked, SketchColors::default().locked);
}
#[test]
fn css_hex_parsing() {
assert_eq!(parse_css_hex("#ffffff"), Some([1.0, 1.0, 1.0]));
assert_eq!(parse_css_hex("#f00"), Some([1.0, 0.0, 0.0]));
assert!(parse_css_hex("red").is_none());
assert_eq!(parse_css_hex("0x00009e").map(|c| c[2]), Some(0x9e as f32 / 255.0));
}
}