use std::collections::HashSet;
pub type Rgba = [f32; 4];
fn hex(hex: u32, alpha: f32) -> Rgba {
let [r, g, b] = crate::color::hex_to_srgb_f32(hex);
[r, g, b, 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(crate::color::hex_to_srgb_f32(n))
}
_ => 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,
pub conflict: 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,
conflict: 0xff5c5c,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeMode {
Auto,
Light,
Dark,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MultiSelectMode {
CtrlClick,
ClickToggles,
}
impl MultiSelectMode {
pub const ALL: [MultiSelectMode; 2] = [MultiSelectMode::CtrlClick, MultiSelectMode::ClickToggles];
pub fn label(&self) -> &'static str {
match self {
MultiSelectMode::CtrlClick => "Ctrl+Click",
MultiSelectMode::ClickToggles => "Click toggles",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaceColorMode {
Uniform,
HashedBySolid,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderSettings {
pub theme: ThemeMode,
pub ui_scale: f32,
pub label_scale: f32,
pub debug_grab_handles: bool,
pub background: [f32; 3],
pub face_color_mode: FaceColorMode,
pub override_model_colors: bool,
pub show_workbench_toolbar: bool,
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 show_faces: bool,
pub show_edges: bool,
pub show_vertices: bool,
pub axis_length_px: f32,
pub viewcube_size_px: f32,
pub pick_double_sided: bool,
pub multi_select: MultiSelectMode,
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,
pub sketch_conflict_color: Rgba,
pub workbench: String,
pub assembly_auto_solve: bool,
pub show_constraint_graphics: bool,
pub bom_columns: String,
}
impl Default for RenderSettings {
fn default() -> Self {
let sk = SketchColors::default();
Self {
theme: ThemeMode::Auto,
ui_scale: 1.0,
label_scale: 1.0,
debug_grab_handles: false,
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,
show_faces: true,
show_edges: true,
show_vertices: true,
override_model_colors: false,
show_workbench_toolbar: true,
axis_length_px: 46.0,
viewcube_size_px: brep_gizmos::view_cube::ViewCube::DEFAULT_SIZE_PX,
pick_double_sided: true,
multi_select: MultiSelectMode::ClickToggles,
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),
sketch_conflict_color: hex(sk.conflict, 1.0),
workbench: "modeling".to_string(),
assembly_auto_solve: true,
show_constraint_graphics: true,
bom_columns: String::new(),
}
}
}
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);
color("sketchConflictColor", &mut self.sketch_conflict_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("theme").and_then(|v| v.as_str()) {
match v.trim().to_ascii_lowercase().as_str() {
"auto" => self.theme = ThemeMode::Auto,
"light" => self.theme = ThemeMode::Light,
"dark" => self.theme = ThemeMode::Dark,
_ => {}
}
}
if let Some(v) = value.get("uiScale").and_then(|v| v.as_f64()) {
self.ui_scale = (v as f32).clamp(0.5, 3.0);
}
if let Some(v) = value.get("labelScale").and_then(|v| v.as_f64()) {
self.label_scale = (v as f32).clamp(0.25, 3.0);
}
if let Some(v) = value.get("debugGrabHandles").and_then(|v| v.as_bool()) {
self.debug_grab_handles = v;
}
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("showFaces").and_then(|v| v.as_bool()) {
self.show_faces = v;
}
if let Some(v) = value.get("showEdges").and_then(|v| v.as_bool()) {
self.show_edges = v;
}
if let Some(v) = value.get("showVertices").and_then(|v| v.as_bool()) {
self.show_vertices = v;
}
if let Some(v) = value.get("overrideModelColors").and_then(|v| v.as_bool()) {
self.override_model_colors = v;
}
if let Some(v) = value.get("showWorkbenchToolbar").and_then(|v| v.as_bool()) {
self.show_workbench_toolbar = 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("viewcubeSizePx").and_then(|v| v.as_f64()) {
self.viewcube_size_px = (v as f32).clamp(40.0, 230.0);
}
if let Some(v) = value.get("pickDoubleSided").and_then(|v| v.as_bool()) {
self.pick_double_sided = v;
}
if let Some(v) = value.get("multiSelect").and_then(|v| v.as_str()) {
let skeleton: String = v
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_ascii_lowercase();
match skeleton.as_str() {
"ctrlclick" => self.multi_select = MultiSelectMode::CtrlClick,
"clicktoggles" => self.multi_select = MultiSelectMode::ClickToggles,
_ => {}
}
}
if let Some(v) = value.get("workbench").and_then(|v| v.as_str()) {
self.workbench = v.to_string();
}
if let Some(v) = value.get("assemblyAutoSolve").and_then(|v| v.as_bool()) {
self.assembly_auto_solve = v;
}
if let Some(v) = value.get("bomColumns").and_then(|v| v.as_str()) {
self.bom_columns = v.to_string();
}
if let Some(v) = value.get("showConstraintGraphics").and_then(|v| v.as_bool()) {
self.show_constraint_graphics = 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!({
"theme": match self.theme {
ThemeMode::Auto => "auto",
ThemeMode::Light => "light",
ThemeMode::Dark => "dark",
},
"uiScale": self.ui_scale as f64,
"labelScale": self.label_scale as f64,
"debugGrabHandles": self.debug_grab_handles,
"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,
"showFaces": self.show_faces,
"showEdges": self.show_edges,
"showVertices": self.show_vertices,
"overrideModelColors": self.override_model_colors,
"showWorkbenchToolbar": self.show_workbench_toolbar,
"axisLengthPx": self.axis_length_px as f64,
"viewcubeSizePx": self.viewcube_size_px as f64,
"pickDoubleSided": self.pick_double_sided,
"multiSelect": self.multi_select.label(),
"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),
"sketchConflictColor": rgba_to_css_hex(self.sketch_conflict_color),
"workbench": self.workbench,
"assemblyAutoSolve": self.assembly_auto_solve,
"showConstraintGraphics": self.show_constraint_graphics,
"bomColumns": self.bom_columns,
})
.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),
conflict: rgba_to_u32(self.sketch_conflict_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 {
format!("#{:06x}", rgba_to_u32([rgb[0], rgb[1], rgb[2], 1.0]))
}
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<String> },
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(
"theme",
"Theme",
"Appearance",
FieldKind::Enum { variants: ["auto", "light", "dark"].iter().map(|s| s.to_string()).collect() },
),
f(
"uiScale",
"UI scale",
"Appearance",
FieldKind::Range { min: 0.5, max: 3.0, step: 0.05 },
),
f(
"labelScale",
"Label scale",
"Appearance",
FieldKind::Range { min: 0.25, max: 3.0, step: 0.05 },
),
f(
"showWorkbenchToolbar",
"Show workbench actions toolbar",
"Appearance",
FieldKind::Bool,
),
f("background", "Background", "Scene", FieldKind::Color),
f(
"axisLengthPx",
"Axis length (px)",
"Scene",
FieldKind::Number { min: 0.0, max: 512.0, step: 1.0 },
),
f(
"viewcubeSizePx",
"ViewCube size (px)",
"Scene",
FieldKind::Number { min: 40.0, max: 230.0, step: 1.0 },
),
f(
"renderQuality",
"Render Quality",
"Scene",
FieldKind::Enum { variants: RENDER_QUALITY.iter().map(|(label, _)| label.to_string()).collect() },
),
f(
"faceColorMode",
"Face color mode",
"Faces",
FieldKind::Enum { variants: ["uniform", "hashedBySolid"].iter().map(|s| s.to_string()).collect() },
),
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("showFaces", "Show faces", "Faces", FieldKind::Bool),
f(
"overrideModelColors",
"Override model colors",
"Faces",
FieldKind::Bool,
),
f("showEdges", "Show edges", "Edges", 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("showVertices", "Show vertices", "Vertices", FieldKind::Bool),
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(
"multiSelect",
"Multi-select",
"Picking",
FieldKind::Enum {
variants: MultiSelectMode::ALL.iter().map(|m| m.label().to_string()).collect(),
},
),
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),
f("sketchConflictColor", "Conflicting constraint", "Sketch", FieldKind::Color),
f("debugGrabHandles", "Debug grab handles", "Debug", FieldKind::Bool),
]
}
#[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 hovered_datums: HashSet<String>,
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()
&& self.hovered_datums.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> {
crate::json_support::string_values(group.get(key))
.map(str::to_string)
.collect()
};
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_datums = names(hovered, "datums");
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
}
}
}