Files
solidcraft/crates/ui-egui/src/dialogs_frame.rs
T
3045d0787e Dialog curve picks keep their sketch (#4057) (#4081)
* Dialog curve picks keep their sketch (#3726)

* Format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Adapt context-menu test to Sel::SketchCurve's sketch field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Path Flange picks keep their sketch too

Path Flange (#4096) landed after this PR and still looked its path up in the newest sketch with those ids.

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:20:03 +00:00

216 lines
9.5 KiB
Rust

//! Frame Member (`frame.member`): structural members along the lines of a frame sketch. The lines
//! are picked in the view; the section is a built-in profile with its dimensions, or the profiles
//! picked in another sketch. Anchor, angle and mirror place the section on each line.
use serde_json::{Map, Value, json};
use solidcraft_engine::Sel;
use solidcraft_engine::doc::expr::Kind as ValueKind;
use solidcraft_engine::doc::frame::{self, MemberProfile};
use solidcraft_engine::doc::{FeatureKind, ProfileSel};
use crate::SolveApp;
use crate::dialogs::{Kind, check, curves_sketch, form_row, profile_indices, put, shown_expr};
use crate::selection::{PROFILES, SKETCH_LINES, SelInput, SelectionExt as _};
use crate::ui_kit::{choice_row, expr_row, hint_row, text_row};
/// The anchors as `frame.member` names them, and their labels.
const ANCHORS: [(&str, &str); 11] = [
("origin", "Origin"),
("center", "Center"),
("centroid", "Centroid"),
("top_left", "Top Left"),
("top", "Top Middle"),
("top_right", "Top Right"),
("left", "Left Middle"),
("right", "Right Middle"),
("bottom_left", "Bottom Left"),
("bottom", "Bottom Middle"),
("bottom_right", "Bottom Right"),
];
/// The Frame Member dialog's values (the picks are its selection inputs).
#[derive(Clone, Debug)]
pub struct Member {
/// Every line of the frame sketch, now and as lines are added, not only the lines picked.
pub all: bool,
/// The built-in profile (an index into `frame::profile_kinds`), used when no section is picked.
pub kind: usize,
/// Each built-in profile's dimensions, its required ones then its optional ones; kept per
/// profile so switching back finds them again. An empty optional one takes its default.
pub dims: Vec<Vec<String>>,
/// A profile sketch whose whole drawn section is the member's (nested loops alternating), as
/// a feature made that way keeps it while no section is picked.
pub whole: Option<u64>,
pub anchor: usize,
pub angle: String,
pub mirror: bool,
pub name: String,
}
impl Member {
fn new() -> Member {
let dims = frame::profile_kinds()
.iter()
.map(|k| {
let example = serde_json::to_value(&k.example).unwrap_or(Value::Null);
let given = k.required.iter().map(|d| example.get(*d).and_then(Value::as_f64).map(|v| format!("{v} mm")).unwrap_or_default());
given.chain(k.optional.iter().map(|_| String::new())).collect()
})
.collect();
// A rectangular tube is the most common member.
let kind = frame::profile_kinds().iter().position(|k| k.kind == "rect_tube").unwrap_or(0);
Member { all: false, kind, dims, whole: None, anchor: 0, angle: String::new(), mirror: false, name: String::new() }
}
}
/// The dialog for Frame Member.
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
(id == "frame.member")
.then(|| (Kind::Frame(Member::new()), vec![SelInput::new("Lines", SKETCH_LINES, true), SelInput::new("Section (optional)", PROFILES, true)]))
}
fn tr(text: &str) -> &str {
crate::i18n::tr_label(text)
}
/// The dialog's own rows (after the selection inputs); true when Enter was pressed in a value.
pub fn rows(app: &SolveApp, ui: &mut egui::Ui, m: &mut Member, inputs: &[SelInput]) -> bool {
let mut enter = false;
form_row(ui, "All Lines", |ui| check(ui, &mut m.all));
let picked = !crate::selection::items(inputs, 1).is_empty();
if let Some(sketch) = m.whole.filter(|_| !picked) {
let name = app.session.doc.feature(sketch).map(|f| f.name.clone()).unwrap_or_default();
let mut on = true;
form_row(ui, "Section", |ui| crate::ui_kit::checkbox(ui, &name, &mut on));
if !on {
m.whole = None;
}
} else if !picked {
let kinds = frame::profile_kinds();
let labels: Vec<&str> = kinds.iter().map(|k| k.label).collect();
choice_row(ui, "Profile", "frame_profile", &mut m.kind, &labels);
if let (Some(k), Some(values)) = (kinds.get(m.kind), m.dims.get_mut(m.kind)) {
for (i, (name, v)) in k.required.iter().chain(k.optional).zip(values.iter_mut()).enumerate() {
let label = frame::dimension_label(name);
enter |= if i < k.required.len() { expr_row(ui, label, v) } else { hint_row(ui, label, v, tr("default")) };
}
}
}
let anchors: Vec<&str> = ANCHORS.iter().map(|a| a.1).collect();
choice_row(ui, "Anchor", "frame_anchor", &mut m.anchor, &anchors);
enter |= hint_row(ui, "Angle", &mut m.angle, "0 deg");
form_row(ui, "Mirror", |ui| check(ui, &mut m.mirror));
enter |= text_row(ui, "Name", &mut m.name, tr("from the profile")).0;
enter
}
/// The command the dialog's OK runs (`extra`: parameters kept from the feature being edited).
pub fn commands(app: &SolveApp, m: &Member, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
let section = crate::selection::items(inputs, 1).profiles();
let section_sketch = section.first().map(|x| x.0).or(m.whole);
if section.iter().any(|x| Some(x.0) != section_sketch) {
return Err("the section's profiles must be in one sketch".into());
}
// A line picked in the view comes with its sketch (line ids are a sketch's own); a line
// selected before the dialog opened is looked for in the newest sketch with it, not the
// section's.
let picks = crate::selection::items(inputs, 0);
let mut lines = Vec::new();
let mut known = None;
for pick in picks {
match pick {
Sel::ExternalSketchCurve { sketch, id } | Sel::SketchCurve { id, sketch: Some(sketch) } => {
if known.is_some_and(|k| k != *sketch) {
return Err("the lines must be in one sketch".into());
}
known = Some(*sketch);
lines.push(id.clone());
}
Sel::SketchCurve { id, sketch: None } => lines.push(id.clone()),
_ => {}
}
}
if lines.is_empty() {
return Err("select the frame sketch's lines first".into());
}
let sketch = known.or_else(|| curves_sketch(app, &lines, section_sketch)).ok_or("the lines must be in one sketch, not the section's")?;
let mut p = json!({"sketch": sketch});
if !m.all {
p["lines"] = json!(lines);
}
if let Some(ps) = section_sketch {
p["profile_sketch"] = json!(ps);
if !section.is_empty() {
p["profiles"] = json!(section.iter().map(|x| x.1).collect::<Vec<_>>());
}
} else {
let kinds = frame::profile_kinds();
let (k, values) = kinds.get(m.kind).zip(m.dims.get(m.kind)).ok_or("choose a profile")?;
let mut profile = json!({"kind": k.kind});
for (name, v) in k.required.iter().chain(k.optional).zip(values) {
if !v.trim().is_empty() {
profile[*name] = json!(v);
}
}
p["profile"] = profile;
}
if m.anchor != 0
&& let Some((anchor, _)) = ANCHORS.get(m.anchor)
{
p["anchor"] = json!(anchor);
}
put(&mut p, &[("angle", &m.angle), ("body_name", m.name.trim())]);
if m.mirror {
p["mirror"] = json!(true);
}
if let Value::Object(o) = &mut p {
for (key, v) in extra {
o.entry(key.clone()).or_insert_with(|| v.clone());
}
}
Ok(vec![("frame.member".to_string(), p)])
}
/// The dialog editing a frame member feature, filled from it.
pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelInput>, Map<String, Value>)> {
let FeatureKind::FrameMember { path_sketch, lines, profile, anchor, angle, mirror, body_name, .. } = kind else { return None };
let (_, mut inputs) = start(app, "frame.member")?;
let st = app.session.model.state();
let mut m = Member::new();
m.all = lines.is_empty();
let ids = if m.all { frame::member_line_ids(&st, kind).unwrap_or_default() } else { lines.clone() };
if let Some(i) = inputs.get_mut(0) {
i.items = ids.into_iter().map(|id| Sel::ExternalSketchCurve { sketch: path_sketch.0, id }).collect();
}
match profile {
MemberProfile::Library { profile } => {
let kinds = frame::profile_kinds();
if let Some((index, k)) = kinds.iter().enumerate().find(|(_, k)| k.kind == profile.kind)
&& let Some(values) = m.dims.get_mut(index)
{
m.kind = index;
for (name, v) in k.required.iter().chain(k.optional).zip(values.iter_mut()) {
*v = profile.dims.get(*name).map(|e| shown_expr(e, ValueKind::Length)).unwrap_or_default();
}
}
}
MemberProfile::Sketch { sketch, profiles: ProfileSel::All } => m.whole = Some(sketch.0),
MemberProfile::Sketch { sketch, profiles } => {
let picks = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
if let Some(i) = inputs.get_mut(1) {
i.items = picks.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
}
}
}
let anchor = serde_json::to_value(anchor).ok();
m.anchor = ANCHORS.iter().position(|(a, _)| anchor.as_ref().and_then(Value::as_str) == Some(*a)).unwrap_or(0);
m.angle = angle.as_deref().map(|a| shown_expr(a, ValueKind::Angle)).unwrap_or_default();
m.mirror = *mirror;
m.name = body_name.clone().unwrap_or_default();
Some((Kind::Frame(m), inputs, Map::new()))
}
#[cfg(test)]
#[path = "dialogs_frame_tests.rs"]
mod tests;