Feat/Sketch: associative pattern tools with independent directions and Flip/Center controls (#1601)

Thanks @Karl-VonRichter.
This commit is contained in:
Karl Von Richter authored and GitHub committed 2026-10-16 00:32:32 -04:00
1 parent 44968dbfe6
commit 371578a06f
36 files changed
+1394 -87

No files matched your search

+103 -22
View File
@@ -819,7 +819,8 @@ impl Dialog {
/// Dialogs holding a table get the wide limit.
pub fn is_wide(&self) -> bool {
matches!(self.kind, Kind::Params { .. }) || matches!(&self.kind, Kind::Motion(k) if k.wide())
matches!(self.kind, Kind::Params { .. } | Kind::SketchModify(crate::sketch_modify_ui::Mod::PatternRect { .. }))
|| matches!(&self.kind, Kind::Motion(k) if k.wide())
}
pub fn wants_picks(&self) -> bool {
@@ -1609,6 +1610,10 @@ fn selection_label(s: &Session, item: &Sel) -> String {
/// Selection input rows: label, "N selected" (or a hint) and a clear button; clicking a row
/// makes it the active input.
fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session) {
input_rows_range(d, ui, s, 0..d.inputs.len());
}
fn input_rows_range(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, range: std::ops::Range<usize>) {
// Edit Text shows its Path input among its own rows.
if matches!(d.kind, Kind::Text(_)) {
return;
@@ -1616,7 +1621,7 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session) {
let t = Tokens::get();
let mut clear: Option<usize> = None;
let mut activate: Option<usize> = None;
for (i, inp) in d.inputs.iter().enumerate() {
for (i, inp) in d.inputs.iter().enumerate().skip(range.start).take(range.end.saturating_sub(range.start)) {
row_label(ui, inp.label);
ui.horizontal(|ui| {
let active = i == d.active;
@@ -1685,6 +1690,87 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session) {
}
}
/// Each live reference sits with the independent values it drives.
fn rectangular_pattern_rows(d: &mut Dialog, ui: &mut egui::Ui, session: &Session) -> bool {
use crate::sketch_modify_ui::Mod;
use solidcraft_engine::sketch::PatternAlignment;
let mut enter = false;
ui.vertical(|ui| {
ui.label(tr("dialog.selection_guidance"));
egui::Grid::new("pattern_source").spacing(vec2(10.0, 8.0)).show(ui, |ui| input_rows_range(d, ui, session, 0..1));
ui.separator();
ui.columns(2, |columns| {
for (index, ui) in columns.iter_mut().enumerate() {
ui.strong(tr(if index == 0 { "Direction 1" } else { "Direction 2" }));
egui::Grid::new(("pattern_direction", index)).spacing(vec2(8.0, 8.0)).show(ui, |ui| {
input_rows_range(d, ui, session, index + 1..index + 2);
let Kind::SketchModify(Mod::PatternRect { count, spacing, count2, spacing2, alignment, alignment2, reversed, reversed2 }) =
&mut d.kind
else {
return;
};
let (count, spacing, alignment, reversed, count_label, spacing_label, alignment_label) = if index == 0 {
(count, spacing, alignment, reversed, "Quantity", "Spacing", "Alignment")
} else {
(count2, spacing2, alignment2, reversed2, "Quantity 2", "Spacing 2", "Alignment 2")
};
row_label(ui, count_label);
enter |= field(ui, count);
ui.end_row();
row_label(ui, spacing_label);
enter |= field(ui, spacing);
ui.end_row();
row_label(ui, alignment_label);
ui.horizontal(|ui| {
pattern_toggle(ui, alignment_label, "Flip direction", "pattern_flip", reversed);
let mut centered = *alignment == PatternAlignment::Center;
pattern_toggle(ui, alignment_label, "Center", "pattern_center", &mut centered);
*alignment = if centered { PatternAlignment::Center } else { PatternAlignment::Right };
});
ui.end_row();
});
}
});
ui.small(tr("Counts include the original. Center balances copies; Flip chooses the extra side for even totals."));
});
ui.end_row();
enter
}
pub(crate) fn pattern_toggle(ui: &mut egui::Ui, row: &str, label: &str, icon: &str, selected: &mut bool) {
let response = ui.add(egui::Button::new("").selected(*selected).min_size(vec2(28.0, 24.0)));
let t = Tokens::get();
crate::icons::paint(ui.painter(), response.rect.shrink(3.0), icon, t.icon, t.icon_fill, t.accent);
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, *selected, tr(label)));
crate::scenario::publish_handle(&format!("dialog:{row}:{label}"), response.rect.center());
if response.on_hover_text(tr(label)).clicked() {
*selected = !*selected;
}
}
fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut egui::Ui, full_width: bool) {
if matches!(d.kind, Kind::SketchModify(_))
&& d.previews()
&& let Err(error) = apply_commands(app, d)
{
d.refuse(app, error);
}
if let Some(error) = d.error.as_ref().or(app.preview.error.as_ref()).cloned() {
if !full_width {
row_label(ui, "");
}
ui.vertical(|ui| {
ui.add(egui::Label::new(RichText::new(error.as_str()).color(Tokens::get().error)).wrap());
let response = ui.button("Create Issue…");
crate::scenario::publish_handle("dialog:create_issue", response.rect.center());
if response.clicked() {
crate::issue_report::begin(app, ctx, Some(d), Some(&error));
}
});
ui.end_row();
}
}
/// Width limits of a docked dialog: shrink-wrapped to its content within these.
use crate::frame::{DIALOG_MAX_W, DIALOG_MIN_W, DIALOG_WIDE_MAX_W};
/// Width of value fields and choice boxes in a dialog.
@@ -1998,6 +2084,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut cancel = false;
let mut enter = false;
let wide = d.is_wide();
let pattern_columns = matches!(d.kind, Kind::SketchModify(crate::sketch_modify_ui::Mod::PatternRect { .. }));
let raw_title = title(&d.kind);
let heading = if d.editing.is_some() { crate::i18n::format("dialog.edit_title", &[("title", tr(raw_title))]) } else { tr(raw_title).to_string() };
let frame = crate::frame::dock_frame(ctx, side);
@@ -2027,7 +2114,13 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
// Long texts (errors, notes) wrap instead of widening the dialog.
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
let margin = horizontal_frame;
let min_w = if wide { 480.0 } else { DIALOG_MIN_W } - margin;
let min_w = if pattern_columns {
540.0
} else if wide {
480.0
} else {
DIALOG_MIN_W
} - margin;
ui.set_min_width(min_w);
ui.set_max_width(if wide { DIALOG_WIDE_MAX_W } else { DIALOG_MAX_W } - margin);
// Rows that end at the right edge line up with last frame's content width.
@@ -2081,7 +2174,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if let Kind::Motion(k @ crate::dialogs_motion::Mo::Configurations { .. }) = &mut d.kind {
enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs);
}
egui::Grid::new("sc_dialog_grid").num_columns(2).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
egui::Grid::new("sc_dialog_grid").num_columns(if pattern_columns { 1 } else { 2 }).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
if pattern_columns {
enter |= rectangular_pattern_rows(&mut d, ui, &app.session);
error_rows(app, ctx, &mut d, ui, true);
return;
}
if matches!(&d.kind, Kind::Motion(crate::dialogs_motion::Mo::Configurations { .. })) {
return;
}
@@ -2798,24 +2896,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
}
}
if matches!(d.kind, Kind::SketchModify(_))
&& d.previews()
&& let Err(error) = apply_commands(app, &d)
{
d.refuse(app, error);
}
if let Some(e) = d.error.as_ref().or(app.preview.error.as_ref()).cloned() {
row_label(ui, "");
ui.vertical(|ui| {
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
let response = ui.button("Create Issue…");
crate::scenario::publish_handle("dialog:create_issue", response.rect.center());
if response.clicked() {
crate::issue_report::begin(app, ctx, Some(&d), Some(&e));
}
});
ui.end_row();
}
error_rows(app, ctx, &mut d, ui, false);
});
});
ui.add_space(6.0);