Merge upstream/main into fix/704-active-intersection-snap

This commit is contained in:
gianlucafiore committed 2026-08-08 22:13:54 -03:00
commit 17b4eafa3b
71 files changed
+4887 -2862

No files matched your search

+2
View File
@@ -100,6 +100,7 @@ impl OpenCADStudio {
self.ucs_icon_hover = false;
self.tabs[i].pan_mode = false;
self.tabs[i].orbit_mode = false;
self.tabs[i].zoom_dynamic_mode = false;
let _ = self.on_viewport_exit();
}
@@ -1939,6 +1940,7 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.remember_current_view();
self.tabs[i]
.scene
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
+70 -13
View File
@@ -805,40 +805,94 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZEA" => {
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZOOM EA" | "ZEA" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all_model_viewports();
self.command_line.push_output(crate::t!("Zoom Extents — All Viewports").as_ref());
}
"ZOOM EXTENTS" | "ZOOMEXTENTS" | "ZE" => {
"ZOOM EXTENTS" | "ZOOM E" | "ZOOMEXTENTS" | "ZE" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all();
self.command_line.push_output(crate::t!("Zoom Extents").as_ref());
}
"ZOOM IN" | "ZI" => {
"ZOOM IN" | "ZOOM I" | "ZI" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.zoom_camera(1.0 / 1.5);
self.command_line.push_output(crate::t!("Zoom In").as_ref());
}
"ZOOM OUT" | "ZO" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.zoom_camera(1.5);
self.command_line.push_output(crate::t!("Zoom Out").as_ref());
}
// ZOOM ALL — fit the configured drawing limits.
"ZOOM ALL" | "ZOOM A" | "ZA" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all_with_limits();
self.command_line.push_output(crate::t!("Zoom All").as_ref());
}
"ZOOM PREVIOUS" | "ZOOM P" | "ZP" => {
if self.tabs[i].scene.restore_previous_view() {
self.command_line.push_output(crate::t!("Zoom Previous").as_ref());
} else {
self.command_line
.push_error(crate::t!("ZOOM: no previous view.").as_ref());
}
}
"ZOOM OBJECT" | "ZOOM O" | "ZOBJ" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(handle, _)| handle)
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let command = SelectObjectsCommand::new("ZOOM OBJECT");
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
self.tabs[i].scene.remember_current_view();
if self.tabs[i].scene.zoom_to_entities(&handles) {
self.command_line.push_output(crate::t!("Zoom Object").as_ref());
} else {
self.command_line.push_error(
crate::t!("ZOOM: selected objects have no visible bounds.").as_ref(),
);
}
}
}
"ZOOM DYNAMIC" | "ZOOM D" | "ZD" => {
self.tabs[i].zoom_dynamic_mode = true;
self.clear_navigation_hover(i);
self.command_line.push_output(
crate::t!(
"ZOOM Dynamic: drag horizontally to pan and vertically to zoom. Press Esc to exit."
)
.as_ref(),
);
}
// ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5")
cmd if cmd.starts_with("ZOOM SCALE ") || cmd.starts_with("ZS ") => {
cmd if cmd.starts_with("ZOOM SCALE ")
|| cmd.starts_with("ZOOM S ")
|| cmd.starts_with("ZS ") =>
{
let rest = cmd
.split_once(' ')
.and_then(|(_, r)| r.split_once(' ').map(|(_, v)| v).or(Some(r)))
.strip_prefix("ZOOM SCALE ")
.or_else(|| cmd.strip_prefix("ZOOM S "))
.or_else(|| cmd.strip_prefix("ZS "))
.unwrap_or("1");
if let Ok(factor) = rest.trim().parse::<f32>() {
if factor > 0.0 {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.zoom_camera(1.0 / factor);
self.command_line
.push_output(crate::tf!("Zoom Scale ×{factor:.3}").as_ref());
@@ -866,15 +920,18 @@ impl OpenCADStudio {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"ZOOM",
"ZOOM [Window / Extents / Extents All / All / In / Out / Scale]:",
"ZOOM [Window / Extents / Previous / Object / All / Dynamic / Extents All / In / Out / Scale]:",
vec![
("Window", "WINDOW", None),
("Extents", "EXTENTS", None),
("Extents All", "EXTENTS ALL", None),
("All", "ALL", None),
("In", "IN", None),
("Window", "W", None),
("Extents", "E", None),
("Previous", "P", None),
("Object", "O", None),
("All", "A", None),
("Dynamic", "D", None),
("Extents All", "EA", None),
("In", "I", None),
("Out", "OUT", None),
("Scale", "SCALE", Some("ZOOM scale factor (e.g. 2 or 0.5):")),
("Scale", "S", Some("ZOOM scale factor (e.g. 2 or 0.5):")),
],
);
self.command_line.push_info(&c.prompt());
+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));
+9
View File
@@ -98,6 +98,7 @@ impl OpenCADStudio {
// command arms below re-enable the selected one).
self.tabs[i].pan_mode = false;
self.tabs[i].orbit_mode = false;
self.tabs[i].zoom_dynamic_mode = false;
// Reset the last committed point so the first click of the new command
// is not constrained by ortho/polar relative to a previous command's endpoint.
self.last_point = None;
@@ -300,6 +301,8 @@ inventory::submit!(crate::command::CommandRegistration {
"CUI",
"DSETTINGS",
"GRID",
"ISODRAFT",
"ISOPLANE",
"OSNAP",
"POLAR",
"QUICKPROPERTIES",
@@ -445,6 +448,12 @@ inventory::submit!(crate::command::CommandRegistration {
"FITSPLINE",
// System variables (typeable directly).
"MIRRTEXT",
"ZOOMWHEEL",
"ZOOMFACTOR",
"CURSORSIZE",
"PICKBOX",
"CURSORTYPE",
"SNAPANG",
"ATTREQ",
"ATTDIA",
"DIMASSOC",
+101 -2
View File
@@ -866,6 +866,12 @@ impl OpenCADStudio {
cmd if matches!(
cmd.split_whitespace().next().unwrap_or(""),
"MIRRTEXT"
| "ZOOMWHEEL"
| "ZOOMFACTOR"
| "CURSORSIZE"
| "PICKBOX"
| "CURSORTYPE"
| "SNAPANG"
| "TEXTFILL"
| "ATTREQ"
| "ATTDIA"
@@ -936,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 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.
@@ -1028,6 +1034,87 @@ impl OpenCADStudio {
.ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
None => Ok((format!("MIRRTEXT = {}", h.mirror_text as i32), false)),
},
"ZOOMWHEEL" => match &value {
Some(v) => match parse_bool(v) {
Some(reversed) => {
self.zoom_wheel_reversed = reversed;
Ok((
format!("ZOOMWHEEL = {}", reversed as i32),
true,
))
}
None => Err("SETVAR: 0 or 1 required.".into()),
},
None => Ok((
format!(
"ZOOMWHEEL = {}",
self.zoom_wheel_reversed as i32
),
false,
)),
},
"ZOOMFACTOR" => match &value {
Some(v) => match v.parse::<i32>() {
Ok(factor) if (3..=100).contains(&factor) => {
self.zoom_factor = factor;
Ok((format!("ZOOMFACTOR = {factor}"), true))
}
_ => Err("SETVAR: integer from 3 to 100 required.".into()),
},
None => {
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 {
@@ -1579,7 +1666,19 @@ impl OpenCADStudio {
match outcome {
Ok((msg, changed)) => {
if changed {
self.tabs[i].dirty = true;
if matches!(
name.as_str(),
"ZOOMWHEEL"
| "ZOOMFACTOR"
| "CURSORSIZE"
| "PICKBOX"
| "CURSORTYPE"
| "SNAPANG"
) {
self.persist_settings_if_changed();
} else {
self.tabs[i].dirty = true;
}
self.command_line.push_output(&msg);
} else {
// Queried with no value (`changed == false`):
+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;
+4
View File
@@ -212,6 +212,9 @@ pub(super) struct DocumentTab {
/// Interactive 3-D orbit mode. While active, a left-button drag follows
/// the same camera-orbit path as Shift + middle-button drag.
pub(super) orbit_mode: bool,
/// Interactive ZOOM Dynamic mode. A left drag pans horizontally and zooms
/// vertically until Escape or another command ends the mode.
pub(super) zoom_dynamic_mode: bool,
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
@@ -476,6 +479,7 @@ impl DocumentTab {
is_start: false,
pan_mode: false,
orbit_mode: false,
zoom_dynamic_mode: false,
plugin_state: HashMap::new(),
suspended_cmd: 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())
+52 -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;
@@ -444,10 +444,31 @@ pub(super) struct OpenCADStudio {
polar_mode: bool,
/// Polar tracking angle increment in degrees (15 / 30 / 45 / 90).
polar_increment_deg: f32,
/// Reverse the mouse-wheel zoom direction when true (ZOOMWHEEL = 1).
zoom_wheel_reversed: bool,
/// Mouse-wheel zoom sensitivity, clamped to 3..=100 (ZOOMFACTOR).
zoom_factor: i32,
/// Crosshair size setting (CURSORSIZE, 1..=100).
cursor_size: i32,
/// Selection-box size setting (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`
@@ -1498,6 +1519,7 @@ pub enum ModalKind {
LayerStateManager,
LayerTranslator,
DrawingUnits,
DraftingSettings,
LayerStateEditor,
Plot,
PrintAll,
@@ -1764,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.
@@ -2090,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).
@@ -3030,8 +3070,19 @@ impl OpenCADStudio {
ortho_mode: false,
polar_mode: false,
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,
+81
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`]).
@@ -97,6 +154,21 @@ pub struct UserSettings {
pub dyn_input: bool,
pub polar: bool,
pub polar_increment_deg: f32,
pub zoom_wheel_reversed: bool,
pub zoom_factor: i32,
/// CURSORSIZE: normalized crosshair reach; 5 retains the original 60 px arms.
pub cursor_size: i32,
/// PICKBOX: normalized visible-box and click-aperture size.
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-
@@ -166,6 +238,15 @@ impl Default for UserSettings {
dyn_input: true,
polar: false,
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"),
+7 -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};
@@ -660,16 +660,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// drag. Orbit exits silently; PAN keeps its existing message.
if self.tabs[self.active_tab].pan_mode
|| self.tabs[self.active_tab].orbit_mode
|| self.tabs[self.active_tab].zoom_dynamic_mode
{
let i = self.active_tab;
let was_pan = self.tabs[i].pan_mode;
self.tabs[i].pan_mode = false;
self.tabs[i].orbit_mode = false;
self.tabs[i].zoom_dynamic_mode = false;
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = false;
sel.middle_last_pos = None;
sel.orbit_pivot = None;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing_locked = false;
}
if was_pan {
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
+2 -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};
@@ -143,6 +143,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
&& self.active_modal.is_none()
&& !self.tabs[i].pan_mode
&& !self.tabs[i].orbit_mode
&& !self.tabs[i].zoom_dynamic_mode
{
self.ribbon.deactivate_tool();
}
+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};
+30 -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};
@@ -265,6 +265,15 @@ impl OpenCADStudio {
dyn_input: self.dyn_input,
polar: self.polar_mode,
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: {
@@ -299,6 +308,23 @@ impl OpenCADStudio {
self.dyn_input = s.dyn_input;
self.polar_mode = s.polar;
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;
@@ -584,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
+88 -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;
@@ -2831,6 +2834,7 @@ impl OpenCADStudio {
Message::ViewCubeHome => {
let i = self.active_tab;
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
let r_ucs = self.tabs[i].scene.viewcube_ucs_mat();
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i]
@@ -2847,6 +2851,7 @@ impl OpenCADStudio {
Message::ViewCubeRoll(cw) => {
let i = self.active_tab;
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
let ang = if cw {
std::f32::consts::FRAC_PI_2
} else {
@@ -2873,6 +2878,7 @@ impl OpenCADStudio {
};
let i = self.active_tab;
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i]
.scene
@@ -3011,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);
@@ -3651,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 => {
@@ -5146,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};
+169 -32
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.
@@ -739,6 +759,39 @@ impl OpenCADStudio {
if mid_down {
if let Some(last) = mid_last {
let (dx, dy) = (p.x - last.x, p.y - last.y);
if self.tabs[i].zoom_dynamic_mode {
let bounds = self.tabs[i]
.scene
.active_model_tile_bounds(vp_size.0, vp_size.1);
drop(sel);
let zoom_delta = -dy * 0.03;
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i].scene.pan_active_viewport(dx, 0.0, bounds);
self.tabs[i]
.scene
.zoom_active_viewport(zoom_delta, None);
} else {
let local = iced::Point {
x: p.x - bounds.x,
y: p.y - bounds.y,
};
let local_bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: bounds.width,
height: bounds.height,
};
let mut camera = self.tabs[i].scene.camera.borrow_mut();
camera.pan_screen(dx, 0.0, bounds.height);
camera.zoom_about_point(local, local_bounds, zoom_delta);
}
self.tabs[i].scene.camera_generation += 1;
self.tabs[i]
.scene
.record_nav_perf(crate::scene::NavPerfOp::Zoom, move_started);
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
return Task::none();
}
// Shift+MMB drag orbits the model view instead of panning
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
// scheme (#229). Floating viewports and paper keep the
@@ -981,13 +1034,24 @@ 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();
self.snapper.from_point = Some(grip.origin_world.as_vec3());
// Keep the updated drafting basis from main so grid/snap follows the
// current drafting axes, including isometric drafting and SNAPANG.
let (go, gr) = self.drafting_grid_basis(i);
let base = grip.origin_world;
let ucs_xf = self.tabs[i].ucs_xform();
let mut construction_cursor = if self.ortho_mode {
Some(ortho_constrain(raw, base, &ucs_xf))
Some(drafting_constrain(
raw,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
raw,
@@ -1000,7 +1064,8 @@ impl OpenCADStudio {
&ucs_xf,
);
// POLAR must actually be engaged on one of its rays.
// POLAR being enabled is not enough: the cursor must actually
// be engaged on one of the polar tracking rays.
let delta = pt - base;
let step = (self.polar_increment_deg as f64).to_radians();
let angle = delta.y.atan2(delta.x);
@@ -1023,7 +1088,6 @@ impl OpenCADStudio {
pt.z = base.z;
}
}
// `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 = self.snapper.snap(
@@ -1061,7 +1125,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,
@@ -1071,8 +1135,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
@@ -1103,6 +1167,9 @@ impl OpenCADStudio {
self.polar_mode,
self.polar_increment_deg,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
);
}
} else {
@@ -1116,7 +1183,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,
@@ -1440,9 +1514,10 @@ impl OpenCADStudio {
bounds,
)
} else {
let (go, gr) = self.tabs[i].ucs_grid_basis();
// The point the active rubber-band starts from.
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.
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
// Build the temporary construction direction before OSNAP.
@@ -1455,7 +1530,14 @@ impl OpenCADStudio {
let ucs_xf = self.tabs[i].ucs_xform();
let mut pt = if self.ortho_mode {
Some(ortho_constrain(snap_cursor, base, &ucs_xf))
Some(drafting_constrain(
snap_cursor,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
snap_cursor,
@@ -1540,7 +1622,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,
@@ -1549,8 +1631,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
@@ -1613,6 +1695,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 {
@@ -1636,7 +1721,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,
@@ -1806,6 +1898,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -2145,7 +2238,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, None);
@@ -2389,11 +2482,21 @@ impl OpenCADStudio {
// Interactive navigation tools reuse the middle-button movement path,
// so no selection/pick logic runs while the left button drives them.
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
if self.tabs[i].orbit_mode
|| self.tabs[i].pan_mode
|| self.tabs[i].zoom_dynamic_mode
{
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = true;
sel.middle_last_pos = Some(p);
if self.tabs[i].zoom_dynamic_mode {
sel.box_anchor = Some(p);
sel.box_current = Some(p);
sel.box_crossing = false;
sel.box_crossing_locked = true;
}
return Task::none();
}
@@ -2561,11 +2664,18 @@ impl OpenCADStudio {
// Navigation mode: end this drag but keep the tool armed for the next
// left drag (exit is Esc / another command). Mirror of the press.
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
if self.tabs[i].orbit_mode
|| self.tabs[i].pan_mode
|| self.tabs[i].zoom_dynamic_mode
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = false;
sel.middle_last_pos = None;
sel.orbit_pivot = None;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing_locked = false;
drop(sel);
self.arm_hover_after_navigation(i);
return Task::none();
@@ -2791,7 +2901,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());
let construction_cursor = if is_window_corner {
@@ -2801,7 +2911,14 @@ impl OpenCADStudio {
let ucs_xf = self.tabs[i].ucs_xform();
let mut pt = if self.ortho_mode {
Some(ortho_constrain(snap_cursor, base, &ucs_xf))
Some(drafting_constrain(
snap_cursor,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
snap_cursor,
@@ -2879,7 +2996,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,
@@ -2888,8 +3005,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
@@ -2908,7 +3025,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,
@@ -2993,7 +3117,7 @@ impl OpenCADStudio {
view_rot2,
eye2,
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
let include_fills = self.tabs[i]
.active_cmd
@@ -3014,6 +3138,7 @@ impl OpenCADStudio {
eye2,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -3542,7 +3667,7 @@ impl OpenCADStudio {
view_rot,
eye,
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
let candidate_handles = self.tabs[i]
.scene
@@ -3562,6 +3687,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.into_iter()
.filter_map(|s| Scene::handle_from_wire_name(s))
@@ -3583,6 +3709,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -3924,7 +4051,7 @@ impl OpenCADStudio {
view_rot,
eye,
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
let candidate_handles = self.tabs[i]
.scene
@@ -3939,6 +4066,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
@@ -4043,7 +4171,7 @@ impl OpenCADStudio {
view_rot,
eye,
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
scene::pick::hit_test::click_hit(
p,
@@ -4052,6 +4180,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.and_then(|h| {
@@ -4157,6 +4286,7 @@ impl OpenCADStudio {
let i = self.active_tab;
self.clear_navigation_hover(i);
self.ribbon.close_dropdown();
self.tabs[i].scene.remember_current_view();
let now = Instant::now();
let is_double = {
let sel = self.tabs[i].scene.selection.borrow();
@@ -4185,11 +4315,16 @@ impl OpenCADStudio {
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
let nav_started = Instant::now();
let s = match delta {
let mut s = match delta {
mouse::ScrollDelta::Lines { y, .. } => y,
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
};
s *= self.zoom_factor as f32 / 60.0;
if self.zoom_wheel_reversed {
s = -s;
}
let i = self.active_tab;
self.tabs[i].scene.remember_current_view();
self.clear_navigation_hover(i);
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
@@ -4354,6 +4489,7 @@ impl OpenCADStudio {
fn snap_view_region(&mut self, region: CubeRegion, r_ucs: glam::Mat4) -> Task<Message> {
let i = self.active_tab;
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
let mut region = region;
// "Already there → flip to opposite" check: compare the
// current gaze direction with the region's target gaze.
@@ -4450,7 +4586,7 @@ impl OpenCADStudio {
view_rot,
eye,
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
let candidate_ms = candidate_started.elapsed().as_secs_f64() * 1000.0;
let candidate_count = hover_candidates.len();
@@ -4470,6 +4606,7 @@ impl OpenCADStudio {
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(Scene::handle_from_wire_name);
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
+32 -2
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(),
@@ -680,10 +699,19 @@ impl OpenCADStudio {
dividers,
pane_move_rect,
pane_drop_rect,
tab.pan_mode || tab.orbit_mode,
tab.pan_mode || tab.orbit_mode || tab.zoom_dynamic_mode,
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,
)
},