Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 19 kB View raw
1use bone_document::{ 2 BrepError, ExtrudeError, ProfileDefect, Sketch, SketchEntityKind, SketchRelation, 3 SketchStatusReport, TruckGap, 4}; 5use bone_types::{ 6 BrepSlot, RebuildError, SketchDimensionId, SketchEntityId, SketchItemId, SketchRelationId, 7 SketchStatus, 8}; 9use bone_ui::frame::FrameCtx; 10use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 11use bone_ui::strings::{StringKey, StringTable}; 12use bone_ui::theme::{CadColors, Color}; 13use bone_ui::widgets::{ 14 LabelText, Panel, PanelState, PanelTitlebar, PanelVariant, WidgetPaint, show_panel, 15}; 16use bone_ui::{WidgetId, WidgetKey}; 17 18use crate::strings; 19 20const STATUS_PANEL_WIDTH_PX: f32 = 280.0; 21const STATUS_PANEL_TITLE_HEIGHT_PX: f32 = 28.0; 22const STATUS_PANEL_ROW_HEIGHT_PX: f32 = 22.0; 23const STATUS_PANEL_PADDING_PX: f32 = 8.0; 24const STATUS_PANEL_MAX_ROWS: usize = 12; 25const STATUS_PANEL_GAP_PX: f32 = 6.0; 26 27#[must_use] 28pub fn status_label_key(status: SketchStatus) -> StringKey { 29 match status { 30 SketchStatus::UnderDefined => strings::STATUS_SKETCH_UNDER_DEFINED, 31 SketchStatus::FullyDefined => strings::STATUS_SKETCH_FULLY_DEFINED, 32 SketchStatus::OverDefined => strings::STATUS_SKETCH_OVER_DEFINED, 33 SketchStatus::NoSolutionFound => strings::STATUS_SKETCH_NO_SOLUTION, 34 SketchStatus::InvalidSolutionFound => strings::STATUS_SKETCH_INVALID, 35 SketchStatus::Dangling => strings::STATUS_SKETCH_DANGLING, 36 } 37} 38 39#[must_use] 40pub fn status_color(status: SketchStatus, cad: &CadColors) -> Color { 41 match status { 42 SketchStatus::UnderDefined => cad.sketch_under_defined, 43 SketchStatus::FullyDefined => cad.sketch_fully_defined, 44 SketchStatus::OverDefined 45 | SketchStatus::NoSolutionFound 46 | SketchStatus::InvalidSolutionFound => cad.sketch_over_defined, 47 SketchStatus::Dangling => cad.sketch_dangling, 48 } 49} 50 51#[derive(Copy, Clone, Debug)] 52pub enum ExtrudeStatus<'a> { 53 Valid, 54 Failed(&'a ExtrudeError), 55} 56 57impl<'a> ExtrudeStatus<'a> { 58 #[must_use] 59 pub fn error(self) -> Option<&'a ExtrudeError> { 60 match self { 61 Self::Valid => None, 62 Self::Failed(error) => Some(error), 63 } 64 } 65} 66 67#[must_use] 68pub fn extrude_badge_style(status: ExtrudeStatus<'_>, cad: &CadColors) -> (StringKey, Color) { 69 match status { 70 ExtrudeStatus::Valid => (strings::STATUS_EXTRUDE_VALID, cad.sketch_fully_defined), 71 ExtrudeStatus::Failed( 72 ExtrudeError::Kernel(BrepError::DanglingEdge { .. } | BrepError::DanglingVertex { .. }) 73 | ExtrudeError::PlaneUnresolved(RebuildError::DanglingReference(_)), 74 ) => (strings::STATUS_EXTRUDE_DANGLING, cad.sketch_dangling), 75 ExtrudeStatus::Failed(_) => (strings::STATUS_EXTRUDE_INVALID, cad.sketch_invalid), 76 } 77} 78 79fn extrude_panel_line(error: &ExtrudeError, strings_table: &StringTable) -> LabelText { 80 match error { 81 ExtrudeError::UnsolvedSketch(_) => LabelText::Key(strings::EXTRUDE_PANEL_UNSOLVED_SKETCH), 82 ExtrudeError::Kernel(kernel) => kernel_panel_line(kernel, strings_table), 83 ExtrudeError::PlaneUnresolved(rebuild) => LabelText::Key(rebuild_error_key(*rebuild)), 84 } 85} 86 87fn rebuild_error_key(error: RebuildError) -> StringKey { 88 match error { 89 RebuildError::DanglingReference(_) => strings::EXTRUDE_PANEL_DANGLING_REFERENCE, 90 RebuildError::NonPlanarSketchTarget => strings::EXTRUDE_PANEL_NON_PLANAR_TARGET, 91 RebuildError::UpstreamUnresolved => strings::EXTRUDE_PANEL_UPSTREAM_UNRESOLVED, 92 RebuildError::Build(_) => strings::EXTRUDE_PANEL_INTERNAL, 93 } 94} 95 96fn kernel_panel_line(error: &BrepError, strings_table: &StringTable) -> LabelText { 97 match error { 98 BrepError::InvalidProfile { reason } => LabelText::Key(profile_defect_key(*reason)), 99 BrepError::EmptyExtrudeDepth => LabelText::Key(strings::EXTRUDE_PANEL_EMPTY_DEPTH), 100 BrepError::ShellNotClosed => LabelText::Key(strings::EXTRUDE_PANEL_SHELL_OPEN), 101 BrepError::DegenerateEdge { edge } => brep_entity_line( 102 strings::EXTRUDE_PANEL_DEGENERATE_EDGE, 103 strings_table, 104 edge.slot(), 105 ), 106 BrepError::DanglingEdge { edge } => brep_entity_line( 107 strings::EXTRUDE_PANEL_DANGLING_EDGE, 108 strings_table, 109 edge.slot(), 110 ), 111 BrepError::DanglingVertex { vertex } => brep_entity_line( 112 strings::EXTRUDE_PANEL_DANGLING_VERTEX, 113 strings_table, 114 vertex.slot(), 115 ), 116 BrepError::TruckUnsupported { detail } => LabelText::Key(truck_gap_key(*detail)), 117 BrepError::MissingLabel { .. } 118 | BrepError::ReattachMismatch { .. } 119 | BrepError::ReattachOrder 120 | BrepError::BlobSerialize 121 | BrepError::BlobParse 122 | BrepError::StepSyntax 123 | BrepError::StepNoData 124 | BrepError::StepShellMalformed 125 | BrepError::StepEmpty 126 | BrepError::StepMultipleSolids { .. } 127 | BrepError::StepUnsupported { .. } 128 | BrepError::Canceled => LabelText::Key(strings::EXTRUDE_PANEL_INTERNAL), 129 } 130} 131 132fn profile_defect_key(defect: ProfileDefect) -> StringKey { 133 match defect { 134 ProfileDefect::OpenLoop => strings::EXTRUDE_PANEL_PROFILE_OPEN_LOOP, 135 ProfileDefect::BranchingVertex => strings::EXTRUDE_PANEL_PROFILE_BRANCHING, 136 ProfileDefect::SelfIntersectingLoop => strings::EXTRUDE_PANEL_PROFILE_SELF_INTERSECTING, 137 ProfileDefect::ZeroArea => strings::EXTRUDE_PANEL_PROFILE_ZERO_AREA, 138 ProfileDefect::UncontainedLoop => strings::EXTRUDE_PANEL_PROFILE_UNCONTAINED, 139 ProfileDefect::OverlappingLoops => strings::EXTRUDE_PANEL_PROFILE_OVERLAPPING, 140 } 141} 142 143fn truck_gap_key(gap: TruckGap) -> StringKey { 144 match gap { 145 TruckGap::ReverseNormal | TruckGap::AxisDirection | TruckGap::ReferenceDirection => { 146 strings::EXTRUDE_PANEL_UNSUPPORTED_DIRECTION 147 } 148 TruckGap::ThroughAll 149 | TruckGap::UpToNext 150 | TruckGap::UpToVertex 151 | TruckGap::UpToSurface 152 | TruckGap::OffsetFromSurface 153 | TruckGap::UpToBody => strings::EXTRUDE_PANEL_UNSUPPORTED_END, 154 TruckGap::Draft => strings::EXTRUDE_PANEL_UNSUPPORTED_DRAFT, 155 TruckGap::ThinWall => strings::EXTRUDE_PANEL_UNSUPPORTED_THIN, 156 } 157} 158 159fn brep_entity_line(key: StringKey, strings_table: &StringTable, slot: BrepSlot) -> LabelText { 160 LabelText::Owned(strings_table.resolve(key).replace("{n}", &slot.to_string())) 161} 162 163#[must_use] 164pub fn status_panel_rect(status_bar: LayoutRect, row_count: usize) -> LayoutRect { 165 let rows = row_count.clamp(1, STATUS_PANEL_MAX_ROWS); 166 #[allow( 167 clippy::cast_precision_loss, 168 reason = "row count is bounded by STATUS_PANEL_MAX_ROWS" 169 )] 170 let body_height = rows as f32 * STATUS_PANEL_ROW_HEIGHT_PX + 2.0 * STATUS_PANEL_PADDING_PX; 171 let panel_height = STATUS_PANEL_TITLE_HEIGHT_PX + body_height; 172 let panel_width = STATUS_PANEL_WIDTH_PX; 173 let bar_right = status_bar.origin.x.value() + status_bar.size.width.value(); 174 let origin_x = (bar_right - panel_width).max(status_bar.origin.x.value()); 175 let origin_y = status_bar.origin.y.value() - panel_height - STATUS_PANEL_GAP_PX; 176 LayoutRect::new( 177 LayoutPos::new(LayoutPx::new(origin_x), LayoutPx::new(origin_y.max(0.0))), 178 LayoutSize::new(LayoutPx::new(panel_width), LayoutPx::new(panel_height)), 179 ) 180} 181 182pub fn render_status_panel( 183 ctx: &mut FrameCtx<'_>, 184 panel_id: WidgetId, 185 panel_state: &mut PanelState, 186 status_bar_rect: LayoutRect, 187 report: &SketchStatusReport, 188 sketch: &Sketch, 189 paints: &mut Vec<WidgetPaint>, 190) { 191 let lines = compose_panel_lines(report, sketch, ctx.strings); 192 render_diagnostics_panel( 193 ctx, 194 panel_id, 195 panel_state, 196 status_bar_rect, 197 strings::STATUS_PANEL_TITLE, 198 lines, 199 paints, 200 ); 201} 202 203pub fn render_extrude_panel( 204 ctx: &mut FrameCtx<'_>, 205 panel_id: WidgetId, 206 panel_state: &mut PanelState, 207 status_bar_rect: LayoutRect, 208 error: &ExtrudeError, 209 paints: &mut Vec<WidgetPaint>, 210) { 211 let lines = vec![extrude_panel_line(error, ctx.strings)]; 212 render_diagnostics_panel( 213 ctx, 214 panel_id, 215 panel_state, 216 status_bar_rect, 217 strings::EXTRUDE_PANEL_TITLE, 218 lines, 219 paints, 220 ); 221} 222 223fn render_diagnostics_panel( 224 ctx: &mut FrameCtx<'_>, 225 panel_id: WidgetId, 226 panel_state: &mut PanelState, 227 status_bar_rect: LayoutRect, 228 title: StringKey, 229 lines: Vec<LabelText>, 230 paints: &mut Vec<WidgetPaint>, 231) { 232 let rect = status_panel_rect(status_bar_rect, lines.len()); 233 let response = show_panel( 234 ctx, 235 Panel::new(panel_id, rect, panel_state) 236 .variant(PanelVariant::Card) 237 .titlebar(PanelTitlebar { 238 label: title, 239 height: LayoutPx::new(STATUS_PANEL_TITLE_HEIGHT_PX), 240 collapsible: false, 241 }), 242 ); 243 paints.extend(response.paint); 244 let Some(body) = response.body_rect else { 245 return; 246 }; 247 lines.into_iter().enumerate().for_each(|(i, line)| { 248 #[allow( 249 clippy::cast_precision_loss, 250 reason = "row index bounded by STATUS_PANEL_MAX_ROWS" 251 )] 252 let y = body.origin.y.value() 253 + STATUS_PANEL_PADDING_PX 254 + (i as f32) * STATUS_PANEL_ROW_HEIGHT_PX; 255 paints.push(WidgetPaint::Label { 256 rect: LayoutRect::new( 257 LayoutPos::new( 258 LayoutPx::new(body.origin.x.value() + STATUS_PANEL_PADDING_PX), 259 LayoutPx::new(y), 260 ), 261 LayoutSize::new( 262 LayoutPx::saturating_nonneg( 263 body.size.width.value() - 2.0 * STATUS_PANEL_PADDING_PX, 264 ), 265 LayoutPx::new(STATUS_PANEL_ROW_HEIGHT_PX), 266 ), 267 ), 268 text: line, 269 color: ctx.theme().colors.text_primary(), 270 role: ctx.theme().typography.body, 271 }); 272 }); 273} 274 275fn compose_panel_lines( 276 report: &SketchStatusReport, 277 sketch: &Sketch, 278 strings_table: &StringTable, 279) -> Vec<LabelText> { 280 let total = report.offending().len(); 281 if total == 0 { 282 return vec![LabelText::Key(strings::STATUS_PANEL_EMPTY)]; 283 } 284 if total <= STATUS_PANEL_MAX_ROWS { 285 return report 286 .offending() 287 .iter() 288 .map(|item| offending_label(*item, sketch, strings_table)) 289 .collect(); 290 } 291 let visible = STATUS_PANEL_MAX_ROWS - 1; 292 let rest = total - visible; 293 let head = report 294 .offending() 295 .iter() 296 .take(visible) 297 .map(|item| offending_label(*item, sketch, strings_table)); 298 let more_template = strings_table.resolve(strings::STATUS_PANEL_MORE); 299 let more_line = LabelText::Owned(more_template.replace("{n}", &rest.to_string())); 300 head.chain(core::iter::once(more_line)).collect() 301} 302 303fn offending_label(item: SketchItemId, sketch: &Sketch, strings_table: &StringTable) -> LabelText { 304 let (kind_key, ordinal) = match item { 305 SketchItemId::Relation(id) => ( 306 sketch 307 .relations() 308 .get(id) 309 .map(|rel| relation_label_key(*rel)), 310 relation_ordinal(sketch, id), 311 ), 312 SketchItemId::Dimension(id) => ( 313 sketch 314 .dimensions() 315 .get(id) 316 .map(|dim| dimension_label_key(*dim)), 317 dimension_ordinal(sketch, id), 318 ), 319 SketchItemId::Entity(id) => ( 320 sketch 321 .entities() 322 .get(id) 323 .map(|entity| entity_label_key(entity.kind())), 324 entity_ordinal(sketch, id), 325 ), 326 }; 327 let kind_str = strings_table.resolve(kind_key.unwrap_or(strings::STATUS_PANEL_KIND_UNKNOWN)); 328 match ordinal { 329 Some(o) => LabelText::Owned(format!("{kind_str} #{}", o + 1)), 330 None => LabelText::Owned(kind_str.to_owned()), 331 } 332} 333 334fn relation_ordinal(sketch: &Sketch, id: SketchRelationId) -> Option<usize> { 335 sketch.relation_order().iter().position(|x| *x == id) 336} 337 338fn dimension_ordinal(sketch: &Sketch, id: SketchDimensionId) -> Option<usize> { 339 sketch.dimension_order().iter().position(|x| *x == id) 340} 341 342fn entity_ordinal(sketch: &Sketch, id: SketchEntityId) -> Option<usize> { 343 sketch.entity_order().iter().position(|x| *x == id) 344} 345 346fn relation_label_key(rel: SketchRelation) -> StringKey { 347 match rel { 348 SketchRelation::Coincident(_, _) => strings::TOOL_COINCIDENT, 349 SketchRelation::Horizontal(_) => strings::TOOL_HORIZONTAL, 350 SketchRelation::Vertical(_) => strings::TOOL_VERTICAL, 351 SketchRelation::Parallel(_, _) => strings::TOOL_PARALLEL, 352 SketchRelation::Perpendicular(_, _) => strings::TOOL_PERPENDICULAR, 353 SketchRelation::Tangent(_, _) => strings::TOOL_TANGENT, 354 SketchRelation::Equal(_, _) => strings::TOOL_EQUAL, 355 SketchRelation::Concentric(_, _) => strings::TOOL_CONCENTRIC, 356 SketchRelation::Midpoint { .. } => strings::TOOL_MIDPOINT, 357 SketchRelation::Symmetric { .. } => strings::TOOL_SYMMETRIC, 358 SketchRelation::Fix(_) => strings::TOOL_FIX, 359 } 360} 361 362fn dimension_label_key(dim: bone_document::SketchDimension) -> StringKey { 363 use bone_document::SketchDimension as Dim; 364 match dim { 365 Dim::Linear { .. } => strings::STATUS_PANEL_KIND_LINEAR, 366 Dim::Angular { .. } => strings::STATUS_PANEL_KIND_ANGULAR, 367 Dim::Radius { .. } => strings::STATUS_PANEL_KIND_RADIUS, 368 Dim::Diameter { .. } => strings::STATUS_PANEL_KIND_DIAMETER, 369 } 370} 371 372fn entity_label_key(kind: SketchEntityKind) -> StringKey { 373 match kind { 374 SketchEntityKind::Point => strings::STATUS_PANEL_KIND_POINT, 375 SketchEntityKind::Line => strings::STATUS_PANEL_KIND_LINE, 376 SketchEntityKind::Arc => strings::STATUS_PANEL_KIND_ARC, 377 SketchEntityKind::Circle => strings::STATUS_PANEL_KIND_CIRCLE, 378 } 379} 380 381#[must_use] 382pub fn status_badge_widget_id(parent: WidgetId) -> WidgetId { 383 parent.child(WidgetKey::new("status.badge")) 384} 385 386#[must_use] 387pub fn status_panel_widget_id(parent: WidgetId) -> WidgetId { 388 parent.child(WidgetKey::new("status.panel")) 389} 390 391#[must_use] 392pub fn extrude_badge_widget_id(parent: WidgetId) -> WidgetId { 393 parent.child(WidgetKey::new("status.extrude.badge")) 394} 395 396#[must_use] 397pub fn extrude_panel_widget_id(parent: WidgetId) -> WidgetId { 398 parent.child(WidgetKey::new("status.extrude.panel")) 399} 400 401#[cfg(test)] 402mod tests { 403 use super::{ 404 BrepError, ExtrudeError, ExtrudeStatus, SketchStatus, TruckGap, extrude_badge_style, 405 extrude_panel_line, status_color, status_label_key, 406 }; 407 use crate::strings; 408 use bone_types::BrepEdgeId; 409 use bone_ui::strings::Locale; 410 use bone_ui::theme::Theme; 411 412 #[test] 413 fn extrude_badge_maps_states_to_keys_and_colors() { 414 let cad = Theme::light().cad; 415 let dangling = ExtrudeError::Kernel(BrepError::DanglingEdge { 416 edge: BrepEdgeId::default(), 417 }); 418 let invalid = ExtrudeError::Kernel(BrepError::EmptyExtrudeDepth); 419 assert_eq!( 420 extrude_badge_style(ExtrudeStatus::Valid, &cad), 421 (strings::STATUS_EXTRUDE_VALID, cad.sketch_fully_defined) 422 ); 423 assert_eq!( 424 extrude_badge_style(ExtrudeStatus::Failed(&dangling), &cad), 425 (strings::STATUS_EXTRUDE_DANGLING, cad.sketch_dangling) 426 ); 427 assert_eq!( 428 extrude_badge_style(ExtrudeStatus::Failed(&invalid), &cad), 429 (strings::STATUS_EXTRUDE_INVALID, cad.sketch_invalid) 430 ); 431 } 432 433 #[test] 434 fn invalid_and_dangling_tokens_differ() { 435 let cad = Theme::light().cad; 436 assert_ne!(cad.sketch_invalid, cad.sketch_dangling); 437 assert_ne!(cad.sketch_invalid, cad.sketch_over_defined); 438 } 439 440 #[test] 441 fn extrude_panel_lines_render_through_string_table() { 442 let table = strings::make_strings(Locale::EnUs); 443 let depth = ExtrudeError::Kernel(BrepError::EmptyExtrudeDepth); 444 assert_eq!( 445 extrude_panel_line(&depth, &table).resolve(&table), 446 "Extrude depth is zero" 447 ); 448 let dangling = ExtrudeError::Kernel(BrepError::DanglingEdge { 449 edge: BrepEdgeId::default(), 450 }); 451 let text = extrude_panel_line(&dangling, &table) 452 .resolve(&table) 453 .to_owned(); 454 assert!(text.starts_with("Edge ")); 455 assert!(!text.contains("{n}")); 456 let draft = ExtrudeError::Kernel(BrepError::TruckUnsupported { 457 detail: TruckGap::Draft, 458 }); 459 assert_eq!( 460 extrude_panel_line(&draft, &table).resolve(&table), 461 "Draft is not supported yet" 462 ); 463 } 464 465 #[test] 466 fn each_status_maps_to_expected_label_key() { 467 assert_eq!( 468 status_label_key(SketchStatus::UnderDefined), 469 strings::STATUS_SKETCH_UNDER_DEFINED 470 ); 471 assert_eq!( 472 status_label_key(SketchStatus::FullyDefined), 473 strings::STATUS_SKETCH_FULLY_DEFINED 474 ); 475 assert_eq!( 476 status_label_key(SketchStatus::OverDefined), 477 strings::STATUS_SKETCH_OVER_DEFINED 478 ); 479 assert_eq!( 480 status_label_key(SketchStatus::NoSolutionFound), 481 strings::STATUS_SKETCH_NO_SOLUTION 482 ); 483 assert_eq!( 484 status_label_key(SketchStatus::InvalidSolutionFound), 485 strings::STATUS_SKETCH_INVALID 486 ); 487 assert_eq!( 488 status_label_key(SketchStatus::Dangling), 489 strings::STATUS_SKETCH_DANGLING 490 ); 491 } 492 493 #[test] 494 fn over_no_solution_and_invalid_share_red_token() { 495 let cad = Theme::light().cad; 496 assert_eq!( 497 status_color(SketchStatus::OverDefined, &cad), 498 cad.sketch_over_defined 499 ); 500 assert_eq!( 501 status_color(SketchStatus::NoSolutionFound, &cad), 502 cad.sketch_over_defined 503 ); 504 assert_eq!( 505 status_color(SketchStatus::InvalidSolutionFound, &cad), 506 cad.sketch_over_defined 507 ); 508 assert_eq!( 509 status_color(SketchStatus::Dangling, &cad), 510 cad.sketch_dangling 511 ); 512 assert_eq!( 513 status_color(SketchStatus::UnderDefined, &cad), 514 cad.sketch_under_defined 515 ); 516 assert_eq!( 517 status_color(SketchStatus::FullyDefined, &cad), 518 cad.sketch_fully_defined 519 ); 520 } 521}