feat(cursor): add crosshair options
Add persistent cursor sizing, pick aperture, color, pointer mode, isometric drafting planes, and SNAPANG-aligned grid behavior.\n\nRefs #413
This commit is contained in:
parent
188ac80b2e
commit
1738f9a79b
25 changed files with 989 additions and 126 deletions
|
|
@ -224,6 +224,43 @@ impl OpenCADStudio {
|
||||||
"SNAP" => {
|
"SNAP" => {
|
||||||
return Some(Task::done(Message::ToggleGridSnap));
|
return Some(Task::done(Message::ToggleGridSnap));
|
||||||
}
|
}
|
||||||
|
// ISOPLANE — cycle the isometric drafting axis pair (F5).
|
||||||
|
"ISOPLANE" => {
|
||||||
|
return Some(Task::done(Message::CycleIsoPlane));
|
||||||
|
}
|
||||||
|
cmd if cmd.starts_with("ISOPLANE ") => {
|
||||||
|
let plane = match cmd.trim_start_matches("ISOPLANE").trim() {
|
||||||
|
"LEFT" | "L" => Some(crate::app::settings::IsoPlane::Left),
|
||||||
|
"TOP" | "T" => Some(crate::app::settings::IsoPlane::Top),
|
||||||
|
"RIGHT" | "R" => Some(crate::app::settings::IsoPlane::Right),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(plane) = plane {
|
||||||
|
return Some(Task::done(Message::SetIsoPlane(plane)));
|
||||||
|
}
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("ISOPLANE: expected Left, Top, or Right.").as_ref());
|
||||||
|
}
|
||||||
|
// ISODRAFT — enable or disable isometric drafting.
|
||||||
|
"ISODRAFT" => {
|
||||||
|
return Some(Task::done(Message::ToggleIsometricDrafting));
|
||||||
|
}
|
||||||
|
cmd if cmd.starts_with("ISODRAFT ") => {
|
||||||
|
let requested = match cmd.trim_start_matches("ISODRAFT").trim() {
|
||||||
|
"1" | "ON" => Some(true),
|
||||||
|
"0" | "OFF" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
match requested {
|
||||||
|
Some(value) if value != self.isometric_drafting => {
|
||||||
|
return Some(Task::done(Message::ToggleIsometricDrafting));
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => self
|
||||||
|
.command_line
|
||||||
|
.push_error(crate::t!("ISODRAFT: expected On or Off.").as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
// POLAR — toggle polar tracking.
|
// POLAR — toggle polar tracking.
|
||||||
"POLAR" => {
|
"POLAR" => {
|
||||||
return Some(Task::done(Message::TogglePolar));
|
return Some(Task::done(Message::TogglePolar));
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||||
"CUI",
|
"CUI",
|
||||||
"DSETTINGS",
|
"DSETTINGS",
|
||||||
"GRID",
|
"GRID",
|
||||||
|
"ISODRAFT",
|
||||||
|
"ISOPLANE",
|
||||||
"OSNAP",
|
"OSNAP",
|
||||||
"POLAR",
|
"POLAR",
|
||||||
"QUICKPROPERTIES",
|
"QUICKPROPERTIES",
|
||||||
|
|
@ -448,6 +450,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||||
"MIRRTEXT",
|
"MIRRTEXT",
|
||||||
"ZOOMWHEEL",
|
"ZOOMWHEEL",
|
||||||
"ZOOMFACTOR",
|
"ZOOMFACTOR",
|
||||||
|
"CURSORSIZE",
|
||||||
|
"PICKBOX",
|
||||||
|
"CURSORTYPE",
|
||||||
|
"SNAPANG",
|
||||||
"ATTREQ",
|
"ATTREQ",
|
||||||
"ATTDIA",
|
"ATTDIA",
|
||||||
"DIMASSOC",
|
"DIMASSOC",
|
||||||
|
|
|
||||||
|
|
@ -868,6 +868,10 @@ impl OpenCADStudio {
|
||||||
"MIRRTEXT"
|
"MIRRTEXT"
|
||||||
| "ZOOMWHEEL"
|
| "ZOOMWHEEL"
|
||||||
| "ZOOMFACTOR"
|
| "ZOOMFACTOR"
|
||||||
|
| "CURSORSIZE"
|
||||||
|
| "PICKBOX"
|
||||||
|
| "CURSORTYPE"
|
||||||
|
| "SNAPANG"
|
||||||
| "TEXTFILL"
|
| "TEXTFILL"
|
||||||
| "ATTREQ"
|
| "ATTREQ"
|
||||||
| "ATTDIA"
|
| "ATTDIA"
|
||||||
|
|
@ -938,7 +942,7 @@ impl OpenCADStudio {
|
||||||
let value = it.next().map(|s| s.trim().to_string());
|
let value = it.next().map(|s| s.trim().to_string());
|
||||||
if name.is_empty() || name == "?" {
|
if name.is_empty() || name == "?" {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Parse a boolean given as 0/1 or ON/OFF.
|
// Parse a boolean given as 0/1 or ON/OFF.
|
||||||
|
|
@ -1061,6 +1065,56 @@ impl OpenCADStudio {
|
||||||
Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false))
|
Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CURSORSIZE" => match &value {
|
||||||
|
Some(v) => match v.parse::<i32>() {
|
||||||
|
Ok(size) if (1..=100).contains(&size) => {
|
||||||
|
self.cursor_size = size;
|
||||||
|
Ok((format!("CURSORSIZE = {size}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 1 to 100 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("CURSORSIZE = {}", self.cursor_size), false)),
|
||||||
|
},
|
||||||
|
"PICKBOX" => match &value {
|
||||||
|
Some(v) => match v.parse::<i32>() {
|
||||||
|
Ok(size) if (0..=50).contains(&size) => {
|
||||||
|
self.pick_box = size;
|
||||||
|
Ok((format!("PICKBOX = {size}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 to 50 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("PICKBOX = {}", self.pick_box), false)),
|
||||||
|
},
|
||||||
|
"CURSORTYPE" => match &value {
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
"0" => {
|
||||||
|
self.cursor_type = crate::app::settings::CursorType::Crosshair;
|
||||||
|
Ok(("CURSORTYPE = 0".to_string(), true))
|
||||||
|
}
|
||||||
|
"1" => {
|
||||||
|
self.cursor_type = crate::app::settings::CursorType::Pointer;
|
||||||
|
Ok(("CURSORTYPE = 1".to_string(), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((
|
||||||
|
format!(
|
||||||
|
"CURSORTYPE = {}",
|
||||||
|
i32::from(self.cursor_type == crate::app::settings::CursorType::Pointer)
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
"SNAPANG" => match &value {
|
||||||
|
Some(v) => match v.parse::<f32>() {
|
||||||
|
Ok(angle) if angle.is_finite() => {
|
||||||
|
self.snap_angle_deg = angle.rem_euclid(360.0);
|
||||||
|
Ok((format!("SNAPANG = {}", self.snap_angle_deg), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: finite numeric value required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SNAPANG = {}", self.snap_angle_deg), false)),
|
||||||
|
},
|
||||||
// Global (not stored in the drawing): fill vs. hollow
|
// Global (not stored in the drawing): fill vs. hollow
|
||||||
// TrueType text. The active tab re-tessellates below.
|
// TrueType text. The active tab re-tessellates below.
|
||||||
"TEXTFILL" => match &value {
|
"TEXTFILL" => match &value {
|
||||||
|
|
@ -1612,7 +1666,15 @@ impl OpenCADStudio {
|
||||||
match outcome {
|
match outcome {
|
||||||
Ok((msg, changed)) => {
|
Ok((msg, changed)) => {
|
||||||
if changed {
|
if changed {
|
||||||
if matches!(name.as_str(), "ZOOMWHEEL" | "ZOOMFACTOR") {
|
if matches!(
|
||||||
|
name.as_str(),
|
||||||
|
"ZOOMWHEEL"
|
||||||
|
| "ZOOMFACTOR"
|
||||||
|
| "CURSORSIZE"
|
||||||
|
| "PICKBOX"
|
||||||
|
| "CURSORTYPE"
|
||||||
|
| "SNAPANG"
|
||||||
|
) {
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
|
|
||||||
|
|
@ -209,11 +209,11 @@ fn rgb_to_color(rgb: [u8; 3]) -> iced::Color {
|
||||||
iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2])
|
iced::Color::from_rgb8(rgb[0], rgb[1], rgb[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rgb_to_hex(rgb: [u8; 3]) -> String {
|
pub(crate) fn rgb_to_hex(rgb: [u8; 3]) -> String {
|
||||||
format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
|
format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_hex(value: &str) -> Option<[u8; 3]> {
|
pub(crate) fn parse_hex(value: &str) -> Option<[u8; 3]> {
|
||||||
let value = value.trim().strip_prefix('#').unwrap_or(value.trim());
|
let value = value.trim().strip_prefix('#').unwrap_or(value.trim());
|
||||||
if value.len() != 6 {
|
if value.len() != 6 {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
||||||
|
|
@ -260,19 +260,56 @@ pub(super) fn ucs_rotated_z(origin: glam::DVec3, angle_z: f32) -> Ucs {
|
||||||
|
|
||||||
// ── Drawing constraint helpers ─────────────────────────────────────────────
|
// ── Drawing constraint helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS
|
/// The two live drafting directions in degrees inside the active UCS plane.
|
||||||
/// plane — ortho follows the user's coordinate system, not world axes. `xf` is
|
pub(super) fn drafting_angles(
|
||||||
/// identity for plain WCS, so the world-XY behaviour is unchanged there.
|
isometric: bool,
|
||||||
pub(super) fn ortho_constrain(pt: glam::DVec3, base: glam::DVec3, xf: &UcsXform) -> glam::DVec3 {
|
iso_plane: super::settings::IsoPlane,
|
||||||
|
snap_angle_deg: f32,
|
||||||
|
) -> [f64; 2] {
|
||||||
|
let base = if isometric {
|
||||||
|
iso_plane.angles()
|
||||||
|
} else {
|
||||||
|
[0.0, 90.0]
|
||||||
|
};
|
||||||
|
base.map(|angle| angle + snap_angle_deg as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the live drafting directions into world-space axes.
|
||||||
|
pub(super) fn drafting_axes(
|
||||||
|
x: glam::DVec3,
|
||||||
|
y: glam::DVec3,
|
||||||
|
z: glam::DVec3,
|
||||||
|
isometric: bool,
|
||||||
|
iso_plane: super::settings::IsoPlane,
|
||||||
|
snap_angle_deg: f32,
|
||||||
|
) -> (glam::DVec3, glam::DVec3, glam::DVec3) {
|
||||||
|
let [a, b] = drafting_angles(isometric, iso_plane, snap_angle_deg);
|
||||||
|
let direction = |degrees: f64| {
|
||||||
|
let radians = degrees.to_radians();
|
||||||
|
(x * radians.cos() + y * radians.sin()).normalize_or(x)
|
||||||
|
};
|
||||||
|
(direction(a), direction(b), z.normalize_or(x.cross(y)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constrain `pt` to the nearest live drafting direction from `base`.
|
||||||
|
pub(super) fn drafting_constrain(
|
||||||
|
pt: glam::DVec3,
|
||||||
|
base: glam::DVec3,
|
||||||
|
xf: &UcsXform,
|
||||||
|
isometric: bool,
|
||||||
|
iso_plane: super::settings::IsoPlane,
|
||||||
|
snap_angle_deg: f32,
|
||||||
|
) -> glam::DVec3 {
|
||||||
let p = xf.to_ucs(pt);
|
let p = xf.to_ucs(pt);
|
||||||
let b = xf.to_ucs(base);
|
let b = xf.to_ucs(base);
|
||||||
let dx = (p.x - b.x).abs();
|
let delta = glam::DVec2::new(p.x - b.x, p.y - b.y);
|
||||||
let dy = (p.y - b.y).abs();
|
let [a, c] = drafting_angles(isometric, iso_plane, snap_angle_deg).map(|degrees| {
|
||||||
let c = if dx >= dy {
|
let radians = degrees.to_radians();
|
||||||
glam::DVec3::new(p.x, b.y, p.z)
|
glam::DVec2::new(radians.cos(), radians.sin())
|
||||||
} else {
|
});
|
||||||
glam::DVec3::new(b.x, p.y, p.z)
|
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() { a } else { c };
|
||||||
};
|
let projected = direction * delta.dot(direction);
|
||||||
|
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
||||||
xf.to_wcs(c)
|
xf.to_wcs(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,6 +378,9 @@ pub(super) fn axis_lock_capture(
|
||||||
polar: bool,
|
polar: bool,
|
||||||
step_deg: f32,
|
step_deg: f32,
|
||||||
xf: &UcsXform,
|
xf: &UcsXform,
|
||||||
|
isometric: bool,
|
||||||
|
iso_plane: super::settings::IsoPlane,
|
||||||
|
snap_angle_deg: f32,
|
||||||
) -> Option<glam::DVec3> {
|
) -> Option<glam::DVec3> {
|
||||||
let p = xf.to_ucs(cursor);
|
let p = xf.to_ucs(cursor);
|
||||||
let b = xf.to_ucs(base);
|
let b = xf.to_ucs(base);
|
||||||
|
|
@ -349,8 +389,20 @@ pub(super) fn axis_lock_capture(
|
||||||
if dx.hypot(dy) < 1e-9 {
|
if dx.hypot(dy) < 1e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let step = (if polar { step_deg as f64 } else { 90.0 }).to_radians();
|
let ang = if polar {
|
||||||
let ang = (dy.atan2(dx) / step).round() * step;
|
let base = (snap_angle_deg as f64).to_radians();
|
||||||
|
let step = (step_deg as f64).to_radians();
|
||||||
|
((dy.atan2(dx) - base) / step).round() * step + base
|
||||||
|
} else {
|
||||||
|
let delta = glam::DVec2::new(dx, dy);
|
||||||
|
let [a, b] = drafting_angles(isometric, iso_plane, snap_angle_deg).map(|degrees| {
|
||||||
|
let radians = degrees.to_radians();
|
||||||
|
glam::DVec2::new(radians.cos(), radians.sin())
|
||||||
|
});
|
||||||
|
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() { a } else { b };
|
||||||
|
let direction = if delta.dot(direction) < 0.0 { -direction } else { direction };
|
||||||
|
direction.y.atan2(direction.x)
|
||||||
|
};
|
||||||
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
||||||
let dir = xf.to_wcs(b + dir_ucs) - xf.to_wcs(b);
|
let dir = xf.to_wcs(b + dir_ucs) - xf.to_wcs(b);
|
||||||
(dir.length_squared() > 1e-12).then(|| dir.normalize())
|
(dir.length_squared() > 1e-12).then(|| dir.normalize())
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ mod mtext_editor;
|
||||||
pub mod plugin_host;
|
pub mod plugin_host;
|
||||||
mod properties;
|
mod properties;
|
||||||
mod recent;
|
mod recent;
|
||||||
mod settings;
|
pub(crate) mod settings;
|
||||||
mod shortcuts;
|
mod shortcuts;
|
||||||
mod style_ops;
|
mod style_ops;
|
||||||
mod text_inline;
|
mod text_inline;
|
||||||
|
|
@ -448,10 +448,27 @@ pub(super) struct OpenCADStudio {
|
||||||
zoom_wheel_reversed: bool,
|
zoom_wheel_reversed: bool,
|
||||||
/// Mouse-wheel zoom sensitivity, clamped to 3..=100 (ZOOMFACTOR).
|
/// Mouse-wheel zoom sensitivity, clamped to 3..=100 (ZOOMFACTOR).
|
||||||
zoom_factor: i32,
|
zoom_factor: i32,
|
||||||
|
/// Crosshair reach as a viewport percentage (CURSORSIZE, 1..=100).
|
||||||
|
cursor_size: i32,
|
||||||
|
/// Selection-box half-size and click aperture in pixels (PICKBOX, 0..=50).
|
||||||
|
pick_box: i32,
|
||||||
|
/// Drawing viewport cursor style (CURSORTYPE).
|
||||||
|
cursor_type: settings::CursorType,
|
||||||
|
/// Explicit crosshair colour; `None` retains automatic contrast.
|
||||||
|
crosshair_color: Option<[u8; 3]>,
|
||||||
|
/// Editable Options buffer for the crosshair colour.
|
||||||
|
crosshair_color_input: String,
|
||||||
|
/// Isometric drafting state and active axis pair.
|
||||||
|
isometric_drafting: bool,
|
||||||
|
iso_plane: settings::IsoPlane,
|
||||||
|
/// Drafting-grid/crosshair rotation in degrees (SNAPANG).
|
||||||
|
snap_angle_deg: f32,
|
||||||
/// Show grid lines in the viewport (F7).
|
/// Show grid lines in the viewport (F7).
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
||||||
dyn_input: bool,
|
dyn_input: bool,
|
||||||
|
/// Currently visible page in the application Options dialog.
|
||||||
|
options_tab: crate::ui::window::options::OptionsTab,
|
||||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
/// When true (default), saving over an existing file first writes a `.bak`
|
/// When true (default), saving over an existing file first writes a `.bak`
|
||||||
|
|
@ -1502,6 +1519,7 @@ pub enum ModalKind {
|
||||||
LayerStateManager,
|
LayerStateManager,
|
||||||
LayerTranslator,
|
LayerTranslator,
|
||||||
DrawingUnits,
|
DrawingUnits,
|
||||||
|
DraftingSettings,
|
||||||
LayerStateEditor,
|
LayerStateEditor,
|
||||||
Plot,
|
Plot,
|
||||||
PrintAll,
|
PrintAll,
|
||||||
|
|
@ -1768,6 +1786,16 @@ pub enum Message {
|
||||||
SaveDialogPathPicked(Option<std::path::PathBuf>),
|
SaveDialogPathPicked(Option<std::path::PathBuf>),
|
||||||
/// Open the application-wide Options dialog.
|
/// Open the application-wide Options dialog.
|
||||||
OptionsOpen,
|
OptionsOpen,
|
||||||
|
/// Switch the visible page in Options.
|
||||||
|
OptionsTabChanged(crate::ui::window::options::OptionsTab),
|
||||||
|
/// Set CURSORSIZE from the Display-page slider.
|
||||||
|
CursorSizeChanged(i32),
|
||||||
|
/// Set PICKBOX from the Selection-page slider.
|
||||||
|
PickBoxChanged(i32),
|
||||||
|
/// Set CURSORTYPE from Options.
|
||||||
|
CursorTypeChanged(settings::CursorType),
|
||||||
|
/// Edit the optional crosshair RGB value; blank restores automatic contrast.
|
||||||
|
CrosshairColorChanged(String),
|
||||||
/// Set the default type/version used when first saving a new drawing.
|
/// Set the default type/version used when first saving a new drawing.
|
||||||
DefaultSaveFormatChanged(String),
|
DefaultSaveFormatChanged(String),
|
||||||
/// Select one of Iced's built-in themes or the editable Custom theme.
|
/// Select one of Iced's built-in themes or the editable Custom theme.
|
||||||
|
|
@ -2094,6 +2122,14 @@ pub enum Message {
|
||||||
ToggleSnapEnabled,
|
ToggleSnapEnabled,
|
||||||
/// Toggle grid-snap on/off — F9 / SNAP status-bar button.
|
/// Toggle grid-snap on/off — F9 / SNAP status-bar button.
|
||||||
ToggleGridSnap,
|
ToggleGridSnap,
|
||||||
|
/// Enable or disable isometric drafting.
|
||||||
|
ToggleIsometricDrafting,
|
||||||
|
/// Select one isometric drafting axis pair.
|
||||||
|
SetIsoPlane(settings::IsoPlane),
|
||||||
|
/// Advance Left → Top → Right, enabling isometric drafting if necessary.
|
||||||
|
CycleIsoPlane,
|
||||||
|
/// Reset SNAPANG and return the active drafting coordinate system to World.
|
||||||
|
ResetDraftingRotation,
|
||||||
/// Toggle the ViewCube 3D gizmo visibility (NAVVCUBE).
|
/// Toggle the ViewCube 3D gizmo visibility (NAVVCUBE).
|
||||||
ToggleViewCube,
|
ToggleViewCube,
|
||||||
/// Toggle the Properties panel visibility (PROPERTIES).
|
/// Toggle the Properties panel visibility (PROPERTIES).
|
||||||
|
|
@ -3036,8 +3072,17 @@ impl OpenCADStudio {
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
zoom_wheel_reversed: false,
|
zoom_wheel_reversed: false,
|
||||||
zoom_factor: 60,
|
zoom_factor: 60,
|
||||||
|
cursor_size: 5,
|
||||||
|
pick_box: 3,
|
||||||
|
cursor_type: settings::CursorType::Crosshair,
|
||||||
|
crosshair_color: None,
|
||||||
|
crosshair_color_input: String::new(),
|
||||||
|
isometric_drafting: false,
|
||||||
|
iso_plane: settings::IsoPlane::Left,
|
||||||
|
snap_angle_deg: 0.0,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
|
options_tab: crate::ui::window::options::OptionsTab::General,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
backup_on_save: true,
|
backup_on_save: true,
|
||||||
file_assoc_enabled: true,
|
file_assoc_enabled: true,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,63 @@
|
||||||
use crate::snap::SnapType;
|
use crate::snap::SnapType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Cursor shown over the drawing viewport.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum CursorType {
|
||||||
|
#[default]
|
||||||
|
Crosshair,
|
||||||
|
Pointer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorType {
|
||||||
|
pub const ALL: [Self; 2] = [Self::Crosshair, Self::Pointer];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Crosshair => "Crosshair",
|
||||||
|
Self::Pointer => "Desktop pointer",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Active pair of axes while isometric drafting is enabled.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum IsoPlane {
|
||||||
|
#[default]
|
||||||
|
Left,
|
||||||
|
Top,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsoPlane {
|
||||||
|
pub const ALL: [Self; 3] = [Self::Left, Self::Top, Self::Right];
|
||||||
|
|
||||||
|
pub fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Left => Self::Top,
|
||||||
|
Self::Top => Self::Right,
|
||||||
|
Self::Right => Self::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Left => "Left",
|
||||||
|
Self::Top => "Top",
|
||||||
|
Self::Right => "Right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two drafting directions, in degrees in the active UCS plane.
|
||||||
|
pub fn angles(self) -> [f64; 2] {
|
||||||
|
match self {
|
||||||
|
Self::Left => [90.0, 150.0],
|
||||||
|
Self::Top => [30.0, 150.0],
|
||||||
|
Self::Right => [30.0, 90.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Canonical order of the user-toggleable object-snap modes. Drives the
|
/// Canonical order of the user-toggleable object-snap modes. Drives the
|
||||||
/// deterministic order when decoding the `$OSMODE` bitmask (see
|
/// deterministic order when decoding the `$OSMODE` bitmask (see
|
||||||
/// [`snaps_from_osmode`]).
|
/// [`snaps_from_osmode`]).
|
||||||
|
|
@ -99,6 +156,19 @@ pub struct UserSettings {
|
||||||
pub polar_increment_deg: f32,
|
pub polar_increment_deg: f32,
|
||||||
pub zoom_wheel_reversed: bool,
|
pub zoom_wheel_reversed: bool,
|
||||||
pub zoom_factor: i32,
|
pub zoom_factor: i32,
|
||||||
|
/// CURSORSIZE: crosshair arm reach as a percentage of the viewport.
|
||||||
|
pub cursor_size: i32,
|
||||||
|
/// PICKBOX: visible selection-box half-size and click aperture in pixels.
|
||||||
|
pub pick_box: i32,
|
||||||
|
/// CURSORTYPE: crosshair or the platform pointer over the drawing.
|
||||||
|
pub cursor_type: CursorType,
|
||||||
|
/// Explicit crosshair RGB. `None` keeps automatic background contrast.
|
||||||
|
pub crosshair_color: Option<[u8; 3]>,
|
||||||
|
/// Isometric drafting changes the grid and crosshair to the active axis pair.
|
||||||
|
pub isometric_drafting: bool,
|
||||||
|
pub iso_plane: IsoPlane,
|
||||||
|
/// SNAPANG in degrees, applied in the active UCS plane.
|
||||||
|
pub snap_angle_deg: f32,
|
||||||
pub otrack: bool,
|
pub otrack: bool,
|
||||||
// Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
|
// Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
|
||||||
// stored in the document header, not here (they used to be persisted app-
|
// stored in the document header, not here (they used to be persisted app-
|
||||||
|
|
@ -170,6 +240,13 @@ impl Default for UserSettings {
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
zoom_wheel_reversed: false,
|
zoom_wheel_reversed: false,
|
||||||
zoom_factor: 60,
|
zoom_factor: 60,
|
||||||
|
cursor_size: 5,
|
||||||
|
pick_box: 3,
|
||||||
|
cursor_type: CursorType::Crosshair,
|
||||||
|
crosshair_color: None,
|
||||||
|
isometric_drafting: false,
|
||||||
|
iso_plane: IsoPlane::Left,
|
||||||
|
snap_angle_deg: 0.0,
|
||||||
otrack: false,
|
otrack: false,
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
disabled_plugins: Vec::new(),
|
disabled_plugins: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub(super) fn default_bindings() -> BTreeMap<String, String> {
|
||||||
("F1".to_string(), "HELP"),
|
("F1".to_string(), "HELP"),
|
||||||
("F2".to_string(), "COMMANDHISTORY"),
|
("F2".to_string(), "COMMANDHISTORY"),
|
||||||
("F3".to_string(), "TOGGLEOSNAP"),
|
("F3".to_string(), "TOGGLEOSNAP"),
|
||||||
|
("F5".to_string(), "ISOPLANE"),
|
||||||
("F7".to_string(), "GRID"),
|
("F7".to_string(), "GRID"),
|
||||||
("F8".to_string(), "ORTHO"),
|
("F8".to_string(), "ORTHO"),
|
||||||
("F9".to_string(), "SNAP"),
|
("F9".to_string(), "SNAP"),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use super::util::*;
|
||||||
use crate::ui::window::block_palette::BlockPaletteMsg;
|
use crate::ui::window::block_palette::BlockPaletteMsg;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
@ -267,6 +267,13 @@ impl OpenCADStudio {
|
||||||
polar_increment_deg: self.polar_increment_deg,
|
polar_increment_deg: self.polar_increment_deg,
|
||||||
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||||
zoom_factor: self.zoom_factor,
|
zoom_factor: self.zoom_factor,
|
||||||
|
cursor_size: self.cursor_size,
|
||||||
|
pick_box: self.pick_box,
|
||||||
|
cursor_type: self.cursor_type,
|
||||||
|
crosshair_color: self.crosshair_color,
|
||||||
|
isometric_drafting: self.isometric_drafting,
|
||||||
|
iso_plane: self.iso_plane,
|
||||||
|
snap_angle_deg: self.snap_angle_deg,
|
||||||
otrack: self.snapper.otrack_enabled,
|
otrack: self.snapper.otrack_enabled,
|
||||||
default_assoc_prompted: self.default_assoc_prompted,
|
default_assoc_prompted: self.default_assoc_prompted,
|
||||||
disabled_plugins: {
|
disabled_plugins: {
|
||||||
|
|
@ -303,6 +310,21 @@ impl OpenCADStudio {
|
||||||
self.polar_increment_deg = s.polar_increment_deg;
|
self.polar_increment_deg = s.polar_increment_deg;
|
||||||
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
||||||
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
||||||
|
self.cursor_size = s.cursor_size.clamp(1, 100);
|
||||||
|
self.pick_box = s.pick_box.clamp(0, 50);
|
||||||
|
self.cursor_type = s.cursor_type;
|
||||||
|
self.crosshair_color = s.crosshair_color;
|
||||||
|
self.crosshair_color_input = s
|
||||||
|
.crosshair_color
|
||||||
|
.map(crate::app::config::rgb_to_hex)
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.isometric_drafting = s.isometric_drafting;
|
||||||
|
self.iso_plane = s.iso_plane;
|
||||||
|
self.snap_angle_deg = if s.snap_angle_deg.is_finite() {
|
||||||
|
s.snap_angle_deg.rem_euclid(360.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
||||||
// open / tab switch), not app-global, so they are not applied here.
|
// open / tab switch), not app-global, so they are not applied here.
|
||||||
self.snapper.otrack_enabled = s.otrack;
|
self.snapper.otrack_enabled = s.otrack;
|
||||||
|
|
@ -588,6 +610,9 @@ impl OpenCADStudio {
|
||||||
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
|
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
|
||||||
self.plot_dialog = cfg.plot;
|
self.plot_dialog = cfg.plot;
|
||||||
self.shortcut_bindings = cfg.shortcuts.bindings.into_iter().collect();
|
self.shortcut_bindings = cfg.shortcuts.bindings.into_iter().collect();
|
||||||
|
self.shortcut_bindings
|
||||||
|
.entry("F5".to_string())
|
||||||
|
.or_insert_with(|| "ISOPLANE".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the config only when it changed since the last write, so a toggle
|
/// Write the config only when it changed since the last write, so a toggle
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,9 @@ impl OpenCADStudio {
|
||||||
if self.active_modal == Some(ScaleManager) {
|
if self.active_modal == Some(ScaleManager) {
|
||||||
self.scale_stage_discard();
|
self.scale_stage_discard();
|
||||||
}
|
}
|
||||||
|
if self.active_modal == Some(DraftingSettings) {
|
||||||
|
self.snap_popup_open = false;
|
||||||
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if self.active_modal == Some(FileInUse) {
|
if self.active_modal == Some(FileInUse) {
|
||||||
self.pending_save_failure = None;
|
self.pending_save_failure = None;
|
||||||
|
|
@ -3014,6 +3017,43 @@ impl OpenCADStudio {
|
||||||
self.sync_vport_display(self.active_tab);
|
self.sync_vport_display(self.active_tab);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ToggleIsometricDrafting => {
|
||||||
|
self.isometric_drafting = !self.isometric_drafting;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::SetIsoPlane(plane) => {
|
||||||
|
self.isometric_drafting = true;
|
||||||
|
self.iso_plane = plane;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::CycleIsoPlane => {
|
||||||
|
if self.isometric_drafting {
|
||||||
|
self.iso_plane = self.iso_plane.next();
|
||||||
|
} else {
|
||||||
|
self.isometric_drafting = true;
|
||||||
|
}
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"Isometric plane: {}.",
|
||||||
|
self.iso_plane.label()
|
||||||
|
).as_ref());
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ResetDraftingRotation => {
|
||||||
|
self.snap_angle_deg = 0.0;
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.tabs[i].active_ucs.is_some() {
|
||||||
|
self.tabs[i].active_ucs = None;
|
||||||
|
self.commit_active_ucs_change(i, "UCS");
|
||||||
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
}
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("Drafting rotation reset to World at 0°.").as_ref());
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::ToggleGrid => {
|
Message::ToggleGrid => {
|
||||||
self.show_grid ^= true;
|
self.show_grid ^= true;
|
||||||
self.sync_vport_display(self.active_tab);
|
self.sync_vport_display(self.active_tab);
|
||||||
|
|
@ -3654,11 +3694,20 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ToggleSnapPopup => {
|
Message::ToggleSnapPopup => {
|
||||||
self.snap_popup_open ^= true;
|
if self.active_modal == Some(super::ModalKind::DraftingSettings) {
|
||||||
|
self.close_active_modal();
|
||||||
|
self.snap_popup_open = false;
|
||||||
|
} else {
|
||||||
|
self.active_modal = Some(super::ModalKind::DraftingSettings);
|
||||||
|
self.snap_popup_open = true;
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::CloseSnapPopup => {
|
Message::CloseSnapPopup => {
|
||||||
self.snap_popup_open = false;
|
self.snap_popup_open = false;
|
||||||
|
if self.active_modal == Some(super::ModalKind::DraftingSettings) {
|
||||||
|
self.close_active_modal();
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::SnapSelectAll => {
|
Message::SnapSelectAll => {
|
||||||
|
|
@ -5149,6 +5198,41 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::OptionsTabChanged(tab) => {
|
||||||
|
self.options_tab = tab;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CursorSizeChanged(value) => {
|
||||||
|
self.cursor_size = value.clamp(1, 100);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PickBoxChanged(value) => {
|
||||||
|
self.pick_box = value.clamp(0, 50);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CursorTypeChanged(value) => {
|
||||||
|
self.cursor_type = value;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CrosshairColorChanged(value) => {
|
||||||
|
self.crosshair_color_input = value.clone();
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
self.crosshair_color = None;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
} else if let Some(rgb) = crate::app::config::parse_hex(&value) {
|
||||||
|
self.crosshair_color = Some(rgb);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::DefaultSaveFormatChanged(format) => {
|
Message::DefaultSaveFormatChanged(format) => {
|
||||||
self.default_save_format =
|
self.default_save_format =
|
||||||
crate::io::canonical_save_format(&format).to_string();
|
crate::io::canonical_save_format(&format).to_string();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
axis_lock_apply, axis_lock_capture, ortho_constrain, parse_coord, polar_constrain_near,
|
axis_lock_apply, axis_lock_capture, drafting_axes, drafting_constrain, parse_coord, polar_constrain_near,
|
||||||
ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
|
ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
|
@ -171,6 +171,26 @@ impl OpenCADStudio {
|
||||||
.set_active_tile_grid_snap(grid_on, snap_on);
|
.set_active_tile_grid_snap(grid_on, snap_on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grid origin plus the rotated/isometric axes used by both drawing and snap.
|
||||||
|
pub(in crate::app) fn drafting_grid_basis(
|
||||||
|
&self,
|
||||||
|
i: usize,
|
||||||
|
) -> (glam::Vec3, (glam::Vec3, glam::Vec3, glam::Vec3)) {
|
||||||
|
let (origin, rotation) = self.tabs[i].ucs_grid_basis();
|
||||||
|
let x = rotation.transform_vector3(glam::Vec3::X).as_dvec3();
|
||||||
|
let y = rotation.transform_vector3(glam::Vec3::Y).as_dvec3();
|
||||||
|
let z = rotation.transform_vector3(glam::Vec3::Z).as_dvec3();
|
||||||
|
let (x, y, z) = drafting_axes(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
|
(origin, (x.as_vec3(), y.as_vec3(), z.as_vec3()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Adopt the active viewport's grid *display* into the live toggle. Called
|
/// Adopt the active viewport's grid *display* into the live toggle. Called
|
||||||
/// on load and whenever the active tab or viewport changes, so the grid
|
/// on load and whenever the active tab or viewport changes, so the grid
|
||||||
/// drawing follows the active viewport.
|
/// drawing follows the active viewport.
|
||||||
|
|
@ -1014,7 +1034,7 @@ impl OpenCADStudio {
|
||||||
// snapping must drop its foot from this point, including when a
|
// snapping must drop its foot from this point, including when a
|
||||||
// hot-grip set is moved by the same drag vector.
|
// hot-grip set is moved by the same drag vector.
|
||||||
self.snapper.from_point = Some(grip.origin_world.as_vec3());
|
self.snapper.from_point = Some(grip.origin_world.as_vec3());
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
// `raw` is already model space (viewport camera or paper→model),
|
// `raw` is already model space (viewport camera or paper→model),
|
||||||
// and the wires are model space, so the snap result is model.
|
// and the wires are model space, so the snap result is model.
|
||||||
let snap_hit =
|
let snap_hit =
|
||||||
|
|
@ -1044,7 +1064,7 @@ impl OpenCADStudio {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes();
|
let (_, (ucs_x, ucs_y, _)) = self.drafting_grid_basis(i);
|
||||||
|
|
||||||
self.snapper.otrack_snap(
|
self.snapper.otrack_snap(
|
||||||
raw,
|
raw,
|
||||||
|
|
@ -1054,8 +1074,8 @@ impl OpenCADStudio {
|
||||||
polar_step,
|
polar_step,
|
||||||
Some(grip.origin_world),
|
Some(grip.origin_world),
|
||||||
self.ortho_mode,
|
self.ortho_mode,
|
||||||
ucs_x,
|
ucs_x.as_dvec3(),
|
||||||
ucs_y,
|
ucs_y.as_dvec3(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -1086,6 +1106,9 @@ impl OpenCADStudio {
|
||||||
self.polar_mode,
|
self.polar_mode,
|
||||||
self.polar_increment_deg,
|
self.polar_increment_deg,
|
||||||
&ucs_xf,
|
&ucs_xf,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1099,7 +1122,14 @@ impl OpenCADStudio {
|
||||||
let base = grip.origin_world;
|
let base = grip.origin_world;
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
let ucs_xf = self.tabs[i].ucs_xform();
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
snapped = ortho_constrain(snapped, base, &ucs_xf);
|
snapped = drafting_constrain(
|
||||||
|
snapped,
|
||||||
|
base,
|
||||||
|
&ucs_xf,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
snapped = polar_constrain_near(
|
snapped = polar_constrain_near(
|
||||||
snapped,
|
snapped,
|
||||||
|
|
@ -1423,7 +1453,7 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
// The snapper is a screen-space (f32) engine; the f64
|
// The snapper is a screen-space (f32) engine; the f64
|
||||||
// base only matters for typed-input precision, so hand it
|
// base only matters for typed-input precision, so hand it
|
||||||
// the downcast point here.
|
// the downcast point here.
|
||||||
|
|
@ -1472,7 +1502,7 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes();
|
let (_, (ucs_x, ucs_y, _)) = self.drafting_grid_basis(i);
|
||||||
self.snapper.otrack_snap(
|
self.snapper.otrack_snap(
|
||||||
cursor_world,
|
cursor_world,
|
||||||
view_rot,
|
view_rot,
|
||||||
|
|
@ -1481,8 +1511,8 @@ impl OpenCADStudio {
|
||||||
step,
|
step,
|
||||||
self.last_point,
|
self.last_point,
|
||||||
self.ortho_mode && !is_window_corner,
|
self.ortho_mode && !is_window_corner,
|
||||||
ucs_x,
|
ucs_x.as_dvec3(),
|
||||||
ucs_y,
|
ucs_y.as_dvec3(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -1545,6 +1575,9 @@ impl OpenCADStudio {
|
||||||
self.polar_mode,
|
self.polar_mode,
|
||||||
self.polar_increment_deg,
|
self.polar_increment_deg,
|
||||||
&ucs_xf,
|
&ucs_xf,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if !self.shift_down {
|
} else if !self.shift_down {
|
||||||
|
|
@ -1568,7 +1601,14 @@ impl OpenCADStudio {
|
||||||
if let Some(base) = self.last_point {
|
if let Some(base) = self.last_point {
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
let ucs_xf = self.tabs[i].ucs_xform();
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base, &ucs_xf);
|
pt = drafting_constrain(
|
||||||
|
pt,
|
||||||
|
base,
|
||||||
|
&ucs_xf,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
pt = polar_constrain_near(
|
pt = polar_constrain_near(
|
||||||
pt,
|
pt,
|
||||||
|
|
@ -1738,6 +1778,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
|
|
@ -2077,7 +2118,7 @@ impl OpenCADStudio {
|
||||||
self.snapper.grid_spacing = grid_spacing;
|
self.snapper.grid_spacing = grid_spacing;
|
||||||
// No rubber-band origin (perp/extension feet don't apply to a free drag).
|
// No rubber-band origin (perp/extension feet don't apply to a free drag).
|
||||||
self.snapper.from_point = None;
|
self.snapper.from_point = None;
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
let snap_hit = self
|
let snap_hit = self
|
||||||
.snapper
|
.snapper
|
||||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
||||||
|
|
@ -2740,7 +2781,7 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
||||||
self.snapper.snap(
|
self.snapper.snap(
|
||||||
snap_cursor,
|
snap_cursor,
|
||||||
|
|
@ -2779,7 +2820,7 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes();
|
let (_, (ucs_x, ucs_y, _)) = self.drafting_grid_basis(i);
|
||||||
self.snapper.otrack_snap(
|
self.snapper.otrack_snap(
|
||||||
raw,
|
raw,
|
||||||
view_rot,
|
view_rot,
|
||||||
|
|
@ -2788,8 +2829,8 @@ impl OpenCADStudio {
|
||||||
step,
|
step,
|
||||||
self.last_point,
|
self.last_point,
|
||||||
self.ortho_mode && !is_window_corner,
|
self.ortho_mode && !is_window_corner,
|
||||||
ucs_x,
|
ucs_x.as_dvec3(),
|
||||||
ucs_y,
|
ucs_y.as_dvec3(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -2808,7 +2849,14 @@ impl OpenCADStudio {
|
||||||
if let Some(base) = self.last_point {
|
if let Some(base) = self.last_point {
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
let ucs_xf = self.tabs[i].ucs_xform();
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base, &ucs_xf);
|
pt = drafting_constrain(
|
||||||
|
pt,
|
||||||
|
base,
|
||||||
|
&ucs_xf,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
pt = polar_constrain_near(
|
pt = polar_constrain_near(
|
||||||
pt,
|
pt,
|
||||||
|
|
@ -2893,7 +2941,7 @@ impl OpenCADStudio {
|
||||||
view_rot2,
|
view_rot2,
|
||||||
eye2,
|
eye2,
|
||||||
bounds,
|
bounds,
|
||||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
self.pick_box.max(1) as f32 * 2.0,
|
||||||
);
|
);
|
||||||
let include_fills = self.tabs[i]
|
let include_fills = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
|
|
@ -2914,6 +2962,7 @@ impl OpenCADStudio {
|
||||||
eye2,
|
eye2,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
|
|
@ -3442,7 +3491,7 @@ impl OpenCADStudio {
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
self.pick_box.max(1) as f32 * 2.0,
|
||||||
);
|
);
|
||||||
let candidate_handles = self.tabs[i]
|
let candidate_handles = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -3462,6 +3511,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|s| Scene::handle_from_wire_name(s))
|
.filter_map(|s| Scene::handle_from_wire_name(s))
|
||||||
|
|
@ -3483,6 +3533,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
|
|
@ -3824,7 +3875,7 @@ impl OpenCADStudio {
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
self.pick_box.max(1) as f32 * 2.0,
|
||||||
);
|
);
|
||||||
let candidate_handles = self.tabs[i]
|
let candidate_handles = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -3839,6 +3890,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
|
|
@ -3943,7 +3995,7 @@ impl OpenCADStudio {
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
self.pick_box.max(1) as f32 * 2.0,
|
||||||
);
|
);
|
||||||
scene::pick::hit_test::click_hit(
|
scene::pick::hit_test::click_hit(
|
||||||
p,
|
p,
|
||||||
|
|
@ -3952,6 +4004,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
.and_then(|h| {
|
.and_then(|h| {
|
||||||
|
|
@ -4357,7 +4410,7 @@ impl OpenCADStudio {
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
self.pick_box.max(1) as f32 * 2.0,
|
||||||
);
|
);
|
||||||
let candidate_ms = candidate_started.elapsed().as_secs_f64() * 1000.0;
|
let candidate_ms = candidate_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
let candidate_count = hover_candidates.len();
|
let candidate_count = hover_candidates.len();
|
||||||
|
|
@ -4377,6 +4430,7 @@ impl OpenCADStudio {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
self.tabs[i].scene.document.header.lineweight_display,
|
self.tabs[i].scene.document.header.lineweight_display,
|
||||||
|
self.pick_box.max(1) as f32,
|
||||||
)
|
)
|
||||||
.and_then(Scene::handle_from_wire_name);
|
.and_then(Scene::handle_from_wire_name);
|
||||||
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
|
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,14 @@ impl OpenCADStudio {
|
||||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||||
let model_basis = {
|
let model_basis = {
|
||||||
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
||||||
|
let (ux, uy, uz) = super::helpers::drafting_axes(
|
||||||
|
ux,
|
||||||
|
uy,
|
||||||
|
uz,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
(o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
|
(o, (ux.as_vec3(), uy.as_vec3(), uz.as_vec3()))
|
||||||
};
|
};
|
||||||
let grid: Vec<crate::ui::overlay::GridParams> = tab
|
let grid: Vec<crate::ui::overlay::GridParams> = tab
|
||||||
|
|
@ -342,7 +350,7 @@ impl OpenCADStudio {
|
||||||
.grid_views(vw, vh)
|
.grid_views(vw, vh)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(bounds, cam, handle)| {
|
.map(|(bounds, cam, handle)| {
|
||||||
let (origin, axes): (glam::DVec3, _) = if is_paper {
|
let (origin, mut axes): (glam::DVec3, _) = if is_paper {
|
||||||
match tab.ucs_from_viewport(handle) {
|
match tab.ucs_from_viewport(handle) {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
let (o, ux, uy, uz) =
|
let (o, ux, uy, uz) =
|
||||||
|
|
@ -357,6 +365,17 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
model_basis
|
model_basis
|
||||||
};
|
};
|
||||||
|
if is_paper {
|
||||||
|
let (ux, uy, uz) = super::helpers::drafting_axes(
|
||||||
|
axes.0.as_dvec3(),
|
||||||
|
axes.1.as_dvec3(),
|
||||||
|
axes.2.as_dvec3(),
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
);
|
||||||
|
axes = (ux.as_vec3(), uy.as_vec3(), uz.as_vec3());
|
||||||
|
}
|
||||||
crate::ui::overlay::GridParams {
|
crate::ui::overlay::GridParams {
|
||||||
view_rot: cam.view_proj_rte(bounds),
|
view_rot: cam.view_proj_rte(bounds),
|
||||||
eye: cam.eye(),
|
eye: cam.eye(),
|
||||||
|
|
@ -684,6 +703,15 @@ impl OpenCADStudio {
|
||||||
self.ribbon.open_dropdown.is_some(),
|
self.ribbon.open_dropdown.is_some(),
|
||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_background(tab, is_paper),
|
crosshair_background(tab, is_paper),
|
||||||
|
crate::ui::overlay::CrosshairOptions {
|
||||||
|
size_percent: self.cursor_size,
|
||||||
|
pick_box: self.pick_box,
|
||||||
|
cursor_type: self.cursor_type,
|
||||||
|
color: self.crosshair_color,
|
||||||
|
isometric: self.isometric_drafting,
|
||||||
|
iso_plane: self.iso_plane,
|
||||||
|
snap_angle_deg: self.snap_angle_deg,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1693,6 +1721,8 @@ impl OpenCADStudio {
|
||||||
self.polar_increment_deg,
|
self.polar_increment_deg,
|
||||||
self.dyn_input,
|
self.dyn_input,
|
||||||
self.snapper.otrack_enabled,
|
self.snapper.otrack_enabled,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
layout_names.clone(),
|
layout_names.clone(),
|
||||||
block_tabs,
|
block_tabs,
|
||||||
layout_names.into_iter().skip(1).collect(),
|
layout_names.into_iter().skip(1).collect(),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ impl OpenCADStudio {
|
||||||
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
|
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
|
||||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||||
|
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
|
||||||
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
|
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
|
||||||
Some(K::Plot) => crate::tr!("modal-plot"),
|
Some(K::Plot) => crate::tr!("modal-plot"),
|
||||||
Some(K::PrintAll) => t!("Print All").into_owned(),
|
Some(K::PrintAll) => t!("Print All").into_owned(),
|
||||||
|
|
@ -96,6 +97,31 @@ impl OpenCADStudio {
|
||||||
&self.ui_theme,
|
&self.ui_theme,
|
||||||
&self.theme_color_inputs,
|
&self.theme_color_inputs,
|
||||||
self.language,
|
self.language,
|
||||||
|
self.options_tab,
|
||||||
|
self.cursor_size,
|
||||||
|
self.pick_box,
|
||||||
|
self.cursor_type,
|
||||||
|
self.crosshair_color,
|
||||||
|
&self.crosshair_color_input,
|
||||||
|
flow,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
super::super::ModalKind::DraftingSettings => sized_flow(
|
||||||
|
ex,
|
||||||
|
520,
|
||||||
|
560,
|
||||||
|
|flow| {
|
||||||
|
crate::ui::window::drafting_settings::view_window(
|
||||||
|
&self.snapper,
|
||||||
|
self.show_grid,
|
||||||
|
self.snapper.grid_snap(),
|
||||||
|
self.ortho_mode,
|
||||||
|
self.polar_mode,
|
||||||
|
self.snapper.otrack_enabled,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
flow,
|
flow,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,10 @@ use crate::scene::model::mesh_model::MeshModel;
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::scene::pick::interaction_index::WireSource;
|
use crate::scene::pick::interaction_index::WireSource;
|
||||||
|
|
||||||
/// Pixel radius used for single-click wire detection.
|
|
||||||
pub const CLICK_THRESHOLD_PX: f32 = 8.0;
|
|
||||||
|
|
||||||
/// Pick radius for one wire, in screen pixels.
|
/// Pick radius for one wire, in screen pixels.
|
||||||
///
|
///
|
||||||
/// A wire renders as a band `line_weight_px` wide, so testing every wire at the
|
/// A wire renders as a band `line_weight_px` wide, so testing every wire at the
|
||||||
/// bare [`CLICK_THRESHOLD_PX`] would leave the outer part of a heavy line
|
/// configured base radius would leave the outer part of a heavy line
|
||||||
/// unselectable — the cursor would sit on solid ink and miss. Widening to the
|
/// unselectable — the cursor would sit on solid ink and miss. Widening to the
|
||||||
/// rendered half-width keeps "looks like I'm on it" and "picks it" the same
|
/// rendered half-width keeps "looks like I'm on it" and "picks it" the same
|
||||||
/// thing at any zoom: both quantities are screen-space, so the relation holds
|
/// thing at any zoom: both quantities are screen-space, so the relation holds
|
||||||
|
|
@ -35,13 +32,13 @@ pub const CLICK_THRESHOLD_PX: f32 = 8.0;
|
||||||
/// renders 7.97 px half-width), so this only bites for out-of-range weights —
|
/// renders 7.97 px half-width), so this only bites for out-of-range weights —
|
||||||
/// and it keeps the two sides from silently drifting apart if the display boost
|
/// and it keeps the two sides from silently drifting apart if the display boost
|
||||||
/// in `view::render::lineweight_to_px` ever changes.
|
/// in `view::render::lineweight_to_px` ever changes.
|
||||||
pub fn pick_tolerance_px(wire: &WireModel, lw_display: bool) -> f32 {
|
pub fn pick_tolerance_px(wire: &WireModel, lw_display: bool, base_radius_px: f32) -> f32 {
|
||||||
let half_width = if lw_display {
|
let half_width = if lw_display {
|
||||||
wire.line_weight_px * 0.5
|
wire.line_weight_px * 0.5
|
||||||
} else {
|
} else {
|
||||||
0.5
|
0.5
|
||||||
};
|
};
|
||||||
CLICK_THRESHOLD_PX.max(half_width)
|
base_radius_px.max(1.0).max(half_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is `aabb` — a wire's world-space XY box — further than `tol` pixels from
|
/// Is `aabb` — a wire's world-space XY box — further than `tol` pixels from
|
||||||
|
|
@ -199,6 +196,7 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
|
||||||
eye: glam::DVec3,
|
eye: glam::DVec3,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
lw_display: bool,
|
lw_display: bool,
|
||||||
|
base_radius_px: f32,
|
||||||
) -> Option<&'a str> {
|
) -> Option<&'a str> {
|
||||||
// A click outside the pane rectangle (e.g. on the paper around a floating
|
// A click outside the pane rectangle (e.g. on the paper around a floating
|
||||||
// viewport) must not reach geometry scissored out of the viewport.
|
// viewport) must not reach geometry scissored out of the viewport.
|
||||||
|
|
@ -243,7 +241,7 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
|
||||||
bounds,
|
bounds,
|
||||||
);
|
);
|
||||||
let d = dist_point_to_segment(cursor, p0, p1);
|
let d = dist_point_to_segment(cursor, p0, p1);
|
||||||
if d < pick_tolerance_px(wire, lw_display) && d < best_dist {
|
if d < pick_tolerance_px(wire, lw_display, base_radius_px) && d < best_dist {
|
||||||
best_dist = d;
|
best_dist = d;
|
||||||
best = Some(&wire.name);
|
best = Some(&wire.name);
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +249,7 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
|
||||||
} else {
|
} else {
|
||||||
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
|
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
|
||||||
for wire in wires.iter() {
|
for wire in wires.iter() {
|
||||||
let tol = pick_tolerance_px(wire, lw_display);
|
let tol = pick_tolerance_px(wire, lw_display, base_radius_px);
|
||||||
// Cheap AABB pre-reject (flat view only; never for the unbounded
|
// Cheap AABB pre-reject (flat view only; never for the unbounded
|
||||||
// sentinel used by previews / greeked text).
|
// sentinel used by previews / greeked text).
|
||||||
if z_flat
|
if z_flat
|
||||||
|
|
@ -446,6 +444,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
eye: glam::DVec3,
|
eye: glam::DVec3,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
lw_display: bool,
|
lw_display: bool,
|
||||||
|
base_radius_px: f32,
|
||||||
) -> Vec<&'a str> {
|
) -> Vec<&'a str> {
|
||||||
if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
|
if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
@ -474,7 +473,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
bounds,
|
bounds,
|
||||||
);
|
);
|
||||||
let d = dist_point_to_segment(cursor, p0, p1);
|
let d = dist_point_to_segment(cursor, p0, p1);
|
||||||
if d < pick_tolerance_px(wire, lw_display) {
|
if d < pick_tolerance_px(wire, lw_display, base_radius_px) {
|
||||||
best_by_wire
|
best_by_wire
|
||||||
.entry(segment.wire)
|
.entry(segment.wire)
|
||||||
.and_modify(|best| *best = best.min(d))
|
.and_modify(|best| *best = best.min(d))
|
||||||
|
|
@ -488,7 +487,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
for wire in wires.iter() {
|
for wire in wires.iter() {
|
||||||
let tol = pick_tolerance_px(wire, lw_display);
|
let tol = pick_tolerance_px(wire, lw_display, base_radius_px);
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
let mut best_for_wire = tol;
|
let mut best_for_wire = tol;
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
|
|
@ -543,7 +542,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
matched.insert(triangle.wire);
|
matched.insert(triangle.wire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -562,7 +561,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -597,7 +596,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
matched.insert(triangle.wire);
|
matched.insert(triangle.wire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -616,7 +615,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -641,7 +640,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if text_quad_hit_area(cursor, vertices, view_rot, eye, bounds).is_some() {
|
if text_quad_hit_area(cursor, vertices, view_rot, eye, bounds).is_some() {
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
matched.insert(glyph.wire);
|
matched.insert(glyph.wire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -651,7 +650,7 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if text_quad_hit_area(cursor, &wire.text_verts, view_rot, eye, bounds).is_some() {
|
if text_quad_hit_area(cursor, &wire.text_verts, view_rot, eye, bounds).is_some() {
|
||||||
hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
|
hits.push((base_radius_px.max(1.0), wire.name.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2038,16 +2037,16 @@ mod aabb_reject_tests {
|
||||||
|
|
||||||
let eye = glam::DVec3::ZERO;
|
let eye = glam::DVec3::ZERO;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true),
|
click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true, 8.0),
|
||||||
Some("5")
|
Some("5")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true),
|
click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true, 8.0),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
// The far wire must be rejected without hiding the near one.
|
// The far wire must be rejected without hiding the near one.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
click_hit(cursor, &[far, near], vp, eye, bounds, true),
|
click_hit(cursor, &[far, near], vp, eye, bounds, true, 8.0),
|
||||||
Some("5")
|
Some("5")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/snap.rs
36
src/snap.rs
|
|
@ -11,7 +11,7 @@ use iced::{Point, Rectangle};
|
||||||
use crate::command::TangentObject;
|
use crate::command::TangentObject;
|
||||||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||||
use crate::scene::pick::interaction_index::WireSource;
|
use crate::scene::pick::interaction_index::WireSource;
|
||||||
use crate::ui::overlay::CROSSHAIR_ARM;
|
const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0;
|
||||||
|
|
||||||
// ── Snap type ─────────────────────────────────────────────────────────────
|
// ── Snap type ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ impl Default for Snapper {
|
||||||
enabled,
|
enabled,
|
||||||
grid_snap_on: false,
|
grid_snap_on: false,
|
||||||
grid_spacing: 1.0,
|
grid_spacing: 1.0,
|
||||||
osnap_radius_px: CROSSHAIR_ARM * 0.25,
|
osnap_radius_px: DEFAULT_OSNAP_RADIUS_PX,
|
||||||
otrack_enabled: false,
|
otrack_enabled: false,
|
||||||
tracking_points: Vec::new(),
|
tracking_points: Vec::new(),
|
||||||
tracking_dirs: Vec::new(),
|
tracking_dirs: Vec::new(),
|
||||||
|
|
@ -830,7 +830,7 @@ impl Snapper {
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
Vec3::ZERO,
|
Vec3::ZERO,
|
||||||
Mat4::IDENTITY,
|
(Vec3::X, Vec3::Y, Vec3::Z),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -843,10 +843,10 @@ impl Snapper {
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
eye: glam::DVec3,
|
eye: glam::DVec3,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
// Grid origin (render/wire space) and UCS→world rotation, so grid snap
|
// Grid origin and live drafting axes, so grid snap lands on the same
|
||||||
// lands on the UCS grid the user sees. `(ZERO, IDENTITY)` = world grid.
|
// rotated or isometric grid the user sees.
|
||||||
grid_origin: Vec3,
|
grid_origin: Vec3,
|
||||||
grid_rot: Mat4,
|
grid_axes: (Vec3, Vec3, Vec3),
|
||||||
) -> Option<SnapResult> {
|
) -> Option<SnapResult> {
|
||||||
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
||||||
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
||||||
|
|
@ -881,13 +881,27 @@ impl Snapper {
|
||||||
let s = self.grid_spacing as f64;
|
let s = self.grid_spacing as f64;
|
||||||
if s.abs() > 1e-9 {
|
if s.abs() > 1e-9 {
|
||||||
// Round in the UCS grid frame, then map back to world.
|
// Round in the UCS grid frame, then map back to world.
|
||||||
let ax = grid_rot.transform_vector3(Vec3::X).as_dvec3();
|
let (ax, ay, az) = grid_axes;
|
||||||
let ay = grid_rot.transform_vector3(Vec3::Y).as_dvec3();
|
let ax = ax.normalize_or(Vec3::X).as_dvec3();
|
||||||
let az = grid_rot.transform_vector3(Vec3::Z).as_dvec3();
|
let ay = ay.normalize_or(Vec3::Y).as_dvec3();
|
||||||
|
let az = az.normalize_or(Vec3::Z).as_dvec3();
|
||||||
let origin = grid_origin.as_dvec3();
|
let origin = grid_origin.as_dvec3();
|
||||||
let rel = cursor_world - origin;
|
let rel = cursor_world - origin;
|
||||||
let ux = (rel.dot(ax) / s).round() * s;
|
// The isometric pairs are oblique, so dot products alone are
|
||||||
let uy = (rel.dot(ay) / s).round() * s;
|
// not coordinates. Invert their 2×2 Gram matrix before rounding.
|
||||||
|
let aa = ax.dot(ax);
|
||||||
|
let ab = ax.dot(ay);
|
||||||
|
let bb = ay.dot(ay);
|
||||||
|
let det = aa * bb - ab * ab;
|
||||||
|
let (ux, uy) = if det.abs() > 1e-9 {
|
||||||
|
let ra = rel.dot(ax);
|
||||||
|
let rb = rel.dot(ay);
|
||||||
|
((ra * bb - rb * ab) / det, (rb * aa - ra * ab) / det)
|
||||||
|
} else {
|
||||||
|
(rel.dot(ax), rel.dot(ay))
|
||||||
|
};
|
||||||
|
let ux = (ux / s).round() * s;
|
||||||
|
let uy = (uy / s).round() * s;
|
||||||
let uz = (rel.dot(az) / s).round() * s;
|
let uz = (rel.dot(az) / s).round() * s;
|
||||||
let gp = origin + ax * ux + ay * uy + az * uz;
|
let gp = origin + ax * ux + ay * uy + az * uz;
|
||||||
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,23 @@ use iced::widget::canvas;
|
||||||
use iced::{Color, Element, Length, Point, Size, Theme};
|
use iced::{Color, Element, Length, Point, Size, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::app::settings::{CursorType, IsoPlane};
|
||||||
use crate::scene::model::object::GripShape;
|
use crate::scene::model::object::GripShape;
|
||||||
use crate::scene::SelectionState;
|
use crate::scene::SelectionState;
|
||||||
|
|
||||||
/// Half-size of the crosshair center square in screen pixels (square = SQ*2 × SQ*2).
|
|
||||||
pub const CROSSHAIR_SQ: f32 = 7.5;
|
|
||||||
/// Arm length of the crosshair from center — used as the snap aperture radius.
|
|
||||||
pub const CROSSHAIR_ARM: f32 = 60.0;
|
|
||||||
use crate::snap::SnapType;
|
use crate::snap::SnapType;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct CrosshairOptions {
|
||||||
|
pub size_percent: i32,
|
||||||
|
pub pick_box: i32,
|
||||||
|
pub cursor_type: CursorType,
|
||||||
|
pub color: Option<[u8; 3]>,
|
||||||
|
pub isometric: bool,
|
||||||
|
pub iso_plane: IsoPlane,
|
||||||
|
pub snap_angle_deg: f32,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Grip marker data ──────────────────────────────────────────────────────
|
// ── Grip marker data ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Describes one grip to be drawn in the viewport overlay.
|
/// Describes one grip to be drawn in the viewport overlay.
|
||||||
|
|
@ -223,6 +231,7 @@ pub fn selection_overlay<'a>(
|
||||||
suppressed: bool,
|
suppressed: bool,
|
||||||
hover_locked: bool,
|
hover_locked: bool,
|
||||||
crosshair_bg: [f32; 4],
|
crosshair_bg: [f32; 4],
|
||||||
|
crosshair: CrosshairOptions,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(SelectionCanvas {
|
canvas(SelectionCanvas {
|
||||||
selection,
|
selection,
|
||||||
|
|
@ -243,6 +252,7 @@ pub fn selection_overlay<'a>(
|
||||||
suppressed,
|
suppressed,
|
||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_bg,
|
crosshair_bg,
|
||||||
|
crosshair,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -297,6 +307,7 @@ struct SelectionCanvas {
|
||||||
/// Background of the active drawing space. Crosshair contrast follows this
|
/// Background of the active drawing space. Crosshair contrast follows this
|
||||||
/// rather than the UI theme, which may be light over a dark model viewport.
|
/// rather than the UI theme, which may be light over a dark model viewport.
|
||||||
crosshair_bg: [f32; 4],
|
crosshair_bg: [f32; 4],
|
||||||
|
crosshair: CrosshairOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) {
|
fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) {
|
||||||
|
|
@ -432,7 +443,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
// cursor entirely. `Interaction::None` would let the stack fall
|
// cursor entirely. `Interaction::None` would let the stack fall
|
||||||
// through to a sibling — `Hidden` is the explicit "no cursor"
|
// through to a sibling — `Hidden` is the explicit "no cursor"
|
||||||
// signal that actually suppresses the OS arrow.
|
// signal that actually suppresses the OS arrow.
|
||||||
if cursor.is_over(bounds) {
|
if cursor.is_over(bounds) && self.crosshair.cursor_type == CursorType::Crosshair {
|
||||||
mouse::Interaction::Hidden
|
mouse::Interaction::Hidden
|
||||||
} else {
|
} else {
|
||||||
mouse::Interaction::default()
|
mouse::Interaction::default()
|
||||||
|
|
@ -924,11 +935,28 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
// top of it would double up the visual feedback.
|
// top of it would double up the visual feedback.
|
||||||
let over_divider = self.divider_under(cursor, bounds);
|
let over_divider = self.divider_under(cursor, bounds);
|
||||||
// PAN mode replaces the crosshair with a hand cursor.
|
// PAN mode replaces the crosshair with a hand cursor.
|
||||||
if !over_viewcube && !over_divider && !self.pan_mode && !self.suppressed {
|
if !over_viewcube
|
||||||
|
&& !over_divider
|
||||||
|
&& !self.pan_mode
|
||||||
|
&& !self.suppressed
|
||||||
|
&& self.crosshair.cursor_type == CursorType::Crosshair
|
||||||
|
{
|
||||||
if let Some(cp) = self.selection.last_move_pos {
|
if let Some(cp) = self.selection.last_move_pos {
|
||||||
let [r, g, b, a] = crate::scene::view::render::adapt_to_bg(
|
let [r, g, b, a] = self.crosshair.color.map_or_else(
|
||||||
[1.0, 1.0, 1.0, 0.90],
|
|| {
|
||||||
self.crosshair_bg,
|
crate::scene::view::render::adapt_to_bg(
|
||||||
|
[1.0, 1.0, 1.0, 0.90],
|
||||||
|
self.crosshair_bg,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|[r, g, b]| {
|
||||||
|
[
|
||||||
|
r as f32 / 255.0,
|
||||||
|
g as f32 / 255.0,
|
||||||
|
b as f32 / 255.0,
|
||||||
|
0.90,
|
||||||
|
]
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let color = Color { r, g, b, a };
|
let color = Color { r, g, b, a };
|
||||||
let stroke = canvas::Stroke {
|
let stroke = canvas::Stroke {
|
||||||
|
|
@ -936,38 +964,38 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
style: canvas::Style::Solid(color),
|
style: canvas::Style::Solid(color),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let sq = CROSSHAIR_SQ; // square half-size → 15×15
|
let sq = self.crosshair.pick_box.clamp(0, 50) as f32;
|
||||||
let arm = CROSSHAIR_ARM; // crosshair arm length from center
|
let arm = bounds.width.hypot(bounds.height)
|
||||||
|
* self.crosshair.size_percent.clamp(1, 100) as f32
|
||||||
// Horizontal arms (start at square edge, end at arm length)
|
/ 100.0;
|
||||||
let h_left = canvas::Path::new(|b| {
|
let base_angles: [f64; 2] = if self.crosshair.isometric {
|
||||||
b.move_to(Point::new(cp.x - sq, cp.y));
|
self.crosshair.iso_plane.angles()
|
||||||
b.line_to(Point::new(cp.x - arm, cp.y));
|
} else {
|
||||||
});
|
[0.0, 90.0]
|
||||||
let h_right = canvas::Path::new(|b| {
|
};
|
||||||
b.move_to(Point::new(cp.x + sq, cp.y));
|
for angle in base_angles {
|
||||||
b.line_to(Point::new(cp.x + arm, cp.y));
|
let rad = (angle + self.crosshair.snap_angle_deg as f64).to_radians();
|
||||||
});
|
let dir = Point::new(rad.cos() as f32, -rad.sin() as f32);
|
||||||
// Vertical arms
|
let gap = if sq > 0.0 {
|
||||||
let v_top = canvas::Path::new(|b| {
|
sq / dir.x.abs().max(dir.y.abs()).max(1e-6)
|
||||||
b.move_to(Point::new(cp.x, cp.y - sq));
|
} else {
|
||||||
b.line_to(Point::new(cp.x, cp.y - arm));
|
0.0
|
||||||
});
|
};
|
||||||
let v_bot = canvas::Path::new(|b| {
|
let arms = canvas::Path::new(|path| {
|
||||||
b.move_to(Point::new(cp.x, cp.y + sq));
|
path.move_to(Point::new(cp.x + dir.x * gap, cp.y + dir.y * gap));
|
||||||
b.line_to(Point::new(cp.x, cp.y + arm));
|
path.line_to(Point::new(cp.x + dir.x * arm, cp.y + dir.y * arm));
|
||||||
});
|
path.move_to(Point::new(cp.x - dir.x * gap, cp.y - dir.y * gap));
|
||||||
// Center square
|
path.line_to(Point::new(cp.x - dir.x * arm, cp.y - dir.y * arm));
|
||||||
let square = canvas::Path::rectangle(
|
});
|
||||||
Point::new(cp.x - sq, cp.y - sq),
|
frame.stroke(&arms, stroke.clone());
|
||||||
Size::new(sq * 2.0, sq * 2.0),
|
}
|
||||||
);
|
if sq > 0.0 {
|
||||||
|
let square = canvas::Path::rectangle(
|
||||||
frame.stroke(&h_left, stroke.clone());
|
Point::new(cp.x - sq, cp.y - sq),
|
||||||
frame.stroke(&h_right, stroke.clone());
|
Size::new(sq * 2.0, sq * 2.0),
|
||||||
frame.stroke(&v_top, stroke.clone());
|
);
|
||||||
frame.stroke(&v_bot, stroke.clone());
|
frame.stroke(&square, stroke);
|
||||||
frame.stroke(&square, stroke);
|
}
|
||||||
|
|
||||||
// Locked-layer badge: a small padlock beside the crosshair when
|
// Locked-layer badge: a small padlock beside the crosshair when
|
||||||
// the hovered object sits on a locked layer (issue: locked
|
// the hovered object sits on a locked layer (issue: locked
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
//! OpenCADStudio-style OSNAP status menu.
|
//! OpenCADStudio-style OSNAP status menu.
|
||||||
|
|
||||||
use iced::widget::{button, container, row, text};
|
use iced::widget::{button, checkbox, column, container, row, text};
|
||||||
use iced::{Background, Element, Fill, Length, Theme};
|
use iced::{Background, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::app::settings::IsoPlane;
|
||||||
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||||
use crate::ui::statusbar::status_menu::Entry;
|
use crate::ui::statusbar::status_menu::Entry;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
|
pub fn menu_entries<'a>(
|
||||||
|
snapper: &'a Snapper,
|
||||||
|
isometric: bool,
|
||||||
|
iso_plane: IsoPlane,
|
||||||
|
) -> Vec<Entry<'a>> {
|
||||||
let all_on = snapper.all_on();
|
let all_on = snapper.all_on();
|
||||||
let none_on = snapper.none_on();
|
let none_on = snapper.none_on();
|
||||||
|
|
||||||
|
|
@ -30,7 +35,49 @@ pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([0, 4]);
|
.padding([0, 4]);
|
||||||
|
|
||||||
let mut entries = vec![Entry::stay(header), Entry::stay(divider)];
|
let mut iso_planes = row![].spacing(3);
|
||||||
|
for plane in IsoPlane::ALL {
|
||||||
|
iso_planes = iso_planes.push(
|
||||||
|
button(text(t!(plane.label())).size(10))
|
||||||
|
.on_press(Message::SetIsoPlane(plane))
|
||||||
|
.style(if isometric && iso_plane == plane {
|
||||||
|
button::primary
|
||||||
|
} else {
|
||||||
|
button::secondary
|
||||||
|
})
|
||||||
|
.padding([3, 8]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let drafting = column![
|
||||||
|
row![
|
||||||
|
checkbox(isometric)
|
||||||
|
.on_toggle(|_| Message::ToggleIsometricDrafting)
|
||||||
|
.size(14),
|
||||||
|
text(t!("Isometric drafting")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
iso_planes,
|
||||||
|
text(t!("F5 cycles the active plane.")).size(10),
|
||||||
|
]
|
||||||
|
.spacing(5)
|
||||||
|
.padding([5, 8]);
|
||||||
|
let drafting_divider = container(iced::widget::Space::new().height(1))
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.palette().background.weak.color,
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.padding([0, 4]);
|
||||||
|
|
||||||
|
let mut entries = vec![
|
||||||
|
Entry::stay(header),
|
||||||
|
Entry::stay(divider),
|
||||||
|
Entry::stay(drafting),
|
||||||
|
Entry::stay(drafting_divider),
|
||||||
|
];
|
||||||
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
|
for &(snap_type, _glyph, label) in ALL_SNAP_MODES {
|
||||||
entries.push(Entry::stay(snap_row(
|
entries.push(Entry::stay(snap_row(
|
||||||
snap_type,
|
snap_type,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ impl StatusBar {
|
||||||
polar_increment_deg: f32,
|
polar_increment_deg: f32,
|
||||||
dyn_input: bool,
|
dyn_input: bool,
|
||||||
otrack: bool,
|
otrack: bool,
|
||||||
|
isometric_drafting: bool,
|
||||||
|
iso_plane: crate::app::settings::IsoPlane,
|
||||||
layouts: Vec<String>,
|
layouts: Vec<String>,
|
||||||
block_tabs: Vec<String>,
|
block_tabs: Vec<String>,
|
||||||
reorderable_layouts: Vec<String>,
|
reorderable_layouts: Vec<String>,
|
||||||
|
|
@ -297,7 +299,11 @@ impl StatusBar {
|
||||||
osnap_active,
|
osnap_active,
|
||||||
snapper.snap_enabled,
|
snapper.snap_enabled,
|
||||||
tooltip_hidden,
|
tooltip_hidden,
|
||||||
crate::ui::popup::snap_popup::menu_entries(snapper),
|
crate::ui::popup::snap_popup::menu_entries(
|
||||||
|
snapper,
|
||||||
|
isometric_drafting,
|
||||||
|
iso_plane,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
135
src/ui/window/drafting_settings.rs
Normal file
135
src/ui/window/drafting_settings.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
use crate::app::settings::IsoPlane;
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::snap::{Snapper, ALL_SNAP_MODES};
|
||||||
|
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
||||||
|
use iced::{Element, Fill};
|
||||||
|
|
||||||
|
pub fn view_window<'a>(
|
||||||
|
snapper: &'a Snapper,
|
||||||
|
grid: bool,
|
||||||
|
grid_snap: bool,
|
||||||
|
ortho: bool,
|
||||||
|
polar: bool,
|
||||||
|
otrack: bool,
|
||||||
|
isometric: bool,
|
||||||
|
iso_plane: IsoPlane,
|
||||||
|
snap_angle_deg: f32,
|
||||||
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let toggle = |value: bool, label: std::borrow::Cow<'a, str>, message: Message| {
|
||||||
|
row![
|
||||||
|
checkbox(value).on_toggle(move |_| message.clone()).size(15),
|
||||||
|
text(label).size(12),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
};
|
||||||
|
|
||||||
|
let drafting_modes = column![
|
||||||
|
text(crate::t!("Drafting modes")).size(15),
|
||||||
|
Space::new().height(8),
|
||||||
|
toggle(grid, crate::t!("Grid display"), Message::ToggleGrid),
|
||||||
|
toggle(grid_snap, crate::t!("Grid snap"), Message::ToggleGridSnap),
|
||||||
|
toggle(ortho, crate::t!("Ortho"), Message::ToggleOrtho),
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
button::secondary
|
||||||
|
})
|
||||||
|
.padding([5, 12]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let isometric_controls = column![
|
||||||
|
text(crate::t!("Isometric drafting")).size(15),
|
||||||
|
Space::new().height(8),
|
||||||
|
toggle(
|
||||||
|
isometric,
|
||||||
|
crate::t!("Enable isometric drafting"),
|
||||||
|
Message::ToggleIsometricDrafting,
|
||||||
|
),
|
||||||
|
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)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(7);
|
||||||
|
|
||||||
|
let mut snap_modes = column![
|
||||||
|
text(crate::t!("Object snap modes")).size(15),
|
||||||
|
Space::new().height(8),
|
||||||
|
toggle(
|
||||||
|
snapper.snap_enabled,
|
||||||
|
crate::t!("Enable object snap"),
|
||||||
|
Message::ToggleSnapEnabled,
|
||||||
|
),
|
||||||
|
row![
|
||||||
|
button(text(crate::t!("Select All")).size(10))
|
||||||
|
.on_press(Message::SnapSelectAll)
|
||||||
|
.style(button::secondary)
|
||||||
|
.padding([4, 10]),
|
||||||
|
button(text(crate::t!("Clear All")).size(10))
|
||||||
|
.on_press(Message::SnapClearAll)
|
||||||
|
.style(button::secondary)
|
||||||
|
.padding([4, 10]),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
]
|
||||||
|
.spacing(5);
|
||||||
|
for &(snap_type, _, label) in ALL_SNAP_MODES {
|
||||||
|
snap_modes = snap_modes.push(
|
||||||
|
row![
|
||||||
|
checkbox(snapper.is_on(snap_type))
|
||||||
|
.on_toggle(move |_| Message::ToggleSnap(snap_type))
|
||||||
|
.size(14),
|
||||||
|
text(crate::t!(label)).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let close = button(text(crate::tr!("action-close")).size(12))
|
||||||
|
.on_press(Message::CloseModal)
|
||||||
|
.padding([6, 18])
|
||||||
|
.style(button::secondary);
|
||||||
|
let content = column![
|
||||||
|
drafting_modes,
|
||||||
|
Space::new().height(20),
|
||||||
|
isometric_controls,
|
||||||
|
Space::new().height(20),
|
||||||
|
snap_modes,
|
||||||
|
]
|
||||||
|
.width(sizing.width);
|
||||||
|
let body = column![
|
||||||
|
scrollable(content).spacing(8).height(sizing.height),
|
||||||
|
Space::new().height(12),
|
||||||
|
row![Space::new().width(Fill), close],
|
||||||
|
]
|
||||||
|
.width(sizing.width)
|
||||||
|
.height(sizing.height);
|
||||||
|
|
||||||
|
container(body)
|
||||||
|
.style(container::rounded_box)
|
||||||
|
.padding([16, 18])
|
||||||
|
.width(sizing.width)
|
||||||
|
.height(sizing.height)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod block_palette;
|
||||||
pub mod layout_manager;
|
pub mod layout_manager;
|
||||||
pub mod layer_state_manager;
|
pub mod layer_state_manager;
|
||||||
pub mod drawing_units;
|
pub mod drawing_units;
|
||||||
|
pub mod drafting_settings;
|
||||||
pub mod layer_translator;
|
pub mod layer_translator;
|
||||||
pub mod plot;
|
pub mod plot;
|
||||||
pub mod print_all;
|
pub mod print_all;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
use crate::app::config::UiThemeConfig;
|
use crate::app::config::UiThemeConfig;
|
||||||
|
use crate::app::settings::CursorType;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, column, container, row, scrollable, text, text_input, Space,
|
button, column, container, row, scrollable, slider, text, text_input, Space,
|
||||||
};
|
};
|
||||||
use iced::{Background, Border, Element, Theme};
|
use iced::{Background, Border, Element, Fill, Theme};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum OptionsTab {
|
||||||
|
#[default]
|
||||||
|
General,
|
||||||
|
Display,
|
||||||
|
Selection,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct Labelled<T> {
|
struct Labelled<T> {
|
||||||
value: T,
|
value: T,
|
||||||
|
|
@ -24,6 +33,12 @@ pub fn view_window<'a>(
|
||||||
ui_theme: &'a UiThemeConfig,
|
ui_theme: &'a UiThemeConfig,
|
||||||
theme_color_inputs: &'a [String; 6],
|
theme_color_inputs: &'a [String; 6],
|
||||||
language: crate::i18n::Language,
|
language: crate::i18n::Language,
|
||||||
|
active_tab: OptionsTab,
|
||||||
|
cursor_size: i32,
|
||||||
|
pick_box: i32,
|
||||||
|
cursor_type: CursorType,
|
||||||
|
crosshair_color: Option<[u8; 3]>,
|
||||||
|
crosshair_color_input: &'a str,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
|
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
|
||||||
|
|
@ -62,6 +77,18 @@ pub fn view_window<'a>(
|
||||||
.find(|choice| choice.value == language)
|
.find(|choice| choice.value == language)
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
|
let cursor_options = CursorType::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|value: CursorType| Labelled {
|
||||||
|
label: crate::t!(value.label()).into_owned(),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let selected_cursor = cursor_options
|
||||||
|
.iter()
|
||||||
|
.find(|choice| choice.value == cursor_type)
|
||||||
|
.cloned();
|
||||||
|
|
||||||
let palette = ui_theme.palette.to_iced();
|
let palette = ui_theme.palette.to_iced();
|
||||||
let colors = [
|
let colors = [
|
||||||
(crate::tr!("options-color-background"), palette.background),
|
(crate::tr!("options-color-background"), palette.background),
|
||||||
|
|
@ -104,7 +131,7 @@ pub fn view_window<'a>(
|
||||||
.padding([6, 18])
|
.padding([6, 18])
|
||||||
.style(button::secondary);
|
.style(button::secondary);
|
||||||
|
|
||||||
let content = column![
|
let general = column![
|
||||||
text(crate::tr!("options-language-section")).size(15),
|
text(crate::tr!("options-language-section")).size(15),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
|
|
@ -154,7 +181,29 @@ pub fn view_window<'a>(
|
||||||
))
|
))
|
||||||
.size(11)
|
.size(11)
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
Space::new().height(22),
|
]
|
||||||
|
.spacing(0)
|
||||||
|
.width(sizing.width);
|
||||||
|
|
||||||
|
let crosshair_rgb = crosshair_color.unwrap_or([255, 255, 255]);
|
||||||
|
let crosshair_swatch = container(Space::new())
|
||||||
|
.width(28)
|
||||||
|
.height(22)
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||||
|
crosshair_rgb[0],
|
||||||
|
crosshair_rgb[1],
|
||||||
|
crosshair_rgb[2],
|
||||||
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: theme.palette().background.strong.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let display = column![
|
||||||
text(crate::tr!("options-theme-section")).size(15),
|
text(crate::tr!("options-theme-section")).size(15),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
|
|
@ -175,11 +224,96 @@ pub fn view_window<'a>(
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
Space::new().height(12),
|
Space::new().height(12),
|
||||||
color_controls,
|
color_controls,
|
||||||
|
Space::new().height(24),
|
||||||
|
text(crate::t!("Crosshair")).size(15),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Crosshair size")).size(12).width(140),
|
||||||
|
slider(1..=100, cursor_size.clamp(1, 100), Message::CursorSizeChanged)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{}%", cursor_size.clamp(1, 100)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Cursor type")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
selected_cursor,
|
||||||
|
cursor_options,
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::CursorTypeChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Crosshair color")).size(12).width(140),
|
||||||
|
crosshair_swatch,
|
||||||
|
text_input(crate::t!("#RRGGBB or blank").as_ref(), crosshair_color_input)
|
||||||
|
.on_input(Message::CrosshairColorChanged)
|
||||||
|
.width(150),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(6),
|
||||||
|
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
|
||||||
|
.size(11)
|
||||||
|
.width(sizing.width),
|
||||||
]
|
]
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.width(sizing.width);
|
.width(sizing.width);
|
||||||
|
|
||||||
|
let selection = column![
|
||||||
|
text(crate::t!("Selection")).size(15),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Pick box size")).size(12).width(140),
|
||||||
|
slider(0..=50, pick_box.clamp(0, 50), Message::PickBoxChanged)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(pick_box.clamp(0, 50).to_string()).size(11).width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(8),
|
||||||
|
text(crate::t!(
|
||||||
|
"Controls both the visible selection box and the click aperture."
|
||||||
|
))
|
||||||
|
.size(11)
|
||||||
|
.width(sizing.width),
|
||||||
|
]
|
||||||
|
.spacing(0)
|
||||||
|
.width(sizing.width);
|
||||||
|
|
||||||
|
let content: Element<'a, Message> = match active_tab {
|
||||||
|
OptionsTab::General => general.into(),
|
||||||
|
OptionsTab::Display => display.into(),
|
||||||
|
OptionsTab::Selection => selection.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_button = |label, tab| {
|
||||||
|
let selected = active_tab == tab;
|
||||||
|
button(text(label).size(12))
|
||||||
|
.on_press(Message::OptionsTabChanged(tab))
|
||||||
|
.padding([6, 14])
|
||||||
|
.style(if selected { button::primary } else { button::secondary })
|
||||||
|
};
|
||||||
|
let tabs = row![
|
||||||
|
tab_button(crate::t!("General"), OptionsTab::General),
|
||||||
|
tab_button(crate::t!("Display"), OptionsTab::Display),
|
||||||
|
tab_button(crate::t!("Selection"), OptionsTab::Selection),
|
||||||
|
]
|
||||||
|
.spacing(6);
|
||||||
|
|
||||||
let body = column![
|
let body = column![
|
||||||
|
tabs,
|
||||||
|
Space::new().height(12),
|
||||||
// Keep the scrollbar in its own lane instead of floating over the
|
// Keep the scrollbar in its own lane instead of floating over the
|
||||||
// controls at the trailing edge of the Options content.
|
// controls at the trailing edge of the Options content.
|
||||||
scrollable(content).spacing(8).height(sizing.height),
|
scrollable(content).spacing(8).height(sizing.height),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue