forked from 1b-agent/opencadstudio-upstream
Harden parametric sketch constraints
This commit is contained in:
1 parent
41c07e9858
commit
9b58ca8a93
101 files changed
+3128
-13767
No files matched your search
+4
-12
@@ -728,12 +728,7 @@ struct SelectionCanvas {
|
||||
crosshair_bg: [f32; 4],
|
||||
crosshair: CrosshairOptions,
|
||||
selection_visual: SelectionVisualOptions,
|
||||
/// One glyph per persistent sketch constraint in the current scope
|
||||
/// (design doc §6.3/§7): screen anchor + label text (its symbol, plus a
|
||||
/// dimensional kind's driving value — `sketch_constraints::glyph_label`)
|
||||
/// + whether `solve_scope`'s `classify_redundant` call (stage 10/11)
|
||||
/// flagged this constraint as redundant/conflicting — the design doc
|
||||
/// §6.3's recommended "glyph color, not wire recoloring" state cue.
|
||||
/// Constraint glyph anchor, label, and conflict state for the current scope.
|
||||
constraint_glyphs: Vec<(Point, String, bool)>,
|
||||
}
|
||||
|
||||
@@ -1630,18 +1625,15 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
frame.stroke(&b1, stroke.clone());
|
||||
frame.stroke(&b2, stroke);
|
||||
}
|
||||
// Persistent sketch-constraint glyphs (design doc §6.3/§7): a small
|
||||
// pill at each constraint's anchor point, its symbol (plus value for
|
||||
// a dimensional kind) as text — visual-only for now, no hit-testing.
|
||||
// Constraint glyphs are visual-only and have no hit testing.
|
||||
if !self.constraint_glyphs.is_empty() {
|
||||
const GLYPH_SIZE: f32 = 11.0;
|
||||
const GLYPH_PAD_X: f32 = 5.0;
|
||||
const GLYPH_PAD_Y: f32 = 2.0;
|
||||
let normal_bg = theme.palette().primary.base.color;
|
||||
let normal_fg = theme.palette().primary.base.text;
|
||||
// Design doc §6.3's recommended color cue: a constraint
|
||||
// `solve_scope` flagged as redundant/conflicting (stage 10/11's
|
||||
// `classify_redundant`) gets the danger palette instead of the
|
||||
// A redundant or conflicting constraint gets the danger palette
|
||||
// instead of the
|
||||
// ordinary primary one — same information a resolver panel
|
||||
// would show, surfaced right on the geometry.
|
||||
let conflict_bg = theme.palette().danger.base.color;
|
||||
|
||||
@@ -1397,8 +1397,7 @@ impl PropertiesPanel {
|
||||
}
|
||||
|
||||
/// One named-parameter row: editable name + formula, with the live-
|
||||
/// resolved value (or error) shown alongside — the "AutoCAD style" the
|
||||
/// Parameters section is modeled on, embedded in the panel instead of
|
||||
/// resolved value (or error) shown alongside, embedded in the panel instead of
|
||||
/// the old separate modal. Each field commits on submit (Enter / losing
|
||||
/// focus), not per keystroke — `ParameterTable::set` validates
|
||||
/// immediately and would otherwise reject a formula mid-type (the same
|
||||
|
||||
@@ -133,9 +133,9 @@ pub(super) fn group_anchor<'a>(title: &str, content: Element<'a, Message>) -> El
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn group_title<'a>(title: &'static str, open: &Option<String>, font_size: f32) -> Element<'a, Message> {
|
||||
pub(super) fn group_title<'a>(title: &'static str, open: &Option<String>) -> Element<'a, Message> {
|
||||
if title != "Draw" || TOOLS.is_empty() {
|
||||
return container(text(t!(title)).size(font_size).style(muted_text_style))
|
||||
return container(text(t!(title)).size(9).style(muted_text_style))
|
||||
.padding([1, 4])
|
||||
.into();
|
||||
}
|
||||
@@ -148,7 +148,7 @@ pub(super) fn group_title<'a>(title: &'static str, open: &Option<String>, font_s
|
||||
PosReport::new(
|
||||
PANEL_ID,
|
||||
button(
|
||||
row![text(t!(title)).size(font_size), arrow]
|
||||
row![text(t!(title)).size(9), arrow]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
|
||||
+6
-51
@@ -96,12 +96,6 @@ pub struct Ribbon {
|
||||
tool_bar_h: Arc<AtomicU32>,
|
||||
/// User-chosen panel density (persisted). `Auto` sizes by window width.
|
||||
collapse_mode: CollapseMode,
|
||||
/// Font size (px) of the caption under a large button's icon (persisted;
|
||||
/// Options → Display → "Ribbon label size").
|
||||
label_font_size: i32,
|
||||
/// Font size (px) of a panel's own title, shown beneath its tools
|
||||
/// (persisted; Options → Display → "Ribbon section title size").
|
||||
group_title_font_size: i32,
|
||||
/// Set by `CollapsePanels` when the tool row is in its tight state; the mode
|
||||
/// selector hides itself then to give the cramped tab row its space back.
|
||||
collapse_tight: Arc<AtomicBool>,
|
||||
@@ -198,8 +192,6 @@ impl Ribbon {
|
||||
tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())),
|
||||
collapse_mode: CollapseMode::default(),
|
||||
collapse_tight: Arc::new(AtomicBool::new(false)),
|
||||
label_font_size: 10,
|
||||
group_title_font_size: 9,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,28 +206,6 @@ impl Ribbon {
|
||||
self.collapse_mode
|
||||
}
|
||||
|
||||
/// Change the icon-label font size. Persistence is handled by the caller
|
||||
/// via the consolidated app config (`save_config`).
|
||||
pub fn set_label_font_size(&mut self, size: i32) {
|
||||
self.label_font_size = size.clamp(8, 16);
|
||||
}
|
||||
|
||||
/// The current icon-label font size (for saving into the app config).
|
||||
pub fn label_font_size(&self) -> i32 {
|
||||
self.label_font_size
|
||||
}
|
||||
|
||||
/// Change the panel-title font size. Persistence is handled by the
|
||||
/// caller via the consolidated app config (`save_config`).
|
||||
pub fn set_group_title_font_size(&mut self, size: i32) {
|
||||
self.group_title_font_size = size.clamp(7, 14);
|
||||
}
|
||||
|
||||
/// The current panel-title font size (for saving into the app config).
|
||||
pub fn group_title_font_size(&self) -> i32 {
|
||||
self.group_title_font_size
|
||||
}
|
||||
|
||||
/// Current tab-bar height as last measured by the `WrapBar` widget.
|
||||
fn tab_bar_height(&self) -> f32 {
|
||||
f32::from_bits(self.tab_bar_h.load(Ordering::Relaxed))
|
||||
@@ -633,8 +603,6 @@ impl Ribbon {
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
self.label_font_size,
|
||||
self.group_title_font_size,
|
||||
),
|
||||
render_group(
|
||||
true,
|
||||
@@ -649,8 +617,6 @@ impl Ribbon {
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
self.label_font_size,
|
||||
self.group_title_font_size,
|
||||
),
|
||||
collapse_button(
|
||||
g,
|
||||
@@ -665,7 +631,6 @@ impl Ribbon {
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
self.label_font_size,
|
||||
false,
|
||||
),
|
||||
collapse_button(
|
||||
@@ -681,14 +646,13 @@ impl Ribbon {
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
self.label_font_size,
|
||||
true,
|
||||
),
|
||||
],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), widgets::tool_bar_h(self.group_title_font_size as f32))
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
|
||||
.report_height(self.tool_bar_h.clone())
|
||||
.report_tight(self.collapse_tight.clone())
|
||||
.mode(self.collapse_mode)
|
||||
@@ -1339,7 +1303,7 @@ impl Ribbon {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single ribbon panel (tools + group label), `tool_bar_h(group_title_font_size)` tall.
|
||||
/// Render a single ribbon panel (tools + group label), fixed `TOOL_BAR_H` tall.
|
||||
/// When `compact`, large tools/dropdowns are drawn as small icon columns.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_group<'a>(
|
||||
@@ -1355,8 +1319,6 @@ fn render_group<'a>(
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
label_font_size: i32,
|
||||
group_title_font_size: i32,
|
||||
) -> Element<'a, Message> {
|
||||
let mut items_row: Vec<Element<Message>> = Vec::new();
|
||||
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
||||
@@ -1373,7 +1335,6 @@ fn render_group<'a>(
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact,
|
||||
label_font_size: label_font_size as f32,
|
||||
};
|
||||
|
||||
for item in &group.tools {
|
||||
@@ -1411,12 +1372,12 @@ fn render_group<'a>(
|
||||
|
||||
draw_panel::group_anchor(group.title, column![
|
||||
tools_el,
|
||||
draw_panel::group_title(group.title, open_dd, group_title_font_size as f32),
|
||||
draw_panel::group_title(group.title, open_dd),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(0)
|
||||
.padding([3u16, 4])
|
||||
.height(Length::Fixed(widgets::tool_bar_h(group_title_font_size as f32)))
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.into())
|
||||
}
|
||||
|
||||
@@ -1479,7 +1440,6 @@ fn collapse_button<'a>(
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
label_font_size: i32,
|
||||
// When set, this is the TIGHT step (reached once even the all-collapsed row
|
||||
// overflows): the whole panel becomes a single non-running button — the first
|
||||
// tool's icon + a ▾ that opens the tools flyout. Nothing runs directly here.
|
||||
@@ -1520,9 +1480,7 @@ fn collapse_button<'a>(
|
||||
.style(button::subtle)
|
||||
.width(Fill)
|
||||
.padding([3, 5]);
|
||||
// This tight-mode panel *title* (not an icon caption) keeps its own
|
||||
// fixed size regardless of the icon-label font-size setting.
|
||||
return automatic_large_button(localized_title, content.into(), 9.0);
|
||||
return automatic_large_button(localized_title, content.into());
|
||||
}
|
||||
|
||||
// Collapsed (not yet tight): a large representative-tool face — a live button
|
||||
@@ -1545,7 +1503,6 @@ fn collapse_button<'a>(
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact: false,
|
||||
label_font_size: label_font_size as f32,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1564,7 +1521,6 @@ fn collapse_button<'a>(
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact: false,
|
||||
label_font_size: label_font_size as f32,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1589,8 +1545,7 @@ fn collapse_button<'a>(
|
||||
.style(button::subtle)
|
||||
.width(Fill)
|
||||
.padding([1, 4]);
|
||||
// Same fixed-size opener title as the tight-mode branch above.
|
||||
let opener = automatic_large_button(localized_title, opener.into(), 9.0);
|
||||
let opener = automatic_large_button(localized_title, opener.into());
|
||||
|
||||
// The large face fills a fixed slot so a collapsed panel is shorter than a full
|
||||
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
|
||||
|
||||
+28
-92
@@ -56,40 +56,15 @@ pub(super) const LARGE_W: f32 = ROW_H * 2.2;
|
||||
const LARGE_LABEL_HPAD: f32 = 8.0;
|
||||
/// Large ribbon labels use at most two lines before their button grows.
|
||||
const LARGE_LABEL_LINES: f32 = 2.0;
|
||||
const LARGE_LABEL_SIZE: f32 = 10.0;
|
||||
/// Width of a 1-row (small) button.
|
||||
pub(super) const SMALL_W: f32 = ROW_H;
|
||||
/// Width of the ▾ strip on a small dropdown.
|
||||
pub(super) const ARROW_W: f32 = ROW_H * 0.4;
|
||||
/// Height of the ▾ strip at the bottom of a large dropdown.
|
||||
pub(super) const LARGE_ARR: f32 = ROW_H * 0.55;
|
||||
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label,
|
||||
/// at the default 9px group-title font size. Use [`tool_bar_h`] once the live
|
||||
/// `group_title_font_size` setting is in scope — this constant is only the
|
||||
/// floor `tool_bar_h` never drops below, kept as its own name for the few
|
||||
/// call sites that predate the size becoming user-configurable.
|
||||
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label.
|
||||
pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
|
||||
|
||||
/// The group-title label's rendered height (`group_title_font_size == 9`)
|
||||
/// baked into [`TOOL_BAR_H`]'s fixed 18px allowance (6px padding + this).
|
||||
const GROUP_TITLE_LABEL_H_AT_DEFAULT: f32 = 12.0;
|
||||
const GROUP_TITLE_FONT_SIZE_DEFAULT: f32 = 9.0;
|
||||
|
||||
/// Ribbon tool-area height for a given group-title font size.
|
||||
///
|
||||
/// The panel column that holds a group's icons above its title label is a
|
||||
/// fixed height (`TOOL_BAR_H`, sized for the *default* title font). Raising
|
||||
/// the "Section title size" option (`RibbonConfig::group_title_font_size`,
|
||||
/// `src/app/config.rs`) grows the title text without growing that fixed
|
||||
/// column, so the icon row above it — which fills whatever space is left —
|
||||
/// gets squeezed and its icons visibly shrink. Scaling the label's share of
|
||||
/// the height with the font size (proportionally to the default 12px
|
||||
/// allowance at the default 9px size) keeps the icon row's share constant
|
||||
/// instead. Clamped to never go *below* `TOOL_BAR_H`, so the default and
|
||||
/// smaller settings render pixel-identically to before this existed.
|
||||
pub(super) fn tool_bar_h(group_title_font_size: f32) -> f32 {
|
||||
let label_h = GROUP_TITLE_LABEL_H_AT_DEFAULT * (group_title_font_size / GROUP_TITLE_FONT_SIZE_DEFAULT);
|
||||
(3.0 * ROW_H + 6.0 + label_h).max(TOOL_BAR_H)
|
||||
}
|
||||
/// Height of a collapsed panel button's large representative face (big icon +
|
||||
/// its label). A collapsed button is this face plus the title opener, so it is
|
||||
/// shorter than a full 3-row panel — the ribbon height follows it down.
|
||||
@@ -111,11 +86,8 @@ const COMBO_PANEL_PAD: Padding = Padding {
|
||||
|
||||
thread_local! {
|
||||
/// Ribbon layout runs on every pointer-driven view update. Cache the font-
|
||||
/// measured width per (translated label, font size) so the automatic
|
||||
/// sizing stays cheap — keyed on both since the same label text is
|
||||
/// measured at more than one size (a full-density button's own caption
|
||||
/// vs. a collapsed panel's smaller opener title).
|
||||
static LARGE_WIDTH_CACHE: RefCell<HashMap<(String, u32), f32>> =
|
||||
/// measured width per translated label so the automatic sizing stays cheap.
|
||||
static LARGE_WIDTH_CACHE: RefCell<HashMap<String, f32>> =
|
||||
RefCell::new(HashMap::default());
|
||||
}
|
||||
|
||||
@@ -124,7 +96,6 @@ fn ribbon_label_bounds(
|
||||
label: &str,
|
||||
width: f32,
|
||||
wrapping: advanced_text::Wrapping,
|
||||
label_size: f32,
|
||||
) -> Size {
|
||||
use advanced_text::{Paragraph as _, Renderer as _};
|
||||
|
||||
@@ -132,7 +103,7 @@ fn ribbon_label_bounds(
|
||||
advanced_text::Text {
|
||||
content: label,
|
||||
bounds: Size::new(width, f32::INFINITY),
|
||||
size: Pixels(label_size),
|
||||
size: Pixels(LARGE_LABEL_SIZE),
|
||||
line_height: advanced_text::LineHeight::default(),
|
||||
font: renderer.default_font(),
|
||||
align_x: advanced_text::Alignment::Center,
|
||||
@@ -146,41 +117,27 @@ fn ribbon_label_bounds(
|
||||
paragraph.min_bounds()
|
||||
}
|
||||
|
||||
/// Widest a large button will grow to keep its label on one line before
|
||||
/// falling back to two-line wrapping instead. A label pre-broken with an
|
||||
/// explicit `\n` (long labels get hand-wrapped at the source, e.g. "Viewport
|
||||
/// Configuration") measures its own natural per-line width regardless, so
|
||||
/// this only ever engages for a plain label with no manual break.
|
||||
const LARGE_LABEL_MAX_SINGLE_LINE_W: f32 = LARGE_W * 2.0;
|
||||
|
||||
/// Measure the translated label at the normal button width. Prefers widening
|
||||
/// the button to keep the label on one line — most labels are short enough
|
||||
/// for this to be a small, one-time bump — falling back to two-line wrapping
|
||||
/// only once even a single line would need excessive width. The binary
|
||||
/// search uses the renderer's real font metrics, so locale and UI scale
|
||||
/// Measure the translated label at the normal button width. It wraps first;
|
||||
/// only labels that would need more than two lines widen their button. The
|
||||
/// binary search uses the renderer's real font metrics, so locale and UI scale
|
||||
/// changes do not rely on character-count estimates.
|
||||
fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32) -> f32 {
|
||||
fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
|
||||
let base_inner = (LARGE_W - LARGE_LABEL_HPAD).max(1.0);
|
||||
let line_height = advanced_text::LineHeight::default()
|
||||
.to_absolute(Pixels(label_size))
|
||||
.to_absolute(Pixels(LARGE_LABEL_SIZE))
|
||||
.0;
|
||||
|
||||
// `Wrapping::Word` (not `WordOrGlyph`): a label that doesn't fit at this
|
||||
// width wraps at the next word boundary instead of splitting mid-word.
|
||||
// The width check catches the rare case Word-wrapping can't help — a
|
||||
// single unbreakable "word" wider than the box — so that case correctly
|
||||
// falls through to the wider stage below instead of reporting a false fit.
|
||||
let fits_one_line = |width: f32| {
|
||||
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
|
||||
let fits = |width: f32| {
|
||||
let bounds = ribbon_label_bounds(
|
||||
renderer,
|
||||
label,
|
||||
width,
|
||||
advanced_text::Wrapping::Word,
|
||||
label_size,
|
||||
);
|
||||
bounds.width <= width + 0.5 && bounds.height <= line_height + 0.5
|
||||
bounds.width <= width + 0.5 && bounds.height <= max_label_height
|
||||
};
|
||||
if fits_one_line(base_inner) {
|
||||
|
||||
if fits(base_inner) {
|
||||
return LARGE_W;
|
||||
}
|
||||
|
||||
@@ -189,33 +146,18 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32)
|
||||
label,
|
||||
f32::INFINITY,
|
||||
advanced_text::Wrapping::None,
|
||||
label_size,
|
||||
)
|
||||
.width
|
||||
.max(base_inner);
|
||||
if natural <= LARGE_LABEL_MAX_SINGLE_LINE_W {
|
||||
if !fits(natural) {
|
||||
return (natural + LARGE_LABEL_HPAD).ceil();
|
||||
}
|
||||
|
||||
// Long enough that a single line would make the button unreasonably
|
||||
// wide — wrap to two lines instead, at the narrowest width (up to the
|
||||
// same cap) that fits.
|
||||
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
|
||||
let fits_two_lines = |width: f32| {
|
||||
let bounds = ribbon_label_bounds(
|
||||
renderer,
|
||||
label,
|
||||
width,
|
||||
advanced_text::Wrapping::Word,
|
||||
label_size,
|
||||
);
|
||||
bounds.width <= width + 0.5 && bounds.height <= max_label_height
|
||||
};
|
||||
let mut low = base_inner;
|
||||
let mut high = LARGE_LABEL_MAX_SINGLE_LINE_W;
|
||||
let mut high = natural;
|
||||
for _ in 0..10 {
|
||||
let mid = (low + high) * 0.5;
|
||||
if fits_two_lines(mid) {
|
||||
if fits(mid) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid;
|
||||
@@ -224,15 +166,14 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str, label_size: f32)
|
||||
(high + LARGE_LABEL_HPAD).ceil().max(LARGE_W)
|
||||
}
|
||||
|
||||
fn automatic_large_width(renderer: &iced::Renderer, label: &str, label_size: f32) -> f32 {
|
||||
let key = (label.to_string(), label_size.to_bits());
|
||||
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(&key).copied()) {
|
||||
fn automatic_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
|
||||
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(label).copied()) {
|
||||
return width;
|
||||
}
|
||||
|
||||
let width = measure_large_width(renderer, label, label_size);
|
||||
let width = measure_large_width(renderer, label);
|
||||
LARGE_WIDTH_CACHE.with(|cache| {
|
||||
cache.borrow_mut().insert(key, width);
|
||||
cache.borrow_mut().insert(label.to_string(), width);
|
||||
});
|
||||
width
|
||||
}
|
||||
@@ -240,7 +181,6 @@ fn automatic_large_width(renderer: &iced::Renderer, label: &str, label_size: f32
|
||||
struct AutomaticLargeWidth<'a> {
|
||||
label: String,
|
||||
content: Element<'a, Message>,
|
||||
label_size: f32,
|
||||
}
|
||||
|
||||
impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
|
||||
@@ -266,7 +206,7 @@ impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
|
||||
renderer: &iced::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let width = automatic_large_width(renderer, &self.label, self.label_size);
|
||||
let width = automatic_large_width(renderer, &self.label);
|
||||
self.content.as_widget_mut().layout(
|
||||
tree,
|
||||
renderer,
|
||||
@@ -346,9 +286,8 @@ impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
|
||||
pub(super) fn automatic_large_button<'a>(
|
||||
label: String,
|
||||
content: Element<'a, Message>,
|
||||
label_size: f32,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(AutomaticLargeWidth { label, content, label_size })
|
||||
Element::new(AutomaticLargeWidth { label, content })
|
||||
}
|
||||
|
||||
// ── Tab-bar constants ──────────────────────────────────────────────────────
|
||||
@@ -693,9 +632,6 @@ pub(super) struct RenderCtx<'a> {
|
||||
pub style_ctx: &'a StyleContext<'a>,
|
||||
/// When compact, the Properties panel's Match button shrinks to a small icon.
|
||||
pub compact: bool,
|
||||
/// Font size (px) of the caption under a large button's icon (Options →
|
||||
/// Display → "Ribbon label size").
|
||||
pub label_font_size: f32,
|
||||
}
|
||||
|
||||
/// A large dropdown button: the current icon on top, its ▾ directly beneath the
|
||||
@@ -745,7 +681,7 @@ pub(super) fn render_large_dropdown<'a>(
|
||||
.align_x(iced::Center)
|
||||
.align_y(iced::Center),
|
||||
text(label.clone())
|
||||
.size(ctx.label_font_size)
|
||||
.size(10)
|
||||
.width(Fill)
|
||||
.align_x(iced::Center)
|
||||
.wrapping(advanced_text::Wrapping::Word),
|
||||
@@ -798,7 +734,7 @@ pub(super) fn render_large_dropdown<'a>(
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
PosReport::new(id, automatic_large_button(label, content.into(), ctx.label_font_size))
|
||||
PosReport::new(id, automatic_large_button(label, content.into()))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -878,7 +814,7 @@ pub(super) fn render_large<'a>(
|
||||
.align_x(iced::Center)
|
||||
.align_y(iced::Center),
|
||||
text(label.clone())
|
||||
.size(ctx.label_font_size)
|
||||
.size(10)
|
||||
.width(Fill)
|
||||
.align_x(iced::Center)
|
||||
.wrapping(advanced_text::Wrapping::Word),
|
||||
@@ -899,7 +835,7 @@ pub(super) fn render_large<'a>(
|
||||
left: 4.0,
|
||||
});
|
||||
tooltip(
|
||||
automatic_large_button(label, btn.into(), ctx.label_font_size),
|
||||
automatic_large_button(label, btn.into()),
|
||||
make_tip(tip_text),
|
||||
TipPos::Right,
|
||||
)
|
||||
|
||||
+6
-13
@@ -125,13 +125,13 @@ impl StatusBar {
|
||||
// Which pills the user has chosen to show on the bar.
|
||||
config: &'a StatusBarConfig,
|
||||
menu_data: StatusMenuData<'a>,
|
||||
// Remaining degrees of freedom for the current sketch scope's
|
||||
// persistent constraints (design doc §6.3) — `None` when the scope
|
||||
// Remaining degrees of freedom for the current sketch scope.
|
||||
// `None` when the scope
|
||||
// has no `SketchConstraintSet` yet (nothing constrained), so the
|
||||
// badge stays invisible until it's actually relevant.
|
||||
sketch_dof: Option<usize>,
|
||||
// Number of redundant/conflicting constraints `solve_scope` flagged
|
||||
// in the current sketch scope (design doc §6.4, stage 11) — 0 hides
|
||||
// Number of redundant/conflicting constraints in the current scope.
|
||||
// Zero hides
|
||||
// the pill entirely, so an ordinarily/fully-constrained drawing sees
|
||||
// no new clutter.
|
||||
sketch_conflicts: usize,
|
||||
@@ -274,11 +274,7 @@ impl StatusBar {
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
// Design doc §6.4 (stage 11): a bounded v1 of the guided conflict
|
||||
// resolver — click removes one flagged constraint at a time. See
|
||||
// `OpenCADStudio::resolve_one_sketch_conflict`'s doc comment for the
|
||||
// full scope-down rationale versus the design doc's named-candidate,
|
||||
// cyclable-preview panel.
|
||||
// Each click removes one flagged constraint.
|
||||
if sketch_conflicts > 0 {
|
||||
pills.push(
|
||||
tip(
|
||||
@@ -1082,10 +1078,7 @@ fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Like [`status_pill`], but with the theme's `success` accent — design doc
|
||||
/// §6.3's "simple color-coded constraint state" cue: used for the DOF badge
|
||||
/// when a sketch scope has reached zero remaining degrees of freedom, so
|
||||
/// "fully constrained" is visible at a glance without opening anything.
|
||||
/// A success-colored status pill used when no degrees of freedom remain.
|
||||
fn success_pill(label: impl Into<String>) -> Element<'static, Message> {
|
||||
container(text(label.into()).size(12))
|
||||
.style(|theme: &Theme| {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
//! Named-parameter table editor — an in-canvas modal (Plan B), mirroring
|
||||
//! `alias_editor.rs`'s buffered-rows pattern exactly. Opened by PARAMETERS
|
||||
//! (`docs/named_parameters_design.md` stage 4). Rows are `(name, formula)`,
|
||||
//! Named-parameter table editor using buffered `(name, formula)` rows.
|
||||
//! edited in a working buffer (`OpenCADStudio::named_parameter_editor_rows`)
|
||||
//! and committed to `Scene::named_parameters` only on Apply — matching the
|
||||
//! alias editor's "closing discards unapplied edits" convention, which
|
||||
@@ -25,9 +23,7 @@ use iced::widget::tooltip::Position as TipPos;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, tooltip, Space};
|
||||
use iced::{Background, Element, Length, Theme};
|
||||
|
||||
/// Short "Kind handle" label for one referenced entity — the same handle
|
||||
/// display (`0x…`) AutoCAD/this app's own conventions use, since entities
|
||||
/// have no user-facing name to show instead.
|
||||
/// Short "Kind handle" label for an entity without a user-facing name.
|
||||
fn entity_label(scene: &Scene, handle: Handle) -> String {
|
||||
match scene.document.get_entity(handle) {
|
||||
Some(EntityType::Line(_)) => format!("Line {handle}"),
|
||||
@@ -46,7 +42,12 @@ fn usage_lines(scene: &Scene, name: &str) -> Vec<String> {
|
||||
.parameter_usage(name)
|
||||
.iter()
|
||||
.map(|u| {
|
||||
let entities = u.entities.iter().map(|h| entity_label(scene, *h)).collect::<Vec<_>>().join(", ");
|
||||
let entities = u
|
||||
.entities
|
||||
.iter()
|
||||
.map(|h| entity_label(scene, *h))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("{:?}: {entities}", u.kind)
|
||||
})
|
||||
.collect()
|
||||
@@ -74,14 +75,20 @@ fn used_by_cell<'a>(scene: &Scene, name: &str) -> Element<'a, Message> {
|
||||
None => counts.push((kind, 1)),
|
||||
}
|
||||
}
|
||||
let summary = counts.iter().map(|(k, n)| format!("{k} ×{n}")).collect::<Vec<_>>().join(", ");
|
||||
let summary = counts
|
||||
.iter()
|
||||
.map(|(k, n)| format!("{k} ×{n}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let mut body = column![].spacing(2);
|
||||
for line in &lines {
|
||||
body = body.push(text(line.clone()).size(11));
|
||||
}
|
||||
tooltip(
|
||||
text(summary).size(11).style(muted_style),
|
||||
container(body).style(container::bordered_box).padding([4, 8]),
|
||||
container(body)
|
||||
.style(container::bordered_box)
|
||||
.padding([4, 8]),
|
||||
TipPos::Top,
|
||||
)
|
||||
.into()
|
||||
@@ -107,11 +114,15 @@ pub struct ParamEditorRow {
|
||||
const GUTTER: f32 = 16.0;
|
||||
|
||||
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style { color: Some(theme.palette().background.base.text.scale_alpha(0.68)) }
|
||||
iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
|
||||
}
|
||||
}
|
||||
|
||||
fn danger_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style { color: Some(theme.palette().danger.base.color) }
|
||||
iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every buffer index sharing a (trimmed) name with at least one other
|
||||
@@ -130,7 +141,11 @@ pub(crate) fn duplicate_name_rows(rows: &[ParamEditorRow]) -> std::collections::
|
||||
by_name.entry(name).or_default().push(idx);
|
||||
}
|
||||
}
|
||||
by_name.into_values().filter(|idxs| idxs.len() > 1).flatten().collect()
|
||||
by_name
|
||||
.into_values()
|
||||
.filter(|idxs| idxs.len() > 1)
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Builds a scratch table from every row's *current* text and resolves each
|
||||
@@ -184,7 +199,11 @@ fn preview(rows: &[ParamEditorRow]) -> Vec<Option<Result<f64, String>>> {
|
||||
/// unapplied edits to a row's name in the buffer don't retroactively
|
||||
/// relabel what's shown, same as the rest of this editor only takes effect
|
||||
/// on Apply.
|
||||
pub fn view_window<'a>(rows: &'a [ParamEditorRow], scene: &'a Scene, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
|
||||
pub fn view_window<'a>(
|
||||
rows: &'a [ParamEditorRow],
|
||||
scene: &'a Scene,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let title = text(t!("Named Parameters")).size(15);
|
||||
let hint = text(t!(
|
||||
"Type a name and a formula (e.g. hole_dia = 12, hole_spacing = 2 * hole_dia + 1.5). Apply to save and re-solve; closing discards unapplied edits."
|
||||
@@ -192,17 +211,25 @@ pub fn view_window<'a>(rows: &'a [ParamEditorRow], scene: &'a Scene, sizing: cra
|
||||
.size(11)
|
||||
.style(muted_style);
|
||||
|
||||
let gutter = iced::Padding { top: 0.0, right: GUTTER, bottom: 0.0, left: 0.0 };
|
||||
let gutter = iced::Padding {
|
||||
top: 0.0,
|
||||
right: GUTTER,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
};
|
||||
const NAME_WIDTH: f32 = 120.0;
|
||||
const VALUE_WIDTH: f32 = 100.0;
|
||||
const USED_BY_WIDTH: f32 = 160.0;
|
||||
|
||||
let head = container(
|
||||
row![
|
||||
container(text(t!("Name")).size(11).style(muted_style)).width(Length::Fixed(NAME_WIDTH)),
|
||||
container(text(t!("Name")).size(11).style(muted_style))
|
||||
.width(Length::Fixed(NAME_WIDTH)),
|
||||
container(text(t!("Formula")).size(11).style(muted_style)).width(sizing.width),
|
||||
container(text(t!("Value")).size(11).style(muted_style)).width(Length::Fixed(VALUE_WIDTH)),
|
||||
container(text(t!("Used by")).size(11).style(muted_style)).width(Length::Fixed(USED_BY_WIDTH)),
|
||||
container(text(t!("Value")).size(11).style(muted_style))
|
||||
.width(Length::Fixed(VALUE_WIDTH)),
|
||||
container(text(t!("Used by")).size(11).style(muted_style))
|
||||
.width(Length::Fixed(USED_BY_WIDTH)),
|
||||
Space::new().width(Length::Fixed(30.0)),
|
||||
]
|
||||
.spacing(8),
|
||||
@@ -213,25 +240,38 @@ pub fn view_window<'a>(rows: &'a [ParamEditorRow], scene: &'a Scene, sizing: cra
|
||||
let mut list = column![].spacing(3);
|
||||
for (idx, row) in rows.iter().enumerate() {
|
||||
let name_box = text_input(t!("name").as_ref(), &row.name)
|
||||
.on_input(move |v| Message::NamedParametersInput { idx, field: ParamField::Name, value: v })
|
||||
.on_input(move |v| Message::NamedParametersInput {
|
||||
idx,
|
||||
field: ParamField::Name,
|
||||
value: v,
|
||||
})
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(NAME_WIDTH));
|
||||
let formula_box = text_input(t!("formula").as_ref(), &row.formula)
|
||||
.on_input(move |v| Message::NamedParametersInput { idx, field: ParamField::Formula, value: v })
|
||||
.on_input(move |v| Message::NamedParametersInput {
|
||||
idx,
|
||||
field: ParamField::Formula,
|
||||
value: v,
|
||||
})
|
||||
.size(13)
|
||||
.padding([3, 6])
|
||||
.width(sizing.width);
|
||||
let value_cell: Element<'_, Message> = match results.get(idx).cloned().flatten() {
|
||||
Some(Ok(value)) => container(text(format!("= {value:.4}")).size(12).style(muted_style)).into(),
|
||||
Some(Ok(value)) => {
|
||||
container(text(format!("= {value:.4}")).size(12).style(muted_style)).into()
|
||||
}
|
||||
Some(Err(msg)) => container(text(msg).size(11).style(danger_style)).into(),
|
||||
None => Space::new().into(),
|
||||
};
|
||||
let used_by_cell = used_by_cell(scene, row.name.trim());
|
||||
let del = button(crate::ui::icons::themed_danger_text(crate::ui::icons::CLOSE, 12.0))
|
||||
.on_press(Message::NamedParametersRemove(idx))
|
||||
.padding([2, 6])
|
||||
.style(button::danger);
|
||||
let del = button(crate::ui::icons::themed_danger_text(
|
||||
crate::ui::icons::CLOSE,
|
||||
12.0,
|
||||
))
|
||||
.on_press(Message::NamedParametersRemove(idx))
|
||||
.padding([2, 6])
|
||||
.style(button::danger);
|
||||
list = list.push(
|
||||
row![
|
||||
name_box,
|
||||
@@ -272,7 +312,10 @@ pub fn view_window<'a>(rows: &'a [ParamEditorRow], scene: &'a Scene, sizing: cra
|
||||
.padding(12)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.base.color)), ..Default::default() })
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -281,12 +324,18 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn row(name: &str, formula: &str) -> ParamEditorRow {
|
||||
ParamEditorRow { name: name.to_string(), formula: formula.to_string() }
|
||||
ParamEditorRow {
|
||||
name: name.to_string(),
|
||||
formula: formula.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_resolves_a_literal_and_a_cross_row_reference() {
|
||||
let rows = vec![row("hole_dia", "5"), row("hole_spacing", "2 * hole_dia + 1.5")];
|
||||
let rows = vec![
|
||||
row("hole_dia", "5"),
|
||||
row("hole_spacing", "2 * hole_dia + 1.5"),
|
||||
];
|
||||
let results = preview(&rows);
|
||||
assert_eq!(results[0], Some(Ok(5.0)));
|
||||
assert_eq!(results[1], Some(Ok(11.5)));
|
||||
@@ -309,7 +358,10 @@ mod tests {
|
||||
let results = preview(&rows);
|
||||
// Row 0 (a = b) is accepted first (b doesn't exist yet, no cycle);
|
||||
// row 1 (b = a) is the one that would close the loop and is rejected.
|
||||
assert!(matches!(results[0], Some(Ok(_))) || matches!(&results[0], Some(Err(e)) if e.contains("not defined")));
|
||||
assert!(
|
||||
matches!(results[0], Some(Ok(_)))
|
||||
|| matches!(&results[0], Some(Err(e)) if e.contains("not defined"))
|
||||
);
|
||||
assert!(matches!(&results[1], Some(Err(e)) if e.to_lowercase().contains("circular")));
|
||||
}
|
||||
|
||||
@@ -332,27 +384,40 @@ mod tests {
|
||||
let results = preview(&rows);
|
||||
assert!(matches!(&results[0], Some(Err(e)) if e.contains("duplicate")));
|
||||
assert!(matches!(&results[1], Some(Err(e)) if e.contains("duplicate")));
|
||||
assert_eq!(results[2], Some(Ok(3.0)), "an unrelated row's name must not be affected");
|
||||
assert_eq!(
|
||||
results[2],
|
||||
Some(Ok(3.0)),
|
||||
"an unrelated row's name must not be affected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_lines_names_the_constraint_kind_and_its_entities() {
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
|
||||
let mut scene = Scene::new();
|
||||
let line = scene.add_entity(acadrust::EntityType::Line(acadrust::entities::Line::from_points(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named("gap".to_string())),
|
||||
);
|
||||
let line = scene.add_entity(acadrust::EntityType::Line(
|
||||
acadrust::entities::Line::from_points(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
|
||||
),
|
||||
));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(
|
||||
"gap".to_string(),
|
||||
)),
|
||||
);
|
||||
|
||||
let lines = usage_lines(&scene, "gap");
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert!(lines[0].starts_with("Distance: Line "), "got {lines:?}");
|
||||
assert!(lines[0].contains(&format!("{line}")), "should name the actual entity handle, got {lines:?}");
|
||||
assert!(
|
||||
lines[0].contains(&format!("{line}")),
|
||||
"should name the actual entity handle, got {lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -163,9 +163,6 @@ pub fn view_window<'a>(
|
||||
crosshair_color: Option<[u8; 3]>,
|
||||
crosshair_color_input: &'a str,
|
||||
lineweight_display_scale: i32,
|
||||
ui_scale: i32,
|
||||
ribbon_label_font_size: i32,
|
||||
ribbon_group_title_font_size: i32,
|
||||
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
|
||||
model_bg_input: &'a str,
|
||||
paper_bg_input: &'a str,
|
||||
@@ -359,16 +356,14 @@ pub fn view_window<'a>(
|
||||
iced::widget::checkbox(write_dwg_native_constraints)
|
||||
.on_toggle(Message::WriteDwgNativeConstraintsChanged)
|
||||
.size(15),
|
||||
text(crate::t!("Write AutoCAD-compatible constraint objects on save")).size(12),
|
||||
text(crate::t!("Write native constraint objects on save")).size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(6),
|
||||
text(crate::t!(
|
||||
"Saves sketch constraints as AutoCAD/BricsCAD's own native objects too, alongside \
|
||||
this app's own format, so other CAD software recognizes them. Off by default — \
|
||||
adds some file size on every save. A drawing that already has these objects keeps \
|
||||
them up to date regardless of this setting."
|
||||
"Saves sketch constraints as native drawing objects alongside this app's own format. \
|
||||
Off by default because it adds file size. Existing native objects stay synchronized."
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
@@ -596,24 +591,6 @@ pub fn view_window<'a>(
|
||||
});
|
||||
|
||||
let mut display = column![
|
||||
text(crate::t!("UI Scale")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
text(crate::t!("Overall size")).size(12).width(150),
|
||||
slider(50..=200, ui_scale.clamp(50, 200), Message::UiScaleChanged)
|
||||
.step(5)
|
||||
.width(Fill),
|
||||
text(format!("{}%", ui_scale.clamp(50, 200))).size(11).width(44),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(8),
|
||||
text(crate::t!(
|
||||
"Scales the whole interface at once — every panel, dialog, the ribbon, and their text — rather than one widget's font size. Takes effect immediately."
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
Space::new().height(24),
|
||||
text(crate::tr!("options", "theme-section")).size(15),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
@@ -871,56 +848,6 @@ pub fn view_window<'a>(
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
)
|
||||
.push(Space::new().height(24))
|
||||
.push(text(crate::t!("Ribbon")).size(15))
|
||||
.push(Space::new().height(10))
|
||||
.push(
|
||||
row![
|
||||
text(crate::t!("Icon label size")).size(12).width(140),
|
||||
slider(
|
||||
8..=16,
|
||||
ribbon_label_font_size.clamp(8, 16),
|
||||
Message::RibbonLabelFontSizeChanged,
|
||||
)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text(format!("{}px", ribbon_label_font_size.clamp(8, 16)))
|
||||
.size(11)
|
||||
.width(44),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.push(Space::new().height(6))
|
||||
.push(
|
||||
text(crate::t!("Font size of the caption under each ribbon button's icon."))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
)
|
||||
.push(Space::new().height(10))
|
||||
.push(
|
||||
row![
|
||||
text(crate::t!("Section title size")).size(12).width(140),
|
||||
slider(
|
||||
7..=14,
|
||||
ribbon_group_title_font_size.clamp(7, 14),
|
||||
Message::RibbonGroupTitleFontSizeChanged,
|
||||
)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text(format!("{}px", ribbon_group_title_font_size.clamp(7, 14)))
|
||||
.size(11)
|
||||
.width(44),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.push(Space::new().height(6))
|
||||
.push(
|
||||
text(crate::t!("Font size of each panel's own title (Draw, Modify, Layers, …)."))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
);
|
||||
|
||||
let display_element = display.spacing(0).width(sizing.width);
|
||||
|
||||
Reference in new issue
Block a user