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:
Hakan Seven committed 2026-08-08 21:32:44 +03:00
1 parent 188ac80b2e
commit 1738f9a79b
25 files changed
+989 -126

No files matched your search

+37
View File
@@ -224,6 +224,43 @@ impl OpenCADStudio {
"SNAP" => {
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" => {
return Some(Task::done(Message::TogglePolar));
+6
View File
@@ -301,6 +301,8 @@ inventory::submit!(crate::command::CommandRegistration {
"CUI",
"DSETTINGS",
"GRID",
"ISODRAFT",
"ISOPLANE",
"OSNAP",
"POLAR",
"QUICKPROPERTIES",
@@ -448,6 +450,10 @@ inventory::submit!(crate::command::CommandRegistration {
"MIRRTEXT",
"ZOOMWHEEL",
"ZOOMFACTOR",
"CURSORSIZE",
"PICKBOX",
"CURSORTYPE",
"SNAPANG",
"ATTREQ",
"ATTDIA",
"DIMASSOC",
+64 -2
View File
@@ -868,6 +868,10 @@ impl OpenCADStudio {
"MIRRTEXT"
| "ZOOMWHEEL"
| "ZOOMFACTOR"
| "CURSORSIZE"
| "PICKBOX"
| "CURSORTYPE"
| "SNAPANG"
| "TEXTFILL"
| "ATTREQ"
| "ATTDIA"
@@ -938,7 +942,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
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 {
// Parse a boolean given as 0/1 or ON/OFF.
@@ -1061,6 +1065,56 @@ impl OpenCADStudio {
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
// TrueType text. The active tab re-tessellates below.
"TEXTFILL" => match &value {
@@ -1612,7 +1666,15 @@ impl OpenCADStudio {
match outcome {
Ok((msg, 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();
} else {
self.tabs[i].dirty = true;
+2 -2
View File
@@ -209,11 +209,11 @@ fn rgb_to_color(rgb: [u8; 3]) -> iced::Color {
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])
}
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());
if value.len() != 6 {
return None;
+65 -13
View File
@@ -260,19 +260,56 @@ pub(super) fn ucs_rotated_z(origin: glam::DVec3, angle_z: f32) -> Ucs {
// ── Drawing constraint helpers ─────────────────────────────────────────────
/// Constrain `pt` to the nearest 90° direction from `base`, in the active UCS
/// plane — ortho follows the user's coordinate system, not world axes. `xf` is
/// identity for plain WCS, so the world-XY behaviour is unchanged there.
pub(super) fn ortho_constrain(pt: glam::DVec3, base: glam::DVec3, xf: &UcsXform) -> glam::DVec3 {
/// The two live drafting directions in degrees inside the active UCS plane.
pub(super) fn drafting_angles(
isometric: bool,
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 b = xf.to_ucs(base);
let dx = (p.x - b.x).abs();
let dy = (p.y - b.y).abs();
let c = if dx >= dy {
glam::DVec3::new(p.x, b.y, p.z)
} else {
glam::DVec3::new(b.x, p.y, p.z)
};
let delta = glam::DVec2::new(p.x - b.x, p.y - b.y);
let [a, c] = 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(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)
}
@@ -341,6 +378,9 @@ pub(super) fn axis_lock_capture(
polar: bool,
step_deg: f32,
xf: &UcsXform,
isometric: bool,
iso_plane: super::settings::IsoPlane,
snap_angle_deg: f32,
) -> Option<glam::DVec3> {
let p = xf.to_ucs(cursor);
let b = xf.to_ucs(base);
@@ -349,8 +389,20 @@ pub(super) fn axis_lock_capture(
if dx.hypot(dy) < 1e-9 {
return None;
}
let step = (if polar { step_deg as f64 } else { 90.0 }).to_radians();
let ang = (dy.atan2(dx) / step).round() * step;
let ang = if polar {
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 = xf.to_wcs(b + dir_ucs) - xf.to_wcs(b);
(dir.length_squared() > 1e-12).then(|| dir.normalize())
+46 -1
View File
@@ -18,7 +18,7 @@ mod mtext_editor;
pub mod plugin_host;
mod properties;
mod recent;
mod settings;
pub(crate) mod settings;
mod shortcuts;
mod style_ops;
mod text_inline;
@@ -448,10 +448,27 @@ pub(super) struct OpenCADStudio {
zoom_wheel_reversed: bool,
/// Mouse-wheel zoom sensitivity, clamped to 3..=100 (ZOOMFACTOR).
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: bool,
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
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).
pub texteditmode: bool,
/// When true (default), saving over an existing file first writes a `.bak`
@@ -1502,6 +1519,7 @@ pub enum ModalKind {
LayerStateManager,
LayerTranslator,
DrawingUnits,
DraftingSettings,
LayerStateEditor,
Plot,
PrintAll,
@@ -1768,6 +1786,16 @@ pub enum Message {
SaveDialogPathPicked(Option<std::path::PathBuf>),
/// Open the application-wide Options dialog.
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.
DefaultSaveFormatChanged(String),
/// Select one of Iced's built-in themes or the editable Custom theme.
@@ -2094,6 +2122,14 @@ pub enum Message {
ToggleSnapEnabled,
/// Toggle grid-snap on/off — F9 / SNAP status-bar button.
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).
ToggleViewCube,
/// Toggle the Properties panel visibility (PROPERTIES).
@@ -3036,8 +3072,17 @@ impl OpenCADStudio {
polar_increment_deg: 45.0,
zoom_wheel_reversed: false,
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,
dyn_input: true,
options_tab: crate::ui::window::options::OptionsTab::General,
texteditmode: false,
backup_on_save: true,
file_assoc_enabled: true,
+77
View File
@@ -11,6 +11,63 @@
use crate::snap::SnapType;
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
/// deterministic order when decoding the `$OSMODE` bitmask (see
/// [`snaps_from_osmode`]).
@@ -99,6 +156,19 @@ pub struct UserSettings {
pub polar_increment_deg: f32,
pub zoom_wheel_reversed: bool,
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,
// Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
// 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,
zoom_wheel_reversed: false,
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,
default_assoc_prompted: false,
disabled_plugins: Vec::new(),
+1
View File
@@ -17,6 +17,7 @@ pub(super) fn default_bindings() -> BTreeMap<String, String> {
("F1".to_string(), "HELP"),
("F2".to_string(), "COMMANDHISTORY"),
("F3".to_string(), "TOGGLEOSNAP"),
("F5".to_string(), "ISOPLANE"),
("F7".to_string(), "GRID"),
("F8".to_string(), "ORTHO"),
("F9".to_string(), "SNAP"),
+1 -1
View File
@@ -4,7 +4,7 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
+1 -1
View File
@@ -5,7 +5,7 @@ use super::util::*;
use crate::ui::window::block_palette::BlockPaletteMsg;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
+1 -1
View File
@@ -4,7 +4,7 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
+26 -1
View File
@@ -4,7 +4,7 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
@@ -267,6 +267,13 @@ impl OpenCADStudio {
polar_increment_deg: self.polar_increment_deg,
zoom_wheel_reversed: self.zoom_wheel_reversed,
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,
default_assoc_prompted: self.default_assoc_prompted,
disabled_plugins: {
@@ -303,6 +310,21 @@ impl OpenCADStudio {
self.polar_increment_deg = s.polar_increment_deg;
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
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
// open / tab switch), not app-global, so they are not applied here.
self.snapper.otrack_enabled = s.otrack;
@@ -588,6 +610,9 @@ impl OpenCADStudio {
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot;
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
+85 -1
View File
@@ -149,6 +149,9 @@ impl OpenCADStudio {
if self.active_modal == Some(ScaleManager) {
self.scale_stage_discard();
}
if self.active_modal == Some(DraftingSettings) {
self.snap_popup_open = false;
}
#[cfg(not(target_arch = "wasm32"))]
if self.active_modal == Some(FileInUse) {
self.pending_save_failure = None;
@@ -3014,6 +3017,43 @@ impl OpenCADStudio {
self.sync_vport_display(self.active_tab);
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 => {
self.show_grid ^= true;
self.sync_vport_display(self.active_tab);
@@ -3654,11 +3694,20 @@ impl OpenCADStudio {
Task::none()
}
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()
}
Message::CloseSnapPopup => {
self.snap_popup_open = false;
if self.active_modal == Some(super::ModalKind::DraftingSettings) {
self.close_active_modal();
}
Task::none()
}
Message::SnapSelectAll => {
@@ -5149,6 +5198,41 @@ impl OpenCADStudio {
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) => {
self.default_save_format =
crate::io::canonical_save_format(&format).to_string();
+1 -1
View File
@@ -4,7 +4,7 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
+76 -22
View File
@@ -4,7 +4,7 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
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,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
@@ -171,6 +171,26 @@ impl OpenCADStudio {
.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
/// on load and whenever the active tab or viewport changes, so the grid
/// drawing follows the active viewport.
@@ -1014,7 +1034,7 @@ impl OpenCADStudio {
// snapping must drop its foot from this point, including when a
// hot-grip set is moved by the same drag vector.
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),
// and the wires are model space, so the snap result is model.
let snap_hit =
@@ -1044,7 +1064,7 @@ impl OpenCADStudio {
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(
raw,
@@ -1054,8 +1074,8 @@ impl OpenCADStudio {
polar_step,
Some(grip.origin_world),
self.ortho_mode,
ucs_x,
ucs_y,
ucs_x.as_dvec3(),
ucs_y.as_dvec3(),
)
} else {
None
@@ -1086,6 +1106,9 @@ impl OpenCADStudio {
self.polar_mode,
self.polar_increment_deg,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
);
}
} else {
@@ -1099,7 +1122,14 @@ impl OpenCADStudio {
let base = grip.origin_world;
let ucs_xf = self.tabs[i].ucs_xform();
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 {
snapped = polar_constrain_near(
snapped,
@@ -1423,7 +1453,7 @@ impl OpenCADStudio {
bounds,
)
} 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
// base only matters for typed-input precision, so hand it
// the downcast point here.
@@ -1472,7 +1502,7 @@ impl OpenCADStudio {
} else {
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(
cursor_world,
view_rot,
@@ -1481,8 +1511,8 @@ impl OpenCADStudio {
step,
self.last_point,
self.ortho_mode && !is_window_corner,
ucs_x,
ucs_y,
ucs_x.as_dvec3(),
ucs_y.as_dvec3(),
)
} else {
None
@@ -1545,6 +1575,9 @@ impl OpenCADStudio {
self.polar_mode,
self.polar_increment_deg,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
);
}
} else if !self.shift_down {
@@ -1568,7 +1601,14 @@ impl OpenCADStudio {
if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform();
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 {
pt = polar_constrain_near(
pt,
@@ -1738,6 +1778,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -2077,7 +2118,7 @@ impl OpenCADStudio {
self.snapper.grid_spacing = grid_spacing;
// No rubber-band origin (perp/extension feet don't apply to a free drag).
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
.snapper
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
@@ -2740,7 +2781,7 @@ impl OpenCADStudio {
bounds,
)
} 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.snap(
snap_cursor,
@@ -2779,7 +2820,7 @@ impl OpenCADStudio {
} else {
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(
raw,
view_rot,
@@ -2788,8 +2829,8 @@ impl OpenCADStudio {
step,
self.last_point,
self.ortho_mode && !is_window_corner,
ucs_x,
ucs_y,
ucs_x.as_dvec3(),
ucs_y.as_dvec3(),
)
} else {
None
@@ -2808,7 +2849,14 @@ impl OpenCADStudio {
if let Some(base) = self.last_point {
let ucs_xf = self.tabs[i].ucs_xform();
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 {
pt = polar_constrain_near(
pt,
@@ -2893,7 +2941,7 @@ impl OpenCADStudio {
view_rot2,
eye2,
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]
.active_cmd
@@ -2914,6 +2962,7 @@ impl OpenCADStudio {
eye2,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -3442,7 +3491,7 @@ impl OpenCADStudio {
view_rot,
eye,
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]
.scene
@@ -3462,6 +3511,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.into_iter()
.filter_map(|s| Scene::handle_from_wire_name(s))
@@ -3483,6 +3533,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -3824,7 +3875,7 @@ impl OpenCADStudio {
view_rot,
eye,
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]
.scene
@@ -3839,6 +3890,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -3943,7 +3995,7 @@ impl OpenCADStudio {
view_rot,
eye,
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(
p,
@@ -3952,6 +4004,7 @@ impl OpenCADStudio {
eye,
bounds,
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(|h| {
@@ -4357,7 +4410,7 @@ impl OpenCADStudio {
view_rot,
eye,
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_count = hover_candidates.len();
@@ -4377,6 +4430,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
self.pick_box.max(1) as f32,
)
.and_then(Scene::handle_from_wire_name);
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
+31 -1
View File
@@ -335,6 +335,14 @@ impl OpenCADStudio {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let model_basis = {
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()))
};
let grid: Vec<crate::ui::overlay::GridParams> = tab
@@ -342,7 +350,7 @@ impl OpenCADStudio {
.grid_views(vw, vh)
.into_iter()
.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) {
Some(u) => {
let (o, ux, uy, uz) =
@@ -357,6 +365,17 @@ impl OpenCADStudio {
} else {
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 {
view_rot: cam.view_proj_rte(bounds),
eye: cam.eye(),
@@ -684,6 +703,15 @@ impl OpenCADStudio {
self.ribbon.open_dropdown.is_some(),
hover_locked,
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.dyn_input,
self.snapper.otrack_enabled,
self.isometric_drafting,
self.iso_plane,
layout_names.clone(),
block_tabs,
layout_names.into_iter().skip(1).collect(),
+26
View File
@@ -21,6 +21,7 @@ impl OpenCADStudio {
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").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::Plot) => crate::tr!("modal-plot"),
Some(K::PrintAll) => t!("Print All").into_owned(),
@@ -96,6 +97,31 @@ impl OpenCADStudio {
&self.ui_theme,
&self.theme_color_inputs,
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,
)
},