forked from 1b-agent/opencadstudio-upstream
feat: annotation scale system (CANNOSCALE, PSLTSCALE, MultiLeader, Leader)
- Scale popup dropdown replaces cycle behavior; paper-space viewport scale editable when a viewport is active/selected - CANNOSCALE persisted via DXF $USERR1 (read on open, write on save) - PSLTSCALE: linetype dash lengths scaled by viewport anno_scale so dashes appear uniform physical size in paper space - MultiLeader: arrow size, dogleg length, and text height scale with anno_scale; leader line paths (world coords) stay fixed - Leader: arrow size and hookline length scale with anno_scale; also corrects world_offset subtraction that was previously deferred to the TruckObject::Lines path Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
543c626feb
commit
0e5dec0948
9 files changed
+471
-162
No files matched your search
@@ -15,6 +15,7 @@ pub mod plotstyle;
|
||||
pub mod properties;
|
||||
pub mod ribbon;
|
||||
pub mod shortcuts;
|
||||
pub mod scale_popup;
|
||||
pub mod snap_popup;
|
||||
pub mod statusbar;
|
||||
pub mod tablestyle;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Scale picker dropdown — annotation scale (model space) or viewport scale (paper space).
|
||||
//!
|
||||
//! Rendered as a floating overlay above the status bar, same pattern as snap_popup.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// (label, annotation_scale_multiplier, viewport_scale_factor)
|
||||
/// annotation_scale = multiplier for text/dim sizes (50.0 for "1:50")
|
||||
/// vp_scale = custom_scale value on the Viewport entity (0.02 for "1:50")
|
||||
const COMMON_SCALES: &[(&str, f32, f64)] = &[
|
||||
("1:100", 100.0, 0.01),
|
||||
("1:50", 50.0, 0.02),
|
||||
("1:20", 20.0, 0.05),
|
||||
("1:10", 10.0, 0.10),
|
||||
("1:5", 5.0, 0.20),
|
||||
("1:2", 2.0, 0.50),
|
||||
("1:1", 1.0, 1.00),
|
||||
("2:1", 0.5, 2.00),
|
||||
("5:1", 0.2, 5.00),
|
||||
("10:1", 0.1, 10.00),
|
||||
];
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn anno_scale_for_label(label: &str) -> f32 {
|
||||
COMMON_SCALES
|
||||
.iter()
|
||||
.find(|&&(l, _, _)| l == label)
|
||||
.map(|&(_, a, _)| a)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// Full-screen overlay: transparent click-catcher + scale list panel pinned bottom-right.
|
||||
///
|
||||
/// - `is_model`: true = model space (dispatches SetAnnotationScale), false = paper space (SetViewportScale).
|
||||
/// - `current_anno_scale`: current annotation_scale from Scene (used to highlight active row in model space).
|
||||
/// - `viewport_scale`: current vp.custom_scale (used to highlight in paper space).
|
||||
pub fn scale_popup_overlay(
|
||||
is_model: bool,
|
||||
current_anno_scale: f32,
|
||||
viewport_scale: Option<f64>,
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = COMMON_SCALES
|
||||
.iter()
|
||||
.map(|&(label, anno_scale, vp_scale)| {
|
||||
let active = if is_model {
|
||||
(current_anno_scale - anno_scale).abs() < 0.001 * current_anno_scale.max(0.001)
|
||||
} else {
|
||||
viewport_scale
|
||||
.map(|vs| (vs - vp_scale).abs() < 0.001 * vp_scale.max(0.001))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let msg = if is_model {
|
||||
Message::SetAnnotationScale(anno_scale)
|
||||
} else {
|
||||
Message::SetViewportScale(vp_scale)
|
||||
};
|
||||
scale_row(label, active, msg)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let panel = container(column(rows))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(PANEL_BG)),
|
||||
border: Border {
|
||||
color: PANEL_BORDER,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Length::Fixed(120.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseScalePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn scale_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
|
||||
let check = text(if active { "✓" } else { " " })
|
||||
.size(11)
|
||||
.color(if active { CHECK_COLOR } else { Color::TRANSPARENT })
|
||||
.width(Length::Fixed(14.0));
|
||||
|
||||
let lbl = text(label)
|
||||
.size(11)
|
||||
.color(if active { LABEL_ON } else { LABEL_OFF });
|
||||
|
||||
let content = row![check, lbl].spacing(6).align_y(iced::Center);
|
||||
|
||||
button(content)
|
||||
.on_press(msg)
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => ROW_HOVER,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 10])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Colours ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||||
const PANEL_BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 };
|
||||
const ROW_HOVER: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
||||
const CHECK_COLOR: Color = Color { r: 0.35, g: 0.75, b: 1.00, a: 1.0 };
|
||||
const LABEL_ON: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
|
||||
const LABEL_OFF: Color = Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0 };
|
||||
+46
-2
@@ -42,6 +42,12 @@ impl StatusBar {
|
||||
in_mspace: bool,
|
||||
// Whether the layout tabs (Model/Paper) are visible (LAYOUTTAB).
|
||||
show_layout_tabs: bool,
|
||||
// Current annotation scale for model space (1.0 = 1:1, 50.0 = 1:50, etc.).
|
||||
annotation_scale: f32,
|
||||
// True when the scale picker popup is open.
|
||||
scale_popup_open: bool,
|
||||
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
|
||||
scale_pill_enabled: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let menu_btn = button(text("≡").size(14).color(ICON_COLOR))
|
||||
.on_press(Message::Command("MENU".into()))
|
||||
@@ -63,12 +69,24 @@ impl StatusBar {
|
||||
let osnap_active = snapper.is_active();
|
||||
let snap_grid_on = snapper.is_on(crate::snap::SnapType::Grid) && snapper.snap_enabled;
|
||||
|
||||
let scale_label = format_scale(viewport_scale);
|
||||
let vp_label = if viewport_count > 0 {
|
||||
format!("{} VP", viewport_count)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// Scale pill: opens the scale picker popup.
|
||||
// Model space: always interactive, shows annotation scale.
|
||||
// Paper space: interactive only when a viewport is active/selected.
|
||||
let scale_label = if current_layout == "Model" {
|
||||
format_scale(Some(1.0 / annotation_scale as f64))
|
||||
} else {
|
||||
format_scale(viewport_scale)
|
||||
};
|
||||
let scale_element: Element<'_, Message> = if scale_pill_enabled {
|
||||
tip(scale_popup_btn(&scale_label, scale_popup_open), "Annotation / Viewport Scale\nClick to change")
|
||||
} else {
|
||||
status_pill(scale_label).into()
|
||||
};
|
||||
let mut right_status = row![
|
||||
tip(
|
||||
toggle_pill("SNAP", snap_grid_on, Message::ToggleGridSnap),
|
||||
@@ -96,7 +114,7 @@ impl StatusBar {
|
||||
space_mode_btn(¤t_layout, in_mspace),
|
||||
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
||||
),
|
||||
status_pill(scale_label),
|
||||
scale_element,
|
||||
]
|
||||
.spacing(2);
|
||||
if !vp_label.is_empty() {
|
||||
@@ -609,6 +627,32 @@ const SNAP_OFF_HOVER: Color = Color {
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
// ── Scale popup button ────────────────────────────────────────────────────
|
||||
|
||||
fn scale_popup_btn(label: &str, open: bool) -> Element<'static, Message> {
|
||||
let label = label.to_string();
|
||||
button(text(label).size(10).color(if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT }))
|
||||
.on_press(Message::ToggleScalePopup)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match (open, status) {
|
||||
(true, button::Status::Hovered) => SNAP_ON_HOVER,
|
||||
(true, _) => SNAP_ON_BG,
|
||||
(false, button::Status::Hovered) => SNAP_OFF_HOVER,
|
||||
(false, _) => SNAP_OFF_BG,
|
||||
})),
|
||||
border: Border {
|
||||
color: if open { SNAP_BORDER_ON } else { BORDER_COLOR },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
text_color: if open { SNAP_BORDER_ON } else { OSNAP_OFF_TEXT },
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([2, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Scale display ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Formats a viewport scale factor as a human-readable ratio string.
|
||||
|
||||
Reference in new issue
Block a user