1use crate::automation::hit_keys::HitKeyDoc;
73use crate::column_tree::{self, CellKind, ColumnLayout, ColumnSpec, ColumnTreeSpec, RowAction, RowNode};
74use crate::form_view::{form_view, FormViewSpec};
75use brep_render::engine_state::{EngineState, RefSelectTarget};
76use brep_render::features::form_fields_from_schema;
77use brep_render::sheets::SheetState;
78use eframe::egui;
79use serde_json::Value;
80use std::collections::{HashMap, HashSet};
81
82const NAME: &str = "name";
83const KIND: &str = "kind";
84const VALUE: &str = "value";
85const STATUS: &str = "status";
86const ON: &str = "on";
87const ACTIONS: &str = "actions";
88
89const OK_COLOR: &str = "#3fb950";
90const ERROR_COLOR: &str = "#f85149";
91const ACTIVE_COLOR: &str = "#58a6ff";
92const MUTED_COLOR: &str = "#8b949e";
93
94const ACT_OPEN: &str = "open";
96const ACT_CLOSE: &str = "close";
97const ACT_EDIT: &str = "edit";
98const ACT_DELETE_SHEET: &str = "delete-sheet";
99const ACT_DELETE_VIEW: &str = "delete-view";
100const ACT_DELETE_DIM: &str = "delete-dimension";
101const ACT_DELETE_ORD: &str = "delete-ordinate";
102
103enum Action {
105 AddSheet,
106 PlaceView(String),
107 Rename(String, String),
108 Open(Option<String>),
109 Edit(Option<String>),
110 Select(String),
112 DeleteSheet(String),
113 DeleteView(String),
114 DeleteDimension(String),
115 UpdateSheet(String, Value),
116 UpdateView(String, Value),
117 UpdateDimension(String, Value),
118 DeleteOrdinate(String),
119 UpdateOrdinate(String, Value),
120 BeginRefSelect {
125 id: String,
126 path: Vec<String>,
127 label: String,
128 filter: Vec<String>,
129 multiple: bool,
130 seed: Vec<String>,
131 },
132 FinishRefSelect,
134 RefSelectRemove(usize),
136 AddRevision(String),
138 UpdateRevision(String, usize, Value),
140 RemoveRevision(String, usize),
142}
143
144enum RevisionIntent {
148 Add,
149 Update(usize, &'static str, String),
150 Remove(usize),
151}
152
153fn apply(state: &mut EngineState, action: Action) -> Result<(), String> {
156 match action {
157 Action::AddSheet => {
158 state.sheet_add(None);
159 Ok(())
160 }
161 Action::PlaceView(view) => state.sheet_place_view(None, &view, None, None).map(|_| ()),
162 Action::Rename(id, name) => {
163 state.sheet_update(&id, &serde_json::json!({ "name": name }).to_string())
164 }
165 Action::Open(id) => state.sheet_set_open(id.as_deref()),
166 Action::Edit(id) => {
167 state.sheet_set_object_open(id.as_deref());
168 Ok(())
169 }
170 Action::Select(id) => {
171 state.sheet_select_object(Some(&id));
172 Ok(())
173 }
174 Action::DeleteSheet(id) => state.sheet_delete(&id),
175 Action::DeleteView(id) => state.sheet_remove_view(&id),
176 Action::DeleteDimension(id) => state.sheet_remove_dimension(&id),
177 Action::UpdateSheet(id, params) => state.sheet_update(&id, ¶ms.to_string()),
178 Action::UpdateView(id, params) => state.sheet_update_view(&id, ¶ms.to_string()),
179 Action::UpdateDimension(id, params) => {
180 state.sheet_update_dimension(&id, ¶ms.to_string())
181 }
182 Action::DeleteOrdinate(id) => state.sheet_remove_ordinate(&id),
183 Action::UpdateOrdinate(id, params) => {
184 state.sheet_update_ordinate(&id, ¶ms.to_string())
185 }
186 Action::BeginRefSelect { id, path, label, filter, multiple, seed } => {
187 if state.ref_select_active() {
190 state.finish_ref_select();
191 }
192 state.begin_ref_select_for_sheet(&id, path, label, filter, multiple, seed)
193 }
194 Action::FinishRefSelect => {
195 state.finish_ref_select();
196 Ok(())
197 }
198 Action::RefSelectRemove(index) => {
199 state.ref_select_remove(index);
200 Ok(())
201 }
202 Action::AddRevision(id) => state.sheet_add_revision(&id, None, None, None).map(|_| ()),
203 Action::UpdateRevision(id, index, params) => {
204 state.sheet_update_revision(&id, index, ¶ms.to_string())
205 }
206 Action::RemoveRevision(id, index) => state.sheet_remove_revision(&id, index),
207 }
208}
209
210fn sheet_actions(is_open: bool) -> Vec<RowAction> {
213 vec![
214 if is_open {
215 RowAction::new(ACT_CLOSE, "Close sheet").tooltip("Return the viewport to the 3D model")
216 } else {
217 RowAction::new(ACT_OPEN, "Open sheet").tooltip("Draw this sheet in the viewport")
218 },
219 RowAction::new(ACT_EDIT, "Edit sheet").tooltip("Name and paper size"),
220 RowAction::new(ACT_DELETE_SHEET, "Delete sheet").separator_above().destructive(),
221 ]
222}
223
224fn placement_actions() -> Vec<RowAction> {
226 vec![
227 RowAction::new(ACT_EDIT, "Edit placement").tooltip("Which view, where, at what scale, whether its text lies flat on the sheet, and — on a section or a detail — its letter"),
228 RowAction::new(ACT_DELETE_VIEW, "Remove from sheet").separator_above().destructive(),
229 ]
230}
231
232fn dimension_actions() -> Vec<RowAction> {
234 vec![
235 RowAction::new(ACT_EDIT, "Edit dimension").tooltip("Kind, anchors, offset, precision and tolerance"),
236 RowAction::new(ACT_DELETE_DIM, "Remove dimension").separator_above().destructive(),
237 ]
238}
239
240fn ordinate_actions() -> Vec<RowAction> {
242 vec![
243 RowAction::new(ACT_EDIT, "Edit ordinate set").tooltip("Axis, datum, members, baseline offset and precision"),
244 RowAction::new(ACT_DELETE_ORD, "Remove ordinate set").separator_above().destructive(),
245 ]
246}
247
248pub(crate) fn object_actions(sheets: &SheetState, open_sheet: Option<&str>, id: &str) -> Vec<RowAction> {
255 if sheets.find_sheet(id).is_some() {
256 sheet_actions(open_sheet == Some(id))
257 } else if sheets.locate_view(id).is_some() {
258 placement_actions()
259 } else if sheets.locate_dimension(id).is_some() {
260 dimension_actions()
261 } else if sheets.locate_ordinate(id).is_some() {
262 ordinate_actions()
263 } else {
264 Vec::new()
265 }
266}
267
268fn menu_action(id: String, action: &str) -> Option<Action> {
271 match action {
272 ACT_OPEN => Some(Action::Open(Some(id))),
273 ACT_CLOSE => Some(Action::Open(None)),
274 ACT_EDIT => Some(Action::Edit(Some(id))),
275 ACT_DELETE_SHEET => Some(Action::DeleteSheet(id)),
276 ACT_DELETE_VIEW => Some(Action::DeleteView(id)),
277 ACT_DELETE_DIM => Some(Action::DeleteDimension(id)),
278 ACT_DELETE_ORD => Some(Action::DeleteOrdinate(id)),
279 _ => None,
280 }
281}
282
283pub(crate) fn run_row_action(state: &mut EngineState, id: &str, action: &str) -> Result<(), String> {
287 menu_action(id.to_string(), action).map_or(Ok(()), |action| apply(state, action))
288}
289
290pub(crate) fn delete_held_object(state: &mut EngineState, id: &str) -> Result<(), String> {
295 let sheets = state.sheet_state();
296 let entry = if sheets.locate_view(id).is_some() {
297 ACT_DELETE_VIEW
298 } else if sheets.locate_dimension(id).is_some() {
299 ACT_DELETE_DIM
300 } else if sheets.locate_ordinate(id).is_some() {
301 ACT_DELETE_ORD
302 } else {
303 return Err(format!("no placed view, dimension or ordinate set '{id}'"));
304 };
305 run_row_action(state, id, entry)
306}
307
308const SHEET_GLYPH: &str = "\u{E077}";
313
314pub struct SheetsPanel {
315 hits: HashMap<String, egui::Rect>,
316 layout: ColumnLayout,
317 columns: Vec<ColumnSpec>,
318 collapsed: HashSet<String>,
320}
321
322impl Default for SheetsPanel {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl SheetsPanel {
329 pub fn new() -> Self {
330 Self {
331 hits: HashMap::new(),
332 layout: ColumnLayout::default(),
333 columns: vec![
334 ColumnSpec::new(NAME, "Sheet / view", CellKind::Text).width(140.0),
335 ColumnSpec::new(KIND, "", CellKind::Badges).width(28.0),
336 ColumnSpec::new(VALUE, "Paper / placement", CellKind::ReadOnly).width(170.0),
337 ColumnSpec::new(STATUS, "", CellKind::Badges).width(28.0),
338 ColumnSpec::new(ON, "Open", CellKind::Toggle).width(36.0),
339 ColumnSpec::new(ACTIONS, "", CellKind::Actions { label: "\u{22EF}".into() }).width(30.0),
340 ],
341 collapsed: HashSet::new(),
342 }
343 }
344
345 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
347 self.hits.clear();
348 self.hits.insert("sheets:panel:clip".into(), ui.clip_rect());
349 let sheets = state.sheet_state();
350 let open_sheet = state.sheet_open().map(String::from);
351 let open_object = state.sheet_open_object().map(String::from);
352 let selected = state.sheet_selected_object().map(String::from);
353 let view_ids = state.sheet_pmi_view_ids();
354
355 let mut action: Option<Action> = None;
356 let mut close = false;
357 match open_object.as_deref() {
358 Some(id) if sheets.find_sheet(id).is_some() => {
359 let sheet = sheets.find_sheet(id).expect("just matched").clone();
360 self.show_form(ui, state, &sheet.id, "Sheet", &sheet.params(), 0, None, &mut action, &mut close);
361 }
362 Some(id) if sheets.locate_view(id).is_some() => {
363 let placed = sheets
364 .sheets
365 .iter()
366 .find_map(|sheet| sheet.find_view(id))
367 .expect("just located")
368 .clone();
369 let (label, index) = match placed.schema_type() {
373 "sectionView" => ("Section view", 4),
374 "detailView" => ("Detail view", 5),
375 _ => ("Placed view", 1),
376 };
377 self.show_form(ui, state, &placed.id, label, &placed.params(), index, None, &mut action, &mut close);
378 }
379 Some(id) if sheets.locate_dimension(id).is_some() => {
380 let dimension = sheets.find_dimension(id).expect("just located").clone();
381 let inherited = sheets
385 .locate_dimension(id)
386 .and_then(|(sheet, _)| state.sheet_drawing_cached(&sheet))
387 .and_then(|drawing| drawing.dimensions.iter().find(|drawn| drawn.id == dimension.id))
388 .filter(|drawn| !drawn.tolerance_from.is_empty())
389 .map(|drawn| {
390 format!(
391 "Tolerance from PMI dimension {}: {} \u{2014} set one here to override it",
392 drawn.tolerance_from, drawn.text
393 )
394 });
395 self.show_form(ui, state, &dimension.id, "Sheet dimension", &dimension.params(), 2, inherited.as_deref(), &mut action, &mut close);
396 }
397 Some(id) if sheets.locate_ordinate(id).is_some() => {
398 let set = sheets.find_ordinate(id).expect("just located").clone();
399 self.show_form(ui, state, &set.id, "Ordinate set", &set.params(), 3, None, &mut action, &mut close);
400 }
401 _ => self.show_tree(ui, state, &sheets, open_sheet.as_deref(), selected.as_deref(), &view_ids, &mut action),
404 }
405
406 let result = action.map_or(Ok(()), |action| apply(state, action));
407 if let Err(error) = result {
408 state.push_notice(format!("Sheets: {error}"));
409 }
410 if close {
411 if state.ref_select_is_sheet() {
413 state.finish_ref_select();
414 }
415 state.sheet_set_object_open(None);
416 }
417 if let Some(field) = state.ref_select_field() {
420 if field.target == RefSelectTarget::Sheet && state.sheet_open_object() == Some(field.subject.as_str()) {
421 state.ref_select_claim();
422 }
423 }
424 }
425
426 #[allow(clippy::too_many_arguments)]
427 fn show_tree(
428 &mut self,
429 ui: &mut egui::Ui,
430 state: &mut EngineState,
431 sheets: &SheetState,
432 open_sheet: Option<&str>,
433 selected: Option<&str>,
434 view_ids: &[String],
435 action: &mut Option<Action>,
436 ) {
437 ui.horizontal_wrapped(|ui| {
439 let add = ui
440 .add(crate::icon_text::icon_button(ui, &format!("{SHEET_GLYPH} Add sheet")))
441 .on_hover_text("Add an A3 sheet and open it in the sheet viewport");
442 self.hits.insert("sheets:add".into(), add.rect);
443 if add.clicked() {
444 *action = Some(Action::AddSheet);
445 }
446 let can_place = !sheets.sheets.is_empty() && !view_ids.is_empty();
447 let mut place: Option<String> = None;
448 ui.add_enabled_ui(can_place, |ui| {
449 let combo = egui::ComboBox::from_id_salt("sheets-place")
450 .selected_text("+ Place view")
451 .show_ui(ui, |ui| {
452 for id in view_ids {
453 let item = crate::icon_text::selectable_icon_label(ui, false, id);
454 self.hits.insert(format!("sheets:place:{id}"), item.rect);
455 if item.clicked() {
456 place = Some(id.clone());
457 }
458 }
459 });
460 let response = if can_place {
461 combo.response
462 } else if sheets.sheets.is_empty() {
463 combo.response.on_disabled_hover_text("Add a sheet first")
464 } else {
465 combo.response.on_disabled_hover_text("Capture a PMI view first")
466 };
467 self.hits.insert("sheets:place".into(), response.rect);
468 });
469 if let Some(view) = place {
470 *action = Some(Action::PlaceView(view));
471 }
472 });
473 let placed: usize = sheets.sheets.iter().map(|sheet| sheet.views.len()).sum();
474 let dimensions: usize = sheets.sheets.iter().map(|sheet| sheet.dimensions.len()).sum();
475 ui.label(
476 egui::RichText::new(format!(
477 "{} sheet{} | {placed} placed view{} | {dimensions} dimension{} | {}",
478 sheets.sheets.len(),
479 if sheets.sheets.len() == 1 { "" } else { "s" },
480 if placed == 1 { "" } else { "s" },
481 if dimensions == 1 { "" } else { "s" },
482 match open_sheet.and_then(|id| sheets.find_sheet(id)) {
483 Some(sheet) => format!("open: {}", sheet.name),
484 None => "no sheet open".to_string(),
485 }
486 ))
487 .weak(),
488 );
489 if sheets.sheets.is_empty() {
490 ui.label(egui::RichText::new("Add a sheet to place PMI views on paper").weak().italics());
491 }
492 ui.add_space(2.0);
493
494 let drawing = open_sheet.and_then(|id| state.sheet_drawing(id));
500 let rows: Vec<RowNode> = sheets
501 .sheets
502 .iter()
503 .map(|sheet| {
504 let is_open = open_sheet == Some(sheet.id.as_str());
505 let (w, h) = sheet.millimetres();
506 let count = sheet.views.len();
507 let mut row = RowNode::new(&sheet.id)
508 .cell(NAME, Value::String(sheet.name.clone()))
509 .cell(KIND, serde_json::json!([{ "glyph": SHEET_GLYPH, "color": if is_open { ACTIVE_COLOR } else { MUTED_COLOR }, "tooltip": "drawing sheet" }]))
510 .cell(
511 VALUE,
512 Value::String(format!(
513 "{} · {w:.0} × {h:.0} mm · {count} view{}",
514 sheet.size,
515 if count == 1 { "" } else { "s" }
516 )),
517 )
518 .cell(
519 STATUS,
520 if is_open {
521 serde_json::json!([{ "glyph": "\u{25CF}", "color": ACTIVE_COLOR, "tooltip": "shown in the sheet viewport" }])
522 } else {
523 serde_json::json!([])
524 },
525 )
526 .cell(ON, Value::Bool(is_open))
527 .actions(sheet_actions(is_open));
528 row.expanded = !self.collapsed.contains(&sheet.id);
529 row.selected = is_open;
530 row.children = sheet
531 .views
532 .iter()
533 .map(|placed| {
534 let projected = drawing.and_then(|d| d.views.iter().find(|v| v.id == placed.id));
535 let is_section = placed.section.as_ref();
536 let is_detail = placed.detail.as_ref();
537 let (glyph, color, tooltip, status) = match projected {
538 Some(view) if view.error.is_empty() => (
539 "\u{2713}",
540 OK_COLOR,
541 format!(
542 "{}, {}",
543 crate::panels::count(view.edges.len(), "edge", "edges"),
544 crate::panels::count(view.annotations.len(), "annotation", "annotations")
545 ),
546 crate::panels::count(view.edges.len(), "edge", "edges"),
547 ),
548 Some(view) => ("\u{2715}", ERROR_COLOR, view.error.clone(), view.error.clone()),
549 None => ("\u{2013}", MUTED_COLOR, "open the sheet to draw it".into(), String::new()),
550 };
551 let _ = status;
552 let mut child = RowNode::new(&placed.id)
555 .cell(NAME, Value::String(placed.id.clone()))
556 .cell(KIND, match (is_section, is_detail) {
557 (Some(cut), _) => serde_json::json!([{ "glyph": "\u{25E7}", "color": ACTIVE_COLOR, "tooltip": format!("section {}—{} of {}", cut.label, cut.label, cut.source) }]),
558 (_, Some(circle)) => serde_json::json!([{ "glyph": "\u{25CE}", "color": ACTIVE_COLOR, "tooltip": format!("detail {} of {}", circle.label, circle.source) }]),
559 _ => serde_json::json!([{ "glyph": "\u{1F5CE}", "color": ACTIVE_COLOR, "tooltip": "placed PMI view" }]),
560 })
561 .cell(
562 VALUE,
563 Value::String(match (is_section, is_detail) {
564 (Some(cut), _) => format!(
568 "SECTION {}—{} of {} · {}:1 · ({:.0}, {:.0}) mm",
569 cut.label,
570 cut.label,
571 cut.source,
572 trim(placed.scale),
573 placed.position[0],
574 placed.position[1]
575 ),
576 (_, Some(circle)) => format!(
579 "DETAIL {} of {} · {}:1 · ({:.0}, {:.0}) mm",
580 circle.label,
581 circle.source,
582 trim(placed.scale),
583 placed.position[0],
584 placed.position[1]
585 ),
586 _ => format!(
587 "{} · {}:1 · ({:.0}, {:.0}) mm · {}",
588 placed.view,
589 trim(placed.scale),
590 placed.position[0],
591 placed.position[1],
592 if placed.flatten_text { "flat" } else { "projected" }
593 ),
594 }),
595 )
596 .cell(STATUS, serde_json::json!([{ "glyph": glyph, "color": color, "tooltip": tooltip }]))
597 .cell(ON, Value::Null)
598 .actions(placement_actions());
599 child.editable = false;
600 child.selected = selected == Some(placed.id.as_str());
601 child
602 })
603 .collect();
604 row.children.extend(sheet.dimensions.iter().map(|dimension| {
609 let drawn = drawing.and_then(|d| d.dimensions.iter().find(|x| x.id == dimension.id));
610 let (glyph, color, tooltip) = match drawn {
611 Some(d) if d.error.is_empty() => (
612 "\u{2713}",
613 OK_COLOR,
614 if d.tolerance_from.is_empty() {
615 format!("{} \u{2014} {} mm off its anchors", d.text, trim(d.offset_mm))
616 } else {
617 format!(
618 "{} \u{2014} {} mm off its anchors; the tolerance is PMI dimension {}'s",
619 d.text,
620 trim(d.offset_mm),
621 d.tolerance_from
622 )
623 },
624 ),
625 Some(d) => ("\u{2715}", ERROR_COLOR, d.error.clone()),
626 None => ("\u{2013}", MUTED_COLOR, "open the sheet to draw it".into()),
627 };
628 let measured = drawn
629 .filter(|d| d.error.is_empty())
630 .map(|d| match d.tolerance_from.as_str() {
631 "" => d.text.clone(),
632 from => format!("{} (from {from})", d.text),
633 })
634 .unwrap_or_else(|| "\u{2014}".into());
635 let mut child = RowNode::new(&dimension.id)
636 .cell(NAME, Value::String(dimension.id.clone()))
637 .cell(KIND, serde_json::json!([{ "glyph": "\u{2194}", "color": ACTIVE_COLOR, "tooltip": "sheet dimension" }]))
638 .cell(
639 VALUE,
640 Value::String(format!(
641 "{} \u{00B7} {} \u{00B7} {measured}",
642 if dimension.kind == "linear" {
643 dimension.alignment.clone()
644 } else {
645 dimension.kind.clone()
646 },
647 match dimension.anchors.len() {
648 1 => "1 anchor".to_string(),
649 n => format!("{n} anchors"),
650 }
651 )),
652 )
653 .cell(STATUS, serde_json::json!([{ "glyph": glyph, "color": color, "tooltip": tooltip }]))
654 .cell(ON, Value::Null)
655 .actions(dimension_actions());
656 child.editable = false;
657 child.selected = selected == Some(dimension.id.as_str());
658 child
659 }));
660 row.children.extend(sheet.ordinates.iter().map(|set| {
664 let drawn = drawing.and_then(|d| d.ordinates.iter().find(|x| x.id == set.id));
665 let (glyph, color, tooltip) = match drawn {
666 Some(d) if !d.error.is_empty() => ("\u{2715}", ERROR_COLOR, d.error.clone()),
667 Some(d) if d.lost() > 0 => (
668 "\u{2715}",
669 ERROR_COLOR,
670 d.stations
671 .iter()
672 .filter(|station| !station.error.is_empty())
673 .map(|station| station.error.clone())
674 .collect::<Vec<_>>()
675 .join("; "),
676 ),
677 Some(d) => (
678 "\u{2713}",
679 OK_COLOR,
680 format!(
681 "{} stations \u{2014} baseline {} mm from the datum",
682 d.stations.len(),
683 trim(d.offset_mm)
684 ),
685 ),
686 None => ("\u{2013}", MUTED_COLOR, "open the sheet to draw it".into()),
687 };
688 let mut child = RowNode::new(&set.id)
689 .cell(NAME, Value::String(set.id.clone()))
690 .cell(KIND, serde_json::json!([{ "glyph": "\u{22EE}", "color": ACTIVE_COLOR, "tooltip": "ordinate set" }]))
691 .cell(
692 VALUE,
693 Value::String(format!(
694 "{} \u{00B7} {}",
695 set.axis,
696 match set.members.len() {
697 1 => "1 member".to_string(),
698 n => format!("{n} members"),
699 }
700 )),
701 )
702 .cell(STATUS, serde_json::json!([{ "glyph": glyph, "color": color, "tooltip": tooltip }]))
703 .cell(ON, Value::Null)
704 .actions(ordinate_actions());
705 child.editable = false;
706 child.selected = selected == Some(set.id.as_str());
707 child
708 }));
709 row
710 })
711 .collect();
712 let spec = ColumnTreeSpec {
713 id: "sheets",
714 columns: &self.columns,
715 root_label: Some("Sheets"),
716 root_cells: None,
717 empty_hint: Some("(no sheets — Add sheet to start a drawing)"),
718 hits_prefix: "sheets:",
719 drag_rows: false,
720 };
721 let out = column_tree::column_tree(ui, &spec, &mut self.layout, &rows, Some(&mut self.hits));
722
723 if let Some(id) = &out.toggled {
725 if sheets.find_sheet(id).is_some() && !self.collapsed.remove(id) {
726 self.collapsed.insert(id.clone());
727 }
728 }
729 if let Some(click) = out.actions.first() {
730 *action = menu_action(click.row_id.clone(), &click.action);
731 return;
732 }
733 if let Some(edit) = out.edits.first() {
734 let id = edit.row_id.clone();
735 match edit.column.as_str() {
736 NAME if sheets.find_sheet(&id).is_some() => {
737 *action = Some(Action::Rename(id, edit.value.as_str().unwrap_or("").to_string()));
738 }
739 ON if sheets.find_sheet(&id).is_some() => {
740 *action = Some(Action::Open(edit.value.as_bool().unwrap_or(false).then_some(id)));
741 }
742 _ => {}
743 }
744 return;
745 }
746 let held_by_a_sheet = |id: &str| {
747 sheets.locate_view(id).is_some()
748 || sheets.locate_dimension(id).is_some()
749 || sheets.locate_ordinate(id).is_some()
750 };
751 if let Some(id) = &out.double_clicked {
754 if held_by_a_sheet(id) || sheets.find_sheet(id).is_some() {
755 *action = Some(Action::Edit(Some(id.clone())));
756 }
757 return;
758 }
759 if let Some(id) = &out.clicked {
763 if held_by_a_sheet(id) {
764 *action = Some(Action::Select(id.clone()));
765 } else if sheets.find_sheet(id).is_some() && open_sheet != Some(id.as_str()) {
766 *action = Some(Action::Open(Some(id.clone())));
767 }
768 }
769 }
770
771 #[allow(clippy::too_many_arguments)]
775 fn show_form(
776 &mut self,
777 ui: &mut egui::Ui,
778 state: &EngineState,
779 id: &str,
780 label: &str,
781 seed: &Value,
782 catalogue_index: usize,
783 note: Option<&str>,
784 action: &mut Option<Action>,
785 close: &mut bool,
786 ) {
787 let catalogue = state.sheet_catalogue();
788 let Some(schema) = catalogue
789 .get("objects")
790 .and_then(Value::as_array)
791 .and_then(|objects| objects.get(catalogue_index))
792 .cloned()
793 else {
794 *close = true;
795 return;
796 };
797 let fields = form_fields_from_schema(&schema);
798 let mut params = seed.clone();
799 let title = format!("{label} {id}");
800 let is_sheet = catalogue_index == 0;
804 let revisions: Vec<brep_render::sheets::Revision> = if is_sheet {
805 serde_json::from_value(seed.get("revisions").cloned().unwrap_or(Value::Null)).unwrap_or_default()
806 } else {
807 Vec::new()
808 };
809 let revision_rows = std::cell::RefCell::new(revisions);
810 let revision_intents: std::cell::RefCell<Vec<RevisionIntent>> = std::cell::RefCell::new(Vec::new());
811 let revision_hits: std::cell::RefCell<Vec<(String, egui::Rect)>> = std::cell::RefCell::new(Vec::new());
812 let bom = std::cell::RefCell::new(serde_json::from_value::<Option<brep_render::sheets::BomTable>>(seed.get("bomTable").cloned().unwrap_or(Value::Null)).unwrap_or_default());
813 let bom_changed = std::cell::Cell::new(false);
814 let mut columns = if is_sheet { state.sheet_bom_columns() } else { Vec::new() };
815 let config = if state.settings.bom_columns.is_empty() { crate::panels::bom_columns::default_text() } else { state.settings.bom_columns.clone() };
816 for column in crate::panels::bom_columns::parse(&config).columns { let key = column.key(); if !columns.contains(&key) { columns.push(key); } }
817 if let Some(table) = bom.borrow().as_ref() { for column in &table.columns { if !columns.contains(column) { columns.push(column.clone()); } } }
818 columns.sort_by_key(|column| !bom.borrow().as_ref().is_some_and(|table| table.columns.contains(column)));
819 let draw_revisions = |ui: &mut egui::Ui| {
820 ui.label("BOM table");
821 let mut table = bom.borrow_mut();
822 let mut enabled = table.is_some();
823 let response = ui.checkbox(&mut enabled, "Show BOM table");
824 revision_hits.borrow_mut().push(("sheets:bom:enabled".into(), response.rect));
825 if response.changed() {
826 *table = enabled.then(brep_render::sheets::BomTable::default); bom_changed.set(true);
827 }
828 if let Some(table) = table.as_mut() {
829 ui.horizontal(|ui| {
830 ui.label("Position (mm)");
831 for coordinate in &mut table.position { if ui.add(egui::DragValue::new(coordinate).speed(1.)).changed() { bom_changed.set(true); } }
832 });
833 ui.horizontal(|ui| { ui.label("Column width (mm)"); if ui.add(egui::DragValue::new(&mut table.column_width_mm).range(8.0..=200.0)).changed() { bom_changed.set(true); } });
834 ui.label("Columns");
835 egui::ScrollArea::vertical().id_salt(("bom-columns", id)).max_height(180.0).show(ui, |ui| {
836 for column in &columns {
837 let mut selected = table.columns.contains(column);
838 let response = ui.checkbox(&mut selected, column.replace('_', " "));
839 revision_hits.borrow_mut().push((format!("sheets:bom:column:{column}"), response.rect));
840 if response.changed() {
841 if selected { table.columns.push(column.clone()); } else { table.columns.retain(|c| c != column); }
842 bom_changed.set(true);
843 }
844 }
845 });
846 }
847 ui.separator();
848 ui.label("Revisions");
849 draw_revision_rows(ui, id, &revision_rows, &revision_intents, &revision_hits);
850 };
851 let picking = state
854 .ref_select_field()
855 .filter(|f| f.target == RefSelectTarget::Sheet && f.subject == id);
856 let spec = FormViewSpec {
857 title: &title,
858 subtitle: None,
859 fields: &fields,
860 banner: note.map(|note| (note, egui::Color32::from_rgb(0x58, 0xa6, 0xff))),
861 trailing: None,
862 exit_label: "Return to tree",
863 extra: is_sheet.then_some(("Tables", &draw_revisions as &dyn Fn(&mut egui::Ui))),
864 rollback: false,
865 hidden: None,
866 hits_prefix: "sheets:",
867 read_only: state.history.locked(),
868 picking: picking.as_ref().map(|f| crate::form::ActivePick { path: &f.path, names: &f.names }),
869 };
870 let mut out = form_view(ui, &spec, &mut params, Some(&mut self.hits));
871 if is_sheet && bom_changed.get() { params["bomTable"] = serde_json::to_value(bom.into_inner()).unwrap_or(Value::Null); out.changed = true; }
872 for (key, rect) in revision_hits.into_inner() {
873 self.hits.insert(key, rect);
874 }
875 if !out.changed {
878 if let Some(intent) = revision_intents.into_inner().into_iter().next() {
879 *action = Some(match intent {
880 RevisionIntent::Add => Action::AddRevision(id.to_string()),
881 RevisionIntent::Update(index, field, value) => {
882 Action::UpdateRevision(id.to_string(), index, serde_json::json!({ field: value }))
883 }
884 RevisionIntent::Remove(index) => Action::RemoveRevision(id.to_string(), index),
885 });
886 }
887 }
888 let anchor = self.hits.get("sheets:form:feature").map(|rect| rect.min).unwrap_or(egui::Pos2::ZERO);
889 self.hits.insert(format!("sheets:form:object:{id}"), egui::Rect::from_min_size(anchor, egui::Vec2::ZERO));
890 if out.changed {
891 *action = Some(match catalogue_index {
892 0 => Action::UpdateSheet(id.to_string(), params),
893 2 => Action::UpdateDimension(id.to_string(), params),
894 3 => Action::UpdateOrdinate(id.to_string(), params),
895 _ => Action::UpdateView(id.to_string(), params),
897 });
898 }
899 if let Some(activate) = out.ref_activate {
903 *action = Some(Action::BeginRefSelect {
904 id: id.to_string(),
905 path: activate.path,
906 label: activate.label,
907 filter: activate.filter,
908 multiple: activate.multiple,
909 seed: activate.seed,
910 });
911 }
912 if let Some(index) = out.pick_remove {
914 *action = Some(Action::RefSelectRemove(index));
915 }
916 if out.pick_done {
917 *action = Some(Action::FinishRefSelect);
918 }
919 if out.exit_clicked {
920 *close = true;
921 }
922 }
923
924 pub fn hits_json(&self) -> String {
926 crate::automation::hit_rects::hits_json(&self.hits)
927 }
928
929 pub fn clear_hits(&mut self) {
932 self.hits.clear();
933 }
934}
935
936fn draw_revision_rows(
941 ui: &mut egui::Ui,
942 sheet_id: &str,
943 rows: &std::cell::RefCell<Vec<brep_render::sheets::Revision>>,
944 intents: &std::cell::RefCell<Vec<RevisionIntent>>,
945 hits: &std::cell::RefCell<Vec<(String, egui::Rect)>>,
946) {
947 let mut rows = rows.borrow_mut();
948 if rows.is_empty() {
949 ui.label(egui::RichText::new("No revisions \u{2014} the sheet draws no revision table").weak().italics());
950 }
951 for (index, row) in rows.iter_mut().enumerate() {
955 let mut edit = |ui: &mut egui::Ui, field: &'static str, value: &mut String, width: f32, hint: &str| {
956 let response = ui.add(
957 egui::TextEdit::singleline(value)
958 .id(egui::Id::new(("sheet-revision", sheet_id, index, field)))
959 .hint_text(hint)
960 .desired_width(width),
961 );
962 hits.borrow_mut().push((format!("sheets:rev:{index}:{field}"), response.rect));
963 if response.changed() {
964 intents.borrow_mut().push(RevisionIntent::Update(index, field, value.clone()));
965 }
966 };
967 ui.horizontal(|ui| {
968 ui.label(egui::RichText::new("Rev").weak());
969 edit(ui, "rev", &mut row.rev, 36.0, "A");
970 ui.label(egui::RichText::new("Date").weak());
971 edit(ui, "date", &mut row.date, 88.0, "YYYY-MM-DD");
972 let remove = ui
973 .add(crate::icon_text::icon_button(ui, "\u{2715}"))
974 .on_hover_text("Remove this revision row");
975 hits.borrow_mut().push((format!("sheets:rev:{index}:remove"), remove.rect));
976 if remove.clicked() {
977 intents.borrow_mut().push(RevisionIntent::Remove(index));
978 }
979 });
980 let width = ui.available_width().min(240.0);
983 edit(ui, "description", &mut row.description, width, "Description");
984 ui.add_space(4.0);
985 }
986 let add = ui
987 .add(crate::icon_text::icon_button(ui, "+ Add revision"))
988 .on_hover_text("Append a revision row: the next letter, today's date, no description");
989 hits.borrow_mut().push(("sheets:rev:add".into(), add.rect));
990 if add.clicked() {
991 intents.borrow_mut().push(RevisionIntent::Add);
992 }
993}
994
995fn trim(value: f64) -> String {
997 let text = format!("{value:.3}");
998 let text = text.trim_end_matches('0').trim_end_matches('.');
999 if text.is_empty() { "0".into() } else { text.to_string() }
1000}
1001
1002pub static HIT_KEYS: &[HitKeyDoc] = &[
1004 HitKeyDoc { panel: "sheets", prefix: "sheets:panel:clip", meaning: "the pane's clip rect \u{2014} a row scrolled outside it publishes a rect egui will not accept a click on", command: None },
1005 HitKeyDoc { panel: "sheets", prefix: "sheets:add", meaning: "add a sheet at the default paper size and open it", command: Some("sheet_add") },
1006 HitKeyDoc { panel: "sheets", prefix: "sheets:place", meaning: "open the place-view menu", command: None },
1007 HitKeyDoc { panel: "sheets", prefix: "sheets:place:", meaning: "place that saved PMI view on the open sheet (sheets:place:<view id>)", command: Some("sheet_place_view") },
1008 HitKeyDoc { panel: "sheets", prefix: "sheets:row:", meaning: "a sheet, placement, dimension or ordinate-set row (sheets:row:<id>) \u{2014} a click opens a sheet in the sheet viewport or SELECTS what it holds, a double click opens that row's dialog", command: None },
1009 HitKeyDoc { panel: "sheets", prefix: "sheets:cell:", meaning: "a row cell (sheets:cell:<id>:<column>) \u{2014} `sheets:cell:<sheet id>:on` is the toggle that OPENS a sheet in the sheet viewport", command: Some("sheet_open") },
1010 HitKeyDoc { panel: "sheets", prefix: "sheets:box:", meaning: "a row's collapse box (sheets:box:<row id>) \u{2014} a sheet's box folds its placed views away", command: None },
1011 HitKeyDoc { panel: "sheets", prefix: "sheets:col:", meaning: "a table column header (sheets:col:<field>) \u{2014} click to sort", command: None },
1012 HitKeyDoc { panel: "sheets", prefix: "sheets:grip:", meaning: "a column's resize grip (sheets:grip:<field>)", command: None },
1013 HitKeyDoc { panel: "sheets", prefix: "sheets:menu:", meaning: "a row's action-menu trigger cell (sheets:menu:<row id>); a right-click on the row opens the same menu", command: None },
1014 HitKeyDoc { panel: "sheets", prefix: "sheets:menuitem:", meaning: "one entry of the OPEN row action menu (sheets:menuitem:<row id>:<action>) \u{2014} published only while the menu is up", command: None },
1015 HitKeyDoc { panel: "sheets", prefix: "sheets:form:", meaning: "the open sheet / placement form (sheets:form:object:<id>, sheets:form:feature, sheets:form:return)", command: Some("sheet_update") },
1016 HitKeyDoc { panel: "sheets", prefix: "sheets:field:", meaning: "one schema field of the open form (sheets:field:<path>) \u{2014} the paper size, the placed view, its position and scale, `sheets:field:flattenText`, the checkbox that lays that view's annotation text flat on the sheet, and a sheet dimension's kind, alignment, offset and precision and its tolerance block — `sheets:field:tolMode` (none / symmetric / deviation / limits), `sheets:field:tolUpper`, `sheets:field:tolLower` and the `sheets:field:isReference` checkbox, which an ordinate set's form carries too. `sheets:field:anchors#activate` focuses the field and opens the anchor pick on the paper (anchors are clicked as `sheet/anchor:<ref>`; the same key, now Done, commits a list, and a single field commits on its pick), `sheets:field:anchors#line<n>` is one anchor row and `sheets:field:anchors#x<n>` drops it. An ORDINATE SET's form adds `sheets:field:axis` and its two reference fields `sheets:field:datum` and `sheets:field:members`; a SECTION's form its `sheets:field:cut` (the cutting line), `sheets:field:sectionLabel` and `sheets:field:sectionFlip`; a DETAIL's form its `sheets:field:centre`, `sheets:field:rim` and `sheets:field:detailLabel` \u{2014} each reference field with the same `#activate`, `#line<n>` and `#x<n>` keys", command: Some("sheet_update_dimension") },
1017 HitKeyDoc { panel: "sheets", prefix: "sheets:group:", meaning: "a collapsible group header of the open form (sheets:group:<name>)", command: None },
1018 HitKeyDoc { panel: "sheets", prefix: "sheets:bom:", meaning: "the sheet BOM table toggle and column choices: sheets:bom:enabled, sheets:bom:column:<attribute>", command: Some("sheet_update") },
1019 HitKeyDoc { panel: "sheets", prefix: "sheets:rev:", meaning: "the SHEET dialog's Revisions section: `sheets:rev:add` appends a row (the next letter, today's date), and per row `sheets:rev:<n>:rev`, `sheets:rev:<n>:date` and `sheets:rev:<n>:description` are its three text edits and `sheets:rev:<n>:remove` removes it. Rows keep their order; the sheet draws them as the revision table beside its title block", command: Some("sheet_add_revision") },
1020];
1021
1022
1023
1024