1use brep_render::engine_state::EngineState;
16
17use crate::column_tree::RowAction;
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
21pub enum ComponentAction {
22 WorkPart,
25 OpenPart,
29 SavePartAs,
32 EditFeature,
35 Move,
38 UnfixMove,
41 MoveCopy,
45 Replace,
49 ToggleFixed,
53 ToggleVisible,
55 Info,
57 Delete,
60}
61
62impl ComponentAction {
63 pub const ALL: [ComponentAction; 12] = [
65 ComponentAction::WorkPart,
66 ComponentAction::OpenPart,
67 ComponentAction::SavePartAs,
68 ComponentAction::EditFeature,
69 ComponentAction::Move,
70 ComponentAction::UnfixMove,
71 ComponentAction::MoveCopy,
72 ComponentAction::Replace,
73 ComponentAction::ToggleFixed,
74 ComponentAction::ToggleVisible,
75 ComponentAction::Info,
76 ComponentAction::Delete,
77 ];
78
79 pub fn id(self) -> &'static str {
81 match self {
82 ComponentAction::WorkPart => "make-work-part",
83 ComponentAction::OpenPart => "open-part",
84 ComponentAction::SavePartAs => "save-part-as",
85 ComponentAction::EditFeature => "edit-feature",
86 ComponentAction::Move => "move",
87 ComponentAction::UnfixMove => "unfix-move",
88 ComponentAction::MoveCopy => "move-copy",
89 ComponentAction::Replace => "replace",
90 ComponentAction::ToggleFixed => "toggle-fixed",
91 ComponentAction::ToggleVisible => "toggle-visible",
92 ComponentAction::Info => "info",
93 ComponentAction::Delete => "delete",
94 }
95 }
96
97 pub fn from_id(id: &str) -> Option<Self> {
99 Self::ALL.into_iter().find(|action| action.id() == id)
100 }
101
102 pub fn label(self, on: bool) -> &'static str {
105 match self {
106 ComponentAction::WorkPart => "\u{25CE} Make Work Part",
107 ComponentAction::OpenPart => "Open part",
108 ComponentAction::SavePartAs => "Save part as\u{2026}",
109 ComponentAction::EditFeature => "\u{270E} Edit feature",
110 ComponentAction::Move => "\u{2725} Move",
111 ComponentAction::UnfixMove => "\u{1F513} Unfix and Move",
112 ComponentAction::MoveCopy => "\u{29C9} Move Copy",
113 ComponentAction::Replace => "\u{21C4} Replace\u{2026}",
114 ComponentAction::ToggleFixed if on => "\u{1F513} Unfix",
115 ComponentAction::ToggleFixed => "\u{1F512} Fix",
116 ComponentAction::ToggleVisible if on => "\u{1f441} Hide",
117 ComponentAction::ToggleVisible => "\u{1f441} Show",
118 ComponentAction::Info => "\u{1f575} Info",
119 ComponentAction::Delete => "\u{2716} Delete",
120 }
121 }
122
123 pub fn tooltip(self) -> &'static str {
125 match self {
126 ComponentAction::WorkPart => "Edit this part here, in the context of the assembly (double click)",
127 ComponentAction::OpenPart => "Open this part in a tab of its own; the assembly updates as you edit",
128 ComponentAction::SavePartAs => "This part has no file of its own: save it to a new part file you name",
129 ComponentAction::EditFeature => "Roll to this component's feature and open it in the history",
130 ComponentAction::Move => "Move/rotate gizmo on-off (arrows + arcs together)",
131 ComponentAction::UnfixMove => {
132 "Unfix this component and move it (unfixed only when you drag; one undo step)"
133 }
134 ComponentAction::MoveCopy => {
135 "Add another instance of this part and drag it into place with the move gizmo"
136 }
137 ComponentAction::Replace => {
138 "Show another part here (a family member, a variation, any part); mates keep their faces by name"
139 }
140 ComponentAction::ToggleFixed => "Fix this part in place, or free it, for the solver (its Fixed setting)",
141 ComponentAction::ToggleVisible => "Hide or show this component",
142 ComponentAction::Info => "Open a pinned Info window for this component",
143 ComponentAction::Delete => "Delete this component instance",
144 }
145 }
146}
147
148#[derive(Clone, PartialEq, Eq, Debug)]
152pub enum ComponentActionRequest {
153 SavePartAs { component_id: String },
155 Replace { component_id: String },
157 WorkPart { component_path: String, in_place: bool },
160 Info { targets: Vec<String> },
162 EditFeature { feature_id: String },
164}
165
166pub const PATTERN_INFO: &str = "pattern-info";
168
169pub struct MenuSubject {
173 pub ids: Vec<String>,
174 pub embedded: bool,
177 pub fixed: Vec<Result<bool, String>>,
180 pub visible: bool,
182}
183
184impl MenuSubject {
185 pub fn of(state: &EngineState, ids: &[String]) -> Self {
188 let instance = ids.first().and_then(|id| brep_render::brep_kernel::pattern_component_instance(id));
189 let owner = |id: &String| match &instance {
190 Some((_, seed, _)) => seed.clone(),
191 None => id.clone(),
192 };
193 let embedded = ids.first().and_then(|id| state.component_info(&owner(id))).is_some_and(|info| {
194 !state.part_source(&info.part_name).is_some_and(|(key, _)| !key.is_empty())
195 });
196 let fixed = ids
197 .iter()
198 .map(|id| {
199 if instance.is_none() && id.contains(':') {
200 state.occurrence_fixed(id)
201 } else {
202 state
203 .component_info(&owner(id))
204 .map(|info| info.fixed)
205 .ok_or_else(|| format!("'{id}' is not an assembly component"))
206 }
207 })
208 .collect();
209 Self { ids: ids.to_vec(), embedded, fixed, visible: all_visible(state, ids) }
210 }
211}
212
213fn member_solids(state: &EngineState, id: &str) -> Vec<String> {
215 let prefix = format!("{id}:");
216 state
217 .scene
218 .solids()
219 .iter()
220 .filter(|solid| solid.name.starts_with(&prefix))
221 .map(|solid| solid.name.clone())
222 .collect()
223}
224
225fn all_visible(state: &EngineState, ids: &[String]) -> bool {
227 ids.iter().all(|id| {
228 let prefix = format!("{id}:");
229 state.scene.solids().iter().filter(|solid| solid.name.starts_with(&prefix)).all(|solid| solid.visible)
230 })
231}
232
233pub fn component_menu(subject: &MenuSubject) -> Vec<RowAction> {
237 let ids = &subject.ids;
238 let Some(first) = ids.first() else { return Vec::new() };
239 let instance = brep_render::brep_kernel::pattern_component_instance(first);
240 let nested = instance.is_none() && first.contains(':');
241 let rolled_up = ids.len() > 1;
242 let n = ids.len();
243 let fixed_states = &subject.fixed;
244 let any_fixed = fixed_states.iter().any(|state| matches!(state, Ok(true)));
245 let all_fixed = fixed_states.iter().all(|state| matches!(state, Ok(true)));
246 let visible = subject.visible;
247 let unpack = |verb: &str| format!("{n} placements on this row \u{2014} switch to Unpacked to {verb} one");
248 let inside = "A part inside a sub-assembly: make the sub-assembly the Work part to do this";
249 let copy = "A pattern copy: it follows its seed \u{2014} act on the seed";
250 let mut menu = Vec::new();
251 if let Some((pattern, seed, number)) = &instance {
252 menu.push(
253 RowAction::new(PATTERN_INFO, format!("Instance {number} of {pattern}: follows {seed}"))
254 .disabled("A pattern copy: these act on its seed, and every copy follows"),
255 );
256 }
257 for action in ComponentAction::ALL {
258 let label = match action {
259 ComponentAction::ToggleFixed => action.label(any_fixed),
260 ComponentAction::ToggleVisible => action.label(visible),
261 _ => action.label(false),
262 };
263 let entry = RowAction::new(action.id(), label).tooltip(action.tooltip());
264 let entry = match action {
265 ComponentAction::SavePartAs if !subject.embedded => continue,
266 ComponentAction::UnfixMove if !any_fixed || nested || instance.is_some() => continue,
267 ComponentAction::Move | ComponentAction::UnfixMove | ComponentAction::MoveCopy | ComponentAction::Delete
268 if instance.is_some() =>
269 {
270 entry.disabled(copy)
271 }
272 ComponentAction::ToggleFixed if instance.is_some() => entry.disabled(copy),
273 ComponentAction::Move
274 | ComponentAction::UnfixMove
275 | ComponentAction::MoveCopy
276 | ComponentAction::Delete
277 | ComponentAction::EditFeature
278 if nested =>
279 {
280 entry.disabled(inside)
281 }
282 ComponentAction::Move if rolled_up => entry.disabled(unpack("move")),
283 ComponentAction::UnfixMove if rolled_up => entry.disabled(unpack("move")),
284 ComponentAction::MoveCopy if rolled_up => entry.disabled(unpack("copy")),
285 ComponentAction::Delete if rolled_up => entry.disabled(unpack("delete")),
286 ComponentAction::Move if all_fixed => {
287 entry.disabled("This component is fixed \u{2014} Unfix it, or use Unfix and Move")
288 }
289 ComponentAction::ToggleFixed if rolled_up && nested => entry.disabled(unpack("fix or unfix")),
294 ComponentAction::ToggleFixed if rolled_up => entry.tooltip(if any_fixed {
295 format!("Unfix the fixed placements among the {n} on this row (one undo step)")
296 } else {
297 format!("Fix all {n} placements on this row in place (one undo step)")
298 }),
299 ComponentAction::ToggleFixed => match fixed_states.first() {
300 Some(Err(reason)) => entry.disabled(reason.clone()),
301 _ => entry,
302 },
303 _ => entry,
304 };
305 menu.push(match action {
306 ComponentAction::Delete => entry.separator_above().destructive(),
308 ComponentAction::EditFeature | ComponentAction::Move => entry.separator_above(),
309 _ => entry,
310 });
311 }
312 menu
313}
314
315pub fn run_component_action(
317 state: &mut EngineState,
318 action: ComponentAction,
319 component_id: &str,
320) -> Option<ComponentActionRequest> {
321 run_component_actions(state, action, &[component_id.to_string()])
322}
323
324pub fn run_component_actions(
330 state: &mut EngineState,
331 action: ComponentAction,
332 ids: &[String],
333) -> Option<ComponentActionRequest> {
334 let first = ids.first()?.clone();
335 let seed = brep_render::brep_kernel::pattern_component_instance(&first).map(|(_, seed, _)| seed);
338 let target = match (&seed, action) {
339 (Some(_), ComponentAction::Replace) | (None, _) => first.clone(),
340 (Some(seed), _) => seed.clone(),
341 };
342 match action {
343 ComponentAction::Move => {
344 state.component_move_toggle(&target);
347 None
348 }
349 ComponentAction::UnfixMove => {
350 state.component_move_unfix_toggle(&[target]);
351 None
352 }
353 ComponentAction::MoveCopy => {
354 let _ = state.component_move_copy(&target);
357 None
358 }
359 ComponentAction::ToggleFixed if ids.len() > 1 => {
360 let any_fixed = ids.iter().any(|id| state.component_info(id).is_some_and(|info| info.fixed));
363 if let Err(error) = state.set_components_fixed(ids, !any_fixed) {
364 state.push_notice(error);
365 }
366 None
367 }
368 ComponentAction::ToggleFixed if target.contains(':') => {
369 let toggled = state
372 .occurrence_fixed(&target)
373 .and_then(|fixed| state.set_component_fixed(&target, !fixed));
374 if let Err(error) = toggled {
375 state.push_notice(error);
376 }
377 None
378 }
379 ComponentAction::ToggleFixed => {
380 let Some(info) = state.component_info(&target) else {
381 state.push_notice(format!("'{target}' is not an assembly component"));
382 return None;
383 };
384 if let Err(error) = state.set_component_fixed(&target, !info.fixed) {
387 state.push_notice(error);
388 }
389 None
390 }
391 ComponentAction::ToggleVisible => {
392 let ids: Vec<String> = if seed.is_some() { vec![first] } else { ids.to_vec() };
393 let visible = !all_visible(state, &ids);
394 let solids: Vec<String> = ids.iter().flat_map(|id| member_solids(state, id)).collect();
395 state.set_visible_many(&solids, visible);
396 None
397 }
398 ComponentAction::Info => {
399 let ids: Vec<String> = if seed.is_some() { vec![first] } else { ids.to_vec() };
400 let targets: Vec<String> = ids.iter().flat_map(|id| member_solids(state, id)).collect();
401 (!targets.is_empty()).then_some(ComponentActionRequest::Info { targets })
402 }
403 ComponentAction::Delete => {
404 let _ = state.delete_feature(&target);
408 None
409 }
410 ComponentAction::SavePartAs => Some(ComponentActionRequest::SavePartAs { component_id: target }),
411 ComponentAction::EditFeature => {
412 let index = state.history.index_of(&target);
413 match index {
414 Some(index) => {
415 state.roll_to(index);
416 Some(ComponentActionRequest::EditFeature { feature_id: target })
417 }
418 None => {
419 state.push_notice(format!("'{target}' has no feature of its own"));
420 None
421 }
422 }
423 }
424 ComponentAction::Replace => Some(ComponentActionRequest::Replace { component_id: target }),
425 ComponentAction::WorkPart | ComponentAction::OpenPart => Some(ComponentActionRequest::WorkPart {
426 component_path: target,
427 in_place: action == ComponentAction::WorkPart,
428 }),
429 }
430}
431
432pub fn part_source_key(state: &EngineState, component_id: &str) -> Option<String> {
439 let info = state.component_info(component_id)?;
440 let document: serde_json::Value =
441 serde_json::from_str(&state.history_request_json()).ok()?;
442 document["partsLibrary"][&info.part_name]["sourceKey"]
443 .as_str()
444 .filter(|key| !key.is_empty())
445 .map(str::to_string)
446}
447