forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
caec8dc53b
9 files changed
+1535
-315
No files matched your search
@@ -0,0 +1,170 @@
|
|||||||
|
use super::OpenCADStudio;
|
||||||
|
use crate::ui::window::drafting_settings::{DraftingSettingsState, DraftingSettingsTab};
|
||||||
|
|
||||||
|
impl DraftingSettingsState {
|
||||||
|
pub(crate) fn from_app(app: &OpenCADStudio) -> Self {
|
||||||
|
Self {
|
||||||
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
|
snap_on: app.snapper.grid_snap(),
|
||||||
|
grid_on: app.show_grid,
|
||||||
|
isometric: app.isometric_drafting,
|
||||||
|
iso_plane: app.iso_plane,
|
||||||
|
snap_angle_deg: app.snap_angle_deg,
|
||||||
|
polar_on: app.polar_mode,
|
||||||
|
ortho_on: app.ortho_mode,
|
||||||
|
polar_increment_deg: app.polar_increment_deg,
|
||||||
|
osnap_on: app.snapper.snap_enabled,
|
||||||
|
otrack_on: app.snapper.otrack_enabled,
|
||||||
|
snap_modes: app.snapper.enabled.clone(),
|
||||||
|
osnap3d_on: false,
|
||||||
|
dyn_input_on: app.dyn_input,
|
||||||
|
quick_props_on: app.quick_properties,
|
||||||
|
selection_cycling_on: app.selection_cycling,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenCADStudio {
|
||||||
|
pub(super) fn drafting_settings_dirty(&self) -> bool {
|
||||||
|
match (&self.drafting_settings_state, &self.drafting_settings_saved) {
|
||||||
|
(Some(curr), Some(saved)) => curr.is_dirty(saved),
|
||||||
|
(Some(_), None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn apply_drafting_settings(&mut self) {
|
||||||
|
if let Some(state) = &self.drafting_settings_state {
|
||||||
|
self.show_grid = state.grid_on;
|
||||||
|
self.snapper.grid_snap_on = state.snap_on;
|
||||||
|
self.isometric_drafting = state.isometric;
|
||||||
|
self.iso_plane = state.iso_plane;
|
||||||
|
self.snap_angle_deg = state.snap_angle_deg;
|
||||||
|
self.polar_mode = state.polar_on;
|
||||||
|
self.ortho_mode = state.ortho_on;
|
||||||
|
self.polar_increment_deg = state.polar_increment_deg;
|
||||||
|
self.snapper.snap_enabled = state.osnap_on;
|
||||||
|
self.snapper.otrack_enabled = state.otrack_on;
|
||||||
|
self.snapper.enabled = state.snap_modes.clone();
|
||||||
|
self.dyn_input = state.dyn_input_on;
|
||||||
|
self.quick_properties = state.quick_props_on;
|
||||||
|
self.selection_cycling = state.selection_cycling_on;
|
||||||
|
self.sync_vport_display(self.active_tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drafting_settings_tab_switching_not_dirty() {
|
||||||
|
let state1 = DraftingSettingsState {
|
||||||
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
|
snap_on: false,
|
||||||
|
grid_on: true,
|
||||||
|
isometric: false,
|
||||||
|
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||||
|
snap_angle_deg: 0.0,
|
||||||
|
polar_on: false,
|
||||||
|
ortho_on: false,
|
||||||
|
polar_increment_deg: 15.0,
|
||||||
|
osnap_on: true,
|
||||||
|
otrack_on: false,
|
||||||
|
snap_modes: rustc_hash::FxHashSet::default(),
|
||||||
|
osnap3d_on: false,
|
||||||
|
dyn_input_on: false,
|
||||||
|
quick_props_on: false,
|
||||||
|
selection_cycling_on: false,
|
||||||
|
};
|
||||||
|
let mut state2 = state1.clone();
|
||||||
|
assert!(!state2.is_dirty(&state1));
|
||||||
|
|
||||||
|
// Switching tabs should NOT mark it dirty
|
||||||
|
state2.active_tab = DraftingSettingsTab::ObjectSnap;
|
||||||
|
assert!(!state2.is_dirty(&state1));
|
||||||
|
|
||||||
|
// Toggling a setting should mark it dirty
|
||||||
|
state2.grid_on = false;
|
||||||
|
assert!(state2.is_dirty(&state1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_drafting_settings_dirty_toggles() {
|
||||||
|
let base = DraftingSettingsState {
|
||||||
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
|
snap_on: false,
|
||||||
|
grid_on: true,
|
||||||
|
isometric: false,
|
||||||
|
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||||
|
snap_angle_deg: 0.0,
|
||||||
|
polar_on: false,
|
||||||
|
ortho_on: false,
|
||||||
|
polar_increment_deg: 15.0,
|
||||||
|
osnap_on: true,
|
||||||
|
otrack_on: false,
|
||||||
|
snap_modes: rustc_hash::FxHashSet::default(),
|
||||||
|
osnap3d_on: false,
|
||||||
|
dyn_input_on: false,
|
||||||
|
quick_props_on: false,
|
||||||
|
selection_cycling_on: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.isometric = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.iso_plane = crate::app::settings::IsoPlane::Top;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_angle_deg = 45.0;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.polar_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.ortho_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.polar_increment_deg = 30.0;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.osnap_on = false;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.otrack_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_modes.insert(crate::snap::SnapType::Endpoint);
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.osnap3d_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.dyn_input_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.quick_props_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.selection_cycling_on = true;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ pub use automation::{export_headless, serve};
|
|||||||
mod command_driver;
|
mod command_driver;
|
||||||
pub(crate) mod commands;
|
pub(crate) mod commands;
|
||||||
mod document;
|
mod document;
|
||||||
|
mod drafting_settings;
|
||||||
pub(crate) mod expr_eval;
|
pub(crate) mod expr_eval;
|
||||||
mod find_replace;
|
mod find_replace;
|
||||||
pub(crate) mod helpers;
|
pub(crate) mod helpers;
|
||||||
@@ -415,6 +416,9 @@ pub(super) struct OpenCADStudio {
|
|||||||
win_size: (f32, f32),
|
win_size: (f32, f32),
|
||||||
snapper: Snapper,
|
snapper: Snapper,
|
||||||
snap_popup_open: bool,
|
snap_popup_open: bool,
|
||||||
|
drafting_settings_state: Option<crate::ui::window::drafting_settings::DraftingSettingsState>,
|
||||||
|
drafting_settings_saved: Option<crate::ui::window::drafting_settings::DraftingSettingsState>,
|
||||||
|
drafting_settings_close_confirm: bool,
|
||||||
scale_popup_open: bool,
|
scale_popup_open: bool,
|
||||||
/// True while the polar-tracking angle picker is open.
|
/// True while the polar-tracking angle picker is open.
|
||||||
polar_popup_open: bool,
|
polar_popup_open: bool,
|
||||||
@@ -2619,6 +2623,29 @@ pub enum Message {
|
|||||||
SnapSelectAll,
|
SnapSelectAll,
|
||||||
/// Disable all snap modes.
|
/// Disable all snap modes.
|
||||||
SnapClearAll,
|
SnapClearAll,
|
||||||
|
// ── Drafting Settings Dialog ──────────────────────────────────────────
|
||||||
|
DraftingSettingsTabChanged(crate::ui::window::drafting_settings::DraftingSettingsTab),
|
||||||
|
DraftingSettingsToggleGrid,
|
||||||
|
DraftingSettingsToggleSnap,
|
||||||
|
DraftingSettingsToggleIsometric,
|
||||||
|
DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane),
|
||||||
|
DraftingSettingsResetRotation,
|
||||||
|
DraftingSettingsTogglePolar,
|
||||||
|
DraftingSettingsToggleOrtho,
|
||||||
|
DraftingSettingsToggleOsnap,
|
||||||
|
DraftingSettingsToggleOtrack,
|
||||||
|
DraftingSettingsToggleSnapMode(crate::snap::SnapType),
|
||||||
|
DraftingSettingsSnapSelectAll,
|
||||||
|
DraftingSettingsSnapClearAll,
|
||||||
|
DraftingSettingsToggle3dOsnap,
|
||||||
|
DraftingSettingsToggleDynInput,
|
||||||
|
DraftingSettingsToggleQuickProps,
|
||||||
|
DraftingSettingsToggleSelCycling,
|
||||||
|
DraftingSettingsApply,
|
||||||
|
DraftingSettingsOk,
|
||||||
|
DraftingSettingsClose,
|
||||||
|
DraftingSettingsCloseDiscard,
|
||||||
|
DraftingSettingsCloseKeep,
|
||||||
/// Toggle a ribbon dropdown open/closed.
|
/// Toggle a ribbon dropdown open/closed.
|
||||||
ToggleRibbonDropdown(String),
|
ToggleRibbonDropdown(String),
|
||||||
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
|
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
|
||||||
@@ -3548,6 +3575,9 @@ impl OpenCADStudio {
|
|||||||
win_size: (1280.0, 720.0),
|
win_size: (1280.0, 720.0),
|
||||||
snapper: Snapper::default(),
|
snapper: Snapper::default(),
|
||||||
snap_popup_open: false,
|
snap_popup_open: false,
|
||||||
|
drafting_settings_state: None,
|
||||||
|
drafting_settings_saved: None,
|
||||||
|
drafting_settings_close_confirm: false,
|
||||||
scale_popup_open: false,
|
scale_popup_open: false,
|
||||||
polar_popup_open: false,
|
polar_popup_open: false,
|
||||||
polar_custom_input: String::new(),
|
polar_custom_input: String::new(),
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
if self.active_modal == Some(DraftingSettings) {
|
if self.active_modal == Some(DraftingSettings) {
|
||||||
self.snap_popup_open = false;
|
self.snap_popup_open = false;
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
|
self.drafting_settings_state = None;
|
||||||
|
self.drafting_settings_saved = None;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if self.active_modal == Some(FileInUse) {
|
if self.active_modal == Some(FileInUse) {
|
||||||
@@ -4072,9 +4075,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Message::ToggleSnapPopup => {
|
Message::ToggleSnapPopup => {
|
||||||
if self.active_modal == Some(super::ModalKind::DraftingSettings) {
|
if self.active_modal == Some(super::ModalKind::DraftingSettings) {
|
||||||
|
if !self.drafting_settings_close_confirm && self.drafting_settings_dirty() {
|
||||||
|
self.drafting_settings_close_confirm = true;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
self.snap_popup_open = false;
|
self.snap_popup_open = false;
|
||||||
} else {
|
} else {
|
||||||
|
let st = crate::ui::window::drafting_settings::DraftingSettingsState::from_app(self);
|
||||||
|
self.drafting_settings_saved = Some(st.clone());
|
||||||
|
self.drafting_settings_state = Some(st);
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
self.active_modal = Some(super::ModalKind::DraftingSettings);
|
self.active_modal = Some(super::ModalKind::DraftingSettings);
|
||||||
self.snap_popup_open = true;
|
self.snap_popup_open = true;
|
||||||
}
|
}
|
||||||
@@ -4096,6 +4107,147 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Drafting Settings Dialog ──────────────────────────────────
|
||||||
|
Message::DraftingSettingsTabChanged(tab) => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.active_tab = tab;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleGrid => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.grid_on = !state.grid_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleSnap => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_on = !state.snap_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleIsometric => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.isometric = !state.isometric;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsSetIsoPlane(plane) => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.isometric = true;
|
||||||
|
state.iso_plane = plane;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsResetRotation => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_angle_deg = 0.0;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsTogglePolar => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.polar_on = !state.polar_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleOrtho => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.ortho_on = !state.ortho_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleOsnap => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.osnap_on = !state.osnap_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleOtrack => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.otrack_on = !state.otrack_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleSnapMode(snap_type) => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
if !state.snap_modes.remove(&snap_type) {
|
||||||
|
state.snap_modes.insert(snap_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsSnapSelectAll => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
for &(snap_type, _, _) in crate::snap::ALL_SNAP_MODES {
|
||||||
|
state.snap_modes.insert(snap_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsSnapClearAll => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_modes.clear();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggle3dOsnap => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.osnap3d_on = !state.osnap3d_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleDynInput => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.dyn_input_on = !state.dyn_input_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleQuickProps => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.quick_props_on = !state.quick_props_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleSelCycling => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.selection_cycling_on = !state.selection_cycling_on;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsApply => {
|
||||||
|
self.apply_drafting_settings();
|
||||||
|
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsOk => {
|
||||||
|
self.apply_drafting_settings();
|
||||||
|
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.close_active_modal();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsClose => {
|
||||||
|
if !self.drafting_settings_close_confirm && self.drafting_settings_dirty() {
|
||||||
|
self.drafting_settings_close_confirm = true;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
|
self.close_active_modal();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsCloseDiscard => {
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
|
self.close_active_modal();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsCloseKeep => {
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Ribbon dropdowns ──────────────────────────────────────────
|
// ── Ribbon dropdowns ──────────────────────────────────────────
|
||||||
Message::ToggleRibbonDropdown(id) => {
|
Message::ToggleRibbonDropdown(id) => {
|
||||||
if self.tabs[self.active_tab].is_start {
|
if self.tabs[self.active_tab].is_start {
|
||||||
@@ -6655,6 +6807,14 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.shortcut_close_confirm = false;
|
self.shortcut_close_confirm = false;
|
||||||
|
if self.active_modal == Some(super::ModalKind::DraftingSettings)
|
||||||
|
&& !self.drafting_settings_close_confirm
|
||||||
|
&& self.drafting_settings_dirty()
|
||||||
|
{
|
||||||
|
self.drafting_settings_close_confirm = true;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.drafting_settings_close_confirm = false;
|
||||||
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
|
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
if resume_open_queue {
|
if resume_open_queue {
|
||||||
|
|||||||
+16
-13
@@ -286,25 +286,28 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
super::super::ModalKind::DraftingSettings => sized_flow(
|
super::super::ModalKind::DraftingSettings => {
|
||||||
|
let state = self.drafting_settings_state.as_ref();
|
||||||
|
let dirty = self.drafting_settings_dirty();
|
||||||
|
let confirm = self.drafting_settings_close_confirm;
|
||||||
|
sized_flow(
|
||||||
ex,
|
ex,
|
||||||
520,
|
700,
|
||||||
560,
|
480,
|
||||||
|flow| {
|
|flow| {
|
||||||
|
if let Some(state) = state {
|
||||||
crate::ui::window::drafting_settings::view_window(
|
crate::ui::window::drafting_settings::view_window(
|
||||||
&self.snapper,
|
state,
|
||||||
self.show_grid,
|
dirty,
|
||||||
self.snapper.grid_snap(),
|
confirm,
|
||||||
self.ortho_mode,
|
|
||||||
self.polar_mode,
|
|
||||||
self.snapper.otrack_enabled,
|
|
||||||
self.isometric_drafting,
|
|
||||||
self.iso_plane,
|
|
||||||
self.snap_angle_deg,
|
|
||||||
flow,
|
flow,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().into()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
}
|
||||||
super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| {
|
super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| {
|
||||||
crate::ui::window::find_replace::view_window(
|
crate::ui::window::find_replace::view_window(
|
||||||
&self.find_replace.search,
|
&self.find_replace.search,
|
||||||
|
|||||||
+66
-12
@@ -768,15 +768,71 @@ pub fn bulge_arc_to_tangent(
|
|||||||
///
|
///
|
||||||
/// An arc band is an annular sector — concave on its inner edge — so this ear
|
/// An arc band is an annular sector — concave on its inner edge — so this ear
|
||||||
/// clips rather than fans.
|
/// clips rather than fans.
|
||||||
|
/// Directly triangulates a band polygon (either a 4-vertex straight trapezoid or
|
||||||
|
/// a 2m-vertex annular sector quad strip) into non-overlapping triangles.
|
||||||
|
/// Falls back to `triangulate_planar` if the vertex count is odd or less than 4.
|
||||||
|
pub(crate) fn triangulate_band_ring(ring: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
||||||
|
let n = ring.len();
|
||||||
|
if n >= 4 && n % 2 == 0 {
|
||||||
|
let m = n / 2;
|
||||||
|
let mut tris = Vec::with_capacity((m - 1) * 6);
|
||||||
|
for j in 0..m - 1 {
|
||||||
|
let p0 = ring[j];
|
||||||
|
let p1 = ring[j + 1];
|
||||||
|
let p2 = ring[2 * m - 2 - j];
|
||||||
|
let p3 = ring[2 * m - 1 - j];
|
||||||
|
tris.push(p0);
|
||||||
|
tris.push(p1);
|
||||||
|
tris.push(p2);
|
||||||
|
tris.push(p0);
|
||||||
|
tris.push(p2);
|
||||||
|
tris.push(p3);
|
||||||
|
}
|
||||||
|
tris
|
||||||
|
} else {
|
||||||
|
crate::entities::mesh::triangulate_planar(ring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn wide_band_tris(origin: [f64; 2], fills: &[Vec<[f32; 2]>]) -> Vec<[f64; 3]> {
|
pub(crate) fn wide_band_tris(origin: [f64; 2], fills: &[Vec<[f32; 2]>]) -> Vec<[f64; 3]> {
|
||||||
let mut out = Vec::new();
|
let mut total_verts = 0;
|
||||||
for poly in fills {
|
for poly in fills {
|
||||||
|
let n = poly.len();
|
||||||
|
if n >= 4 && n % 2 == 0 {
|
||||||
|
total_verts += (n / 2 - 1) * 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(total_verts);
|
||||||
|
let ox = origin[0];
|
||||||
|
let oy = origin[1];
|
||||||
|
for poly in fills {
|
||||||
|
let n = poly.len();
|
||||||
|
if n >= 4 && n % 2 == 0 {
|
||||||
|
let m = n / 2;
|
||||||
|
for j in 0..m - 1 {
|
||||||
|
let v0 = poly[j];
|
||||||
|
let v1 = poly[j + 1];
|
||||||
|
let v2 = poly[2 * m - 2 - j];
|
||||||
|
let v3 = poly[2 * m - 1 - j];
|
||||||
|
let p0 = [ox + v0[0] as f64, oy + v0[1] as f64, 0.0];
|
||||||
|
let p1 = [ox + v1[0] as f64, oy + v1[1] as f64, 0.0];
|
||||||
|
let p2 = [ox + v2[0] as f64, oy + v2[1] as f64, 0.0];
|
||||||
|
let p3 = [ox + v3[0] as f64, oy + v3[1] as f64, 0.0];
|
||||||
|
out.push(p0);
|
||||||
|
out.push(p1);
|
||||||
|
out.push(p2);
|
||||||
|
out.push(p0);
|
||||||
|
out.push(p2);
|
||||||
|
out.push(p3);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let ring: Vec<[f64; 3]> = poly
|
let ring: Vec<[f64; 3]> = poly
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[x, y]| [origin[0] + x as f64, origin[1] + y as f64, 0.0])
|
.map(|&[x, y]| [ox + x as f64, oy + y as f64, 0.0])
|
||||||
.collect();
|
.collect();
|
||||||
out.extend(crate::entities::mesh::triangulate_planar(&ring));
|
out.extend(crate::entities::mesh::triangulate_planar(&ring));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,8 +888,8 @@ pub(crate) fn thick_band_tube(
|
|||||||
push_seg(&mut lines, top[k], top[kn]);
|
push_seg(&mut lines, top[k], top[kn]);
|
||||||
fill_tris.extend_from_slice(&[bot[k], bot[kn], top[kn], bot[k], top[kn], top[k]]);
|
fill_tris.extend_from_slice(&[bot[k], bot[kn], top[kn], bot[k], top[kn], top[k]]);
|
||||||
}
|
}
|
||||||
fill_tris.extend(crate::entities::mesh::triangulate_planar(&bot));
|
fill_tris.extend(triangulate_band_ring(&bot));
|
||||||
fill_tris.extend(crate::entities::mesh::triangulate_planar(&top));
|
fill_tris.extend(triangulate_band_ring(&top));
|
||||||
}
|
}
|
||||||
(fill_tris, lines)
|
(fill_tris, lines)
|
||||||
}
|
}
|
||||||
@@ -1089,19 +1145,17 @@ pub(crate) fn polyline_segment_fill(
|
|||||||
let r = r as f32;
|
let r = r as f32;
|
||||||
let r_outer = |t: f32| r + (hw0 + (hw1 - hw0) * t);
|
let r_outer = |t: f32| r + (hw0 + (hw1 - hw0) * t);
|
||||||
let r_inner = |t: f32| (r - (hw0 + (hw1 - hw0) * t)).max(0.0);
|
let r_inner = |t: f32| (r - (hw0 + (hw1 - hw0) * t)).max(0.0);
|
||||||
let mut boundary = Vec::with_capacity((segs as usize + 1) * 2);
|
let segs_u = segs as usize;
|
||||||
|
let mut boundary = vec![[0.0_f32; 2]; (segs_u + 1) * 2];
|
||||||
let inv = 1.0 / segs as f32;
|
let inv = 1.0 / segs as f32;
|
||||||
for j in 0..=segs {
|
for j in 0..=segs_u {
|
||||||
let t = j as f32 * inv;
|
let t = j as f32 * inv;
|
||||||
let ang = sa + span * t;
|
let ang = sa + span * t;
|
||||||
|
let (sin, cos) = ang.sin_cos();
|
||||||
let ro = r_outer(t);
|
let ro = r_outer(t);
|
||||||
boundary.push([cx + ro * ang.cos(), cy + ro * ang.sin()]);
|
|
||||||
}
|
|
||||||
for j in (0..=segs).rev() {
|
|
||||||
let t = j as f32 * inv;
|
|
||||||
let ang = sa + span * t;
|
|
||||||
let ri = r_inner(t);
|
let ri = r_inner(t);
|
||||||
boundary.push([cx + ri * ang.cos(), cy + ri * ang.sin()]);
|
boundary[j] = [cx + ro * cos, cy + ro * sin];
|
||||||
|
boundary[2 * segs_u + 1 - j] = [cx + ri * cos, cy + ri * sin];
|
||||||
}
|
}
|
||||||
Some(boundary)
|
Some(boundary)
|
||||||
}
|
}
|
||||||
|
|||||||
+137
-146
@@ -627,8 +627,8 @@ pub struct InteractionHandleIndex {
|
|||||||
handles: SpatialSet<u64>,
|
handles: SpatialSet<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WireIndexEntries {
|
struct WireIndexBatch {
|
||||||
wire: Option<Entry3<u32>>,
|
wire_entries: Vec<Entry3<u32>>,
|
||||||
segments: Vec<Entry3<SegmentRef>>,
|
segments: Vec<Entry3<SegmentRef>>,
|
||||||
snap_points: Vec<Entry3<SnapPointRef>>,
|
snap_points: Vec<Entry3<SnapPointRef>>,
|
||||||
key_vertices: Vec<Entry3<KeyVertexRef>>,
|
key_vertices: Vec<Entry3<KeyVertexRef>>,
|
||||||
@@ -636,37 +636,64 @@ struct WireIndexEntries {
|
|||||||
fill_triangles: Vec<Entry3<TriangleRef>>,
|
fill_triangles: Vec<Entry3<TriangleRef>>,
|
||||||
pick_triangles: Vec<Entry3<TriangleRef>>,
|
pick_triangles: Vec<Entry3<TriangleRef>>,
|
||||||
glyphs: Vec<Entry3<GlyphRef>>,
|
glyphs: Vec<Entry3<GlyphRef>>,
|
||||||
unbounded: bool,
|
unbounded_wires: Vec<u32>,
|
||||||
screen_unbounded: bool,
|
screen_unbounded_wires: Vec<u32>,
|
||||||
max_line_half_width_px: f32,
|
max_line_half_width_px: f32,
|
||||||
max_marker_radius_fraction: f32,
|
max_marker_radius_fraction: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries {
|
impl WireIndexBatch {
|
||||||
let mut entries = WireIndexEntries {
|
fn with_capacity_for_chunk(chunk: &[WireModel]) -> Self {
|
||||||
wire: finite_wire_aabb3(wire).map(|aabb| Entry3 {
|
let total_segs: usize = chunk.iter().map(|w| w.points.len().saturating_sub(1)).sum();
|
||||||
|
let total_snaps: usize = chunk.iter().map(|w| w.snap_pts.len()).sum();
|
||||||
|
let total_key_verts: usize = chunk.iter().map(|w| w.key_vertices.len()).sum();
|
||||||
|
let total_key_segs: usize = chunk
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.key_vertices.len().saturating_sub(1))
|
||||||
|
.sum();
|
||||||
|
let total_fills: usize = chunk.iter().map(|w| w.fill_tris.len() / 3).sum();
|
||||||
|
let total_picks: usize = chunk.iter().map(|w| w.pick_tris.len() / 3).sum();
|
||||||
|
let total_glyphs: usize = chunk.iter().map(|w| w.text_verts.len() / 6).sum();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
wire_entries: Vec::with_capacity(chunk.len()),
|
||||||
|
segments: Vec::with_capacity(total_segs),
|
||||||
|
snap_points: Vec::with_capacity(total_snaps),
|
||||||
|
key_vertices: Vec::with_capacity(total_key_verts),
|
||||||
|
key_segments: Vec::with_capacity(total_key_segs),
|
||||||
|
fill_triangles: Vec::with_capacity(total_fills),
|
||||||
|
pick_triangles: Vec::with_capacity(total_picks),
|
||||||
|
glyphs: Vec::with_capacity(total_glyphs),
|
||||||
|
unbounded_wires: Vec::new(),
|
||||||
|
screen_unbounded_wires: Vec::new(),
|
||||||
|
max_line_half_width_px: 0.0,
|
||||||
|
max_marker_radius_fraction: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_wire_index_entries(wire_idx: u32, wire: &WireModel, batch: &mut WireIndexBatch) {
|
||||||
|
if let Some(aabb) = finite_wire_aabb3(wire) {
|
||||||
|
batch.wire_entries.push(Entry3 {
|
||||||
aabb,
|
aabb,
|
||||||
value: wire_idx,
|
value: wire_idx,
|
||||||
}),
|
});
|
||||||
segments: Vec::with_capacity(wire.points.len().saturating_sub(1)),
|
|
||||||
snap_points: Vec::with_capacity(wire.snap_pts.len()),
|
|
||||||
key_vertices: Vec::with_capacity(wire.key_vertices.len()),
|
|
||||||
key_segments: Vec::with_capacity(wire.key_vertices.len().saturating_sub(1)),
|
|
||||||
fill_triangles: Vec::with_capacity(wire.fill_tris.len() / 3),
|
|
||||||
pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3),
|
|
||||||
glyphs: Vec::with_capacity(wire.text_verts.len() / 6),
|
|
||||||
unbounded: false,
|
|
||||||
screen_unbounded: wire.point_marker.is_some(),
|
|
||||||
max_line_half_width_px: if wire.line_weight_px.is_finite() {
|
|
||||||
(wire.line_weight_px * 0.5).max(0.0)
|
|
||||||
} else {
|
} else {
|
||||||
0.0
|
batch.unbounded_wires.push(wire_idx);
|
||||||
},
|
}
|
||||||
max_marker_radius_fraction: wire.point_marker.map_or(0.0, |marker| {
|
if wire.point_marker.is_some() {
|
||||||
marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2
|
batch.screen_unbounded_wires.push(wire_idx);
|
||||||
}),
|
}
|
||||||
};
|
if wire.line_weight_px.is_finite() {
|
||||||
entries.unbounded = entries.wire.is_none();
|
batch.max_line_half_width_px = batch
|
||||||
|
.max_line_half_width_px
|
||||||
|
.max((wire.line_weight_px * 0.5).max(0.0));
|
||||||
|
}
|
||||||
|
if let Some(marker) = wire.point_marker {
|
||||||
|
batch.max_marker_radius_fraction = batch.max_marker_radius_fraction.max(
|
||||||
|
marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if wire.point_marker.is_none() {
|
if wire.point_marker.is_none() {
|
||||||
for start in 0..wire.points.len().saturating_sub(1) {
|
for start in 0..wire.points.len().saturating_sub(1) {
|
||||||
@@ -676,7 +703,7 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
]) else {
|
]) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
entries.segments.push(Entry3 {
|
batch.segments.push(Entry3 {
|
||||||
aabb,
|
aabb,
|
||||||
value: SegmentRef {
|
value: SegmentRef {
|
||||||
wire: wire_idx,
|
wire: wire_idx,
|
||||||
@@ -687,7 +714,7 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
}
|
}
|
||||||
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
||||||
if point.is_finite() {
|
if point.is_finite() {
|
||||||
entries.snap_points.push(Entry3 {
|
batch.snap_points.push(Entry3 {
|
||||||
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
||||||
value: SnapPointRef {
|
value: SnapPointRef {
|
||||||
wire: wire_idx,
|
wire: wire_idx,
|
||||||
@@ -698,7 +725,7 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
}
|
}
|
||||||
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
||||||
if point.iter().all(|value| value.is_finite()) {
|
if point.iter().all(|value| value.is_finite()) {
|
||||||
entries.key_vertices.push(Entry3 {
|
batch.key_vertices.push(Entry3 {
|
||||||
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
||||||
value: KeyVertexRef {
|
value: KeyVertexRef {
|
||||||
wire: wire_idx,
|
wire: wire_idx,
|
||||||
@@ -713,7 +740,7 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
entries.key_segments.push(Entry3 {
|
batch.key_segments.push(Entry3 {
|
||||||
aabb,
|
aabb,
|
||||||
value: KeySegmentRef {
|
value: KeySegmentRef {
|
||||||
wire: wire_idx,
|
wire: wire_idx,
|
||||||
@@ -725,13 +752,13 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
wire_idx,
|
wire_idx,
|
||||||
&wire.fill_tris,
|
&wire.fill_tris,
|
||||||
&wire.fill_tris_low,
|
&wire.fill_tris_low,
|
||||||
&mut entries.fill_triangles,
|
&mut batch.fill_triangles,
|
||||||
);
|
);
|
||||||
append_triangle_entries(
|
append_triangle_entries(
|
||||||
wire_idx,
|
wire_idx,
|
||||||
&wire.pick_tris,
|
&wire.pick_tris,
|
||||||
&wire.pick_tris_low,
|
&wire.pick_tris_low,
|
||||||
&mut entries.pick_triangles,
|
&mut batch.pick_triangles,
|
||||||
);
|
);
|
||||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
||||||
@@ -746,7 +773,7 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
})) else {
|
})) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
entries.glyphs.push(Entry3 {
|
batch.glyphs.push(Entry3 {
|
||||||
aabb,
|
aabb,
|
||||||
value: GlyphRef {
|
value: GlyphRef {
|
||||||
wire: wire_idx,
|
wire: wire_idx,
|
||||||
@@ -754,20 +781,6 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
entries
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_entry_parts<T>(mut parts: Vec<Vec<T>>) -> Vec<T> {
|
|
||||||
let Some((largest, _)) = parts.iter().enumerate().max_by_key(|(_, part)| part.len()) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let mut output = parts.swap_remove(largest);
|
|
||||||
let remaining: usize = parts.iter().map(Vec::len).sum();
|
|
||||||
output.reserve(remaining);
|
|
||||||
for mut part in parts {
|
|
||||||
output.append(&mut part);
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractionHandleIndex {
|
impl InteractionHandleIndex {
|
||||||
@@ -822,127 +835,105 @@ impl InteractionIndex {
|
|||||||
let perf = crate::perf::enabled();
|
let perf = crate::perf::enabled();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let build_started = iced::time::Instant::now();
|
let build_started = iced::time::Instant::now();
|
||||||
let wire_handles: Vec<Option<u64>> = wires
|
let n_wires = wires.len();
|
||||||
.iter()
|
let mut wire_handles = Vec::with_capacity(n_wires);
|
||||||
.map(|wire| wire.name.parse::<u64>().ok())
|
let mut wire_ordinals = Vec::with_capacity(n_wires);
|
||||||
.collect();
|
|
||||||
let mut next_ordinal: rustc_hash::FxHashMap<u64, u32> =
|
let mut next_ordinal: rustc_hash::FxHashMap<u64, u32> =
|
||||||
rustc_hash::FxHashMap::default();
|
rustc_hash::FxHashMap::with_capacity_and_hasher(n_wires.min(4096), Default::default());
|
||||||
let wire_ordinals: Vec<Option<u32>> = wire_handles
|
for wire in wires {
|
||||||
.iter()
|
let handle = wire.name.parse::<u64>().ok();
|
||||||
.map(|handle| {
|
wire_handles.push(handle);
|
||||||
let handle = (*handle)?;
|
wire_ordinals.push(handle.map(|h| {
|
||||||
let ordinal = next_ordinal.entry(handle).or_default();
|
let entry = next_ordinal.entry(h).or_default();
|
||||||
let current = *ordinal;
|
let ord = *entry;
|
||||||
*ordinal += 1;
|
*entry += 1;
|
||||||
Some(current)
|
ord
|
||||||
})
|
}));
|
||||||
.collect();
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let handles_elapsed = build_started.elapsed();
|
let handles_elapsed = build_started.elapsed();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let collect_started = iced::time::Instant::now();
|
let collect_started = iced::time::Instant::now();
|
||||||
let mut wire_entries = Vec::with_capacity(wires.len());
|
|
||||||
let mut unbounded_wires = Vec::new();
|
const CHUNK_SIZE: usize = 256;
|
||||||
let mut screen_unbounded_wires = Vec::new();
|
|
||||||
let mut max_line_half_width_px = 0.0f32;
|
|
||||||
let mut max_marker_radius_fraction = 0.0f32;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let per_wire: Vec<WireIndexEntries> = {
|
let batches: Vec<WireIndexBatch> = {
|
||||||
use crate::par::prelude::*;
|
use crate::par::prelude::*;
|
||||||
wires
|
wires
|
||||||
.par_iter()
|
.par_chunks(CHUNK_SIZE)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
.map(|(chunk_idx, chunk)| {
|
||||||
|
let mut batch = WireIndexBatch::with_capacity_for_chunk(chunk);
|
||||||
|
let base_idx = (chunk_idx * CHUNK_SIZE) as u32;
|
||||||
|
for (i, wire) in chunk.iter().enumerate() {
|
||||||
|
append_wire_index_entries(base_idx + i as u32, wire, &mut batch);
|
||||||
|
}
|
||||||
|
batch
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let per_wire: Vec<WireIndexEntries> = wires
|
let batches: Vec<WireIndexBatch> = wires
|
||||||
.iter()
|
.chunks(CHUNK_SIZE)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
.map(|(chunk_idx, chunk)| {
|
||||||
|
let mut batch = WireIndexBatch::with_capacity_for_chunk(chunk);
|
||||||
|
let base_idx = (chunk_idx * CHUNK_SIZE) as u32;
|
||||||
|
for (i, wire) in chunk.iter().enumerate() {
|
||||||
|
append_wire_index_entries(base_idx + i as u32, wire, &mut batch);
|
||||||
|
}
|
||||||
|
batch
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let collect_elapsed = collect_started.elapsed();
|
let collect_elapsed = collect_started.elapsed();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let flatten_started = iced::time::Instant::now();
|
let flatten_started = iced::time::Instant::now();
|
||||||
let mut segment_parts = Vec::with_capacity(per_wire.len());
|
|
||||||
let mut snap_point_parts = Vec::with_capacity(per_wire.len());
|
let total_wires: usize = batches.iter().map(|b| b.wire_entries.len()).sum();
|
||||||
let mut key_vertex_parts = Vec::with_capacity(per_wire.len());
|
let total_segments: usize = batches.iter().map(|b| b.segments.len()).sum();
|
||||||
let mut key_segment_parts = Vec::with_capacity(per_wire.len());
|
let total_snaps: usize = batches.iter().map(|b| b.snap_points.len()).sum();
|
||||||
let mut fill_triangle_parts = Vec::with_capacity(per_wire.len());
|
let total_key_verts: usize = batches.iter().map(|b| b.key_vertices.len()).sum();
|
||||||
let mut pick_triangle_parts = Vec::with_capacity(per_wire.len());
|
let total_key_segs: usize = batches.iter().map(|b| b.key_segments.len()).sum();
|
||||||
let mut glyph_parts = Vec::with_capacity(per_wire.len());
|
let total_fills: usize = batches.iter().map(|b| b.fill_triangles.len()).sum();
|
||||||
for (wire_idx, entries) in per_wire.into_iter().enumerate() {
|
let total_picks: usize = batches.iter().map(|b| b.pick_triangles.len()).sum();
|
||||||
|
let total_glyphs: usize = batches.iter().map(|b| b.glyphs.len()).sum();
|
||||||
|
let total_unbounded: usize = batches.iter().map(|b| b.unbounded_wires.len()).sum();
|
||||||
|
let total_screen_unbounded: usize =
|
||||||
|
batches.iter().map(|b| b.screen_unbounded_wires.len()).sum();
|
||||||
|
|
||||||
|
let mut wire_entries = Vec::with_capacity(total_wires);
|
||||||
|
let mut segment_entries = Vec::with_capacity(total_segments);
|
||||||
|
let mut snap_point_entries = Vec::with_capacity(total_snaps);
|
||||||
|
let mut key_vertex_entries = Vec::with_capacity(total_key_verts);
|
||||||
|
let mut key_segment_entries = Vec::with_capacity(total_key_segs);
|
||||||
|
let mut fill_triangle_entries = Vec::with_capacity(total_fills);
|
||||||
|
let mut pick_triangle_entries = Vec::with_capacity(total_picks);
|
||||||
|
let mut glyph_entries = Vec::with_capacity(total_glyphs);
|
||||||
|
let mut unbounded_wires = Vec::with_capacity(total_unbounded);
|
||||||
|
let mut screen_unbounded_wires = Vec::with_capacity(total_screen_unbounded);
|
||||||
|
let mut max_line_half_width_px = 0.0f32;
|
||||||
|
let mut max_marker_radius_fraction = 0.0f32;
|
||||||
|
|
||||||
|
for mut batch in batches {
|
||||||
max_line_half_width_px =
|
max_line_half_width_px =
|
||||||
max_line_half_width_px.max(entries.max_line_half_width_px);
|
max_line_half_width_px.max(batch.max_line_half_width_px);
|
||||||
max_marker_radius_fraction =
|
max_marker_radius_fraction =
|
||||||
max_marker_radius_fraction.max(entries.max_marker_radius_fraction);
|
max_marker_radius_fraction.max(batch.max_marker_radius_fraction);
|
||||||
if let Some(entry) = entries.wire {
|
wire_entries.append(&mut batch.wire_entries);
|
||||||
wire_entries.push(entry);
|
segment_entries.append(&mut batch.segments);
|
||||||
|
snap_point_entries.append(&mut batch.snap_points);
|
||||||
|
key_vertex_entries.append(&mut batch.key_vertices);
|
||||||
|
key_segment_entries.append(&mut batch.key_segments);
|
||||||
|
fill_triangle_entries.append(&mut batch.fill_triangles);
|
||||||
|
pick_triangle_entries.append(&mut batch.pick_triangles);
|
||||||
|
glyph_entries.append(&mut batch.glyphs);
|
||||||
|
unbounded_wires.append(&mut batch.unbounded_wires);
|
||||||
|
screen_unbounded_wires.append(&mut batch.screen_unbounded_wires);
|
||||||
}
|
}
|
||||||
if entries.unbounded {
|
|
||||||
unbounded_wires.push(wire_idx as u32);
|
|
||||||
}
|
|
||||||
if entries.screen_unbounded {
|
|
||||||
screen_unbounded_wires.push(wire_idx as u32);
|
|
||||||
}
|
|
||||||
segment_parts.push(entries.segments);
|
|
||||||
snap_point_parts.push(entries.snap_points);
|
|
||||||
key_vertex_parts.push(entries.key_vertices);
|
|
||||||
key_segment_parts.push(entries.key_segments);
|
|
||||||
fill_triangle_parts.push(entries.fill_triangles);
|
|
||||||
pick_triangle_parts.push(entries.pick_triangles);
|
|
||||||
glyph_parts.push(entries.glyphs);
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let (
|
|
||||||
((segment_entries, snap_point_entries), (key_vertex_entries, key_segment_entries)),
|
|
||||||
((fill_triangle_entries, pick_triangle_entries), glyph_entries),
|
|
||||||
) = crate::par::join(
|
|
||||||
|| {
|
|
||||||
crate::par::join(
|
|
||||||
|| {
|
|
||||||
crate::par::join(
|
|
||||||
|| flatten_entry_parts(segment_parts),
|
|
||||||
|| flatten_entry_parts(snap_point_parts),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|| {
|
|
||||||
crate::par::join(
|
|
||||||
|| flatten_entry_parts(key_vertex_parts),
|
|
||||||
|| flatten_entry_parts(key_segment_parts),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|| {
|
|
||||||
let (fill, pick) = crate::par::join(
|
|
||||||
|| flatten_entry_parts(fill_triangle_parts),
|
|
||||||
|| flatten_entry_parts(pick_triangle_parts),
|
|
||||||
);
|
|
||||||
((fill, pick), flatten_entry_parts(glyph_parts))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
let (
|
|
||||||
segment_entries,
|
|
||||||
snap_point_entries,
|
|
||||||
key_vertex_entries,
|
|
||||||
key_segment_entries,
|
|
||||||
fill_triangle_entries,
|
|
||||||
pick_triangle_entries,
|
|
||||||
glyph_entries,
|
|
||||||
) = (
|
|
||||||
flatten_entry_parts(segment_parts),
|
|
||||||
flatten_entry_parts(snap_point_parts),
|
|
||||||
flatten_entry_parts(key_vertex_parts),
|
|
||||||
flatten_entry_parts(key_segment_parts),
|
|
||||||
flatten_entry_parts(fill_triangle_parts),
|
|
||||||
flatten_entry_parts(pick_triangle_parts),
|
|
||||||
flatten_entry_parts(glyph_parts),
|
|
||||||
);
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let flatten_elapsed = flatten_started.elapsed();
|
let flatten_elapsed = flatten_started.elapsed();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|||||||
+278
-50
@@ -201,6 +201,97 @@ impl Default for Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inline stack-allocated collection for up to 16 in-range `WireModel` references,
|
||||||
|
/// falling back to heap if an aperture contains more than 16 wires.
|
||||||
|
/// Avoids heap allocations entirely on the interactive drafting and cursor hover paths.
|
||||||
|
struct InRangeWires<'a> {
|
||||||
|
stack: [Option<&'a WireModel>; 16],
|
||||||
|
heap: Vec<&'a WireModel>,
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InRangeWires<'a> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
stack: [None; 16],
|
||||||
|
heap: Vec::new(),
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn push(&mut self, wire: &'a WireModel) {
|
||||||
|
if self.count < 16 {
|
||||||
|
self.stack[self.count] = Some(wire);
|
||||||
|
} else {
|
||||||
|
if self.heap.is_empty() {
|
||||||
|
self.heap.reserve(16);
|
||||||
|
for slot in &self.stack {
|
||||||
|
if let Some(w) = slot {
|
||||||
|
self.heap.push(*w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.heap.push(wire);
|
||||||
|
}
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.count
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn get(&self, idx: usize) -> Option<&'a WireModel> {
|
||||||
|
if idx >= self.count {
|
||||||
|
None
|
||||||
|
} else if self.count <= 16 {
|
||||||
|
self.stack[idx]
|
||||||
|
} else {
|
||||||
|
self.heap.get(idx).copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn iter(&self) -> InRangeWiresIter<'a, '_> {
|
||||||
|
InRangeWiresIter {
|
||||||
|
wires: self,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InRangeWiresIter<'a, 'b> {
|
||||||
|
wires: &'b InRangeWires<'a>,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Iterator for InRangeWiresIter<'a, 'b> {
|
||||||
|
type Item = &'a WireModel;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let item = self.wires.get(self.index)?;
|
||||||
|
self.index += 1;
|
||||||
|
Some(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let remaining = self.wires.count.saturating_sub(self.index);
|
||||||
|
(remaining, Some(remaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ExactSizeIterator for InRangeWiresIter<'a, 'b> {}
|
||||||
|
|
||||||
impl Snapper {
|
impl Snapper {
|
||||||
/// True when snap is globally on AND at least one mode is configured.
|
/// True when snap is globally on AND at least one mode is configured.
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
@@ -1071,17 +1162,26 @@ impl Snapper {
|
|||||||
// cursor cell held ~1k wires / ~240k points → 15 s pre-gate.)
|
// cursor cell held ~1k wires / ~240k points → 15 s pre-gate.)
|
||||||
const MAX_PAIRWISE_POINTS: usize = 3_000;
|
const MAX_PAIRWISE_POINTS: usize = 3_000;
|
||||||
let mut in_range_pts = 0usize;
|
let mut in_range_pts = 0usize;
|
||||||
if wires.segments().is_none() {
|
let mut in_range_wires = InRangeWires::new();
|
||||||
for w in wires.iter().filter(|w| wire_in_range(w)) {
|
let unindexed = wires.segments().is_none();
|
||||||
|
if unindexed {
|
||||||
|
for w in wires.iter() {
|
||||||
|
if wire_in_range(w) {
|
||||||
in_range_pts += w.points.len();
|
in_range_pts += w.points.len();
|
||||||
if in_range_pts > MAX_PAIRWISE_POINTS {
|
in_range_wires.push(w);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let allow_unindexed_pairwise = in_range_pts <= MAX_PAIRWISE_POINTS;
|
let allow_unindexed_pairwise = in_range_pts <= MAX_PAIRWISE_POINTS;
|
||||||
let local_segments = indexed_segments(wires);
|
let local_segments = indexed_segments(wires);
|
||||||
|
|
||||||
|
// Early out: if no unindexed wires overlap the cursor aperture and
|
||||||
|
// no persistent tracking points exist, discrete/continuous object snaps
|
||||||
|
// cannot hit anything. Avoid running all subsequent pass setups and loops.
|
||||||
|
if unindexed && in_range_wires.is_empty() && self.tracking_points.is_empty() {
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
||||||
let screen = world_to_screen(world, view_rot, eye, bounds);
|
let screen = world_to_screen(world, view_rot, eye, bounds);
|
||||||
if !in_bounds(screen) {
|
if !in_bounds(screen) {
|
||||||
@@ -1138,7 +1238,7 @@ impl Snapper {
|
|||||||
try_snap_hint(world, hint);
|
try_snap_hint(world, hint);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
for &(world, hint) in &wire.snap_pts {
|
for &(world, hint) in &wire.snap_pts {
|
||||||
try_snap_hint(world, hint);
|
try_snap_hint(world, hint);
|
||||||
}
|
}
|
||||||
@@ -1177,10 +1277,7 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
if !wire_in_range(wire) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !wire.key_vertices.is_empty() {
|
if !wire.key_vertices.is_empty() {
|
||||||
for &point in &wire.key_vertices {
|
for &point in &wire.key_vertices {
|
||||||
try_pt(DVec3::from_array(point), SnapType::Endpoint);
|
try_pt(DVec3::from_array(point), SnapType::Endpoint);
|
||||||
@@ -1226,7 +1323,7 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
for wire in in_range_wires.iter() {
|
||||||
for segment in wire.key_vertices.windows(2) {
|
for segment in wire.key_vertices.windows(2) {
|
||||||
let a = DVec3::from_array(segment[0]);
|
let a = DVec3::from_array(segment[0]);
|
||||||
let b = DVec3::from_array(segment[1]);
|
let b = DVec3::from_array(segment[1]);
|
||||||
@@ -1248,10 +1345,7 @@ impl Snapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
if !wire_in_range(wire) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for i in 0..wire.points.len().saturating_sub(1) {
|
for i in 0..wire.points.len().saturating_sub(1) {
|
||||||
let p =
|
let p =
|
||||||
nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
|
nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
|
||||||
@@ -1272,10 +1366,7 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
if !wire_in_range(wire) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for i in 0..wire.points.len().saturating_sub(1) {
|
for i in 0..wire.points.len().saturating_sub(1) {
|
||||||
if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) {
|
if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) {
|
||||||
try_pt(foot, SnapType::Perpendicular);
|
try_pt(foot, SnapType::Perpendicular);
|
||||||
@@ -1301,7 +1392,7 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
for wire in in_range_wires.iter() {
|
||||||
for index in 0..wire.points.len().saturating_sub(1) {
|
for index in 0..wire.points.len().saturating_sub(1) {
|
||||||
if let Some(point) = ray_segment_intersect_3d(
|
if let Some(point) = ray_segment_intersect_3d(
|
||||||
origin,
|
origin,
|
||||||
@@ -1432,21 +1523,15 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if in_range_wires.len() >= 2 {
|
||||||
for i in 0..wires.len() {
|
for i in 0..in_range_wires.len() {
|
||||||
let Some(wire_i) = wires.get(i) else {
|
let Some(wire_i) = in_range_wires.get(i) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !wire_in_range(wire_i) {
|
for j in (i + 1)..in_range_wires.len() {
|
||||||
continue;
|
let Some(wire_j) = in_range_wires.get(j) else {
|
||||||
}
|
|
||||||
for j in (i + 1)..wires.len() {
|
|
||||||
let Some(wire_j) = wires.get(j) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !wire_in_range(wire_j) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Curved pairs are solved exactly (bug #1052); see
|
// Curved pairs are solved exactly (bug #1052); see
|
||||||
// `exact_curve_intersections`'s doc comment.
|
// `exact_curve_intersections`'s doc comment.
|
||||||
if let Some(pts) = exact_curve_intersections(wire_i, wire_j) {
|
if let Some(pts) = exact_curve_intersections(wire_i, wire_j) {
|
||||||
@@ -1609,35 +1694,26 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if in_range_wires.len() >= 2 {
|
||||||
let screen_pts: Vec<Option<Vec<Point>>> = wires
|
let screen_pts: Vec<Vec<Point>> = in_range_wires
|
||||||
.iter()
|
.iter()
|
||||||
.map(|w| {
|
.map(|w| {
|
||||||
if !wire_in_range(w) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(
|
|
||||||
(0..w.points.len())
|
(0..w.points.len())
|
||||||
.map(|i| world_to_screen(wp_f64(w, i), view_rot, eye, bounds))
|
.map(|i| world_to_screen(wp_f64(w, i), view_rot, eye, bounds))
|
||||||
.collect::<Vec<_>>(),
|
.collect()
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for i in 0..wires.len() {
|
for i in 0..in_range_wires.len() {
|
||||||
let Some(ref si) = screen_pts[i] else {
|
let Some(wire_i) = in_range_wires.get(i) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(wire_i) = wires.get(i) else {
|
let si = &screen_pts[i];
|
||||||
continue;
|
for j in (i + 1)..in_range_wires.len() {
|
||||||
};
|
let Some(wire_j) = in_range_wires.get(j) else {
|
||||||
for j in (i + 1)..wires.len() {
|
|
||||||
let Some(ref sj) = screen_pts[j] else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(wire_j) = wires.get(j) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let sj = &screen_pts[j];
|
||||||
for ai in 0..wire_i.points.len().saturating_sub(1) {
|
for ai in 0..wire_i.points.len().saturating_sub(1) {
|
||||||
let sa0 = si[ai];
|
let sa0 = si[ai];
|
||||||
let sa1 = si[ai + 1];
|
let sa1 = si[ai + 1];
|
||||||
@@ -1663,7 +1739,7 @@ impl Snapper {
|
|||||||
// Operates directly on tangent_geoms geometry — independent of the
|
// Operates directly on tangent_geoms geometry — independent of the
|
||||||
// wire.points rendering structure so polyline segments work correctly.
|
// wire.points rendering structure so polyline segments work correctly.
|
||||||
if self.is_on(SnapType::Tangent) {
|
if self.is_on(SnapType::Tangent) {
|
||||||
for wire in wires.iter() {
|
let mut eval_tangent = |wire: &WireModel| {
|
||||||
for tg in &wire.tangent_geoms {
|
for tg in &wire.tangent_geoms {
|
||||||
let (world_pt, d2) = match tg {
|
let (world_pt, d2) = match tg {
|
||||||
TangentGeom::Line { p1, p2 } => {
|
TangentGeom::Line { p1, p2 } => {
|
||||||
@@ -1874,6 +1950,18 @@ impl Snapper {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if unindexed {
|
||||||
|
for wire in in_range_wires.iter() {
|
||||||
|
eval_tangent(wire);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for wire in wires.iter() {
|
||||||
|
if wire_in_range(wire) {
|
||||||
|
eval_tangent(wire);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1953,7 +2041,7 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
for wire in in_range_wires.iter() {
|
||||||
let mut curve_d2 = f32::INFINITY;
|
let mut curve_d2 = f32::INFINITY;
|
||||||
for index in 0..wire.points.len().saturating_sub(1) {
|
for index in 0..wire.points.len().saturating_sub(1) {
|
||||||
let nearest = nearest_on_segment(
|
let nearest = nearest_on_segment(
|
||||||
@@ -3456,4 +3544,144 @@ mod ext_tests {
|
|||||||
assert!(points.iter().all(|point| (point.x - origin).abs() == 3.0 && point.y == origin));
|
assert!(points.iter().all(|point| (point.x - origin).abs() == 3.0 && point.y == origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unindexed_snap_endpoint_and_midpoint_accuracy() {
|
||||||
|
let mut snapper = Snapper::default();
|
||||||
|
snapper.snap_enabled = true;
|
||||||
|
snapper.enabled = [SnapType::Endpoint, SnapType::Midpoint].into_iter().collect();
|
||||||
|
|
||||||
|
let line = WireModel {
|
||||||
|
points: vec![[0.0, 0.0, 0.0], [100.0, 100.0, 0.0]],
|
||||||
|
key_vertices: vec![[0.0, 0.0, 0.0], [100.0, 100.0, 0.0]],
|
||||||
|
aabb: [0.0, 0.0, 100.0, 100.0],
|
||||||
|
tangent_geoms: vec![TangentGeom::Line { p1: [0.0, 0.0, 0.0], p2: [100.0, 100.0, 0.0] }],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let wires = vec![line];
|
||||||
|
|
||||||
|
let view_rot = Mat4::IDENTITY;
|
||||||
|
let bounds = Rectangle { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
|
||||||
|
|
||||||
|
// Test Endpoint snap near (0, 0) with eye at (0, 0, 500)
|
||||||
|
let eye_origin = DVec3::new(0.0, 0.0, 500.0);
|
||||||
|
let cursor_world = DVec3::new(0.01, 0.01, 0.0);
|
||||||
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye_origin, bounds);
|
||||||
|
let res = snapper.snap(
|
||||||
|
cursor_world, cursor_screen, wires.as_slice(),
|
||||||
|
view_rot, eye_origin, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
).expect("should snap to endpoint");
|
||||||
|
assert_eq!(res.snap_type, SnapType::Endpoint);
|
||||||
|
assert_eq!(res.world, DVec3::new(0.0, 0.0, 0.0));
|
||||||
|
|
||||||
|
// Test Midpoint snap near (50, 50) with eye at (50, 50, 500)
|
||||||
|
let eye_mid = DVec3::new(50.0, 50.0, 500.0);
|
||||||
|
let cursor_world = DVec3::new(50.01, 50.01, 0.0);
|
||||||
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye_mid, bounds);
|
||||||
|
let res = snapper.snap(
|
||||||
|
cursor_world, cursor_screen, wires.as_slice(),
|
||||||
|
view_rot, eye_mid, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
).expect("should snap to midpoint");
|
||||||
|
assert_eq!(res.snap_type, SnapType::Midpoint);
|
||||||
|
assert_eq!(res.world, DVec3::new(50.0, 50.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unindexed_snap_empty_space_early_out() {
|
||||||
|
let mut snapper = Snapper::default();
|
||||||
|
snapper.snap_enabled = true;
|
||||||
|
snapper.enable_all();
|
||||||
|
|
||||||
|
let line = WireModel {
|
||||||
|
points: vec![[0.0, 0.0, 0.0], [10.0, 10.0, 0.0]],
|
||||||
|
key_vertices: vec![[0.0, 0.0, 0.0], [10.0, 10.0, 0.0]],
|
||||||
|
aabb: [0.0, 0.0, 10.0, 10.0],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let wires = vec![line];
|
||||||
|
|
||||||
|
let view_rot = Mat4::IDENTITY;
|
||||||
|
let eye = DVec3::new(500.0, 500.0, 500.0);
|
||||||
|
let bounds = Rectangle { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
|
||||||
|
|
||||||
|
// Cursor in far whitespace (5000, 5000)
|
||||||
|
let cursor_world = DVec3::new(5000.0, 5000.0, 0.0);
|
||||||
|
let cursor_screen = Point::new(500.0, 500.0);
|
||||||
|
let res = snapper.snap(
|
||||||
|
cursor_world, cursor_screen, wires.as_slice(),
|
||||||
|
view_rot, eye, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
);
|
||||||
|
assert!(res.is_none(), "whitespace cursor must return None");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unindexed_snap_intersection_pair() {
|
||||||
|
let mut snapper = Snapper::default();
|
||||||
|
snapper.snap_enabled = true;
|
||||||
|
snapper.enabled = [SnapType::Intersection].into_iter().collect();
|
||||||
|
|
||||||
|
let line1 = WireModel {
|
||||||
|
points: vec![[0.0, 0.0, 0.0], [100.0, 100.0, 0.0]],
|
||||||
|
key_vertices: vec![[0.0, 0.0, 0.0], [100.0, 100.0, 0.0]],
|
||||||
|
aabb: [0.0, 0.0, 100.0, 100.0],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let line2 = WireModel {
|
||||||
|
points: vec![[0.0, 100.0, 0.0], [100.0, 0.0, 0.0]],
|
||||||
|
key_vertices: vec![[0.0, 100.0, 0.0], [100.0, 0.0, 0.0]],
|
||||||
|
aabb: [0.0, 0.0, 100.0, 100.0],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let wires = vec![line1, line2];
|
||||||
|
|
||||||
|
let view_rot = Mat4::IDENTITY;
|
||||||
|
let eye = DVec3::new(50.0, 50.0, 500.0);
|
||||||
|
let bounds = Rectangle { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
|
||||||
|
|
||||||
|
// Cursor near intersection (50, 50)
|
||||||
|
let cursor_world = DVec3::new(50.01, 50.01, 0.0);
|
||||||
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||||
|
let res = snapper.snap(
|
||||||
|
cursor_world, cursor_screen, wires.as_slice(),
|
||||||
|
view_rot, eye, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
).expect("should snap to intersection");
|
||||||
|
assert_eq!(res.snap_type, SnapType::Intersection);
|
||||||
|
assert!((res.world - DVec3::new(50.0, 50.0, 0.0)).length() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unindexed_snap_dense_cluster_fallback() {
|
||||||
|
let mut snapper = Snapper::default();
|
||||||
|
snapper.snap_enabled = true;
|
||||||
|
snapper.enabled = [SnapType::Endpoint].into_iter().collect();
|
||||||
|
|
||||||
|
// Generate 25 lines passing through (0, 0) to force > 16 wires in aperture
|
||||||
|
let mut wires = Vec::new();
|
||||||
|
for i in 0..25 {
|
||||||
|
let angle = (i as f64) * 0.1;
|
||||||
|
let p1 = [0.0f32, 0.0, 0.0];
|
||||||
|
let p2 = [(angle.cos() * 50.0) as f32, (angle.sin() * 50.0) as f32, 0.0];
|
||||||
|
let p1_f64 = [0.0f64, 0.0, 0.0];
|
||||||
|
let p2_f64 = [angle.cos() * 50.0, angle.sin() * 50.0, 0.0];
|
||||||
|
wires.push(WireModel {
|
||||||
|
points: vec![p1, p2],
|
||||||
|
key_vertices: vec![p1_f64, p2_f64],
|
||||||
|
aabb: [p1[0].min(p2[0]), p1[1].min(p2[1]), p1[0].max(p2[0]), p1[1].max(p2[1])],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let view_rot = Mat4::IDENTITY;
|
||||||
|
let eye = DVec3::new(0.0, 0.0, 500.0);
|
||||||
|
let bounds = Rectangle { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
|
||||||
|
|
||||||
|
let cursor_world = DVec3::new(0.01, 0.01, 0.0);
|
||||||
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||||
|
let res = snapper.snap(
|
||||||
|
cursor_world, cursor_screen, wires.as_slice(),
|
||||||
|
view_rot, eye, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
).expect("should snap to endpoint even with > 16 wires in aperture");
|
||||||
|
assert_eq!(res.snap_type, SnapType::Endpoint);
|
||||||
|
assert_eq!(res.world, DVec3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+49
-8
@@ -116,9 +116,15 @@ thread_local! {
|
|||||||
// `(address, length)`. See the doc comment on [`themed_handle`]
|
// `(address, length)`. See the doc comment on [`themed_handle`]
|
||||||
// below for the cache key rationale, the sub-slice guard, the
|
// below for the cache key rationale, the sub-slice guard, the
|
||||||
// mirror of `SemanticCacheKey`, and the threading choice.
|
// mirror of `SemanticCacheKey`, and the threading choice.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ThemedCache {
|
||||||
|
mru: [Option<((usize, usize), svg::Handle)>; 2],
|
||||||
|
map: FxHashMap<(usize, usize), svg::Handle>,
|
||||||
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static THEMED_CACHE: RefCell<FxHashMap<(usize, usize), svg::Handle>> =
|
static THEMED_CACHE: RefCell<ThemedCache> =
|
||||||
RefCell::new(FxHashMap::default());
|
RefCell::new(ThemedCache::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up (or build and cache) the `svg::Handle` for a `&'static [u8]`
|
/// Look up (or build and cache) the `svg::Handle` for a `&'static [u8]`
|
||||||
@@ -159,16 +165,42 @@ thread_local! {
|
|||||||
/// trigger a `RefCell` panic on the second `borrow_mut`, because
|
/// trigger a `RefCell` panic on the second `borrow_mut`, because
|
||||||
/// no `borrow_mut` is held while parsing runs. (The same
|
/// no `borrow_mut` is held while parsing runs. (The same
|
||||||
/// `thread_local!` + `RefCell` shape is used by `SEMANTIC_CACHE`.)
|
/// `thread_local!` + `RefCell` shape is used by `SEMANTIC_CACHE`.)
|
||||||
|
#[inline]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn themed_handle(bytes: &'static [u8]) -> svg::Handle {
|
pub fn themed_handle(bytes: &'static [u8]) -> svg::Handle {
|
||||||
let key = (bytes.as_ptr() as usize, bytes.len());
|
let key = (bytes.as_ptr() as usize, bytes.len());
|
||||||
if let Some(handle) = THEMED_CACHE.with(|cache| cache.borrow().get(&key).cloned()) {
|
let hit = THEMED_CACHE.with(|cache| {
|
||||||
|
let mut c = cache.borrow_mut();
|
||||||
|
if let Some((k, ref handle)) = c.mru[0] {
|
||||||
|
if k == key {
|
||||||
|
return Some(handle.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((k, _)) = c.mru[1] {
|
||||||
|
if k == key {
|
||||||
|
c.mru.swap(0, 1);
|
||||||
|
return Some(c.mru[0].as_ref().unwrap().1.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(handle) = c.map.get(&key) {
|
||||||
|
let handle = handle.clone();
|
||||||
|
c.mru[1] = c.mru[0].take();
|
||||||
|
c.mru[0] = Some((key, handle.clone()));
|
||||||
|
return Some(handle);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
if let Some(handle) = hit {
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Miss: build the handle with no cache borrow held, then insert.
|
// Miss: build the handle with no cache borrow held, then insert.
|
||||||
let handle = svg::Handle::from_memory(bytes);
|
let handle = svg::Handle::from_memory(bytes);
|
||||||
THEMED_CACHE.with(|cache| {
|
THEMED_CACHE.with(|cache| {
|
||||||
cache.borrow_mut().insert(key, handle.clone());
|
let mut c = cache.borrow_mut();
|
||||||
|
c.mru[1] = c.mru[0].take();
|
||||||
|
c.mru[0] = Some((key, handle.clone()));
|
||||||
|
c.map.insert(key, handle.clone());
|
||||||
});
|
});
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
@@ -733,20 +765,20 @@ mod themed_cache_tests {
|
|||||||
/// Number of `svg::Handle` entries currently in the `THEMED_CACHE`.
|
/// Number of `svg::Handle` entries currently in the `THEMED_CACHE`.
|
||||||
/// Test-only inspection helper; the cache is private production state.
|
/// Test-only inspection helper; the cache is private production state.
|
||||||
fn themed_cache_size_for_test() -> usize {
|
fn themed_cache_size_for_test() -> usize {
|
||||||
THEMED_CACHE.with(|c| c.borrow().len())
|
THEMED_CACHE.with(|c| c.borrow().map.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when `(ptr, len)` (the address and length of a
|
/// True when `(ptr, len)` (the address and length of a
|
||||||
/// `&'static [u8]`) has an entry in the `THEMED_CACHE`.
|
/// `&'static [u8]`) has an entry in the `THEMED_CACHE`.
|
||||||
/// Test-only inspection helper.
|
/// Test-only inspection helper.
|
||||||
fn themed_cache_contains_for_test(ptr: *const u8, len: usize) -> bool {
|
fn themed_cache_contains_for_test(ptr: *const u8, len: usize) -> bool {
|
||||||
THEMED_CACHE.with(|c| c.borrow().contains_key(&(ptr as usize, len)))
|
THEMED_CACHE.with(|c| c.borrow().map.contains_key(&(ptr as usize, len)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a clone of the cached `svg::Handle` for `(ptr, len)`,
|
/// Returns a clone of the cached `svg::Handle` for `(ptr, len)`,
|
||||||
/// or `None` if no entry is cached. Test-only inspection helper.
|
/// or `None` if no entry is cached. Test-only inspection helper.
|
||||||
fn themed_cache_get_handle_for_test(ptr: *const u8, len: usize) -> Option<svg::Handle> {
|
fn themed_cache_get_handle_for_test(ptr: *const u8, len: usize) -> Option<svg::Handle> {
|
||||||
THEMED_CACHE.with(|c| c.borrow().get(&(ptr as usize, len)).cloned())
|
THEMED_CACHE.with(|c| c.borrow().map.get(&(ptr as usize, len)).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the cache entry for `(ptr, len)`. Test-only helper so
|
/// Removes the cache entry for `(ptr, len)`. Test-only helper so
|
||||||
@@ -755,7 +787,16 @@ mod themed_cache_tests {
|
|||||||
/// run on the same thread).
|
/// run on the same thread).
|
||||||
fn themed_cache_remove_for_test(ptr: *const u8, len: usize) {
|
fn themed_cache_remove_for_test(ptr: *const u8, len: usize) {
|
||||||
THEMED_CACHE.with(|c| {
|
THEMED_CACHE.with(|c| {
|
||||||
c.borrow_mut().remove(&(ptr as usize, len));
|
let mut c = c.borrow_mut();
|
||||||
|
let key = (ptr as usize, len);
|
||||||
|
c.map.remove(&key);
|
||||||
|
for slot in &mut c.mru {
|
||||||
|
if let Some((k, _)) = slot {
|
||||||
|
if *k == key {
|
||||||
|
*slot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,107 @@
|
|||||||
use crate::app::settings::IsoPlane;
|
use crate::app::settings::IsoPlane;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::snap::{Snapper, ALL_SNAP_MODES};
|
use crate::snap::{SnapType, ALL_SNAP_MODES};
|
||||||
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
||||||
use iced::{Element, Fill};
|
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// Active tab in the Drafting Settings dialog.
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum DraftingSettingsTab {
|
||||||
|
#[default]
|
||||||
|
SnapAndGrid,
|
||||||
|
PolarTracking,
|
||||||
|
ObjectSnap,
|
||||||
|
ObjectSnap3D,
|
||||||
|
DynamicInput,
|
||||||
|
QuickProperties,
|
||||||
|
SelectionCycling,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Working buffer of drafting settings edited in the dialog.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DraftingSettingsState {
|
||||||
|
pub active_tab: DraftingSettingsTab,
|
||||||
|
// Snap and Grid
|
||||||
|
pub snap_on: bool,
|
||||||
|
pub grid_on: bool,
|
||||||
|
pub isometric: bool,
|
||||||
|
pub iso_plane: IsoPlane,
|
||||||
|
pub snap_angle_deg: f32,
|
||||||
|
// Polar Tracking
|
||||||
|
pub polar_on: bool,
|
||||||
|
pub ortho_on: bool,
|
||||||
|
pub polar_increment_deg: f32,
|
||||||
|
// Object Snap
|
||||||
|
pub osnap_on: bool,
|
||||||
|
pub otrack_on: bool,
|
||||||
|
pub snap_modes: rustc_hash::FxHashSet<SnapType>,
|
||||||
|
// 3D Object Snap
|
||||||
|
pub osnap3d_on: bool,
|
||||||
|
// Dynamic Input
|
||||||
|
pub dyn_input_on: bool,
|
||||||
|
// Quick Properties
|
||||||
|
pub quick_props_on: bool,
|
||||||
|
// Selection Cycling
|
||||||
|
pub selection_cycling_on: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DraftingSettingsState {
|
||||||
|
/// Check whether settings were modified compared to the saved snapshot.
|
||||||
|
/// Excludes `active_tab` so tab navigation does not mark the dialog dirty.
|
||||||
|
pub fn is_dirty(&self, saved: &Self) -> bool {
|
||||||
|
self.snap_on != saved.snap_on
|
||||||
|
|| self.grid_on != saved.grid_on
|
||||||
|
|| self.isometric != saved.isometric
|
||||||
|
|| self.iso_plane != saved.iso_plane
|
||||||
|
|| (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001
|
||||||
|
|| self.polar_on != saved.polar_on
|
||||||
|
|| self.ortho_on != saved.ortho_on
|
||||||
|
|| (self.polar_increment_deg - saved.polar_increment_deg).abs() > 0.001
|
||||||
|
|| self.osnap_on != saved.osnap_on
|
||||||
|
|| self.otrack_on != saved.otrack_on
|
||||||
|
|| self.snap_modes != saved.snap_modes
|
||||||
|
|| self.osnap3d_on != saved.osnap3d_on
|
||||||
|
|| self.dyn_input_on != saved.dyn_input_on
|
||||||
|
|| self.quick_props_on != saved.quick_props_on
|
||||||
|
|| self.selection_cycling_on != saved.selection_cycling_on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for grouped sub-panels with a light border and header title.
|
||||||
|
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
text(title.into()).size(11).style(|theme: &Theme| text::Style {
|
||||||
|
color: Some(theme.palette().background.strongest.text.scale_alpha(0.75)),
|
||||||
|
}),
|
||||||
|
body.into(),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
)
|
||||||
|
.padding([8, 10])
|
||||||
|
.width(Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
color: theme.palette().background.strong.color,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn view_window<'a>(
|
pub fn view_window<'a>(
|
||||||
snapper: &'a Snapper,
|
state: &'a DraftingSettingsState,
|
||||||
grid: bool,
|
dirty: bool,
|
||||||
grid_snap: bool,
|
close_confirm: bool,
|
||||||
ortho: bool,
|
|
||||||
polar: bool,
|
|
||||||
otrack: bool,
|
|
||||||
isometric: bool,
|
|
||||||
iso_plane: IsoPlane,
|
|
||||||
snap_angle_deg: f32,
|
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let toggle = |value: bool, label: std::borrow::Cow<'a, str>, message: Message| {
|
let toggle = |value: bool, label: Cow<'a, str>, message: Message| {
|
||||||
row![
|
row![
|
||||||
checkbox(value).on_toggle(move |_| message.clone()).size(15),
|
checkbox(value).on_toggle(move |_| message.clone()).size(15),
|
||||||
text(label).size(12),
|
text(label).size(12),
|
||||||
@@ -25,79 +110,247 @@ pub fn view_window<'a>(
|
|||||||
.align_y(iced::Center)
|
.align_y(iced::Center)
|
||||||
};
|
};
|
||||||
|
|
||||||
let drafting_modes = column![
|
// ── Tab Bar ──────────────────────────────────────────────────────────
|
||||||
text(crate::t!("Drafting modes")).size(15),
|
let tab_button = |tab: DraftingSettingsTab, label: Cow<'a, str>| {
|
||||||
Space::new().height(8),
|
let is_active = state.active_tab == tab;
|
||||||
toggle(grid, crate::t!("Grid display"), Message::ToggleGrid),
|
button(text(label).size(11.5))
|
||||||
toggle(grid_snap, crate::t!("Grid snap"), Message::ToggleGridSnap),
|
.on_press(Message::DraftingSettingsTabChanged(tab))
|
||||||
toggle(ortho, crate::t!("Ortho"), Message::ToggleOrtho),
|
.style(if is_active {
|
||||||
toggle(polar, crate::t!("Polar tracking"), Message::TogglePolar),
|
|
||||||
toggle(otrack, crate::t!("Object snap tracking"), Message::ToggleOTrack),
|
|
||||||
]
|
|
||||||
.spacing(6);
|
|
||||||
|
|
||||||
let mut planes = row![].spacing(5);
|
|
||||||
for plane in IsoPlane::ALL {
|
|
||||||
planes = planes.push(
|
|
||||||
button(text(crate::t!(plane.label())).size(11))
|
|
||||||
.on_press(Message::SetIsoPlane(plane))
|
|
||||||
.style(if isometric && iso_plane == plane {
|
|
||||||
button::primary
|
button::primary
|
||||||
} else {
|
} else {
|
||||||
button::secondary
|
button::secondary
|
||||||
})
|
})
|
||||||
.padding([5, 12]),
|
.padding([5, 9])
|
||||||
);
|
};
|
||||||
}
|
|
||||||
let isometric_controls = column![
|
let tab_bar = row![
|
||||||
text(crate::t!("Isometric drafting")).size(15),
|
tab_button(DraftingSettingsTab::SnapAndGrid, crate::t!("Snap and Grid")),
|
||||||
Space::new().height(8),
|
tab_button(DraftingSettingsTab::PolarTracking, crate::t!("Polar Tracking")),
|
||||||
toggle(
|
tab_button(DraftingSettingsTab::ObjectSnap, crate::t!("Object Snap")),
|
||||||
isometric,
|
tab_button(DraftingSettingsTab::ObjectSnap3D, crate::t!("3D Object Snap")),
|
||||||
crate::t!("Enable isometric drafting"),
|
tab_button(DraftingSettingsTab::DynamicInput, crate::t!("Dynamic Input")),
|
||||||
Message::ToggleIsometricDrafting,
|
tab_button(DraftingSettingsTab::QuickProperties, crate::t!("Quick Properties")),
|
||||||
),
|
tab_button(DraftingSettingsTab::SelectionCycling, crate::t!("Selection Cycling")),
|
||||||
planes,
|
|
||||||
text(crate::t!("F5 cycles Left, Top, and Right.")).size(11),
|
|
||||||
Space::new().height(6),
|
|
||||||
row![
|
|
||||||
text(crate::t!("Rotation: %{angle}°", angle = snap_angle_deg)).size(11),
|
|
||||||
button(text(crate::t!("Reset rotation")).size(10))
|
|
||||||
.on_press(Message::ResetDraftingRotation)
|
|
||||||
.style(button::secondary)
|
|
||||||
.padding([4, 10]),
|
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(4)
|
||||||
|
.wrap();
|
||||||
|
|
||||||
|
// ── Tab 1: Snap and Grid ─────────────────────────────────────────────
|
||||||
|
let snap_and_grid_view = {
|
||||||
|
// Left Column: Snap settings
|
||||||
|
let snap_on_toggle = toggle(
|
||||||
|
state.snap_on,
|
||||||
|
crate::t!("Snap On (F9)"),
|
||||||
|
Message::DraftingSettingsToggleSnap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let snap_spacing_group = group(
|
||||||
|
crate::t!("Snap spacing"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
text(crate::t!("Snap X spacing:")).size(11).width(120),
|
||||||
|
text("10.00").size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Snap Y spacing:")).size(11).width(120),
|
||||||
|
text("10.00").size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Equal X and Y spacing")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
]
|
]
|
||||||
.spacing(7);
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
let mut snap_modes = column![
|
let mut iso_planes = row![].spacing(4);
|
||||||
text(crate::t!("Object snap modes")).size(15),
|
for plane in IsoPlane::ALL {
|
||||||
Space::new().height(8),
|
iso_planes = iso_planes.push(
|
||||||
|
button(text(crate::t!(plane.label())).size(11))
|
||||||
|
.on_press(Message::DraftingSettingsSetIsoPlane(plane))
|
||||||
|
.style(if state.isometric && state.iso_plane == plane {
|
||||||
|
button::primary
|
||||||
|
} else {
|
||||||
|
button::secondary
|
||||||
|
})
|
||||||
|
.padding([4, 10]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let snap_type_group = group(
|
||||||
|
crate::t!("Snap type"),
|
||||||
|
column![
|
||||||
toggle(
|
toggle(
|
||||||
snapper.snap_enabled,
|
state.isometric,
|
||||||
crate::t!("Enable object snap"),
|
crate::t!("Enable isometric drafting"),
|
||||||
Message::ToggleSnapEnabled,
|
Message::DraftingSettingsToggleIsometric,
|
||||||
),
|
),
|
||||||
|
iso_planes,
|
||||||
|
text(crate::t!("F5 cycles Left, Top, and Right.")).size(10.5),
|
||||||
|
Space::new().height(4),
|
||||||
row![
|
row![
|
||||||
button(text(crate::t!("Select All")).size(10))
|
text(crate::t!("Rotation: %{angle}°", angle = state.snap_angle_deg)).size(11),
|
||||||
.on_press(Message::SnapSelectAll)
|
button(text(crate::t!("Reset rotation")).size(10))
|
||||||
|
.on_press(Message::DraftingSettingsResetRotation)
|
||||||
.style(button::secondary)
|
.style(button::secondary)
|
||||||
.padding([4, 10]),
|
.padding([3, 8]),
|
||||||
button(text(crate::t!("Clear All")).size(10))
|
]
|
||||||
.on_press(Message::SnapClearAll)
|
.spacing(8)
|
||||||
.style(button::secondary)
|
.align_y(iced::Center),
|
||||||
.padding([4, 10]),
|
|
||||||
]
|
]
|
||||||
.spacing(6),
|
.spacing(6),
|
||||||
]
|
);
|
||||||
.spacing(5);
|
|
||||||
for &(snap_type, _, label) in ALL_SNAP_MODES {
|
let left_col = column![snap_on_toggle, snap_spacing_group, snap_type_group]
|
||||||
snap_modes = snap_modes.push(
|
.spacing(10)
|
||||||
|
.width(Fill);
|
||||||
|
|
||||||
|
// Right Column: Grid settings
|
||||||
|
let grid_on_toggle = toggle(
|
||||||
|
state.grid_on,
|
||||||
|
crate::t!("Grid On (F7)"),
|
||||||
|
Message::DraftingSettingsToggleGrid,
|
||||||
|
);
|
||||||
|
|
||||||
|
let grid_spacing_group = group(
|
||||||
|
crate::t!("Grid spacing"),
|
||||||
|
column![
|
||||||
row![
|
row![
|
||||||
checkbox(snapper.is_on(snap_type))
|
text(crate::t!("Grid X spacing:")).size(11).width(120),
|
||||||
.on_toggle(move |_| Message::ToggleSnap(snap_type))
|
text("10.00").size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Grid Y spacing:")).size(11).width(120),
|
||||||
|
text("10.00").size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Major line every:")).size(11).width(120),
|
||||||
|
text("5").size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
let grid_behavior_group = group(
|
||||||
|
crate::t!("Grid behavior"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Adaptive grid")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Display grid beyond Limits")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
let right_col = column![grid_on_toggle, grid_spacing_group, grid_behavior_group]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill);
|
||||||
|
|
||||||
|
row![left_col, right_col].spacing(16).width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 2: Polar Tracking ────────────────────────────────────────────
|
||||||
|
let polar_tracking_view = {
|
||||||
|
let polar_on_toggle = toggle(
|
||||||
|
state.polar_on,
|
||||||
|
crate::t!("Polar Tracking On (F10)"),
|
||||||
|
Message::DraftingSettingsTogglePolar,
|
||||||
|
);
|
||||||
|
|
||||||
|
let ortho_on_toggle = toggle(
|
||||||
|
state.ortho_on,
|
||||||
|
crate::t!("Ortho mode (F8)"),
|
||||||
|
Message::DraftingSettingsToggleOrtho,
|
||||||
|
);
|
||||||
|
|
||||||
|
let polar_angle_group = group(
|
||||||
|
crate::t!("Polar Angle Settings"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
text(crate::t!("Increment angle:")).size(11).width(130),
|
||||||
|
text(format!("{:.1}°", state.polar_increment_deg)).size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
text(crate::t!(
|
||||||
|
"Polar Tracking guides cursor movement along specified angles. Ortho mode constrains movement to orthogonal axes."
|
||||||
|
))
|
||||||
|
.size(10.5)
|
||||||
|
.style(|theme: &Theme| text::Style {
|
||||||
|
color: Some(theme.palette().background.strongest.text.scale_alpha(0.7)),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![
|
||||||
|
polar_on_toggle,
|
||||||
|
ortho_on_toggle,
|
||||||
|
Space::new().height(4),
|
||||||
|
polar_angle_group,
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 3: Object Snap ───────────────────────────────────────────────
|
||||||
|
let object_snap_view = {
|
||||||
|
let osnap_on_toggle = toggle(
|
||||||
|
state.osnap_on,
|
||||||
|
crate::t!("Object Snap On (F3)"),
|
||||||
|
Message::DraftingSettingsToggleOsnap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let otrack_on_toggle = toggle(
|
||||||
|
state.otrack_on,
|
||||||
|
crate::t!("Object Snap Tracking On (F11)"),
|
||||||
|
Message::DraftingSettingsToggleOtrack,
|
||||||
|
);
|
||||||
|
|
||||||
|
let action_buttons = row![
|
||||||
|
button(text(crate::t!("Select All")).size(10.5))
|
||||||
|
.on_press(Message::DraftingSettingsSnapSelectAll)
|
||||||
|
.style(button::secondary)
|
||||||
|
.padding([4, 10]),
|
||||||
|
button(text(crate::t!("Clear All")).size(10.5))
|
||||||
|
.on_press(Message::DraftingSettingsSnapClearAll)
|
||||||
|
.style(button::secondary)
|
||||||
|
.padding([4, 10]),
|
||||||
|
]
|
||||||
|
.spacing(8);
|
||||||
|
|
||||||
|
// Two columns of snap modes:
|
||||||
|
let total = ALL_SNAP_MODES.len();
|
||||||
|
let mid = (total + 1) / 2;
|
||||||
|
let (first_half, second_half) = ALL_SNAP_MODES.split_at(mid);
|
||||||
|
|
||||||
|
let mut col1 = column![].spacing(6).width(Fill);
|
||||||
|
for &(snap_type, _, label) in first_half {
|
||||||
|
let is_checked = state.snap_modes.contains(&snap_type);
|
||||||
|
col1 = col1.push(
|
||||||
|
row![
|
||||||
|
checkbox(is_checked)
|
||||||
|
.on_toggle(move |_| Message::DraftingSettingsToggleSnapMode(snap_type))
|
||||||
.size(14),
|
.size(14),
|
||||||
text(crate::t!(label)).size(11),
|
text(crate::t!(label)).size(11),
|
||||||
]
|
]
|
||||||
@@ -106,30 +359,320 @@ pub fn view_window<'a>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let close = button(text(crate::tr!("action", "close")).size(12))
|
let mut col2 = column![].spacing(6).width(Fill);
|
||||||
.on_press(Message::CloseModal)
|
for &(snap_type, _, label) in second_half {
|
||||||
.padding([6, 18])
|
let is_checked = state.snap_modes.contains(&snap_type);
|
||||||
.style(button::secondary);
|
col2 = col2.push(
|
||||||
let content = column![
|
row![
|
||||||
drafting_modes,
|
checkbox(is_checked)
|
||||||
Space::new().height(20),
|
.on_toggle(move |_| Message::DraftingSettingsToggleSnapMode(snap_type))
|
||||||
isometric_controls,
|
.size(14),
|
||||||
Space::new().height(20),
|
text(crate::t!(label)).size(11),
|
||||||
snap_modes,
|
|
||||||
]
|
]
|
||||||
.width(sizing.width);
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let snap_modes_group = group(
|
||||||
|
crate::t!("Object Snap modes"),
|
||||||
|
row![col1, col2].spacing(20).width(Fill),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![
|
||||||
|
row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle]
|
||||||
|
.align_y(iced::Center),
|
||||||
|
action_buttons,
|
||||||
|
Space::new().height(4),
|
||||||
|
snap_modes_group,
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 4: 3D Object Snap ────────────────────────────────────────────
|
||||||
|
let object_snap_3d_view = {
|
||||||
|
let osnap3d_toggle = toggle(
|
||||||
|
state.osnap3d_on,
|
||||||
|
crate::t!("3D Object Snap On (F4)"),
|
||||||
|
Message::DraftingSettingsToggle3dOsnap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let placeholder_group = group(
|
||||||
|
crate::t!("3D Object Snap modes"),
|
||||||
|
column![
|
||||||
|
row![checkbox(false).size(14), text(crate::t!("Vertex")).size(11)]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(false).size(14),
|
||||||
|
text(crate::t!("Midpoint on edge")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(false).size(14),
|
||||||
|
text(crate::t!("Center of face")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![checkbox(false).size(14), text(crate::t!("Knot")).size(11)]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(false).size(14),
|
||||||
|
text(crate::t!("Perpendicular to face")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(false).size(14),
|
||||||
|
text(crate::t!("Nearest to face")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![osnap3d_toggle, Space::new().height(4), placeholder_group]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 5: Dynamic Input ─────────────────────────────────────────────
|
||||||
|
let dynamic_input_view = {
|
||||||
|
let dyn_input_toggle = toggle(
|
||||||
|
state.dyn_input_on,
|
||||||
|
crate::t!("Enable Pointer Input (F12)"),
|
||||||
|
Message::DraftingSettingsToggleDynInput,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pointer_group = group(
|
||||||
|
crate::t!("Pointer Input"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Display coordinate input near crosshairs")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Enable dimension input fields")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
let prompt_group = group(
|
||||||
|
crate::t!("Dynamic Prompts"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Show command prompting and command input near crosshairs"))
|
||||||
|
.size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![
|
||||||
|
dyn_input_toggle,
|
||||||
|
Space::new().height(4),
|
||||||
|
pointer_group,
|
||||||
|
prompt_group,
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 6: Quick Properties ──────────────────────────────────────────
|
||||||
|
let quick_properties_view = {
|
||||||
|
let quick_props_toggle = toggle(
|
||||||
|
state.quick_props_on,
|
||||||
|
crate::t!("Display Quick Properties palette on selection"),
|
||||||
|
Message::DraftingSettingsToggleQuickProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
let palette_group = group(
|
||||||
|
crate::t!("Palette Location"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Cursor-dependent position")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(false).size(14),
|
||||||
|
text(crate::t!("Static quadrant location")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![
|
||||||
|
quick_props_toggle,
|
||||||
|
Space::new().height(4),
|
||||||
|
palette_group,
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tab 7: Selection Cycling ─────────────────────────────────────────
|
||||||
|
let selection_cycling_view = {
|
||||||
|
let sel_cycling_toggle = toggle(
|
||||||
|
state.selection_cycling_on,
|
||||||
|
crate::t!("Allow selection cycling"),
|
||||||
|
Message::DraftingSettingsToggleSelCycling,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cycling_group = group(
|
||||||
|
crate::t!("Display Selection Cycling List Box"),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Show cycling badge when objects overlap")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
row![
|
||||||
|
checkbox(true).size(14),
|
||||||
|
text(crate::t!("Display selection candidate list box on click")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
|
||||||
|
column![
|
||||||
|
sel_cycling_toggle,
|
||||||
|
Space::new().height(4),
|
||||||
|
cycling_group,
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Fill)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Active Tab Content ───────────────────────────────────────────────
|
||||||
|
let tab_content: Element<'a, Message> = match state.active_tab {
|
||||||
|
DraftingSettingsTab::SnapAndGrid => snap_and_grid_view.into(),
|
||||||
|
DraftingSettingsTab::PolarTracking => polar_tracking_view.into(),
|
||||||
|
DraftingSettingsTab::ObjectSnap => object_snap_view.into(),
|
||||||
|
DraftingSettingsTab::ObjectSnap3D => object_snap_3d_view.into(),
|
||||||
|
DraftingSettingsTab::DynamicInput => dynamic_input_view.into(),
|
||||||
|
DraftingSettingsTab::QuickProperties => quick_properties_view.into(),
|
||||||
|
DraftingSettingsTab::SelectionCycling => selection_cycling_view.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Bottom Action Buttons ────────────────────────────────────────────
|
||||||
|
let ok_button = button(text(crate::t!("OK")).size(12))
|
||||||
|
.on_press(Message::DraftingSettingsOk)
|
||||||
|
.style(button::primary)
|
||||||
|
.padding([6, 18]);
|
||||||
|
|
||||||
|
let apply_button = button(text(crate::t!("Apply")).size(12))
|
||||||
|
.on_press_maybe(if dirty {
|
||||||
|
Some(Message::DraftingSettingsApply)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.style(if dirty {
|
||||||
|
button::secondary
|
||||||
|
} else {
|
||||||
|
button::text
|
||||||
|
})
|
||||||
|
.padding([6, 18]);
|
||||||
|
|
||||||
|
let close_button = button(text(crate::t!("Close")).size(12))
|
||||||
|
.on_press(Message::DraftingSettingsClose)
|
||||||
|
.style(button::secondary)
|
||||||
|
.padding([6, 18]);
|
||||||
|
|
||||||
|
let action_row = row![
|
||||||
|
Space::new().width(Fill),
|
||||||
|
ok_button,
|
||||||
|
apply_button,
|
||||||
|
close_button,
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center);
|
||||||
|
|
||||||
|
// ── Body Layout ──────────────────────────────────────────────────────
|
||||||
let body = column![
|
let body = column![
|
||||||
scrollable(content).spacing(8).height(sizing.height),
|
tab_bar,
|
||||||
|
Space::new().height(10),
|
||||||
|
scrollable(tab_content).spacing(6).height(Fill),
|
||||||
Space::new().height(12),
|
Space::new().height(12),
|
||||||
row![Space::new().width(Fill), close],
|
action_row,
|
||||||
]
|
]
|
||||||
.width(sizing.width)
|
.width(sizing.width)
|
||||||
.height(sizing.height);
|
.height(sizing.height);
|
||||||
|
|
||||||
container(body)
|
let main_content = container(body)
|
||||||
.style(container::rounded_box)
|
.style(container::rounded_box)
|
||||||
.padding([16, 18])
|
.padding([14, 16])
|
||||||
.width(sizing.width)
|
.width(sizing.width)
|
||||||
.height(sizing.height)
|
.height(sizing.height);
|
||||||
|
|
||||||
|
// ── Unsaved Changes Guard ────────────────────────────────────────────
|
||||||
|
if !close_confirm {
|
||||||
|
return main_content.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let shield = iced::widget::mouse_area(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme
|
||||||
|
.palette()
|
||||||
|
.background
|
||||||
|
.strongest
|
||||||
|
.color
|
||||||
|
.scale_alpha(0.55),
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_press(Message::DraftingSettingsCloseKeep);
|
||||||
|
|
||||||
|
let warning_panel = container(
|
||||||
|
column![
|
||||||
|
text(crate::t!("Unsaved changes will be discarded.")).size(13.5),
|
||||||
|
Space::new().height(12),
|
||||||
|
row![
|
||||||
|
button(text(crate::t!("Discard && close")).size(12))
|
||||||
|
.on_press(Message::DraftingSettingsCloseDiscard)
|
||||||
|
.padding([5, 14])
|
||||||
|
.style(button::danger),
|
||||||
|
button(text(crate::t!("Keep editing")).size(12))
|
||||||
|
.on_press(Message::DraftingSettingsCloseKeep)
|
||||||
|
.padding([5, 14])
|
||||||
|
.style(button::secondary),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
]
|
||||||
|
.spacing(0),
|
||||||
|
)
|
||||||
|
.padding([18, 22])
|
||||||
|
.style(container::rounded_box);
|
||||||
|
|
||||||
|
iced::widget::stack![
|
||||||
|
main_content,
|
||||||
|
shield,
|
||||||
|
container(warning_panel).center_x(Fill).center_y(Fill)
|
||||||
|
]
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user