Another project
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}