diff --git a/src/app/automation.rs b/src/app/automation.rs index 65a541185..951bb5c37 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -505,6 +505,59 @@ mod tests { ); } + #[test] + fn translated_and_rotated_ucs_resolves_absolute_relative_and_polar_input() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 100,200,300"}"#); + app.automation_op(r#"{"op":"run","cmd":"UCS Z 90"}"#); + app.automation_op(r#"{"op":"run","cmd":"LINE 2,3 @5<0"}"#); + + let line = app.tabs[app.active_tab] + .scene + .document + .entities() + .find_map(|entity| match entity { + acadrust::EntityType::Line(line) => Some(line), + _ => None, + }) + .expect("LINE should create one segment"); + let close = |a: f64, b: f64| (a - b).abs() < 1e-9; + assert!(close(line.start.x, 97.0)); + assert!(close(line.start.y, 202.0)); + assert!(close(line.start.z, 300.0)); + assert!(close(line.end.x, 97.0)); + assert!(close(line.end.y, 207.0)); + assert!(close(line.end.z, 300.0)); + } + + #[test] + fn tilted_ucs_places_planar_entities_with_the_plane_normal() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.automation_op( + r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#, + ); + app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#); + + let circle = app.tabs[app.active_tab] + .scene + .document + .entities() + .find_map(|entity| match entity { + acadrust::EntityType::Circle(circle) => Some(circle), + _ => None, + }) + .expect("CIRCLE should create one entity"); + let close = |a: f64, b: f64| (a - b).abs() < 1e-9; + assert!(close(circle.center.x, 2.0)); + assert!(close(circle.center.y, 0.0)); + assert!(close(circle.center.z, 3.0)); + assert!(close(circle.normal.x, 0.0)); + assert!(close(circle.normal.y, -1.0)); + assert!(close(circle.normal.z, 0.0)); + } + #[test] fn value_prompt_commands_inline_args() { // A single-value setting command entered with its value on one line diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index c9468f776..a3657874b 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1888,6 +1888,7 @@ impl OpenCADStudio { &handles, &crate::command::EntityTransform::Rotate { center: glam::DVec3::ZERO, + axis: glam::DVec3::Z, angle_rad, }, ); diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index ebf477099..2d8435441 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -610,7 +610,12 @@ impl OpenCADStudio { if nums.len() < 3 { return Some(Task::none()); } - let place = glam::DVec3::new(nums[0], nums[1], nums[2]); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let place = plane.to_local(glam::DVec3::new(nums[0], nums[1], nums[2])); let handles: Vec = self.tabs[i] .scene .selected_entities() @@ -623,6 +628,9 @@ impl OpenCADStudio { qdim_collect_points(e, &mut pts); } } + for point in &mut pts { + *point = plane.to_local(*point); + } if pts.len() < 2 { self.command_line .push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref()); @@ -671,9 +679,10 @@ impl OpenCADStudio { dim.definition_point = v(def); dim.base.definition_point = v(def); dim.base.actual_measurement = dim.measurement(); - self.commit_entity(acadrust::EntityType::Dimension( + let entity = acadrust::EntityType::Dimension( acadrust::entities::Dimension::Linear(dim), - )); + ); + self.commit_entity(plane.place_entity(entity)); made += 1; } self.tabs[i].dirty = true; @@ -1325,6 +1334,13 @@ fn find_last_linear_dim( fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) { use acadrust::EntityType as ET; let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z); + let ocs = |point: (f64, f64, f64), normal: acadrust::types::Vector3| { + let world = crate::scene::view::transform::ocs_point_to_wcs( + point, + (normal.x, normal.y, normal.z), + ); + glam::DVec3::new(world.0, world.1, world.2) + }; match e { ET::Line(l) => { out.push(p(&l.start)); @@ -1332,11 +1348,12 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) { } ET::LwPolyline(pl) => { for v in &pl.vertices { - out.push(glam::DVec3::new( - v.location.x, - v.location.y, - pl.elevation, - )); + out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal)); + } + } + ET::Polyline2D(pl) => { + for v in &pl.vertices { + out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal)); } } ET::Polyline(pl) => { @@ -1346,10 +1363,13 @@ fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) { } ET::Arc(a) => { for &ang in &[a.start_angle, a.end_angle] { - out.push(glam::DVec3::new( - a.center.x + a.radius * ang.cos(), - a.center.y + a.radius * ang.sin(), - a.center.z, + out.push(ocs( + ( + a.center.x + a.radius * ang.cos(), + a.center.y + a.radius * ang.sin(), + a.center.z, + ), + a.normal, )); } } diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 5b52adf0e..550c81151 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -220,7 +220,7 @@ impl OpenCADStudio { self.command_line.push_error(crate::t!("Usage: UCS SAVE ").as_ref()); } else { // Save the current active UCS under this name. - let ucs = match &self.tabs[i].active_ucs { + let mut ucs = match &self.tabs[i].active_ucs { Some(u) => { let mut saved = u.clone(); saved.name = name.clone(); @@ -228,7 +228,23 @@ impl OpenCADStudio { } None => Ucs::new(&name), // save WCS (identity) }; - self.tabs[i].scene.document.ucss.add_or_replace(ucs); + ucs.handle = self.tabs[i] + .scene + .document + .ucss + .get(&name) + .map(|existing| existing.handle) + .filter(|handle| !handle.is_null()) + .unwrap_or_else(|| { + self.tabs[i].scene.document.allocate_handle() + }); + self.tabs[i] + .scene + .document + .ucss + .add_or_replace(ucs.clone()); + self.tabs[i].active_ucs = Some(ucs); + active_changed = true; self.tabs[i].dirty = true; self.command_line .push_output(crate::tf!("UCS '{}' saved.", name).as_ref()); @@ -238,7 +254,30 @@ impl OpenCADStudio { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { self.command_line.push_error(crate::t!("Usage: UCS DELETE ").as_ref()); - } else if self.tabs[i].scene.document.ucss.remove(&name).is_some() { + } else if let Some(removed) = + self.tabs[i].scene.document.ucss.remove(&name) + { + let removed_handle = removed.handle; + for entity in self.tabs[i].scene.document.entities_mut() { + if let acadrust::EntityType::Viewport(viewport) = entity { + if viewport.ucs_handle == removed_handle { + viewport.ucs_handle = acadrust::Handle::NULL; + } + } + } + let active_matches = self.tabs[i].active_ucs.as_ref().is_some_and(|ucs| { + (!removed_handle.is_null() && ucs.handle == removed_handle) + || ucs.name.eq_ignore_ascii_case(&name) + }); + if active_matches { + if let Some(active) = self.tabs[i].active_ucs.as_mut() { + active.name = "*ACTIVE*".to_string(); + active.handle = acadrust::Handle::NULL; + active.named_ucs_handle = acadrust::Handle::NULL; + active.base_ucs_handle = acadrust::Handle::NULL; + } + active_changed = true; + } self.tabs[i].dirty = true; self.command_line .push_output(crate::tf!("UCS '{}' deleted.", name).as_ref()); @@ -253,14 +292,73 @@ impl OpenCADStudio { self.command_line .push_output(crate::t!("UCS reset to World Coordinate System.").as_ref()); } + "VIEW" | "V" => { + let rotation = self.tabs[i].scene.active_camera_rotation(); + let origin = self.tabs[i] + .active_ucs + .as_ref() + .map(|ucs| ucs.origin) + .unwrap_or(Vector3::ZERO); + let x = rotation * glam::Vec3::X; + let y = rotation * glam::Vec3::Y; + let mut ucs = Ucs::new("*VIEW*"); + ucs.origin = origin; + ucs.x_axis = Vector3::new(x.x as f64, x.y as f64, x.z as f64); + ucs.y_axis = Vector3::new(y.x as f64, y.y as f64, y.z as f64); + self.tabs[i].active_ucs = Some(ucs); + active_changed = true; + self.command_line + .push_output(crate::t!("UCS aligned to the current view.").as_ref()); + } + "3POINTW" => { + let raw = parts.get(2).copied().unwrap_or(""); + let points: Vec = raw + .split('|') + .filter_map(|value| { + super::super::helpers::parse_coord(value).map(|(point, _)| point) + }) + .collect(); + if points.len() == 3 { + let x = (points[1] - points[0]).normalize_or_zero(); + let toward_y = points[2] - points[0]; + let z = x.cross(toward_y).normalize_or_zero(); + let y = z.cross(x).normalize_or_zero(); + if x.length_squared() > 1e-12 + && y.length_squared() > 1e-12 + && z.length_squared() > 1e-12 + { + let mut ucs = Ucs::new("*ACTIVE*"); + ucs.origin = Vector3::new(points[0].x, points[0].y, points[0].z); + ucs.x_axis = Vector3::new(x.x, x.y, x.z); + ucs.y_axis = Vector3::new(y.x, y.y, y.z); + self.tabs[i].active_ucs = Some(ucs); + active_changed = true; + self.command_line.push_output( + crate::t!("UCS defined from three points.").as_ref(), + ); + } else { + self.command_line.push_error( + crate::t!("UCS points must define two non-collinear axes.") + .as_ref(), + ); + } + } else { + self.command_line.push_error( + crate::t!("UCS requires origin, X-axis point and XY-plane point.") + .as_ref(), + ); + } + } // UCS ORIGIN x,y,z — shift the active UCS origin, keep axes - "ORIGIN" | "O" => { + "ORIGIN" | "O" | "ORIGINW" => { let coord_str = parts.get(2).copied().unwrap_or(""); if let Some((pt, _)) = super::super::helpers::parse_coord(coord_str) { // `pt` is in current UCS space; convert to WCS. // The @/# relative-coordinate prefix is ignored // here — a UCS origin is always absolute. - let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs { + let wcs_origin = if sub == "ORIGINW" { + pt + } else if let Some(ref ucs) = self.tabs[i].active_ucs { ucs_to_wcs(pt, ucs) } else { pt @@ -401,9 +499,6 @@ impl OpenCADStudio { } // ── Named Views (VIEW command) ──────────────────────────────── - // PLAN — look straight down at the drawing (top view). The optional - // World/Ucs/Current keyword is accepted; all map to the world top - // view for now. // PLAN — plan view of a UCS (#326). Shows its options right on // activation instead of flipping the view like a cube click; the // view only changes once an option (or the Current default) runs. diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index fd03b041e..53895a4a2 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1787,6 +1787,14 @@ impl OpenCADStudio { { self.tabs[i].active_layer = new_name.clone(); } + if type_str == "UCS" { + if let Some(active) = self.tabs[i].active_ucs.as_mut() { + if active.name.eq_ignore_ascii_case(&old_name) { + active.name = new_name.clone(); + self.tabs[i].persist_active_ucs(); + } + } + } self.tabs[i].dirty = true; self.command_line .push_output(crate::tf!("RENAME: '{}' → '{}'.", old_name, new_name).as_ref()); @@ -2357,7 +2365,15 @@ fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str } true } - "UCS" => rekey(&mut doc.ucss, old, new), + "UCS" => { + if !rekey(&mut doc.ucss, old, new) { + return false; + } + if doc.header.model_space_ucs_name.eq_ignore_ascii_case(old) { + doc.header.model_space_ucs_name = new.to_string(); + } + true + } "VIEW" => rekey(&mut doc.views, old, new), _ => false, } diff --git a/src/app/document.rs b/src/app/document.rs index 3927bf52e..abc2ea656 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -257,11 +257,22 @@ impl DocumentTab { /// is identity (plain WCS). pub(super) fn model_ucs_from_header(&self) -> Option { let h = &self.scene.document.header; - let mut u = Ucs::new(h.model_space_ucs_name.clone()); + let mut u = if h.model_space_ucs_name.is_empty() { + Ucs::new("*ACTIVE*") + } else { + self.scene + .document + .ucss + .get(&h.model_space_ucs_name) + .cloned() + .unwrap_or_else(|| Ucs::new(h.model_space_ucs_name.clone())) + }; u.origin = h.model_space_ucs_origin; u.x_axis = h.model_space_ucs_x_axis; u.y_axis = h.model_space_ucs_y_axis; - if super::helpers::UcsXform::from_ucs(&u).is_identity() { + if h.model_space_ucs_name.is_empty() + && super::helpers::UcsXform::from_ucs(&u).is_identity() + { None } else { Some(u) @@ -278,11 +289,21 @@ impl DocumentTab { if !vp.ucs_per_viewport { return None; } - let mut u = Ucs::new("*VPUCS*"); + let mut u = if vp.ucs_handle.is_null() { + Ucs::new("*VPUCS*") + } else { + self.scene + .document + .ucss + .iter() + .find(|ucs| ucs.handle == vp.ucs_handle) + .cloned() + .unwrap_or_else(|| Ucs::new("*VPUCS*")) + }; u.origin = vp.ucs_origin; u.x_axis = vp.ucs_x_axis; u.y_axis = vp.ucs_y_axis; - if super::helpers::UcsXform::from_ucs(&u).is_identity() { + if vp.ucs_handle.is_null() && super::helpers::UcsXform::from_ucs(&u).is_identity() { None } else { Some(u) @@ -369,34 +390,6 @@ impl DocumentTab { self.scene.viewcube_ucs = self.ucs_xform().rotation_mat(); } - /// UCS→render(wire)-space affine for commands that build axis-aligned - /// geometry. Columns are the UCS axes; translation is the UCS origin in wire - /// space. Identity outside model space (no UCS there). - pub(super) fn ucs_wire_affine(&self) -> glam::Mat4 { - if !self.editing_model_space() { - return glam::Mat4::IDENTITY; - } - let (o, x, y, z) = self.ucs_xform().axes(); - let origin = glam::Vec3::new(o.x as f32, o.y as f32, o.z as f32); - glam::Mat4::from_cols( - x.as_vec3().extend(0.0), - y.as_vec3().extend(0.0), - z.as_vec3().extend(0.0), - origin.extend(1.0), - ) - } - - /// World-space rotation angle (radians) of the active UCS X axis — the - /// default rotation for new text-bearing objects so their text aligns to - /// the user's coordinate system. Zero outside model space / with no UCS. - pub(super) fn ucs_rotation_angle(&self) -> f64 { - if !self.editing_model_space() { - return 0.0; - } - let (_, x, ..) = self.ucs_xform().axes(); - (x.y as f64).atan2(x.x as f64) - } - /// Grid origin (render/wire space) and UCS→world rotation for grid snap and /// the grid overlay. Identity / origin-at-zero outside model space. pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) { diff --git a/src/app/helpers.rs b/src/app/helpers.rs index fc478cb56..94301642f 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -5,7 +5,7 @@ use acadrust::tables::Ucs; /// How a typed coordinate should be interpreted relative to the last /// input point. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(super) enum CoordKind { +pub(crate) enum CoordKind { /// `@x,y` prefix — offset from the last input point. Relative, /// `#x,y` prefix — world/UCS absolute, overriding DYN. @@ -14,12 +14,13 @@ pub(super) enum CoordKind { Default, } -/// Parse a typed coordinate string into a Vec3 plus its interpretation. -/// Accepts "x,y" → Vec3(x, y, 0) -/// "x,y,z" → Vec3(x, y, z) +/// Parse a typed coordinate string into a local Vec3 plus its interpretation. +/// Accepts Cartesian, polar, cylindrical and spherical forms: +/// `x,y`, `x,y,z`, `distance Option<(glam::DVec3, CoordKind)> { +pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { let trimmed = text.trim(); let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') { (CoordKind::Relative, r) @@ -28,6 +29,34 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { } else { (CoordKind::Default, trimmed) }; + let number = |value: &str| value.trim().parse::().ok(); + if let Some((distance, angles)) = rest.split_once('<') { + let distance = number(distance)?; + if let Some((azimuth, elevation)) = angles.split_once('<') { + let azimuth = number(azimuth)?.to_radians(); + let elevation = number(elevation)?.to_radians(); + let horizontal = distance * elevation.cos(); + return Some(( + glam::DVec3::new( + horizontal * azimuth.cos(), + horizontal * azimuth.sin(), + distance * elevation.sin(), + ), + kind, + )); + } + let angle_and_z: Vec<&str> = angles.split(|c| c == ',' || c == ';').collect(); + let (angle, z) = match angle_and_z.as_slice() { + [angle] => (number(angle)?, 0.0), + [angle, z] => (number(angle)?, number(z)?), + _ => return None, + }; + let angle = angle.to_radians(); + return Some(( + glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z), + kind, + )); + } let parts: Vec = rest .split(|c| c == ',' || c == ';') .map(|s| s.trim()) @@ -40,6 +69,23 @@ pub(super) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> { } } +#[cfg(test)] +mod coordinate_parsing_tests { + use super::*; + + #[test] + fn parses_all_coordinate_forms() { + let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9; + assert!(close(parse_coord("1,2").unwrap().0, glam::dvec3(1.0, 2.0, 0.0))); + assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0))); + assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0))); + assert!(close(parse_coord("10<90,4").unwrap().0, glam::dvec3(0.0, 10.0, 4.0))); + assert!(close(parse_coord("10<0<30").unwrap().0, glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0))); + assert_eq!(parse_coord("@10<0").unwrap().1, CoordKind::Relative); + assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute); + } +} + // ── UCS ↔ WCS converter ───────────────────────────────────────────────────── /// The single bridge between WCS (how geometry and the file are stored) and the @@ -122,6 +168,10 @@ impl UcsXform { (self.origin, self.x, self.y, self.z) } + pub(super) fn working_plane(&self) -> crate::command::WorkingPlane { + crate::command::WorkingPlane::new(self.origin, self.x, self.y) + } + /// UCS→world rotation matrix (columns = UCS axes). For consumers that take /// a `Mat4` rotation directly (ViewCube, OTRACK ray directions). The GPU / /// screen layer is f32, so the axes downcast here at the boundary. diff --git a/src/app/mod.rs b/src/app/mod.rs index ce734aaa9..004753823 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod commands; mod document; mod expr_eval; mod find_replace; -mod helpers; +pub(crate) mod helpers; mod history; mod layers; mod model_ops; diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index 921e5ad17..08a1d9556 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -1146,10 +1146,24 @@ impl super::OpenCADStudio { .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } else { - // Align new MText to the active UCS (text runs along the UCS X axis). - mt.rotation = self.tabs[i].ucs_rotation_angle(); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let position = plane.to_local(glam::DVec3::new( + mt.insertion_point.x, + mt.insertion_point.y, + mt.insertion_point.z, + )); + mt.insertion_point = acadrust::types::Vector3::new( + position.x, + position.y, + position.z, + ); + mt.rotation = 0.0; self.push_undo_snapshot(i, "MTEXT"); - let handle = self.commit_entity_handle(EntityType::MText(mt)); + let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt))); if annotative { let scale = self.tabs[i].scene.current_annotation_scale_handle(); if let (Some(handle), Some(scale)) = (handle, scale) { @@ -1217,9 +1231,24 @@ impl super::OpenCADStudio { .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } else { - mt.rotation = self.tabs[i].ucs_rotation_angle(); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let position = plane.to_local(glam::DVec3::new( + mt.insertion_point.x, + mt.insertion_point.y, + mt.insertion_point.z, + )); + mt.insertion_point = acadrust::types::Vector3::new( + position.x, + position.y, + position.z, + ); + mt.rotation = 0.0; self.push_undo_snapshot(i, "MTEXT"); - let handle = self.commit_entity_handle(EntityType::MText(mt)); + let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt))); self.tabs[i].dirty = true; if annotative { let scale = self.tabs[i].scene.current_annotation_scale_handle(); diff --git a/src/app/properties.rs b/src/app/properties.rs index f2122ee88..546c0dc5a 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -375,10 +375,31 @@ impl OpenCADStudio { source_entity, annotation_scale_handle, ); - let entity = contextual.as_ref(); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane); + let entity = &display_entity; let group_names = self.tabs[i].scene.group_names_for_entity(handle); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); + sections.insert( + 0, + crate::scene::model::object::PropSection { + title: t!("Coordinates").into_owned(), + props: vec![crate::entities::common::ro_prop( + t!("Coordinate system").as_ref(), + "coordinate_system", + self.tabs[i] + .active_ucs + .as_ref() + .map(|ucs| ucs.name.clone()) + .unwrap_or_else(|| "WCS".to_string()), + )], + }, + ); // Turn the Material row into an editable picker: the source // options (ByLayer / ByBlock) plus every named material the @@ -1528,7 +1549,37 @@ impl OpenCADStudio { }) .unwrap_or_default(); - let sections = aggregate_sections(&filtered, &text_style_names); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let local_entities: Vec<(Handle, EntityType)> = filtered + .iter() + .map(|(handle, entity)| { + (*handle, dispatch::entity_in_working_plane(entity, plane)) + }) + .collect(); + let local_refs: Vec<(Handle, &EntityType)> = local_entities + .iter() + .map(|(handle, entity)| (*handle, entity)) + .collect(); + let mut sections = aggregate_sections(&local_refs, &text_style_names); + sections.insert( + 0, + crate::scene::model::object::PropSection { + title: t!("Coordinates").into_owned(), + props: vec![crate::entities::common::ro_prop( + t!("Coordinate system").as_ref(), + "coordinate_system", + self.tabs[i] + .active_ucs + .as_ref() + .map(|ucs| ucs.name.clone()) + .unwrap_or_else(|| "WCS".to_string()), + )], + }, + ); ui::PropertiesPanel { choice_combos: sections .iter() diff --git a/src/app/text_inline.rs b/src/app/text_inline.rs index 91086d733..e80209269 100644 --- a/src/app/text_inline.rs +++ b/src/app/text_inline.rs @@ -211,9 +211,15 @@ impl super::OpenCADStudio { .bump_entities(&[(h, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } else { + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let position = plane.to_local(ed.pos); let mut t = Text::with_value( &ed.value, - Vector3::new(ed.pos.x, ed.pos.y, ed.pos.z), + Vector3::new(position.x, position.y, position.z), ) .with_height(ed.height); // New text inherits the document's current text style (STYLE), not @@ -231,10 +237,8 @@ impl super::OpenCADStudio { &self.tabs[i].scene.document, &t.style, ); - // Align new text to the active UCS (baseline along the UCS X axis). - t.rotation = self.tabs[i].ucs_rotation_angle(); self.push_undo_snapshot(i, "TEXT"); - let handle = self.commit_entity_handle(EntityType::Text(t)); + let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t))); if annotative { let scale = self.tabs[i].scene.current_annotation_scale_handle(); if let (Some(handle), Some(scale)) = (handle, scale) { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 0ae9d4478..dd0120c2d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -417,8 +417,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // independent of DYN: `@` forces relative, `#` forces // absolute, and a bare value is absolute. (Relative-by- // default lives in the DYN tooltip path — see - // `dyn_resolve_point` — matching AutoCAD, where the - // command line stays absolute regardless of DYN.) + // `dyn_resolve_point`; command-line coordinates stay + // absolute regardless of the DYN setting.) let want_relative = matches!(kind, CoordKind::Relative); let ucs = self.tabs[i].active_ucs.clone(); let wcs_pt = match (want_relative, self.last_point) { @@ -474,7 +474,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.command_line.push_error(crate::tf!( - "Expected coordinates (x,y) or a number, got: \"{text}\"" + "Expected Cartesian, polar, cylindrical or spherical coordinates, or a number; got: \"{text}\"" ).as_ref()); return self.focus_cmd_input(); } @@ -1874,10 +1874,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } } } else { + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; for &handle in &handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_geom_prop(entity, field, &value); + crate::scene::view::dispatch::apply_geom_prop_in_working_plane( + entity, + field, + &value, + plane, + ); } } } @@ -1936,6 +1946,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { crate::scene::view::dispatch::set_prop_current_vertex( self.tabs[i].properties.prop_vertex, ); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; for &handle in &handles { // Skip objects on a locked layer. if self.tabs[i].scene.is_layer_locked(handle) { @@ -2007,8 +2022,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - crate::scene::view::dispatch::apply_geom_prop( - entity, field, &val, + crate::scene::view::dispatch::apply_geom_prop_in_working_plane( + entity, + field, + &val, + plane, ); } } diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 0e9994bd9..7db870a9c 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -188,13 +188,15 @@ impl OpenCADStudio { /// values. Locked fields use their typed buffer; the rest fall back to /// the live cursor-derived value. Returns `None` when the field set /// isn't one we know how to turn into a point. - /// Hand the active command the current UCS (as a UCS→wire affine) so - /// axis-aligned constructions build square to the user's coordinate system. - /// No-op for commands that don't override `set_ucs`. + /// Hand the active command the current full-precision coordinate frame. pub(in crate::app) fn push_ucs_to_cmd(&mut self, i: usize) { - let ucs = self.tabs[i].ucs_wire_affine(); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; if let Some(c) = self.tabs[i].active_cmd.as_mut() { - c.set_ucs(ucs); + c.set_working_plane(plane); } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index f660f1118..eb7b40e1b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2086,7 +2086,11 @@ impl OpenCADStudio { match (a, b) { (None, None) => true, (Some(a), Some(b)) => { - a.origin == b.origin && a.x_axis == b.x_axis && a.y_axis == b.y_axis + a.origin == b.origin + && a.x_axis == b.x_axis + && a.y_axis == b.y_axis + && a.handle == b.handle + && a.name.eq_ignore_ascii_case(&b.name) } _ => false, } diff --git a/src/command.rs b/src/command.rs index c6f2ede23..f2bc67398 100644 --- a/src/command.rs +++ b/src/command.rs @@ -11,6 +11,132 @@ use crate::scene::Scene; use acadrust::{EntityType, Handle}; use glam::DVec3; +// ── Working plane ───────────────────────────────────────────────────────── + +/// Full-precision coordinate frame used by interactive commands. +/// +/// Points delivered to commands remain WCS points. Commands that construct +/// planar geometry use this frame for their local calculations, then convert +/// the result back to WCS for storage and preview. +#[derive(Clone, Copy, Debug)] +pub struct WorkingPlane { + pub origin: DVec3, + pub x: DVec3, + pub y: DVec3, + pub z: DVec3, +} + +impl Default for WorkingPlane { + fn default() -> Self { + Self { + origin: DVec3::ZERO, + x: DVec3::X, + y: DVec3::Y, + z: DVec3::Z, + } + } +} + +impl WorkingPlane { + pub fn is_identity(self) -> bool { + self.origin.abs_diff_eq(DVec3::ZERO, 1e-12) + && self.x.abs_diff_eq(DVec3::X, 1e-12) + && self.y.abs_diff_eq(DVec3::Y, 1e-12) + && self.z.abs_diff_eq(DVec3::Z, 1e-12) + } + + pub fn new(origin: DVec3, x: DVec3, y: DVec3) -> Self { + let x = x.normalize_or(DVec3::X); + let raw_y = y.normalize_or(DVec3::Y); + let fallback = if x.dot(DVec3::Z).abs() < 0.999 { + DVec3::Z + } else { + DVec3::Y + }; + let z = x.cross(raw_y).normalize_or(x.cross(fallback).normalize()); + let y = z.cross(x).normalize(); + Self { origin, x, y, z } + } + + pub fn to_world(self, point: DVec3) -> DVec3 { + self.origin + self.x * point.x + self.y * point.y + self.z * point.z + } + + pub fn to_local(self, point: DVec3) -> DVec3 { + let delta = point - self.origin; + DVec3::new(delta.dot(self.x), delta.dot(self.y), delta.dot(self.z)) + } + + pub fn vector_to_world(self, vector: DVec3) -> DVec3 { + self.x * vector.x + self.y * vector.y + self.z * vector.z + } + + pub fn vector_to_local(self, vector: DVec3) -> DVec3 { + DVec3::new(vector.dot(self.x), vector.dot(self.y), vector.dot(self.z)) + } + + pub fn angle(self, from: DVec3, to: DVec3) -> Option { + let direction = self.vector_to_local(to - from); + (direction.x.hypot(direction.y) > f64::EPSILON) + .then(|| direction.y.atan2(direction.x)) + } + + pub fn to_world_transform(self) -> acadrust::types::Transform { + use acadrust::types::{Matrix4, Transform}; + Transform::from_matrix(Matrix4 { + m: [ + [self.x.x, self.y.x, self.z.x, self.origin.x], + [self.x.y, self.y.y, self.z.y, self.origin.y], + [self.x.z, self.y.z, self.z.z, self.origin.z], + [0.0, 0.0, 0.0, 1.0], + ], + }) + } + + pub fn to_local_transform(self) -> acadrust::types::Transform { + use acadrust::types::{Matrix4, Transform}; + Transform::from_matrix(Matrix4 { + m: [ + [self.x.x, self.x.y, self.x.z, -self.origin.dot(self.x)], + [self.y.x, self.y.y, self.y.z, -self.origin.dot(self.y)], + [self.z.x, self.z.y, self.z.z, -self.origin.dot(self.z)], + [0.0, 0.0, 0.0, 1.0], + ], + }) + } + + pub fn place_entity(self, mut entity: EntityType) -> EntityType { + crate::scene::view::dispatch::apply_transform( + &mut entity, + &EntityTransform::Affine(self.to_world_transform()), + ); + entity + } +} + +#[cfg(test)] +mod working_plane_tests { + use super::*; + + #[test] + fn translated_and_rotated_frame_round_trips_points_and_vectors() { + let plane = WorkingPlane::new( + DVec3::new(125_000.25, -42_000.5, 810.75), + DVec3::new(0.0, 1.0, 0.0), + DVec3::new(0.0, 0.0, 1.0), + ); + let local = DVec3::new(12.5, -3.25, 7.75); + let vector = DVec3::new(-2.0, 5.0, 1.5); + + assert!(plane + .to_local(plane.to_world(local)) + .abs_diff_eq(local, 1e-9)); + assert!(plane + .vector_to_local(plane.vector_to_world(vector)) + .abs_diff_eq(vector, 1e-12)); + } +} + /// Domain object resolved under the cursor for ObjectPick snapping. #[derive(Clone, Copy, Debug)] pub struct ObjectPickHit { @@ -44,14 +170,22 @@ pub struct AreaPreviewRegion { /// A geometric transformation applied to existing entities. #[derive(Clone)] pub enum EntityTransform { - /// Move every point by the given world-space delta (world XY plane). + /// Move every point by the given world-space delta. Translate(DVec3), - /// Rotate around `center` by `angle_rad` in the world XY plane. - Rotate { center: DVec3, angle_rad: f64 }, + /// Rotate around the axis through `center`. + Rotate { + center: DVec3, + axis: DVec3, + angle_rad: f64, + }, /// Uniform scale from `center` by `factor`. Scale { center: DVec3, factor: f64 }, - /// Mirror across the line through `p1`→`p2` in the world XY plane. - Mirror { p1: DVec3, p2: DVec3 }, + /// Mirror through the plane containing `p1`→`p2` and the working normal. + Mirror { + p1: DVec3, + p2: DVec3, + working_normal: DVec3, + }, /// General affine transform. Used when a complete UCS basis must be baked /// into block-local geometry instead of stored as drawing UCS state. Affine(acadrust::types::Transform), @@ -1296,12 +1430,10 @@ pub trait CadCommand: Send { Vec::new() } - /// Push the active UCS into the command as a UCS→render(wire)-space affine - /// (identity = plain WCS). Commands that build axis-aligned geometry (RECT, - /// rectangular ARRAY, …) override this to store it and rotate their implicit - /// axes into the UCS; most commands work purely from picked points and - /// ignore it. Called before each point / preview dispatch. - fn set_ucs(&mut self, _ucs: glam::Mat4) {} + /// Push the active coordinate frame in full precision. Geometry commands + /// use it for plane-local construction; inquiry and modify commands use it + /// for local deltas, angles and transformation axes. + fn set_working_plane(&mut self, _plane: WorkingPlane) {} /// Push the live Ctrl-key state into the command before each preview/commit /// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 3569f0cdc..007e8d760 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -628,14 +628,34 @@ fn apply_transform(dim: &mut Dimension, t: &EntityTransform) { EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new( d.x as f64, d.y as f64, d.z as f64, )), - EntityTransform::Rotate { center, angle_rad } => { - transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad)) + EntityTransform::Rotate { center, axis, angle_rad } => { + if axis.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) { + transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad)) + } else { + crate::scene::view::transform::apply_standard_transform( + dim, + *center, + *axis, + *angle_rad, + ); + } } EntityTransform::Scale { center, factor } => { transform_dimension_points(dim, |pt| scale_point(pt, *center, *factor)) } - EntityTransform::Mirror { p1, p2 } => { - transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2)) + EntityTransform::Mirror { p1, p2, working_normal } => { + if working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) { + transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2)) + } else { + acadrust::Entity::apply_transform( + dim, + &crate::scene::view::transform::reflection_about_working_line( + *p1, + *p2, + *working_normal, + ), + ); + } } EntityTransform::Affine(transform) => { let old_normal = dim.base().normal; diff --git a/src/entities/extended.rs b/src/entities/extended.rs index a5195ae29..80cb8aca5 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -1305,9 +1305,12 @@ fn entity_transform(transform: &EntityTransform) -> Transform { EntityTransform::Translate(delta) => { Transform::from_translation(Vector3::new(delta.x, delta.y, delta.z)) } - EntityTransform::Rotate { center, angle_rad } => { + EntityTransform::Rotate { center, axis, angle_rad } => { Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z)) - .then(&Transform::from_rotation(Vector3::UNIT_Z, *angle_rad)) + .then(&Transform::from_rotation( + Vector3::new(axis.x, axis.y, axis.z), + *angle_rad, + )) .then(&Transform::from_translation(Vector3::new( center.x, center.y, center.z, ))) @@ -1316,8 +1319,12 @@ fn entity_transform(transform: &EntityTransform) -> Transform { Vector3::new(*factor, *factor, *factor), Vector3::new(center.x, center.y, center.z), ), - EntityTransform::Mirror { p1, p2 } => { - crate::scene::view::transform::reflection_about_xy_line(*p1, *p2) + EntityTransform::Mirror { p1, p2, working_normal } => { + crate::scene::view::transform::reflection_about_working_line( + *p1, + *p2, + *working_normal, + ) } EntityTransform::Affine(transform) => *transform, } diff --git a/src/entities/line.rs b/src/entities/line.rs index 453657949..039e7b00c 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -145,14 +145,21 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) { d.x as f64, d.y as f64, d.z as f64, )); } - EntityTransform::Rotate { center, angle_rad } => { - crate::scene::view::transform::apply_standard_transform(line, *center, *angle_rad); + EntityTransform::Rotate { center, axis, angle_rad } => { + crate::scene::view::transform::apply_standard_transform(line, *center, *axis, *angle_rad); } EntityTransform::Scale { center, factor } => { crate::scene::view::transform::apply_standard_scale(line, *center, *factor); } - EntityTransform::Mirror { p1, p2 } => { - crate::scene::view::transform::mirror_xy_line(line, *p1, *p2); + EntityTransform::Mirror { p1, p2, working_normal } => { + acadrust::Entity::apply_transform( + line, + &crate::scene::view::transform::reflection_about_working_line( + *p1, + *p2, + *working_normal, + ), + ); } EntityTransform::Affine(transform) => { acadrust::Entity::apply_transform(line, transform); diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index a0826ab5e..dc78a758d 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -6,6 +6,7 @@ use acadrust::entities::{Underlay, UnderlayDisplayFlags}; use crate::t; +use glam::DVec3; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip}; @@ -383,7 +384,18 @@ impl Transformable for Underlay { self.insertion_point.y += d.y as f64; self.insertion_point.z += d.z as f64; } - EntityTransform::Mirror { p1, p2 } => { + EntityTransform::Mirror { p1, p2, working_normal } => { + if !working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) { + acadrust::Entity::apply_transform( + self, + &crate::scene::view::transform::reflection_about_working_line( + *p1, + *p2, + *working_normal, + ), + ); + return; + } reflect_xy_point( &mut self.insertion_point.x, &mut self.insertion_point.y, @@ -408,7 +420,16 @@ impl Transformable for Underlay { self.y_scale *= f; self.z_scale *= f; } - EntityTransform::Rotate { center, angle_rad } => { + EntityTransform::Rotate { center, axis, angle_rad } => { + if !axis.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) { + crate::scene::view::transform::apply_standard_transform( + self, + *center, + *axis, + *angle_rad, + ); + return; + } let bx = center.x as f64; let by = center.y as f64; let a = *angle_rad as f64; diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 6de0b5545..d32c9915e 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -5,7 +5,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -29,6 +29,7 @@ enum Step { pub struct AlignedDimensionCommand { step: Step, + plane: WorkingPlane, /// Optional text that replaces the measured value (None = measurement). text_override: Option, /// True while the next typed line is captured as the text override. @@ -43,6 +44,7 @@ impl AlignedDimensionCommand { pub fn new() -> Self { Self { step: Step::First, + plane: WorkingPlane::default(), text_override: None, awaiting_text: false, text_angle: None, @@ -52,6 +54,10 @@ impl AlignedDimensionCommand { } impl CadCommand for AlignedDimensionCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIMALIGNED" } @@ -85,6 +91,9 @@ impl CadCommand for AlignedDimensionCommand { CmdResult::NeedPoint } Step::DimLine { p1, p2 } => { + let p1 = self.plane.to_local(p1); + let p2 = self.plane.to_local(p2); + let pt = self.plane.to_local(pt); let mut dim = DimensionAligned::new(v3(p1), v3(p2)); // The dimension line runs through the cursor: store it as the // definition point and let the renderer project it onto the @@ -106,7 +115,9 @@ impl CadCommand for AlignedDimensionCommand { if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Aligned(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Aligned(dim), + ))) } } } @@ -180,7 +191,29 @@ impl CadCommand for AlignedDimensionCommand { Step::First => return None, Step::Second(p1) => (p1, pt), Step::DimLine { p1, p2 } => { - return Some(preview_aligned(p1, p2, pt)); + let p1 = self.plane.to_local(p1); + let p2 = self.plane.to_local(p2); + let pt = self.plane.to_local(pt); + let mut preview = preview_aligned(p1, p2, pt); + preview.points = preview + .points + .iter() + .map(|point| { + if point[0].is_nan() { + *point + } else { + self.plane + .to_world(DVec3::new( + point[0] as f64, + point[1] as f64, + point[2] as f64, + )) + .as_vec3() + .to_array() + } + }) + .collect(); + return Some(preview); } }; // Preview WireModel points are screen/GPU-side: downcast to f32. diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 86c3cf780..913dd501b 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -2,7 +2,7 @@ use acadrust::entities::{Dimension, DimensionAngular3Pt}; use acadrust::types::Vector3; use acadrust::EntityType; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::{DVec3, Vec3}; @@ -35,15 +35,23 @@ enum Step { pub struct AngularDimensionCommand { step: Step, + plane: WorkingPlane, } impl AngularDimensionCommand { pub fn new() -> Self { - Self { step: Step::Vertex } + Self { + step: Step::Vertex, + plane: WorkingPlane::default(), + } } } impl CadCommand for AngularDimensionCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIMANGULAR" } @@ -84,13 +92,19 @@ impl CadCommand for AngularDimensionCommand { first, second, } => { + let vertex = self.plane.to_local(vertex); + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let pt = self.plane.to_local(pt); let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); dim.definition_point = v3(pt); dim.base.definition_point = v3(pt); dim.base.text_middle_point = v3(pt); dim.base.insertion_point = v3(pt); dim.base.actual_measurement = dim.measurement_degrees(); - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Angular3Pt(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Angular3Pt(dim), + ))) } } } @@ -119,12 +133,24 @@ impl CadCommand for AngularDimensionCommand { vertex, first, second, - } => Some(preview_wire(angular_preview( - vertex.as_vec3(), - first.as_vec3(), - second.as_vec3(), - pt, - ))), + } => { + let points = angular_preview( + self.plane.to_local(vertex).as_vec3(), + self.plane.to_local(first).as_vec3(), + self.plane.to_local(second).as_vec3(), + self.plane.to_local(pt.as_dvec3()).as_vec3(), + ) + .into_iter() + .map(|point| { + if point.x.is_nan() { + point + } else { + self.plane.to_world(point.as_dvec3()).as_vec3() + } + }) + .collect(); + Some(preview_wire(points)) + } } } } diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 19f140fb1..e1ac05d1e 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -5,7 +5,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Vec3}; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -29,6 +29,7 @@ enum Step { pub struct DiameterDimensionCommand { step: Step, + plane: WorkingPlane, /// Optional text that replaces the measured value (None = measurement). text_override: Option, /// True while the next typed line is captured as the text override. @@ -43,6 +44,7 @@ impl DiameterDimensionCommand { pub fn new() -> Self { Self { step: Step::CenterPoint, + plane: WorkingPlane::default(), text_override: None, awaiting_text: false, text_angle: None, @@ -52,6 +54,10 @@ impl DiameterDimensionCommand { } impl CadCommand for DiameterDimensionCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIMDIAMETER" } @@ -83,6 +89,9 @@ impl CadCommand for DiameterDimensionCommand { CmdResult::NeedPoint } Step::TextPoint { center, arc_pt } => { + let center = self.plane.to_local(center); + let arc_pt = self.plane.to_local(arc_pt); + let pt = self.plane.to_local(pt); let mut dim = DimensionDiameter::new(v3(center), v3(arc_pt)); dim.base.definition_point = v3(arc_pt); dim.base.text_middle_point = v3(pt); @@ -94,7 +103,9 @@ impl CadCommand for DiameterDimensionCommand { if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Diameter(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Diameter(dim), + ))) } } } diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index f75b0a9ca..189c511df 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -15,7 +15,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Mat4, Vec3}; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -33,7 +33,7 @@ pub fn tool() -> ToolDef { pub struct LeaderCommand { verts: Vec, - ucs: Mat4, + plane: WorkingPlane, dimension_style: String, text_style: String, text_height: f64, @@ -55,7 +55,7 @@ impl LeaderCommand { }; Self { verts: Vec::new(), - ucs: Mat4::IDENTITY, + plane: WorkingPlane::default(), dimension_style: defaults.style_name, text_style: defaults.text_style_name, text_height: defaults.text_height, @@ -72,8 +72,8 @@ impl CadCommand for LeaderCommand { "LEADER" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { @@ -99,16 +99,21 @@ impl CadCommand for LeaderCommand { } // Place the leader plus an empty MText annotation, link them, then open // the in-place MText editor so the user types the annotation text. + let local: Vec = self + .verts + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); let leader = build_leader( - &self.verts, - self.ucs, + &local, + Mat4::IDENTITY, &self.dimension_style, self.text_height, self.gap, self.arrow_size, ); let displayed_height = self.text_height * self.display_scale; - let (anchor, attach) = annotation_anchor(&self.verts, displayed_height, self.ucs); + let (anchor, attach) = annotation_anchor(&local, displayed_height, Mat4::IDENTITY); let mtext_height = if self.annotative { self.text_height } else { @@ -119,12 +124,15 @@ impl CadCommand for LeaderCommand { anchor, mtext_height, attach, - self.ucs, + Mat4::IDENTITY, &self.text_style, self.annotative, ); CmdResult::CommitManyAndEditText { - entities: vec![EntityType::Leader(leader), EntityType::MText(mtext)], + entities: vec![ + self.plane.place_entity(EntityType::Leader(leader)), + self.plane.place_entity(EntityType::MText(mtext)), + ], edit_index: 1, } } @@ -137,12 +145,31 @@ impl CadCommand for LeaderCommand { if self.verts.is_empty() { return None; } - let mut pts: Vec = self.verts.iter().map(|p| p.as_vec3()).collect(); - pts.push(pt.as_vec3()); - Some(preview_wire( + let mut pts: Vec = self + .verts + .iter() + .map(|point| self.plane.to_local(*point).as_vec3()) + .collect(); + pts.push(self.plane.to_local(pt).as_vec3()); + let mut preview = preview_wire( &pts, (self.arrow_size * self.display_scale) as f32, - )) + ); + preview.points = preview + .points + .iter() + .map(|point| { + if point[0].is_nan() { + *point + } else { + self.plane + .to_world(Vec3::from_array(*point).as_dvec3()) + .as_vec3() + .to_array() + } + }) + .collect(); + Some(preview) } } diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 4d76c7927..286f347c5 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -2,23 +2,18 @@ use acadrust::entities::{Dimension, DimensionLinear}; use acadrust::types::Vector3; use acadrust::EntityType; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; -use glam::{DVec3, Mat4, Vec3}; +use glam::DVec3; use crate::t; -/// World-space measurement axis for a linear dimension between `first` and -/// `second`, chosen as the UCS X or Y axis (whichever the span is closer to). -/// `ucs` is the UCS→wire affine; identity gives the world X/Y behaviour. -fn measure_axis(first: DVec3, second: DVec3, ucs: Mat4) -> DVec3 { - let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X).as_dvec3(); - let uy = ucs.transform_vector3(Vec3::Y).normalize_or(Vec3::Y).as_dvec3(); - let du = ucs.inverse().transform_vector3((second - first).as_vec3()); - if du.x.abs() >= du.y.abs() { - ux +fn measure_axis(first: DVec3, second: DVec3) -> DVec3 { + let delta = second - first; + if delta.x.abs() >= delta.y.abs() { + DVec3::X } else { - uy + DVec3::Y } } @@ -41,7 +36,7 @@ enum Step { pub struct LinearDimensionCommand { step: Step, - ucs: Mat4, + plane: WorkingPlane, /// Optional text that replaces the measured value (None = measurement). text_override: Option, /// True while the next typed line is captured as the text override. @@ -56,7 +51,7 @@ impl LinearDimensionCommand { pub fn new() -> Self { Self { step: Step::FirstPoint, - ucs: Mat4::IDENTITY, + plane: WorkingPlane::default(), text_override: None, awaiting_text: false, text_angle: None, @@ -70,8 +65,8 @@ impl CadCommand for LinearDimensionCommand { "DIMLINEAR" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { @@ -103,21 +98,12 @@ impl CadCommand for LinearDimensionCommand { CmdResult::NeedPoint } Step::DimensionLine { first, second } => { + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let pt = self.plane.to_local(pt); let mut dim = DimensionLinear::new(v3(first), v3(second)); - // Measure along the UCS axis the span is closest to; the DXF - // rotation is that axis's angle in world space. - let axis = measure_axis(first, second, self.ucs); + let axis = measure_axis(first, second); dim.rotation = axis.y.atan2(axis.x); - // Bake the UCS X-axis angle as the text rotation so the - // measurement text reads horizontally in the UCS (i.e. square on - // screen) regardless of the style's force-horizontal flags, - // which would otherwise pin it to world-horizontal and tilt it - // under a rotated UCS. Left at 0 (natural) when there's no UCS. - let ux = self.ucs.transform_vector3(Vec3::X); - let ucs_ang = (ux.y as f64).atan2(ux.x as f64); - if ucs_ang.abs() > 1e-9 { - dim.base.text_rotation = ucs_ang; - } dim.definition_point = v3(pt); dim.base.definition_point = v3(pt); dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis)); @@ -128,7 +114,9 @@ impl CadCommand for LinearDimensionCommand { if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Linear(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Linear(dim), + ))) } } } @@ -206,8 +194,15 @@ impl CadCommand for LinearDimensionCommand { Step::FirstPoint => None, Step::SecondPoint(first) => Some(preview_wire(vec![first, pt])), Step::DimensionLine { first, second } => { - let axis = measure_axis(first, second, self.ucs); - Some(preview_wire(linear_dimension_preview(first, second, pt, axis))) + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let pt = self.plane.to_local(pt); + let axis = measure_axis(first, second); + let points = linear_dimension_preview(first, second, pt, axis) + .into_iter() + .map(|point| self.plane.to_world(point)) + .collect(); + Some(preview_wire(points)) } } } diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 87e4adff6..d8f331169 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -10,7 +10,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Mat4, Vec3}; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -28,7 +28,7 @@ pub fn tool() -> ToolDef { pub struct MLeaderCommand { verts: Vec, - ucs: Mat4, + plane: WorkingPlane, style: Option, display_scale: f64, } @@ -37,7 +37,7 @@ impl MLeaderCommand { pub fn new() -> Self { Self { verts: Vec::new(), - ucs: Mat4::IDENTITY, + plane: WorkingPlane::default(), style: None, display_scale: 1.0, } @@ -54,7 +54,7 @@ impl MLeaderCommand { }; Self { verts: Vec::new(), - ucs: Mat4::IDENTITY, + plane: WorkingPlane::default(), style: Some(style), display_scale, } @@ -66,8 +66,8 @@ impl CadCommand for MLeaderCommand { "MLEADER" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { @@ -93,14 +93,21 @@ impl CadCommand for MLeaderCommand { } // Place the leader with empty text, then open the in-place MText editor // so the user types the annotation into the rich editor. + let local: Vec = self + .verts + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); let ml = build_mleader( "", - &self.verts, - self.ucs, + &local, + Mat4::IDENTITY, self.style.as_ref(), self.display_scale, ); - CmdResult::CommitAndEditText(EntityType::MultiLeader(ml)) + CmdResult::CommitAndEditText( + self.plane.place_entity(EntityType::MultiLeader(ml)), + ) } fn on_escape(&mut self) -> CmdResult { @@ -112,14 +119,33 @@ impl CadCommand for MLeaderCommand { return None; } // Preview / rubber-band is GPU screen-space: downcast to f32 here. - let mut pts: Vec = self.verts.iter().map(|p| p.as_vec3()).collect(); - pts.push(pt.as_vec3()); + let mut pts: Vec = self + .verts + .iter() + .map(|point| self.plane.to_local(*point).as_vec3()) + .collect(); + pts.push(self.plane.to_local(pt).as_vec3()); let arrow_size = self .style .as_ref() .map_or(2.5, |style| style.arrowhead_size) * self.display_scale; - Some(preview_wire(&pts, arrow_size as f32)) + let mut preview = preview_wire(&pts, arrow_size as f32); + preview.points = preview + .points + .iter() + .map(|point| { + if point[0].is_nan() { + *point + } else { + self.plane + .to_world(Vec3::from_array(*point).as_dvec3()) + .as_vec3() + .to_array() + } + }) + .collect(); + Some(preview) } } diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 77fac16ab..72b8ccec9 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -13,7 +13,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Vec3}; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -34,17 +34,23 @@ enum Step { pub struct OrdinateDimCommand { step: Step, + plane: WorkingPlane, } impl OrdinateDimCommand { pub fn new() -> Self { Self { step: Step::FeaturePoint, + plane: WorkingPlane::default(), } } } impl CadCommand for OrdinateDimCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIMORDINATE" } @@ -63,6 +69,8 @@ impl CadCommand for OrdinateDimCommand { CmdResult::NeedPoint } Step::LeaderEndpoint { feature } => { + let feature = self.plane.to_local(feature); + let pt = self.plane.to_local(pt); let is_x = is_x_type(feature, pt); let elbow = ordinate_elbow(feature, pt, is_x); let mut dim = DimensionOrdinate::new(v3(feature), v3(pt), is_x); @@ -73,7 +81,9 @@ impl CadCommand for OrdinateDimCommand { // the wrong (XZ) plane, dropping the Y coordinate. (#150) dim.definition_point = v3(elbow); dim.base.definition_point = v3(elbow); - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Ordinate(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Ordinate(dim), + ))) } } } @@ -86,8 +96,13 @@ impl CadCommand for OrdinateDimCommand { Step::LeaderEndpoint { feature } => feature, _ => return None, }; + let feature = self.plane.to_local(feature); + let pt = self.plane.to_local(pt); let is_x = is_x_type(feature, pt); let elbow = ordinate_elbow(feature, pt, is_x); + let feature = self.plane.to_world(feature); + let elbow = self.plane.to_world(elbow); + let pt = self.plane.to_world(pt); // Screen-only rubber band: downcast to f32 at the preview boundary. Some(preview_wire(vec![ feature.as_vec3(), diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index b41d64f9c..b3fbb7af7 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -2,7 +2,7 @@ use acadrust::entities::{Dimension, DimensionRadius}; use acadrust::types::Vector3; use acadrust::EntityType; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::{DVec3, Vec3}; @@ -27,6 +27,7 @@ enum Step { pub struct RadiusDimensionCommand { step: Step, + plane: WorkingPlane, /// Optional text that replaces the measured value (None = measurement). text_override: Option, /// True while the next typed line is captured as the text override. @@ -41,6 +42,7 @@ impl RadiusDimensionCommand { pub fn new() -> Self { Self { step: Step::CenterPoint, + plane: WorkingPlane::default(), text_override: None, awaiting_text: false, text_angle: None, @@ -50,6 +52,10 @@ impl RadiusDimensionCommand { } impl CadCommand for RadiusDimensionCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIMRADIUS" } @@ -81,6 +87,9 @@ impl CadCommand for RadiusDimensionCommand { CmdResult::NeedPoint } Step::TextPoint { center, point } => { + let center = self.plane.to_local(center); + let point = self.plane.to_local(point); + let pt = self.plane.to_local(pt); let mut dim = DimensionRadius::new(v3(center), v3(point)); dim.base.definition_point = v3(point); dim.base.text_middle_point = v3(pt); @@ -92,7 +101,9 @@ impl CadCommand for RadiusDimensionCommand { if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Radius(dim))) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + Dimension::Radius(dim), + ))) } } } diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 8bedcbff0..7d66e16de 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -12,7 +12,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -45,6 +45,7 @@ pub struct TableCommand { column_width: f64, row_height: f64, preview_scale: f64, + plane: WorkingPlane, } impl TableCommand { @@ -55,6 +56,7 @@ impl TableCommand { column_width: COL_WIDTH, row_height: ROW_HEIGHT, preview_scale: 1.0, + plane: WorkingPlane::default(), } } @@ -81,11 +83,16 @@ impl TableCommand { } else { 1.0 }, + plane: WorkingPlane::default(), } } } impl CadCommand for TableCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "TABLE" } @@ -163,27 +170,34 @@ impl CadCommand for TableCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Step::Insertion { cols, rows } = self.step { - let ins = Vector3::new(pt.x, pt.y, pt.z); + let point = self.plane.to_local(pt); + let ins = Vector3::new(point.x, point.y, point.z); let mut table = TableBuilder::new(rows, cols) .at(ins) .row_height(self.row_height) .column_width(self.column_width) .build(); table.table_style_handle = self.style_handle; - CmdResult::CommitAndExit(EntityType::Table(table)) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Table(table))) } else { CmdResult::NeedPoint } } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { let pt = pt.as_vec3(); + fn on_mouse_move(&mut self, pt: DVec3) -> Option { if let Step::Insertion { cols, rows } = self.step { // Preview: outline of the table bounding box. let w = (cols as f32) * (self.column_width * self.preview_scale) as f32; let h = (rows as f32) * (self.row_height * self.preview_scale) as f32; - let x = pt.x; - let y = pt.y; - let z = pt.z; + let point = self.plane.to_local(pt); + let corners = [ + point, + point + DVec3::X * w as f64, + point + DVec3::new(w as f64, -(h as f64), 0.0), + point - DVec3::Y * h as f64, + ] + .map(|corner| self.plane.to_world(corner).as_vec3().to_array()); + let [p0, p1, p2, p3] = corners; Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, @@ -197,14 +211,7 @@ impl CadCommand for TableCommand { text_verts: Vec::new(), name: "table_preview".into(), points: vec![ - [x, y, z], - [x + w, y, z], - [x + w, y, z], - [x + w, y, z - h], - [x + w, y, z - h], - [x, y, z - h], - [x, y, z - h], - [x, y, z], + p0, p1, p1, p2, p2, p3, p3, p0, ], points_low: Vec::new(), color: WireModel::CYAN, diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 1dc566f50..1d05fb941 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -9,7 +9,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -32,15 +32,23 @@ enum Step { pub struct ToleranceCommand { step: Step, + plane: WorkingPlane, } impl ToleranceCommand { pub fn new() -> Self { - Self { step: Step::Text } + Self { + step: Step::Text, + plane: WorkingPlane::default(), + } } } impl CadCommand for ToleranceCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "TOLERANCE" } @@ -69,9 +77,10 @@ impl CadCommand for ToleranceCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Step::Insertion { text } = &self.step { - let ins = Vector3::new(pt.x, pt.y, pt.z); + let point = self.plane.to_local(pt); + let ins = Vector3::new(point.x, point.y, point.z); let tol = Tolerance::with_text(ins, text.clone()); - CmdResult::CommitAndExit(EntityType::Tolerance(tol)) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol))) } else { CmdResult::NeedPoint } @@ -81,11 +90,18 @@ impl CadCommand for ToleranceCommand { CmdResult::Cancel } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { let pt = pt.as_vec3(); + fn on_mouse_move(&mut self, pt: DVec3) -> Option { if !matches!(self.step, Step::Insertion { .. }) { return None; } - let d = 0.15_f32; + let d = 0.15; + let points = [ + pt - self.plane.x * d, + pt + self.plane.x * d, + DVec3::splat(f64::NAN), + pt - self.plane.y * d, + pt + self.plane.y * d, + ]; Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, @@ -98,13 +114,10 @@ impl CadCommand for ToleranceCommand { dash_align_end: None, text_verts: Vec::new(), name: "tolerance_preview".into(), - points: vec![ - [pt.x - d, pt.y, pt.z], - [pt.x + d, pt.y, pt.z], - [f32::NAN, 0.0, 0.0], - [pt.x, pt.y, pt.z - d], - [pt.x, pt.y, pt.z + d], - ], + points: points + .iter() + .map(|point| point.as_vec3().to_array()) + .collect(), points_low: Vec::new(), color: WireModel::CYAN, selected: false, diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index 64a7c49e8..c2520708e 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -17,7 +17,7 @@ use acadrust::types::Vector3; use acadrust::{Arc as CadArc, EntityType}; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -72,12 +72,18 @@ pub const ICON: IconKind = ICON_3P; // ── Shared math helpers ──────────────────────────────────────────────────── /// Angle in radians from `center` to `pt`. -fn angle_xy(center: DVec3, pt: DVec3) -> f64 { - (pt.y - center.y).atan2(pt.x - center.x) +fn angle_xy(center: DVec3, pt: DVec3, plane: WorkingPlane) -> f64 { + plane.angle(center, pt).unwrap_or(0.0) } /// Build a CCW arc polyline from `start_angle` to `end_angle` (radians). -fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> WireModel { +fn arc_preview( + center: DVec3, + radius: f64, + start_angle: f64, + end_angle: f64, + plane: WorkingPlane, +) -> WireModel { let mut ea = end_angle; while ea < start_angle { ea += TAU; @@ -87,32 +93,36 @@ fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> let pts: Vec<[f64; 3]> = (0..=segs) .map(|i| { let a = start_angle + span * (i as f64 / segs as f64); - [ - center.x + radius * a.cos(), - center.y + radius * a.sin(), - center.z, - ] + let point = center + plane.x * (radius * a.cos()) + plane.y * (radius * a.sin()); + [point.x, point.y, point.z] }) .collect(); WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) } -fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType { - EntityType::Arc(CadArc { +fn make_arc( + center: DVec3, + radius: f64, + start_angle: f64, + end_angle: f64, + plane: WorkingPlane, +) -> EntityType { + let center = plane.to_local(center); + plane.place_entity(EntityType::Arc(CadArc { center: Vector3::new(center.x, center.y, center.z), radius, start_angle, end_angle, ..Default::default() - }) + })) } /// Signed rotation from `prev` to `curr` around `center`. /// Positive = CCW, negative = CW. /// Signed angle (radians) swept from `prev` to `curr` about `center`. -fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3) -> f64 { - let p = prev - center; - let c = curr - center; +fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3, plane: WorkingPlane) -> f64 { + let p = plane.vector_to_local(prev - center); + let c = plane.vector_to_local(curr - center); (p.x * c.y - p.y * c.x).atan2(p.x * c.x + p.y * c.y) } @@ -130,7 +140,13 @@ fn line_wire(a: DVec3, b: DVec3) -> WireModel { } /// Circumscribed circle through three points. -fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { +fn circumcircle( + a: DVec3, + b: DVec3, + c: DVec3, + plane: WorkingPlane, +) -> Option<(DVec3, f64)> { + let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c)); let d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)); if d.abs() < 1e-9 { return None; @@ -144,7 +160,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { + (c.x * c.x + c.y * c.y) * (b.x - a.x)) / d; let center = DVec3::new(ux, uy, a.z); - Some((center, center.distance(a))) + Some((plane.to_world(center), center.distance(a))) } /// True if `angle` lies inside the CCW arc [start..end] (radians). @@ -160,7 +176,13 @@ fn ccw_contains(angle: f64, start: f64, end: f64) -> bool { } /// Arc center+radius from two endpoints and a cursor (sagitta / bow-toward-cursor). -fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> { +fn arc_from_sagitta( + s: DVec3, + e: DVec3, + cursor: DVec3, + plane: WorkingPlane, +) -> Option<(DVec3, f64)> { + let (s, e, cursor) = (plane.to_local(s), plane.to_local(e), plane.to_local(cursor)); let chord_vec = e - s; let chord_len = (chord_vec.x * chord_vec.x + chord_vec.y * chord_vec.y).sqrt(); if chord_len < 1e-6 { @@ -177,28 +199,48 @@ fn arc_from_sagitta(s: DVec3, e: DVec3, cursor: DVec3) -> Option<(DVec3, f64)> { let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5)) .max(0.0) .sqrt(); - Some((mid - perp * h.signum() * d, r)) + Some((plane.to_world(mid - perp * h.signum() * d), r)) } /// Arc center+radius from start, end, and a radius-magnitude point (dist = dist(pt, start)). -fn arc_from_se_radius(s: DVec3, e: DVec3, radius_pt: DVec3) -> Option<(DVec3, f64)> { +fn arc_from_se_radius( + s: DVec3, + e: DVec3, + radius_pt: DVec3, + plane: WorkingPlane, +) -> Option<(DVec3, f64)> { + let (s, e, radius_pt) = ( + plane.to_local(s), + plane.to_local(e), + plane.to_local(radius_pt), + ); let r = s.distance(radius_pt).max(1e-3); let chord_len = s.distance(e); if r < chord_len * 0.5 { return None; } let unit_chord = (e - s) / chord_len; - let perp = DVec3::new(-unit_chord.y, 0.0, unit_chord.x); + let perp = DVec3::new(-unit_chord.y, unit_chord.x, 0.0); let mid = (s + e) * 0.5; let h_sign = (radius_pt - mid).dot(perp).signum(); let d = (r * r - (chord_len * 0.5) * (chord_len * 0.5)) .max(0.0) .sqrt(); - Some((mid - perp * h_sign * d, r)) + Some((plane.to_world(mid - perp * h_sign * d), r)) } /// Arc center+radius from start, end, and a tangent-direction point at start. -fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> { +fn arc_from_direction( + s: DVec3, + e: DVec3, + dir_pt: DVec3, + plane: WorkingPlane, +) -> Option<(DVec3, f64)> { + let (s, e, dir_pt) = ( + plane.to_local(s), + plane.to_local(e), + plane.to_local(dir_pt), + ); let t = (dir_pt - s).normalize_or_zero(); if t.length_squared() < 1e-12 { return None; @@ -211,7 +253,7 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> } let lambda = chord.length_squared() * 0.5 / denom; let center = s + perp_t * lambda; - Some((center, center.distance(s))) + Some((plane.to_world(center), center.distance(s))) } /// Tangent-continuing arc: starts at `s` leaving in unit direction `t` (forward, @@ -225,18 +267,25 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)> /// checking the circle's CCW travel direction at `s` and swapping the angles /// when it opposes `+t`. `flip` (Ctrl) selects the complementary arc — the other /// way around the same circle. -fn arc_continue(s: DVec3, t: DVec3, e: DVec3, flip: bool) -> Option<(DVec3, f64, f64, f64)> { - let (center, radius) = arc_from_direction(s, e, s + t)?; +fn arc_continue( + s: DVec3, + t: DVec3, + e: DVec3, + flip: bool, + plane: WorkingPlane, +) -> Option<(DVec3, f64, f64, f64)> { + let (center, radius) = arc_from_direction(s, e, s + t, plane)?; // CCW travel direction on this circle at s = (s - center) rotated +90°. - let rs = s - center; + let rs = plane.vector_to_local(s - center); + let t = plane.vector_to_local(t); let travel_s = DVec3::new(-rs.y, rs.x, 0.0); let forward = travel_s.dot(t) >= 0.0; // Draw CCW s→e when the CCW-from-s arc leaves along +t; else CCW e→s. `flip` // inverts the choice to draw the complementary arc. let (sa, ea) = if forward ^ flip { - (angle_xy(center, s), angle_xy(center, e)) + (angle_xy(center, s, plane), angle_xy(center, e, plane)) } else { - (angle_xy(center, e), angle_xy(center, s)) + (angle_xy(center, e, plane), angle_xy(center, s, plane)) }; Some((center, radius, sa, ea)) } @@ -264,12 +313,22 @@ pub fn continue_anchor(entity: &EntityType, last: Option) -> Option<(DVec let ep_v = a.end_point(); let sp = DVec3::new(sp_v.x, sp_v.y, sp_v.z); let ep = DVec3::new(ep_v.x, ep_v.y, ep_v.z); + let normal = (a.normal.x, a.normal.y, a.normal.z); + let (ax, ay) = crate::scene::view::transform::ocs_axes(normal); + let tangent = |angle: f64, ccw: bool| { + let sign = if ccw { 1.0 } else { -1.0 }; + DVec3::new( + sign * (-angle.sin() * ax.0 + angle.cos() * ay.0), + sign * (-angle.sin() * ax.1 + angle.cos() * ay.1), + sign * (-angle.sin() * ax.2 + angle.cos() * ay.2), + ) + }; if nearer_first(ep, sp) { // Ended at end_angle: outward tangent = CCW travel there. - Some((ep, DVec3::new(-a.end_angle.sin(), a.end_angle.cos(), 0.0))) + Some((ep, tangent(a.end_angle, true))) } else { // Ended at start_angle: outward tangent = CW travel there. - Some((sp, DVec3::new(a.start_angle.sin(), -a.start_angle.cos(), 0.0))) + Some((sp, tangent(a.start_angle, false))) } } _ => None, @@ -292,6 +351,7 @@ pub struct ArcCommand { sa: f64, prev_pt: Option, cw: bool, + plane: WorkingPlane, } impl ArcCommand { @@ -303,11 +363,16 @@ impl ArcCommand { sa: 0.0, prev_pt: None, cw: false, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC" } @@ -360,16 +425,16 @@ impl CadCommand for ArcCommand { } 1 => { self.r = self.c.distance(pt); - self.sa = angle_xy(self.c, pt); + self.sa = angle_xy(self.c, pt, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); let e = if self.cw { - make_arc(self.c, self.r, ea, self.sa) + make_arc(self.c, self.r, ea, self.sa, self.plane) } else { - make_arc(self.c, self.r, self.sa, ea) + make_arc(self.c, self.r, self.sa, ea, self.plane) }; CmdResult::CommitAndExit(e) } @@ -408,7 +473,7 @@ impl CadCommand for ArcCommand { // Only flip the sweep once the cursor has moved a clear // angular step; keep the reference point until then so slow // moves accumulate and jitter is ignored. - let d = rot_delta(self.c, prev, pt); + let d = rot_delta(self.c, prev, pt, self.plane); if d.abs() > DIR_TOL { self.cw = d < 0.0; self.prev_pt = Some(pt); @@ -416,11 +481,11 @@ impl CadCommand for ArcCommand { } else { self.prev_pt = Some(pt); } - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); Some(if self.cw { - arc_preview(self.c, self.r, ea, self.sa) + arc_preview(self.c, self.r, ea, self.sa, self.plane) } else { - arc_preview(self.c, self.r, self.sa, ea) + arc_preview(self.c, self.r, self.sa, ea, self.plane) }) } _ => None, @@ -432,15 +497,23 @@ impl CadCommand for ArcCommand { pub struct Arc3PCommand { pts: Vec, + plane: WorkingPlane, } impl Arc3PCommand { pub fn new() -> Self { - Self { pts: Vec::new() } + Self { + pts: Vec::new(), + plane: WorkingPlane::default(), + } } } impl CadCommand for Arc3PCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_3P" } @@ -457,22 +530,22 @@ impl CadCommand for Arc3PCommand { return CmdResult::NeedPoint; } let (p1, p2, p3) = (self.pts[0], self.pts[1], self.pts[2]); - match circumcircle(p1, p2, p3) { + match circumcircle(p1, p2, p3, self.plane) { None => { self.pts.pop(); CmdResult::NeedPoint } // collinear — retry Some((center, radius)) => { - let a1 = angle_xy(center, p1); - let a2 = angle_xy(center, p2); - let a3 = angle_xy(center, p3); + let a1 = angle_xy(center, p1, self.plane); + let a2 = angle_xy(center, p2, self.plane); + let a3 = angle_xy(center, p3, self.plane); // Choose arc direction so that p2 lies on the arc from p1 to p3. let (sa, ea) = if ccw_contains(a2, a1, a3) { (a1, a3) } else { (a3, a1) }; - CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) + CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane)) } } } @@ -491,16 +564,16 @@ impl CadCommand for Arc3PCommand { 1 => Some(line_wire(self.pts[0], pt)), _ => { let (p1, p2) = (self.pts[0], self.pts[1]); - if let Some((center, radius)) = circumcircle(p1, p2, pt) { - let a1 = angle_xy(center, p1); - let a2 = angle_xy(center, p2); - let a3 = angle_xy(center, pt); + if let Some((center, radius)) = circumcircle(p1, p2, pt, self.plane) { + let a1 = angle_xy(center, p1, self.plane); + let a2 = angle_xy(center, p2, self.plane); + let a3 = angle_xy(center, pt, self.plane); let (sa, ea) = if ccw_contains(a2, a1, a3) { (a1, a3) } else { (a3, a1) }; - Some(arc_preview(center, radius, sa, ea)) + Some(arc_preview(center, radius, sa, ea, self.plane)) } else { Some(WireModel::solid_f64( "rubber_band".into(), @@ -524,6 +597,7 @@ pub struct ArcSCECommand { sa: f64, prev_pt: Option, cw: bool, + plane: WorkingPlane, } impl ArcSCECommand { @@ -536,11 +610,16 @@ impl ArcSCECommand { sa: 0.0, prev_pt: None, cw: false, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSCECommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SCE" } @@ -564,16 +643,16 @@ impl CadCommand for ArcSCECommand { 1 => { self.c = pt; self.r = pt.distance(self.s); - self.sa = angle_xy(pt, self.s); + self.sa = angle_xy(pt, self.s, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); let e = if self.cw { - make_arc(self.c, self.r, ea, self.sa) + make_arc(self.c, self.r, ea, self.sa, self.plane) } else { - make_arc(self.c, self.r, self.sa, ea) + make_arc(self.c, self.r, self.sa, ea, self.plane) }; CmdResult::CommitAndExit(e) } @@ -596,7 +675,7 @@ impl CadCommand for ArcSCECommand { // Only flip the sweep once the cursor has moved a clear // angular step; keep the reference point until then so slow // moves accumulate and jitter is ignored. - let d = rot_delta(self.c, prev, pt); + let d = rot_delta(self.c, prev, pt, self.plane); if d.abs() > DIR_TOL { self.cw = d < 0.0; self.prev_pt = Some(pt); @@ -604,11 +683,11 @@ impl CadCommand for ArcSCECommand { } else { self.prev_pt = Some(pt); } - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); Some(if self.cw { - arc_preview(self.c, self.r, ea, self.sa) + arc_preview(self.c, self.r, ea, self.sa, self.plane) } else { - arc_preview(self.c, self.r, self.sa, ea) + arc_preview(self.c, self.r, self.sa, ea, self.plane) }) } _ => None, @@ -627,6 +706,7 @@ pub struct ArcSCACommand { sa: f64, prev_pt: Option, cw: bool, + plane: WorkingPlane, } impl ArcSCACommand { @@ -639,11 +719,16 @@ impl ArcSCACommand { sa: 0.0, prev_pt: None, cw: false, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSCACommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SCA" } @@ -671,16 +756,16 @@ impl CadCommand for ArcSCACommand { 1 => { self.c = pt; self.r = pt.distance(self.s); - self.sa = angle_xy(pt, self.s); + self.sa = angle_xy(pt, self.s, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); let e = if self.cw { - make_arc(self.c, self.r, ea, self.sa) + make_arc(self.c, self.r, ea, self.sa, self.plane) } else { - make_arc(self.c, self.r, self.sa, ea) + make_arc(self.c, self.r, self.sa, ea, self.plane) }; CmdResult::CommitAndExit(e) } @@ -701,7 +786,7 @@ impl CadCommand for ArcSCACommand { // Negative span = CW; positive = CCW. let ea = self.sa + span.to_radians(); return Some(CmdResult::CommitAndExit(make_arc( - self.c, self.r, self.sa, ea, + self.c, self.r, self.sa, ea, self.plane, ))); } None @@ -714,7 +799,9 @@ impl CadCommand for ArcSCACommand { anchor: DynAnchor::Point(self.c), fields: vec![DynFieldSpec::new(DynRole::Angle)], guide: DynGuide::Polar, - ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)), + ref_point: Some( + self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(), + ), }) } fn dyn_commit_as_text(&self) -> bool { @@ -722,7 +809,9 @@ impl CadCommand for ArcSCACommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { (self.step == 2).then(|| { - crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64 + crate::command::dyn_display_angle_deg( + (angle_xy(self.c, cursor, self.plane) - self.sa) as f32, + ) as f64 }) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { @@ -733,7 +822,7 @@ impl CadCommand for ArcSCACommand { // Only flip the sweep once the cursor has moved a clear // angular step; keep the reference point until then so slow // moves accumulate and jitter is ignored. - let d = rot_delta(self.c, prev, pt); + let d = rot_delta(self.c, prev, pt, self.plane); if d.abs() > DIR_TOL { self.cw = d < 0.0; self.prev_pt = Some(pt); @@ -741,11 +830,11 @@ impl CadCommand for ArcSCACommand { } else { self.prev_pt = Some(pt); } - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); Some(if self.cw { - arc_preview(self.c, self.r, ea, self.sa) + arc_preview(self.c, self.r, ea, self.sa, self.plane) } else { - arc_preview(self.c, self.r, self.sa, ea) + arc_preview(self.c, self.r, self.sa, ea, self.plane) }) } _ => None, @@ -763,6 +852,7 @@ pub struct ArcSCLCommand { c: DVec3, r: f64, sa: f64, + plane: WorkingPlane, } impl ArcSCLCommand { @@ -773,11 +863,16 @@ impl ArcSCLCommand { c: DVec3::ZERO, r: 0.0, sa: 0.0, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSCLCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SCL" } @@ -805,14 +900,20 @@ impl CadCommand for ArcSCLCommand { 1 => { self.c = pt; self.r = pt.distance(self.s); - self.sa = angle_xy(pt, self.s); + self.sa = angle_xy(pt, self.s, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { let chord = self.s.distance(pt); let ea = end_angle_from_chord_len(self.sa, chord, self.r); - CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea)) + CmdResult::CommitAndExit(make_arc( + self.c, + self.r, + self.sa, + ea, + self.plane, + )) } } } @@ -831,7 +932,7 @@ impl CadCommand for ArcSCLCommand { if chord > 0.0 { let ea = end_angle_from_chord_len(self.sa, chord, self.r); return Some(CmdResult::CommitAndExit(make_arc( - self.c, self.r, self.sa, ea, + self.c, self.r, self.sa, ea, self.plane, ))); } } @@ -859,7 +960,7 @@ impl CadCommand for ArcSCLCommand { 2 => { let chord = self.s.distance(pt); let ea = end_angle_from_chord_len(self.sa, chord, self.r); - Some(arc_preview(self.c, self.r, self.sa, ea)) + Some(arc_preview(self.c, self.r, self.sa, ea, self.plane)) } _ => None, } @@ -873,6 +974,7 @@ pub struct ArcSEACommand { step: u8, s: DVec3, e: DVec3, + plane: WorkingPlane, } impl ArcSEACommand { @@ -881,11 +983,16 @@ impl ArcSEACommand { step: 0, s: DVec3::ZERO, e: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSEACommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SEA" } @@ -908,11 +1015,11 @@ impl CadCommand for ArcSEACommand { self.step = 2; CmdResult::NeedPoint } - _ => match arc_from_sagitta(self.s, self.e, pt) { + _ => match arc_from_sagitta(self.s, self.e, pt, self.plane) { Some((center, radius)) => { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane)) } None => CmdResult::Cancel, }, @@ -931,10 +1038,12 @@ impl CadCommand for ArcSEACommand { match self.step { 1 => Some(line_wire(self.s, pt)), 2 => { - if let Some((center, radius)) = arc_from_sagitta(self.s, self.e, pt) { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - Some(arc_preview(center, radius, sa, ea)) + if let Some((center, radius)) = + arc_from_sagitta(self.s, self.e, pt, self.plane) + { + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + Some(arc_preview(center, radius, sa, ea, self.plane)) } else { Some(line_wire(self.s, self.e)) } @@ -951,6 +1060,7 @@ pub struct ArcSERCommand { step: u8, s: DVec3, e: DVec3, + plane: WorkingPlane, } impl ArcSERCommand { @@ -959,11 +1069,16 @@ impl ArcSERCommand { step: 0, s: DVec3::ZERO, e: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSERCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SER" } @@ -986,11 +1101,11 @@ impl CadCommand for ArcSERCommand { self.step = 2; CmdResult::NeedPoint } - _ => match arc_from_se_radius(self.s, self.e, pt) { + _ => match arc_from_se_radius(self.s, self.e, pt, self.plane) { Some((center, radius)) => { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane)) } None => CmdResult::Cancel, }, @@ -1008,18 +1123,22 @@ impl CadCommand for ArcSERCommand { fn on_text_input(&mut self, text: &str) -> Option { if self.step == 2 { let r: f64 = text.trim().replace(',', ".").parse().ok()?; - let chord = self.s.distance(self.e); + let s = self.plane.to_local(self.s); + let e = self.plane.to_local(self.e); + let chord = s.distance(e); if r > 0.0 && r >= chord / 2.0 { - let mid = (self.s + self.e) * 0.5; + let mid = (s + e) * 0.5; let perp = { - let cv = (self.e - self.s) / chord; + let cv = (e - s) / chord; DVec3::new(-cv.y, cv.x, 0.0) }; let d = (r * r - (chord / 2.0) * (chord / 2.0)).max(0.0).sqrt(); - let center = mid - perp * d; - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - return Some(CmdResult::CommitAndExit(make_arc(center, r, sa, ea))); + let center = self.plane.to_world(mid - perp * d); + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + return Some(CmdResult::CommitAndExit(make_arc( + center, r, sa, ea, self.plane, + ))); } } None @@ -1041,16 +1160,18 @@ impl CadCommand for ArcSERCommand { if self.step != 2 { return None; } - arc_from_se_radius(self.s, self.e, cursor).map(|(_, r)| r) + arc_from_se_radius(self.s, self.e, cursor, self.plane).map(|(_, r)| r) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { match self.step { 1 => Some(line_wire(self.s, pt)), 2 => { - if let Some((center, radius)) = arc_from_se_radius(self.s, self.e, pt) { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - Some(arc_preview(center, radius, sa, ea)) + if let Some((center, radius)) = + arc_from_se_radius(self.s, self.e, pt, self.plane) + { + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + Some(arc_preview(center, radius, sa, ea, self.plane)) } else { Some(line_wire(self.s, self.e)) } @@ -1067,6 +1188,7 @@ pub struct ArcSEDCommand { step: u8, s: DVec3, e: DVec3, + plane: WorkingPlane, } impl ArcSEDCommand { @@ -1075,11 +1197,16 @@ impl ArcSEDCommand { step: 0, s: DVec3::ZERO, e: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcSEDCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_SED" } @@ -1102,11 +1229,11 @@ impl CadCommand for ArcSEDCommand { self.step = 2; CmdResult::NeedPoint } - _ => match arc_from_direction(self.s, self.e, pt) { + _ => match arc_from_direction(self.s, self.e, pt, self.plane) { Some((center, radius)) => { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - CmdResult::CommitAndExit(make_arc(center, radius, sa, ea)) + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + CmdResult::CommitAndExit(make_arc(center, radius, sa, ea, self.plane)) } None => CmdResult::Cancel, }, @@ -1125,10 +1252,12 @@ impl CadCommand for ArcSEDCommand { match self.step { 1 => Some(line_wire(self.s, pt)), 2 => { - if let Some((center, radius)) = arc_from_direction(self.s, self.e, pt) { - let sa = angle_xy(center, self.s); - let ea = angle_xy(center, self.e); - Some(arc_preview(center, radius, sa, ea)) + if let Some((center, radius)) = + arc_from_direction(self.s, self.e, pt, self.plane) + { + let sa = angle_xy(center, self.s, self.plane); + let ea = angle_xy(center, self.e, self.plane); + Some(arc_preview(center, radius, sa, ea, self.plane)) } else { Some(line_wire(self.s, self.e)) } @@ -1148,6 +1277,7 @@ pub struct ArcCSACommand { sa: f64, prev_pt: Option, cw: bool, + plane: WorkingPlane, } impl ArcCSACommand { @@ -1159,11 +1289,16 @@ impl ArcCSACommand { sa: 0.0, prev_pt: None, cw: false, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcCSACommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_CSA" } @@ -1190,16 +1325,16 @@ impl CadCommand for ArcCSACommand { } 1 => { self.r = self.c.distance(pt); - self.sa = angle_xy(self.c, pt); + self.sa = angle_xy(self.c, pt, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); let e = if self.cw { - make_arc(self.c, self.r, ea, self.sa) + make_arc(self.c, self.r, ea, self.sa, self.plane) } else { - make_arc(self.c, self.r, self.sa, ea) + make_arc(self.c, self.r, self.sa, ea, self.plane) }; CmdResult::CommitAndExit(e) } @@ -1219,7 +1354,7 @@ impl CadCommand for ArcCSACommand { let span: f64 = text.trim().replace(',', ".").parse().ok()?; let ea = self.sa + span.to_radians(); return Some(CmdResult::CommitAndExit(make_arc( - self.c, self.r, self.sa, ea, + self.c, self.r, self.sa, ea, self.plane, ))); } None @@ -1230,7 +1365,9 @@ impl CadCommand for ArcCSACommand { anchor: DynAnchor::Point(self.c), fields: vec![DynFieldSpec::new(DynRole::Angle)], guide: DynGuide::Polar, - ref_point: Some(self.c + DVec3::new(self.sa.cos(), self.sa.sin(), 0.0)), + ref_point: Some( + self.c + self.plane.x * self.sa.cos() + self.plane.y * self.sa.sin(), + ), }) } fn dyn_commit_as_text(&self) -> bool { @@ -1238,7 +1375,9 @@ impl CadCommand for ArcCSACommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { (self.step == 2).then(|| { - crate::command::dyn_display_angle_deg((angle_xy(self.c, cursor) - self.sa) as f32) as f64 + crate::command::dyn_display_angle_deg( + (angle_xy(self.c, cursor, self.plane) - self.sa) as f32, + ) as f64 }) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { @@ -1249,7 +1388,7 @@ impl CadCommand for ArcCSACommand { // Only flip the sweep once the cursor has moved a clear // angular step; keep the reference point until then so slow // moves accumulate and jitter is ignored. - let d = rot_delta(self.c, prev, pt); + let d = rot_delta(self.c, prev, pt, self.plane); if d.abs() > DIR_TOL { self.cw = d < 0.0; self.prev_pt = Some(pt); @@ -1257,11 +1396,11 @@ impl CadCommand for ArcCSACommand { } else { self.prev_pt = Some(pt); } - let ea = angle_xy(self.c, pt); + let ea = angle_xy(self.c, pt, self.plane); Some(if self.cw { - arc_preview(self.c, self.r, ea, self.sa) + arc_preview(self.c, self.r, ea, self.sa, self.plane) } else { - arc_preview(self.c, self.r, self.sa, ea) + arc_preview(self.c, self.r, self.sa, ea, self.plane) }) } _ => None, @@ -1278,6 +1417,7 @@ pub struct ArcCSLCommand { s: DVec3, r: f64, sa: f64, + plane: WorkingPlane, } impl ArcCSLCommand { @@ -1288,11 +1428,16 @@ impl ArcCSLCommand { s: DVec3::ZERO, r: 0.0, sa: 0.0, + plane: WorkingPlane::default(), } } } impl CadCommand for ArcCSLCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_CSL" } @@ -1320,14 +1465,20 @@ impl CadCommand for ArcCSLCommand { 1 => { self.s = pt; self.r = self.c.distance(pt); - self.sa = angle_xy(self.c, pt); + self.sa = angle_xy(self.c, pt, self.plane); self.step = 2; CmdResult::NeedPoint } _ => { let chord = self.s.distance(pt); let ea = end_angle_from_chord_len(self.sa, chord, self.r); - CmdResult::CommitAndExit(make_arc(self.c, self.r, self.sa, ea)) + CmdResult::CommitAndExit(make_arc( + self.c, + self.r, + self.sa, + ea, + self.plane, + )) } } } @@ -1346,7 +1497,7 @@ impl CadCommand for ArcCSLCommand { if chord > 0.0 { let ea = end_angle_from_chord_len(self.sa, chord, self.r); return Some(CmdResult::CommitAndExit(make_arc( - self.c, self.r, self.sa, ea, + self.c, self.r, self.sa, ea, self.plane, ))); } } @@ -1374,7 +1525,7 @@ impl CadCommand for ArcCSLCommand { 2 => { let chord = self.s.distance(pt); let ea = end_angle_from_chord_len(self.sa, chord, self.r); - Some(arc_preview(self.c, self.r, self.sa, ea)) + Some(arc_preview(self.c, self.r, self.sa, ea, self.plane)) } _ => None, } @@ -1396,6 +1547,7 @@ pub struct ArcContCommand { /// committed arc was drawn from, so undoing a segment resumes the run /// from the prior anchor instead of ending it. history: Vec<(DVec3, DVec3)>, + plane: WorkingPlane, } impl ArcContCommand { @@ -1404,12 +1556,17 @@ impl ArcContCommand { s, tangent, ctrl: false, + plane: WorkingPlane::default(), history: Vec::new(), } } } impl CadCommand for ArcContCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARC_CONT" } @@ -1420,9 +1577,9 @@ impl CadCommand for ArcContCommand { self.ctrl = ctrl; } fn on_point(&mut self, pt: DVec3) -> CmdResult { - match arc_continue(self.s, self.tangent, pt, self.ctrl) { + match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) { Some((center, radius, sa, ea)) => { - let arc = make_arc(center, radius, sa, ea); + let arc = make_arc(center, radius, sa, ea, self.plane); // Remember the anchor this arc was drawn from — mid-command // Ctrl+Z reverts the commit and resumes from here. self.history.push((self.s, self.tangent)); @@ -1456,8 +1613,10 @@ impl CadCommand for ArcContCommand { CmdResult::Cancel } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - match arc_continue(self.s, self.tangent, pt, self.ctrl) { - Some((center, radius, sa, ea)) => Some(arc_preview(center, radius, sa, ea)), + match arc_continue(self.s, self.tangent, pt, self.ctrl, self.plane) { + Some((center, radius, sa, ea)) => { + Some(arc_preview(center, radius, sa, ea, self.plane)) + } None => Some(line_wire(self.s, pt)), } } diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index 8f5128270..b7dee4772 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -12,7 +12,7 @@ use acadrust::EntityType; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::scene::model::wire_model::WireModel; enum Step { @@ -37,6 +37,7 @@ pub struct AttdefCommand { text_style: String, width_factor: f64, oblique_angle: f64, + plane: WorkingPlane, } impl AttdefCommand { @@ -52,11 +53,16 @@ impl AttdefCommand { text_style, width_factor, oblique_angle, + plane: WorkingPlane::default(), } } } impl CadCommand for AttdefCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ATTDEF" } @@ -134,11 +140,12 @@ impl CadCommand for AttdefCommand { default, } = &self.step { + let point = self.plane.to_local(pt); let mut attdef = AttributeDefinition { tag: tag.clone(), prompt: prompt.clone(), default_value: default.clone(), - insertion_point: Vector3::new(pt.x, pt.y, pt.z), + insertion_point: Vector3::new(point.x, point.y, point.z), height: self.height, text_style: self.text_style.clone(), width_factor: self.width_factor, @@ -146,7 +153,10 @@ impl CadCommand for AttdefCommand { ..Default::default() }; attdef.common.layer = "0".into(); - CmdResult::CommitAndExit(EntityType::AttributeDefinition(attdef)) + CmdResult::CommitAndExit( + self.plane + .place_entity(EntityType::AttributeDefinition(attdef)), + ) } else { CmdResult::NeedPoint } @@ -177,12 +187,19 @@ impl CadCommand for AttdefCommand { } } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { let pt = pt.as_vec3(); + fn on_mouse_move(&mut self, pt: DVec3) -> Option { if !matches!(self.step, Step::Insertion { .. }) { return None; } // Show a small cross at the insertion point. - let d = 0.15_f32; + let d = 0.15; + let points = [ + pt - self.plane.x * d, + pt + self.plane.x * d, + DVec3::splat(f64::NAN), + pt - self.plane.y * d, + pt + self.plane.y * d, + ]; Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, @@ -195,13 +212,10 @@ impl CadCommand for AttdefCommand { dash_align_end: None, text_verts: Vec::new(), name: "attdef_preview".into(), - points: vec![ - [pt.x - d, pt.y, pt.z], - [pt.x + d, pt.y, pt.z], - [f32::NAN, 0.0, 0.0], - [pt.x, pt.y, pt.z - d], - [pt.x, pt.y, pt.z + d], - ], + points: points + .iter() + .map(|point| point.as_vec3().to_array()) + .collect(), points_low: Vec::new(), color: WireModel::CYAN, selected: false, diff --git a/src/modules/draw/draw/centerline.rs b/src/modules/draw/draw/centerline.rs index 616e2c44e..789416bb7 100644 --- a/src/modules/draw/draw/centerline.rs +++ b/src/modules/draw/draw/centerline.rs @@ -19,7 +19,7 @@ use acadrust::{EntityType, Handle, Line}; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; // ── Ribbon definition ───────────────────────────────────────────────────── @@ -191,6 +191,7 @@ pub struct CenterLineCommand { /// The entity injected by the host before `on_entity_pick` runs; `None` /// until the host injects it. picked: Option, + plane: WorkingPlane, } impl CenterLineCommand { @@ -198,19 +199,29 @@ impl CenterLineCommand { Self { first: None, picked: None, + plane: WorkingPlane::default(), } } /// Extract a `LineGeom` from a picked entity; `None` for anything else. - fn as_line(entity: &EntityType) -> Option { + fn as_line(&self, entity: &EntityType) -> Option { match entity { - EntityType::Line(l) => Some(LineGeom::from_line(l)), + EntityType::Line(line) => { + let mut geom = LineGeom::from_line(line); + geom.start = self.plane.to_local(geom.start); + geom.end = self.plane.to_local(geom.end); + Some(geom) + } _ => None, } } } impl CadCommand for CenterLineCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CENTERLINE" } @@ -241,7 +252,7 @@ impl CadCommand for CenterLineCommand { } // Resolve the just-picked entity; ignore the pick if it isn't a Line. let picked = self.picked.take(); - let geom = match picked.as_ref().and_then(Self::as_line) { + let geom = match picked.as_ref().and_then(|entity| self.as_line(entity)) { Some(g) => g, None => return CmdResult::NeedPoint, }; @@ -257,7 +268,9 @@ impl CadCommand for CenterLineCommand { CmdResult::NeedPoint } Some(first) => match compute_center_line(first, geom) { - Some(line) => CmdResult::CommitAndExit(EntityType::Line(line)), + Some(line) => CmdResult::CommitAndExit( + self.plane.place_entity(EntityType::Line(line)), + ), // Degenerate combination (coincident / no usable result): // keep prompting for a usable second line. None => CmdResult::NeedPoint, diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index ea212c0f8..8e6f38430 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -12,7 +12,7 @@ use acadrust::types::Vector3; use acadrust::{Circle, EntityType}; use crate::t; -use crate::command::{CadCommand, CmdResult, DynField, TangentObject}; +use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane}; use crate::modules::draw::defaults; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; @@ -59,16 +59,18 @@ pub const ICON: IconKind = ICON_CR; // ── Shared geometry ──────────────────────────────────────────────────────── -fn circle_wire(center: DVec3, radius: f64) -> WireModel { +fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel { let segs = 64u32; let mut pts: Vec<[f64; 3]> = (0..=segs) .map(|i| { let a = (i as f64) * TAU / segs as f64; - [ - center.x + radius * a.cos(), - center.y + radius * a.sin(), - center.z, - ] + let point = center + + plane.vector_to_world(DVec3::new( + radius * a.cos(), + radius * a.sin(), + 0.0, + )); + [point.x, point.y, point.z] }) .collect(); if let Some(first) = pts.first().cloned() { @@ -77,17 +79,24 @@ fn circle_wire(center: DVec3, radius: f64) -> WireModel { WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) } -fn make_circle(center: DVec3, radius: f64) -> EntityType { - EntityType::Circle(Circle { +fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType { + let center = plane.to_local(center); + plane.place_entity(EntityType::Circle(Circle { center: Vector3::new(center.x, center.y, center.z), radius, ..Default::default() - }) + })) } /// Circumscribed circle through three points. /// Returns `None` if the points are collinear. -fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { +fn circumcircle( + a: DVec3, + b: DVec3, + c: DVec3, + plane: WorkingPlane, +) -> Option<(DVec3, f64)> { + let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c)); let ax = a.x; let ay = a.y; let bx = b.x; @@ -107,7 +116,25 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { + (cx * cx + cy * cy) * (bx - ax)) / d; let center = DVec3::new(ux, uy, a.z); - Some((center, center.distance(a))) + Some((plane.to_world(center), center.distance(a))) +} + +fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 { + let d = plane.vector_to_local(b - a); + d.x.hypot(d.y) +} + +fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { + match object { + TangentObject::Line { p1, p2 } => TangentObject::Line { + p1: plane.to_local(p1), + p2: plane.to_local(p2), + }, + TangentObject::Circle { center, radius } => TangentObject::Circle { + center: plane.to_local(center), + radius, + }, + } } // ── Command: Center, Radius ──────────────────────────────────────────────── @@ -115,6 +142,7 @@ fn circumcircle(a: DVec3, b: DVec3, c: DVec3) -> Option<(DVec3, f64)> { pub struct CircleCommand { step: StepCR, default_r: f64, + plane: WorkingPlane, } enum StepCR { Center, @@ -126,11 +154,16 @@ impl CircleCommand { Self { step: StepCR::Center, default_r: defaults::get_circle_radius(), + plane: WorkingPlane::default(), } } } impl CadCommand for CircleCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE" } @@ -176,9 +209,9 @@ impl CadCommand for CircleCommand { CmdResult::NeedPoint } StepCR::Radius(c) => { - let r = c.distance(pt); + let r = plane_distance(*c, pt, self.plane); defaults::set_circle_radius(r); - CmdResult::CommitAndExit(make_circle(*c, r)) + CmdResult::CommitAndExit(make_circle(*c, r, self.plane)) } } } @@ -186,7 +219,7 @@ impl CadCommand for CircleCommand { if let StepCR::Radius(c) = &self.step { let c = *c; let r = self.default_r; - return CmdResult::CommitAndExit(make_circle(c, r)); + return CmdResult::CommitAndExit(make_circle(c, r, self.plane)); } CmdResult::Cancel } @@ -195,7 +228,7 @@ impl CadCommand for CircleCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { if let StepCR::Radius(c) = &self.step { - Some(circle_wire(*c, c.distance(pt))) + Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane)) } else { None } @@ -217,7 +250,7 @@ impl CadCommand for CircleCommand { let r: f64 = text.trim().replace(',', ".").parse().ok()?; if r > 0.0 { defaults::set_circle_radius(r); - return Some(CmdResult::CommitAndExit(make_circle(*c, r))); + return Some(CmdResult::CommitAndExit(make_circle(*c, r, self.plane))); } } None @@ -251,6 +284,7 @@ impl CadCommand for CircleCommand { pub struct CircleCDCommand { step: StepCR, default_d: f64, + plane: WorkingPlane, } impl CircleCDCommand { @@ -258,11 +292,16 @@ impl CircleCDCommand { Self { step: StepCR::Center, default_d: defaults::get_circle_diam(), + plane: WorkingPlane::default(), } } } impl CadCommand for CircleCDCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE_CD" } @@ -283,9 +322,9 @@ impl CadCommand for CircleCDCommand { CmdResult::NeedPoint } StepCR::Radius(c) => { - let d = c.distance(pt) * 2.0; + let d = plane_distance(*c, pt, self.plane) * 2.0; defaults::set_circle_diam(d); - CmdResult::CommitAndExit(make_circle(*c, d / 2.0)) + CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane)) } } } @@ -293,7 +332,7 @@ impl CadCommand for CircleCDCommand { if let StepCR::Radius(c) = &self.step { let c = *c; let d = self.default_d; - return CmdResult::CommitAndExit(make_circle(c, (d / 2.0) as f64)); + return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane)); } CmdResult::Cancel } @@ -303,7 +342,7 @@ impl CadCommand for CircleCDCommand { fn on_mouse_move(&mut self, pt: DVec3) -> Option { // Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x). if let StepCR::Radius(c) = &self.step { - Some(circle_wire(*c, c.distance(pt))) + Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane)) } else { None } @@ -313,7 +352,7 @@ impl CadCommand for CircleCDCommand { let d: f64 = text.trim().replace(',', ".").parse().ok()?; if d > 0.0 { defaults::set_circle_diam(d); - return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0))); + return Some(CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane))); } } None @@ -339,15 +378,23 @@ impl CadCommand for CircleCDCommand { pub struct Circle2PCommand { p1: Option, + plane: WorkingPlane, } impl Circle2PCommand { pub fn new() -> Self { - Self { p1: None } + Self { + p1: None, + plane: WorkingPlane::default(), + } } } impl CadCommand for Circle2PCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE_2P" } @@ -366,8 +413,8 @@ impl CadCommand for Circle2PCommand { } Some(p1) => { let center = (p1 + pt) * 0.5; - let radius = p1.distance(pt) / 2.0; - CmdResult::CommitAndExit(make_circle(center, radius)) + let radius = plane_distance(p1, pt, self.plane) / 2.0; + CmdResult::CommitAndExit(make_circle(center, radius, self.plane)) } } } @@ -380,8 +427,8 @@ impl CadCommand for Circle2PCommand { fn on_mouse_move(&mut self, pt: DVec3) -> Option { let p1 = self.p1?; let center = (p1 + pt) * 0.5; - let radius = p1.distance(pt) / 2.0; - Some(circle_wire(center, radius)) + let radius = plane_distance(p1, pt, self.plane) / 2.0; + Some(circle_wire(center, radius, self.plane)) } } @@ -389,15 +436,23 @@ impl CadCommand for Circle2PCommand { pub struct Circle3PCommand { pts: Vec, + plane: WorkingPlane, } impl Circle3PCommand { pub fn new() -> Self { - Self { pts: Vec::new() } + Self { + pts: Vec::new(), + plane: WorkingPlane::default(), + } } } impl CadCommand for Circle3PCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE_3P" } @@ -414,8 +469,10 @@ impl CadCommand for Circle3PCommand { return CmdResult::NeedPoint; } let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]); - match circumcircle(a, b, c) { - Some((center, radius)) => CmdResult::CommitAndExit(make_circle(center, radius)), + match circumcircle(a, b, c, self.plane) { + Some((center, radius)) => { + CmdResult::CommitAndExit(make_circle(center, radius, self.plane)) + } None => { self.pts.pop(); CmdResult::NeedPoint @@ -435,14 +492,14 @@ impl CadCommand for Circle3PCommand { // Show circle preview with p1→cursor as diameter (same as 2P). let p1 = self.pts[0]; let center = (p1 + pt) * 0.5; - let radius = p1.distance(pt) / 2.0; - Some(circle_wire(center, radius)) + let radius = plane_distance(p1, pt, self.plane) / 2.0; + Some(circle_wire(center, radius, self.plane)) } _ => { // Show circumcircle preview if non-collinear, else polyline. let (a, b) = (self.pts[0], self.pts[1]); - if let Some((center, radius)) = circumcircle(a, b, pt) { - Some(circle_wire(center, radius)) + if let Some((center, radius)) = circumcircle(a, b, pt, self.plane) { + Some(circle_wire(center, radius, self.plane)) } else { Some(WireModel::solid_f64( "rubber_band".into(), @@ -860,6 +917,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64) pub struct CircleTTRCommand { step: StepTTR, + plane: WorkingPlane, } enum StepTTR { @@ -880,11 +938,16 @@ impl CircleTTRCommand { pub fn new() -> Self { Self { step: StepTTR::First, + plane: WorkingPlane::default(), } } } impl CadCommand for CircleTTRCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE_TTR" } @@ -918,6 +981,8 @@ impl CadCommand for CircleTTRCommand { } fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult { + let obj = tangent_object_local(obj, self.plane); + let hit = self.plane.to_local(hit); match &self.step { StepTTR::First => { self.step = StepTTR::Second { @@ -955,7 +1020,11 @@ impl CadCommand for CircleTTRCommand { let hint = (*hit1 + *hit2) * 0.5; let candidates = ttr_candidates(*obj1, *obj2, r); if let Some(center) = best_of(&candidates, hint) { - Some(CmdResult::CommitAndExit(make_circle(center, r))) + Some(CmdResult::CommitAndExit(make_circle( + self.plane.to_world(center), + r, + self.plane, + ))) } else { Some(CmdResult::Cancel) } @@ -980,6 +1049,7 @@ impl CadCommand for CircleTTRCommand { pub struct CircleTTTCommand { objs: Vec, hits: Vec, + plane: WorkingPlane, } impl CircleTTTCommand { @@ -987,11 +1057,16 @@ impl CircleTTTCommand { Self { objs: Vec::new(), hits: Vec::new(), + plane: WorkingPlane::default(), } } } impl CadCommand for CircleTTTCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "CIRCLE_TTT" } @@ -1013,15 +1088,19 @@ impl CadCommand for CircleTTTCommand { } fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult { - self.objs.push(obj); - self.hits.push(hit); + self.objs.push(tangent_object_local(obj, self.plane)); + self.hits.push(self.plane.to_local(hit)); if self.objs.len() < 3 { return CmdResult::NeedPoint; } let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0; let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]); match best_circle_of(&candidates, hint) { - Some((center, r)) => CmdResult::CommitAndExit(make_circle(center, r)), + Some((center, r)) => CmdResult::CommitAndExit(make_circle( + self.plane.to_world(center), + r, + self.plane, + )), None => { self.objs.pop(); self.hits.pop(); diff --git a/src/modules/draw/draw/dimcenter.rs b/src/modules/draw/draw/dimcenter.rs index 299da3cb5..c725c7e6d 100644 --- a/src/modules/draw/draw/dimcenter.rs +++ b/src/modules/draw/draw/dimcenter.rs @@ -41,28 +41,46 @@ impl DimCenterCommand { } /// Extract (center, radius) from a Circle or Arc; `None` for anything else. - fn center_radius(entity: &EntityType) -> Option<(Vector3, f64)> { + fn center_radius(entity: &EntityType) -> Option<(DVec3, f64, DVec3, DVec3)> { match entity { - EntityType::Circle(c) => Some((c.center, c.radius)), - EntityType::Arc(a) => Some((a.center, a.radius)), + EntityType::Circle(circle) => { + let normal = (circle.normal.x, circle.normal.y, circle.normal.z); + let center = crate::scene::view::transform::ocs_point_to_wcs( + (circle.center.x, circle.center.y, circle.center.z), + normal, + ); + let (x, y) = crate::scene::view::transform::ocs_axes(normal); + Some(( + DVec3::new(center.0, center.1, center.2), + circle.radius, + DVec3::new(x.0, x.1, x.2), + DVec3::new(y.0, y.1, y.2), + )) + } + EntityType::Arc(arc) => { + let normal = (arc.normal.x, arc.normal.y, arc.normal.z); + let center = crate::scene::view::transform::ocs_point_to_wcs( + (arc.center.x, arc.center.y, arc.center.z), + normal, + ); + let (x, y) = crate::scene::view::transform::ocs_axes(normal); + Some(( + DVec3::new(center.0, center.1, center.2), + arc.radius, + DVec3::new(x.0, x.1, x.2), + DVec3::new(y.0, y.1, y.2), + )) + } _ => None, } } /// Build the two cross lines from a center and radius. - fn build_cross(center: Vector3, radius: f64) -> Vec { + fn build_cross(center: DVec3, radius: f64, x: DVec3, y: DVec3) -> Vec { let m = radius * 0.2; - let cx = center.x; - let cy = center.y; - let cz = center.z; - let horizontal = Line::from_points( - Vector3::new(cx - m, cy, cz), - Vector3::new(cx + m, cy, cz), - ); - let vertical = Line::from_points( - Vector3::new(cx, cy - m, cz), - Vector3::new(cx, cy + m, cz), - ); + let to_vector = |point: DVec3| Vector3::new(point.x, point.y, point.z); + let horizontal = Line::from_points(to_vector(center - x * m), to_vector(center + x * m)); + let vertical = Line::from_points(to_vector(center - y * m), to_vector(center + y * m)); vec![EntityType::Line(horizontal), EntityType::Line(vertical)] } } @@ -93,8 +111,8 @@ impl CadCommand for DimCenterCommand { return CmdResult::NeedPoint; } match self.picked.as_ref().and_then(Self::center_radius) { - Some((center, radius)) => { - let lines = Self::build_cross(center, radius); + Some((center, radius, x, y)) => { + let lines = Self::build_cross(center, radius, x, y); CmdResult::ReplaceMany(vec![], lines) } // Picked something that is not a circle or arc — keep prompting. diff --git a/src/modules/draw/draw/donut.rs b/src/modules/draw/draw/donut.rs index 36c21591d..c459492a6 100644 --- a/src/modules/draw/draw/donut.rs +++ b/src/modules/draw/draw/donut.rs @@ -15,12 +15,13 @@ use acadrust::EntityType; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; pub struct DonutCommand { state: DonutState, inner_r: f64, outer_r: f64, + plane: WorkingPlane, } enum DonutState { @@ -35,11 +36,16 @@ impl DonutCommand { state: DonutState::AskInner, inner_r: 0.0, outer_r: 1.0, + plane: WorkingPlane::default(), } } } impl CadCommand for DonutCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DONUT" } @@ -97,9 +103,10 @@ impl CadCommand for DonutCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.state { DonutState::PlaceCenter => { - let entity = make_donut(pt.x as f64, pt.y as f64, self.inner_r, self.outer_r); + let center = self.plane.to_local(pt); + let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r); // Keep command active so user can place more donuts. - CmdResult::CommitEntity(entity) + CmdResult::CommitEntity(self.plane.place_entity(entity)) } _ => CmdResult::NeedPoint, } @@ -119,7 +126,7 @@ impl CadCommand for DonutCommand { } } -fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType { +fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType { use acadrust::types::Vector2; let r_avg = (inner_r + outer_r) / 2.0; let width = outer_r - inner_r; @@ -127,6 +134,7 @@ fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType { let mut p = LwPolyline::new(); p.is_closed = true; p.constant_width = width; + p.elevation = elevation; // Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex) let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy)); diff --git a/src/modules/draw/draw/ellipse.rs b/src/modules/draw/draw/ellipse.rs index 00f0bd565..1b975efde 100644 --- a/src/modules/draw/draw/ellipse.rs +++ b/src/modules/draw/draw/ellipse.rs @@ -9,7 +9,7 @@ use acadrust::types::Vector3; use acadrust::{Ellipse, EntityType}; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -57,13 +57,14 @@ fn ellipse_wire( ratio: f64, // minor/major t_start: f64, t_end: f64, + plane: WorkingPlane, ) -> WireModel { let r_major = major.length(); if r_major < 1e-9 { return WireModel::solid("rubber_band".into(), vec![], WireModel::CYAN, false); } let major_dir = major / r_major; - let v = DVec3::Z.cross(major_dir).normalize(); + let v = plane.z.cross(major_dir).normalize_or(plane.y); let segs = 64u32; // Unwrap t_end so the arc goes counter-clockwise. let t_e = if t_end <= t_start { t_end + TAU } else { t_end }; @@ -88,13 +89,21 @@ fn param_angle(center: DVec3, major_dir: DVec3, v: DVec3, pt: DVec3, ratio: f64) } /// Build the final Ellipse entity. -fn make_ellipse(center: DVec3, major: DVec3, ratio: f64, t_start: f64, t_end: f64) -> Ellipse { +fn make_ellipse( + center: DVec3, + major: DVec3, + ratio: f64, + t_start: f64, + t_end: f64, + plane: WorkingPlane, +) -> Ellipse { Ellipse { center: Vector3::new(center.x, center.y, center.z), major_axis: Vector3::new(major.x, major.y, major.z), minor_axis_ratio: ratio, start_parameter: t_start, end_parameter: t_end, + normal: Vector3::new(plane.z.x, plane.z.y, plane.z.z), ..Default::default() } } @@ -110,17 +119,23 @@ enum CtrStep { pub struct EllipseCommand { step: CtrStep, + plane: WorkingPlane, } impl EllipseCommand { pub fn new() -> Self { Self { step: CtrStep::Center, + plane: WorkingPlane::default(), } } } impl CadCommand for EllipseCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ELLIPSE" } @@ -170,7 +185,7 @@ impl CadCommand for EllipseCommand { let (center, major) = (*center, *major); let ratio = minor_ratio(center, major, pt); CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( - center, major, ratio, 0.0, TAU, + center, major, ratio, 0.0, TAU, self.plane, ))) } } @@ -195,7 +210,7 @@ impl CadCommand for EllipseCommand { if r_minor > 0.0 { let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( - *center, *major, ratio, 0.0, TAU, + *center, *major, ratio, 0.0, TAU, self.plane, )))); } } @@ -207,7 +222,7 @@ impl CadCommand for EllipseCommand { CtrStep::MajorAxis { center } => Some(line_wire(*center, pt)), CtrStep::MinorRatio { center, major } => { let ratio = minor_ratio(*center, *major, pt).max(0.001); - Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) + Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane)) } _ => None, } @@ -250,17 +265,23 @@ enum AxisStep { pub struct EllipseAxisCommand { step: AxisStep, + plane: WorkingPlane, } impl EllipseAxisCommand { pub fn new() -> Self { Self { step: AxisStep::Pt1, + plane: WorkingPlane::default(), } } } impl CadCommand for EllipseAxisCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ELLIPSE_AXIS" } @@ -297,7 +318,7 @@ impl CadCommand for EllipseAxisCommand { let (center, major) = (*center, *major); let ratio = minor_ratio(center, major, pt); CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( - center, major, ratio, 0.0, TAU, + center, major, ratio, 0.0, TAU, self.plane, ))) } } @@ -313,7 +334,7 @@ impl CadCommand for EllipseAxisCommand { if r_minor > 0.0 { let ratio = (r_minor / major.length()).clamp(1e-6, 1.0); return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( - *center, *major, ratio, 0.0, TAU, + *center, *major, ratio, 0.0, TAU, self.plane, )))); } } @@ -326,7 +347,7 @@ impl CadCommand for EllipseAxisCommand { AxisStep::Pt2 { p1 } => Some(line_wire(*p1, pt)), AxisStep::MinorRatio { center, major } => { let ratio = minor_ratio(*center, *major, pt).max(0.001); - Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) + Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane)) } } } @@ -385,6 +406,7 @@ pub struct EllipseArcCommand { step: ArcStep, prev_pt: Option, cw: bool, + plane: WorkingPlane, } impl EllipseArcCommand { @@ -393,11 +415,16 @@ impl EllipseArcCommand { step: ArcStep::Center, prev_pt: None, cw: false, + plane: WorkingPlane::default(), } } } impl CadCommand for EllipseArcCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ELLIPSE_ARC" } @@ -458,7 +485,7 @@ impl CadCommand for EllipseArcCommand { ratio, } => { let (center, major, ratio) = (*center, *major, *ratio); - let t_start = angle_from_point(center, major, ratio, pt); + let t_start = angle_from_point(center, major, ratio, pt, self.plane); self.prev_pt = None; // reset direction tracking for end-angle step self.step = ArcStep::EndAngle { center, @@ -475,11 +502,11 @@ impl CadCommand for EllipseArcCommand { t_start, } => { let (center, major, ratio, t_start) = (*center, *major, *ratio, *t_start); - let t_end = angle_from_point(center, major, ratio, pt); + let t_end = angle_from_point(center, major, ratio, pt, self.plane); let entity = if self.cw { - make_ellipse(center, major, ratio, t_end, t_start) + make_ellipse(center, major, ratio, t_end, t_start, self.plane) } else { - make_ellipse(center, major, ratio, t_start, t_end) + make_ellipse(center, major, ratio, t_start, t_end, self.plane) }; CmdResult::CommitAndExit(EntityType::Ellipse(entity)) } @@ -530,7 +557,7 @@ impl CadCommand for EllipseArcCommand { // Typed degrees: positive = CCW, negative = CW. let t_end = val.to_radians(); return Some(CmdResult::CommitAndExit(EntityType::Ellipse(make_ellipse( - *center, *major, *ratio, *t_start, t_end, + *center, *major, *ratio, *t_start, t_end, self.plane, )))); } _ => {} @@ -543,7 +570,7 @@ impl CadCommand for EllipseArcCommand { ArcStep::MajorAxis { center } => Some(line_wire(*center, pt)), ArcStep::MinorRatio { center, major } => { let ratio = minor_ratio(*center, *major, pt).max(0.001); - Some(ellipse_wire(*center, *major, ratio, 0.0, TAU)) + Some(ellipse_wire(*center, *major, ratio, 0.0, TAU, self.plane)) } ArcStep::StartAngle { center, .. } => { // Only the start angle is being chosen here — show a line from @@ -564,8 +591,8 @@ impl CadCommand for EllipseArcCommand { // tolerance ignores jitter; the reference advances only on a // clear move so slow sweeps accumulate. if let Some(prev) = self.prev_pt { - let t_prev = angle_from_point(*center, *major, *ratio, prev); - let t_cur = angle_from_point(*center, *major, *ratio, pt); + let t_prev = angle_from_point(*center, *major, *ratio, prev, self.plane); + let t_cur = angle_from_point(*center, *major, *ratio, pt, self.plane); let mut d = t_cur - t_prev; while d > std::f64::consts::PI { d -= TAU; @@ -580,11 +607,11 @@ impl CadCommand for EllipseArcCommand { } else { self.prev_pt = Some(pt); } - let t_end = angle_from_point(*center, *major, *ratio, pt); + let t_end = angle_from_point(*center, *major, *ratio, pt, self.plane); Some(if self.cw { - ellipse_wire(*center, *major, *ratio, t_end, *t_start) + ellipse_wire(*center, *major, *ratio, t_end, *t_start, self.plane) } else { - ellipse_wire(*center, *major, *ratio, *t_start, t_end) + ellipse_wire(*center, *major, *ratio, *t_start, t_end, self.plane) }) } _ => None, @@ -638,13 +665,19 @@ fn minor_ratio(center: DVec3, major: DVec3, pt: DVec3) -> f64 { (r_minor / r_major).clamp(1e-6, 1.0) } -fn angle_from_point(center: DVec3, major: DVec3, ratio: f64, pt: DVec3) -> f64 { +fn angle_from_point( + center: DVec3, + major: DVec3, + ratio: f64, + pt: DVec3, + plane: WorkingPlane, +) -> f64 { let r_major = major.length(); if r_major < 1e-9 { return 0.0; } let major_dir = major / r_major; - let v = DVec3::Z.cross(major_dir).normalize(); + let v = plane.z.cross(major_dir).normalize_or(plane.y); param_angle(center, major_dir, v, pt, ratio) } diff --git a/src/modules/draw/draw/helix.rs b/src/modules/draw/draw/helix.rs index 9615756a3..e35d33c62 100644 --- a/src/modules/draw/draw/helix.rs +++ b/src/modules/draw/draw/helix.rs @@ -17,7 +17,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use crate::t; -use crate::command::{CadCommand, CmdResult, DynField}; +use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -58,6 +58,7 @@ pub struct HelixCommand { top_radius: f64, height: f64, turns: f64, + plane: WorkingPlane, } impl HelixCommand { @@ -69,6 +70,7 @@ impl HelixCommand { top_radius: 0.0, height: 0.0, turns: DEFAULT_TURNS, + plane: WorkingPlane::default(), } } @@ -82,11 +84,12 @@ impl HelixCommand { let angle = turns * std::f64::consts::TAU * frac; let radius = base_radius + (top_radius - base_radius) * frac; let z = height * frac; - pts.push(DVec3::new( - self.center.x + radius * angle.cos(), - self.center.y + radius * angle.sin(), - self.center.z + z, - )); + pts.push( + self.center + + self.plane.x * (radius * angle.cos()) + + self.plane.y * (radius * angle.sin()) + + self.plane.z * z, + ); } pts } @@ -110,6 +113,10 @@ impl HelixCommand { } impl CadCommand for HelixCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "HELIX" } @@ -134,7 +141,8 @@ impl CadCommand for HelixCommand { Step::BaseRadius => { // A point click resolves the base radius as the planar distance // from the centre to the picked point. - let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); + let delta = self.plane.vector_to_local(pt - self.center); + let r = delta.x.hypot(delta.y); if r > 0.0 { self.base_radius = r; self.step = Step::TopRadius; @@ -142,14 +150,15 @@ impl CadCommand for HelixCommand { CmdResult::NeedPoint } Step::TopRadius => { - let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); + let delta = self.plane.vector_to_local(pt - self.center); + let r = delta.x.hypot(delta.y); self.top_radius = if r > 0.0 { r } else { self.base_radius }; self.step = Step::Height; CmdResult::NeedPoint } Step::Height => { // Use the elevation of the picked point relative to the centre. - let h = pt.z - self.center.z; + let h = (pt - self.center).dot(self.plane.z); if h.abs() > 0.0 { self.height = h; self.step = Step::Turns; @@ -250,7 +259,8 @@ impl CadCommand for HelixCommand { match self.step { Step::Center => None, Step::BaseRadius => { - let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); + let delta = self.plane.vector_to_local(pt - self.center); + let r = delta.x.hypot(delta.y); if r <= 0.0 { return None; } @@ -267,12 +277,13 @@ impl CadCommand for HelixCommand { // Use the parameters fixed so far; for the height step let the // cursor elevation drive the live height. let height = if self.step == Step::Height { - pt.z - self.center.z + (pt - self.center).dot(self.plane.z) } else { self.height }; let top = if self.step == Step::TopRadius { - let r = ((pt.x - self.center.x).powi(2) + (pt.y - self.center.y).powi(2)).sqrt(); + let delta = self.plane.vector_to_local(pt - self.center); + let r = delta.x.hypot(delta.y); if r > 0.0 { r } else { diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 664651d25..77e54073f 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -11,7 +11,7 @@ use acadrust::EntityType; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::scene::model::wire_model::WireModel; pub struct MlineCommand { @@ -21,6 +21,7 @@ pub struct MlineCommand { style_name: String, style_handle: Option, style_element_count: usize, + plane: WorkingPlane, } impl MlineCommand { @@ -33,6 +34,7 @@ impl MlineCommand { style_name: "Standard".into(), style_handle: None, style_element_count: 2, + plane: WorkingPlane::default(), } } @@ -48,11 +50,16 @@ impl MlineCommand { style_name: style_name.into(), style_handle, style_element_count: style_element_count.max(1), + plane: WorkingPlane::default(), } } } impl CadCommand for MlineCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "MLINE" } @@ -105,14 +112,18 @@ impl CadCommand for MlineCommand { // Close command if (up == "C" || up == "CLOSE") && self.points.len() >= 3 { let entity = build_mline( - &self.points, + &self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect::>(), self.scale, true, &self.style_name, self.style_handle, self.style_element_count, ); - return Some(CmdResult::CommitAndExit(entity)); + return Some(CmdResult::CommitAndExit(self.plane.place_entity(entity))); } // Scale: "S" alone → prompt for value @@ -144,14 +155,18 @@ impl CadCommand for MlineCommand { return CmdResult::Cancel; } let entity = build_mline( - &self.points, + &self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect::>(), self.scale, false, &self.style_name, self.style_handle, self.style_element_count, ); - CmdResult::CommitAndExit(entity) + CmdResult::CommitAndExit(self.plane.place_entity(entity)) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 70bd02411..61542c690 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -15,7 +15,7 @@ use acadrust::{EntityType, Handle, LwPolyline}; use glam::{DVec2, DVec3, Vec2, Vec3}; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -52,6 +52,7 @@ pub struct PlineCommand { /// document makes the partial polyline snappable while later vertices are /// being placed. (#119) live_handle: Option, + plane: WorkingPlane, } impl PlineCommand { @@ -62,6 +63,7 @@ impl PlineCommand { mode: SegMode::Line, last_tangent: None, live_handle: None, + plane: WorkingPlane::default(), } } @@ -78,7 +80,11 @@ impl PlineCommand { // following Arc segment stays tangent-continuous. let n = self.vertices.len(); self.last_tangent = if n >= 2 { - seg_exit_tangent(self.vertices[n - 2], self.vertices[n - 1], self.bulges[n - 2]) + seg_exit_tangent( + self.plane.to_local(self.vertices[n - 2]), + self.plane.to_local(self.vertices[n - 1]), + self.bulges[n - 2], + ) } else { None }; @@ -116,8 +122,12 @@ impl PlineCommand { if self.vertices.len() < 2 { return None; } - let lw_verts: Vec = self + let local: Vec = self .vertices + .iter() + .map(|vertex| self.plane.to_local(*vertex)) + .collect(); + let lw_verts: Vec = local .iter() .enumerate() .map(|(i, v)| { @@ -128,10 +138,11 @@ impl PlineCommand { .collect(); let pline = LwPolyline { vertices: lw_verts, + elevation: local.first().map_or(0.0, |point| point.z), is_closed: closed, ..Default::default() }; - Some(EntityType::LwPolyline(pline)) + Some(self.plane.place_entity(EntityType::LwPolyline(pline))) } } @@ -250,6 +261,10 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[ // ── CadCommand impl ──────────────────────────────────────────────────────── impl CadCommand for PlineCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "PLINE" } @@ -291,11 +306,13 @@ impl CadCommand for PlineCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { if !self.vertices.is_empty() { let last = *self.vertices.last().unwrap(); + let last_local = self.plane.to_local(last); + let pt_local = self.plane.to_local(pt); let last_idx = self.vertices.len() - 1; let bulge = match self.mode { SegMode::Line => { - let d = DVec2::new(pt.x - last.x, pt.y - last.y); + let d = DVec2::new(pt_local.x - last_local.x, pt_local.y - last_local.y); if d.length_squared() > 1e-10 { // Direction only — f32 is sufficient for tangent continuity. self.last_tangent = Some(d.normalize().as_vec2()); @@ -303,8 +320,8 @@ impl CadCommand for PlineCommand { 0.0 } SegMode::Arc => { - let a = DVec2::new(last.x, last.y); - let b = DVec2::new(pt.x, pt.y); + let a = DVec2::new(last_local.x, last_local.y); + let b = DVec2::new(pt_local.x, pt_local.y); let tangent = self .last_tangent .map(|t| t.as_dvec2()) @@ -417,11 +434,13 @@ impl CadCommand for PlineCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - let pt = pt.as_vec3(); + let pt_world = pt; // The committed vertices already render as a real document entity, so // the preview is just the pending segment from the last vertex to the // cursor. (#119) Downcast to f32 for the pixel-space rubber band. - let last = self.vertices.last()?.as_vec3(); + let last_world = *self.vertices.last()?; + let last = self.plane.to_local(last_world).as_vec3(); + let pt = self.plane.to_local(pt_world).as_vec3(); let mut pts: Vec<[f32; 3]> = Vec::new(); match self.mode { @@ -442,9 +461,13 @@ impl CadCommand for PlineCommand { } } + let world_points = pts + .into_iter() + .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) + .collect(); Some(WireModel::solid( "rubber_band".into(), - pts, + world_points, WireModel::CYAN, false, )) diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 3f7868c94..aeeac0073 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -12,7 +12,7 @@ use acadrust::EntityType; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::scene::model::wire_model::WireModel; pub struct ImageCommand { @@ -20,6 +20,7 @@ pub struct ImageCommand { pixel_width: u32, pixel_height: u32, origin: Option, + plane: WorkingPlane, } impl ImageCommand { @@ -29,6 +30,7 @@ impl ImageCommand { pixel_width, pixel_height, origin: None, + plane: WorkingPlane::default(), } } @@ -41,6 +43,8 @@ impl ImageCommand { } fn make_entity(&self, origin: DVec3, width_pt: DVec3) -> EntityType { + let origin = self.plane.to_local(origin); + let width_pt = self.plane.to_local(width_pt); let world_width = (width_pt.x - origin.x).abs().max(0.001); let world_height = world_width / self.aspect(); @@ -56,11 +60,15 @@ impl ImageCommand { ); img.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE | acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY; - EntityType::RasterImage(img) + self.plane.place_entity(EntityType::RasterImage(img)) } } impl CadCommand for ImageCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "IMAGE" } @@ -91,7 +99,7 @@ impl CadCommand for ImageCommand { // If origin is set, place with a default width of 1 unit * pixel count / 100 if let Some(origin) = self.origin { let default_w = (self.pixel_width as f64 / 100.0).max(1.0); - let width_pt = DVec3::new(origin.x + default_w, origin.y, origin.z); + let width_pt = origin + self.plane.x * default_w; let entity = self.make_entity(origin, width_pt); CmdResult::CommitAndExit(entity) } else { @@ -99,15 +107,20 @@ impl CadCommand for ImageCommand { } } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { let pt = pt.as_vec3(); - let origin = self.origin?.as_vec3(); - let world_width = (pt.x - origin.x).abs().max(0.001); - let world_height = world_width / self.aspect() as f32; - - let p0 = [origin.x, origin.y, origin.z]; - let p1 = [origin.x + world_width, origin.y, origin.z]; - let p2 = [origin.x + world_width, origin.y + world_height, origin.z]; - let p3 = [origin.x, origin.y + world_height, origin.z]; + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + let origin = self.origin?; + let origin_local = self.plane.to_local(origin); + let point_local = self.plane.to_local(pt); + let width = (point_local.x - origin_local.x).abs().max(0.001); + let height = width / self.aspect(); + let corners = [ + origin_local, + origin_local + DVec3::X * width, + origin_local + DVec3::new(width, height, 0.0), + origin_local + DVec3::Y * height, + ] + .map(|point| self.plane.to_world(point).as_vec3().to_array()); + let [p0, p1, p2, p3] = corners; Some(WireModel { taper_widths: Vec::new(), diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 83cfb182d..2e6dfc5a9 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -10,7 +10,7 @@ use acadrust::{entities::LwVertex, EntityType}; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -30,6 +30,7 @@ const DEFAULT_ARC_LEN: f64 = 1.0; // default arc bump length pub struct RevCloudCommand { points: Vec, arc_length: f64, + plane: WorkingPlane, } impl RevCloudCommand { @@ -37,11 +38,16 @@ impl RevCloudCommand { Self { points: vec![], arc_length: DEFAULT_ARC_LEN, + plane: WorkingPlane::default(), } } } impl CadCommand for RevCloudCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "REVCLOUD" } @@ -71,8 +77,13 @@ impl CadCommand for RevCloudCommand { if self.points.len() < 3 { return CmdResult::Cancel; } - let entity = make_revcloud(&self.points, self.arc_length); - CmdResult::CommitAndExit(entity) + let local: Vec = self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); + let entity = make_revcloud(&local, self.arc_length); + CmdResult::CommitAndExit(self.plane.place_entity(entity)) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { @@ -153,6 +164,7 @@ fn make_revcloud(pts: &[DVec3], arc_len: f64) -> EntityType { let mut p = LwPolyline::new(); p.is_closed = true; + p.elevation = pts.first().map_or(0.0, |point| point.z); p.vertices = vertices; EntityType::LwPolyline(p) } diff --git a/src/modules/draw/draw/shapes.rs b/src/modules/draw/draw/shapes.rs index 61c47375a..304a50163 100644 --- a/src/modules/draw/draw/shapes.rs +++ b/src/modules/draw/draw/shapes.rs @@ -15,35 +15,32 @@ use acadrust::types::Vector2; use acadrust::{EntityType, LwPolyline}; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; -use glam::{DVec3, Mat4}; +use glam::DVec3; /// Build the four corners of an axis-aligned box between opposite corners `a` /// and `b`, axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = /// world). The two given corners stay put; the other two are placed square to /// the UCS axes instead of the world axes. -fn ucs_box_corners(a: DVec3, b: DVec3, ucs: Mat4) -> [DVec3; 4] { - let ucs = ucs.as_dmat4(); - let inv = ucs.inverse(); - let au = inv.transform_point3(a); - let bu = inv.transform_point3(b); +fn ucs_box_corners(a: DVec3, b: DVec3, plane: WorkingPlane) -> [DVec3; 4] { + let au = plane.to_local(a); + let bu = plane.to_local(b); [ - ucs.transform_point3(DVec3::new(au.x, au.y, au.z)), - ucs.transform_point3(DVec3::new(bu.x, au.y, au.z)), - ucs.transform_point3(DVec3::new(bu.x, bu.y, au.z)), - ucs.transform_point3(DVec3::new(au.x, bu.y, au.z)), + plane.to_world(DVec3::new(au.x, au.y, au.z)), + plane.to_world(DVec3::new(bu.x, au.y, au.z)), + plane.to_world(DVec3::new(bu.x, bu.y, au.z)), + plane.to_world(DVec3::new(au.x, bu.y, au.z)), ] } /// Four corners of a box centred at `c` with half-extents taken from `corner`, /// axis-aligned in the active UCS (`ucs` = UCS→wire affine, identity = world). -fn ucs_box_around_center(c: DVec3, corner: DVec3, ucs: Mat4) -> [DVec3; 4] { - let ucs = ucs.as_dmat4(); - let d = ucs.inverse().transform_vector3(corner - c); - let rx = ucs.transform_vector3(DVec3::new(d.x.abs(), 0.0, 0.0)); - let ry = ucs.transform_vector3(DVec3::new(0.0, d.y.abs(), 0.0)); +fn ucs_box_around_center(c: DVec3, corner: DVec3, plane: WorkingPlane) -> [DVec3; 4] { + let d = plane.vector_to_local(corner - c); + let rx = plane.x * d.x.abs(); + let ry = plane.y * d.y.abs(); [c - rx - ry, c + rx - ry, c + rx + ry, c - rx + ry] } @@ -84,15 +81,18 @@ pub const ICON: IconKind = ICON_RECT; // ── Shared geometry helpers ──────────────────────────────────────────────── -fn make_pline(xy_pairs: &[[f64; 2]]) -> EntityType { - EntityType::LwPolyline(LwPolyline { - vertices: xy_pairs +fn make_pline(points: &[DVec3], plane: WorkingPlane) -> EntityType { + let local: Vec = points.iter().map(|point| plane.to_local(*point)).collect(); + let elevation = local.first().map_or(0.0, |point| point.z); + plane.place_entity(EntityType::LwPolyline(LwPolyline { + vertices: local .iter() - .map(|&[x, y]| LwVertex::new(Vector2::new(x, y))) + .map(|point| LwVertex::new(Vector2::new(point.x, point.y))) .collect(), + elevation, is_closed: true, ..Default::default() - }) + })) } fn wire_loop(pts: Vec<[f64; 3]>) -> WireModel { @@ -114,46 +114,57 @@ fn wire_seg(a: DVec3, b: DVec3) -> WireModel { // ── Polygon geometry ─────────────────────────────────────────────────────── -fn poly_verts_xy(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> Vec<[f64; 2]> { +fn poly_verts( + center: DVec3, + vertex_r: f64, + sides: u32, + start_angle: f64, + plane: WorkingPlane, +) -> Vec { (0..sides) .map(|i| { let a = start_angle + (i as f64) * TAU / sides as f64; - [ - center.x + vertex_r * a.cos(), - center.y + vertex_r * a.sin(), - ] + center + plane.x * (vertex_r * a.cos()) + plane.y * (vertex_r * a.sin()) }) .collect() } -fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel { - let pts: Vec<[f64; 3]> = (0..sides) - .map(|i| { - let a = start_angle + (i as f64) * TAU / sides as f64; - [ - center.x + vertex_r * a.cos(), - center.y + vertex_r * a.sin(), - center.z, - ] - }) +fn poly_wire( + center: DVec3, + vertex_r: f64, + sides: u32, + start_angle: f64, + plane: WorkingPlane, +) -> WireModel { + let pts: Vec<[f64; 3]> = poly_verts(center, vertex_r, sides, start_angle, plane) + .into_iter() + .map(|point| [point.x, point.y, point.z]) .collect(); wire_loop(pts) } -fn angle_xy(from: DVec3, to: DVec3) -> f64 { - (to.y - from.y).atan2(to.x - from.x) +fn angle_xy(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 { + plane.angle(from, to).unwrap_or(0.0) +} + +fn plane_distance(from: DVec3, to: DVec3, plane: WorkingPlane) -> f64 { + let delta = plane.vector_to_local(to - from); + delta.x.hypot(delta.y) } // ── Command: Rectangle — Two Corners (RECT) ────────────────────────────── pub struct RectCommand { a: Option, - ucs: Mat4, + plane: WorkingPlane, } impl RectCommand { pub fn new() -> Self { - Self { a: None, ucs: Mat4::IDENTITY } + Self { + a: None, + plane: WorkingPlane::default(), + } } } @@ -161,8 +172,8 @@ impl CadCommand for RectCommand { fn name(&self) -> &'static str { "RECT" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { if self.a.is_none() { @@ -209,13 +220,8 @@ impl CadCommand for RectCommand { CmdResult::NeedPoint } Some(a) => { - let c = ucs_box_corners(a, pt, self.ucs); - CmdResult::CommitAndExit(make_pline(&[ - [c[0].x, c[0].y], - [c[1].x, c[1].y], - [c[2].x, c[2].y], - [c[3].x, c[3].y], - ])) + let c = ucs_box_corners(a, pt, self.plane); + CmdResult::CommitAndExit(make_pline(&c, self.plane)) } } } @@ -227,7 +233,7 @@ impl CadCommand for RectCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { let a = self.a?; - let c = ucs_box_corners(a, pt, self.ucs); + let c = ucs_box_corners(a, pt, self.plane); Some(wire_loop(vec![ [c[0].x, c[0].y, c[0].z], [c[1].x, c[1].y, c[1].z], @@ -261,6 +267,7 @@ pub struct RectRotCommand { step: u8, a: DVec3, b: DVec3, + plane: WorkingPlane, } impl RectRotCommand { @@ -269,11 +276,16 @@ impl RectRotCommand { step: 0, a: DVec3::ZERO, b: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for RectRotCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "RECT_ROT" } @@ -297,18 +309,18 @@ impl CadCommand for RectRotCommand { CmdResult::NeedPoint } _ => { - let (a, b) = (self.a, self.b); + let (a, b, pt) = ( + self.plane.to_local(self.a), + self.plane.to_local(self.b), + self.plane.to_local(pt), + ); let dir = (b - a).normalize_or_zero(); let perp = DVec3::new(-dir.y, dir.x, 0.0); let h = (pt - b).dot(perp); // signed height let c = b + perp * h; let d = a + perp * h; - CmdResult::CommitAndExit(make_pline(&[ - [a.x, a.y], - [b.x, b.y], - [c.x, c.y], - [d.x, d.y], - ])) + let corners = [a, b, c, d].map(|point| self.plane.to_world(point)); + CmdResult::CommitAndExit(make_pline(&corners, self.plane)) } } } @@ -322,18 +334,20 @@ impl CadCommand for RectRotCommand { match self.step { 1 => Some(wire_seg(self.a, pt)), 2 => { - let (a, b) = (self.a, self.b); + let (a, b, pt) = ( + self.plane.to_local(self.a), + self.plane.to_local(self.b), + self.plane.to_local(pt), + ); let dir = (b - a).normalize_or_zero(); let perp = DVec3::new(-dir.y, dir.x, 0.0); let h = (pt - b).dot(perp); let c = b + perp * h; let d = a + perp * h; - Some(wire_loop(vec![ - [a.x, a.y, a.z], - [b.x, b.y, b.z], - [c.x, c.y, c.z], - [d.x, d.y, d.z], - ])) + let points = [a, b, c, d].map(|point| self.plane.to_world(point)); + Some(wire_loop( + points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), + )) } _ => None, } @@ -356,9 +370,12 @@ impl CadCommand for RectRotCommand { fn dyn_live_value(&self, cursor: DVec3) -> Option { // Live height = perpendicular distance from the cursor to the base edge. (self.step == 2).then(|| { - let dir = (self.b - self.a).normalize_or_zero(); + let dir = self + .plane + .vector_to_local(self.b - self.a) + .normalize_or_zero(); let perp = DVec3::new(-dir.y, dir.x, 0.0); - (cursor - self.b).dot(perp).abs() + self.plane.vector_to_local(cursor - self.b).dot(perp).abs() }) } } @@ -369,12 +386,15 @@ impl CadCommand for RectRotCommand { pub struct RectCenCommand { center: Option, - ucs: Mat4, + plane: WorkingPlane, } impl RectCenCommand { pub fn new() -> Self { - Self { center: None, ucs: Mat4::IDENTITY } + Self { + center: None, + plane: WorkingPlane::default(), + } } } @@ -382,8 +402,8 @@ impl CadCommand for RectCenCommand { fn name(&self) -> &'static str { "RECT_CEN" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { if self.center.is_none() { @@ -399,13 +419,8 @@ impl CadCommand for RectCenCommand { CmdResult::NeedPoint } Some(c) => { - let q = ucs_box_around_center(c, pt, self.ucs); - CmdResult::CommitAndExit(make_pline(&[ - [q[0].x, q[0].y], - [q[1].x, q[1].y], - [q[2].x, q[2].y], - [q[3].x, q[3].y], - ])) + let q = ucs_box_around_center(c, pt, self.plane); + CmdResult::CommitAndExit(make_pline(&q, self.plane)) } } } @@ -417,7 +432,7 @@ impl CadCommand for RectCenCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { let c = self.center?; - let q = ucs_box_around_center(c, pt, self.ucs); + let q = ucs_box_around_center(c, pt, self.plane); Some(wire_loop(vec![ [q[0].x, q[0].y, q[0].z], [q[1].x, q[1].y, q[1].z], @@ -449,6 +464,7 @@ pub struct PolyCommand { sides: u32, step: u8, center: DVec3, + plane: WorkingPlane, } impl PolyCommand { @@ -457,11 +473,16 @@ impl PolyCommand { sides: 6, step: 0, center: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for PolyCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "POLY" } @@ -555,9 +576,10 @@ impl CadCommand for PolyCommand { CmdResult::NeedPoint } _ => { - let r = self.center.distance(pt); - let sa = angle_xy(self.center, pt); - CmdResult::CommitAndExit(make_pline(&poly_verts_xy(self.center, r, self.sides, sa))) + let r = plane_distance(self.center, pt, self.plane); + let sa = angle_xy(self.center, pt, self.plane); + let vertices = poly_verts(self.center, r, self.sides, sa, self.plane); + CmdResult::CommitAndExit(make_pline(&vertices, self.plane)) } } } @@ -577,9 +599,9 @@ impl CadCommand for PolyCommand { if self.step < 2 { return None; } - let r = self.center.distance(pt); - let sa = angle_xy(self.center, pt); - Some(poly_wire(self.center, r, self.sides, sa)) + let r = plane_distance(self.center, pt, self.plane); + let sa = angle_xy(self.center, pt, self.plane); + Some(poly_wire(self.center, r, self.sides, sa, self.plane)) } } @@ -591,6 +613,7 @@ pub struct PolyCCommand { sides: u32, step: u8, center: DVec3, + plane: WorkingPlane, } impl PolyCCommand { @@ -599,11 +622,16 @@ impl PolyCCommand { sides: 6, step: 0, center: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for PolyCCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "POLY_C" } @@ -669,18 +697,20 @@ impl CadCommand for PolyCCommand { CmdResult::NeedPoint } _ => { - let inradius = self.center.distance(pt); + let inradius = plane_distance(self.center, pt, self.plane); let vr = inradius / (PI / self.sides as f64).cos(); // The picked pt is at the midpoint of an edge; the vertex is // offset by half a sector (π/N) from that direction. - let edge_angle = angle_xy(self.center, pt); + let edge_angle = angle_xy(self.center, pt, self.plane); let sa = edge_angle + PI / self.sides as f64; - CmdResult::CommitAndExit(make_pline(&poly_verts_xy( + let vertices = poly_verts( self.center, vr, self.sides, sa, - ))) + self.plane, + ); + CmdResult::CommitAndExit(make_pline(&vertices, self.plane)) } } } @@ -700,10 +730,10 @@ impl CadCommand for PolyCCommand { if self.step < 2 { return None; } - let inradius = self.center.distance(pt); + let inradius = plane_distance(self.center, pt, self.plane); let vr = inradius / (PI / self.sides as f64).cos(); - let sa = angle_xy(self.center, pt) + PI / self.sides as f64; - Some(poly_wire(self.center, vr, self.sides, sa)) + let sa = angle_xy(self.center, pt, self.plane) + PI / self.sides as f64; + Some(poly_wire(self.center, vr, self.sides, sa, self.plane)) } } @@ -715,6 +745,7 @@ pub struct PolyECommand { sides: u32, step: u8, a: DVec3, + plane: WorkingPlane, } impl PolyECommand { @@ -723,11 +754,16 @@ impl PolyECommand { sides: 6, step: 0, a: DVec3::ZERO, + plane: WorkingPlane::default(), } } } impl CadCommand for PolyECommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "POLY_E" } @@ -792,8 +828,11 @@ impl CadCommand for PolyECommand { CmdResult::NeedPoint } _ => { - if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) { - CmdResult::CommitAndExit(make_pline(&poly_verts_xy(center, vr, self.sides, sa))) + if let Some((center, vr, sa)) = + edge_poly_params(self.a, pt, self.sides, self.plane) + { + let vertices = poly_verts(center, vr, self.sides, sa, self.plane); + CmdResult::CommitAndExit(make_pline(&vertices, self.plane)) } else { CmdResult::Cancel } @@ -816,8 +855,10 @@ impl CadCommand for PolyECommand { if self.step < 2 { return None; } - if let Some((center, vr, sa)) = edge_poly_params(self.a, pt, self.sides) { - Some(poly_wire(center, vr, self.sides, sa)) + if let Some((center, vr, sa)) = + edge_poly_params(self.a, pt, self.sides, self.plane) + { + Some(poly_wire(center, vr, self.sides, sa, self.plane)) } else { Some(wire_seg(self.a, pt)) } @@ -826,7 +867,13 @@ impl CadCommand for PolyECommand { /// Compute polygon center, vertex-radius and start-angle from two edge endpoints. /// The polygon is placed on the left side of A→B (CCW convention). -fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> { +fn edge_poly_params( + a: DVec3, + b: DVec3, + sides: u32, + plane: WorkingPlane, +) -> Option<(DVec3, f64, f64)> { + let (a, b) = (plane.to_local(a), plane.to_local(b)); let edge_len = a.distance(b); if edge_len < 1e-6 { return None; @@ -841,8 +888,9 @@ fn edge_poly_params(a: DVec3, b: DVec3, sides: u32) -> Option<(DVec3, f64, f64)> let mid = (a + b) * 0.5; let center = mid + perp * inradius; // First vertex = A - let sa = angle_xy(center, a); - Some((center, vr, sa)) + let center_world = plane.to_world(center); + let sa = angle_xy(center_world, plane.to_world(a), plane); + Some((center_world, vr, sa)) } diff --git a/src/modules/draw/draw/sketch.rs b/src/modules/draw/draw/sketch.rs index aae724e29..cc56eb51a 100644 --- a/src/modules/draw/draw/sketch.rs +++ b/src/modules/draw/draw/sketch.rs @@ -17,7 +17,7 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -45,6 +45,7 @@ pub struct SketchCommand { strokes: Vec>, /// True while the pen is down and samples accumulate into the current stroke. pen_down: bool, + plane: WorkingPlane, } impl SketchCommand { @@ -52,15 +53,20 @@ impl SketchCommand { Self { strokes: Vec::new(), pen_down: false, + plane: WorkingPlane::default(), } } /// Build one lightweight polyline from a stroke's sampled points, or `None` /// when the stroke has too few points to form a segment. - fn build_stroke(points: &[DVec3]) -> Option { + fn build_stroke(&self, points: &[DVec3]) -> Option { if points.len() < 2 { return None; } + let points: Vec = points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); let elevation = points[0].z; let mut pl = LwPolyline::new(); pl.is_closed = false; @@ -70,19 +76,23 @@ impl SketchCommand { .map(|p| LwVertex::new(Vector2::new(p.x, p.y))) .collect(); pl.normal = Vector3::UNIT_Z; - Some(EntityType::LwPolyline(pl)) + Some(self.plane.place_entity(EntityType::LwPolyline(pl))) } /// All strokes that have enough points, as committable entities. fn build_all(&self) -> Vec { self.strokes .iter() - .filter_map(|s| Self::build_stroke(s)) + .filter_map(|stroke| self.build_stroke(stroke)) .collect() } } impl CadCommand for SketchCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "SKETCH" } diff --git a/src/modules/draw/draw/solid2d.rs b/src/modules/draw/draw/solid2d.rs index a2417f0e4..8a8221bac 100644 --- a/src/modules/draw/draw/solid2d.rs +++ b/src/modules/draw/draw/solid2d.rs @@ -3,7 +3,7 @@ // Command: SOLID (reachable as SO / SOLID2D — the bare SOLID verb currently // toggles the shaded display) — pick three or four corner points and commit a // filled triangle or quadrilateral. Four points may be picked either around -// the perimeter (the natural order shown by the rubber band) or in AutoCAD's +// the perimeter (the natural order shown by the rubber band) or in the // traditional Z pattern. The command normalizes both forms to the DXF corner // order before committing. Enter after the third point commits a triangle. @@ -12,7 +12,7 @@ use acadrust::types::Vector3; use acadrust::EntityType; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -34,11 +34,15 @@ pub fn tool() -> ToolDef { pub struct Solid2dCommand { /// Corner points picked so far (3 → triangle, 4 → quadrilateral). points: Vec, + plane: WorkingPlane, } impl Solid2dCommand { pub fn new() -> Self { - Self { points: Vec::new() } + Self { + points: Vec::new(), + plane: WorkingPlane::default(), + } } fn v3(p: DVec3) -> Vector3 { @@ -86,6 +90,10 @@ impl Solid2dCommand { } impl CadCommand for Solid2dCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "SOLID" } @@ -112,8 +120,13 @@ impl CadCommand for Solid2dCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { self.points.push(pt); if self.points.len() == 4 { - let solid = Self::solid_from_four_points(&self.points); - CmdResult::CommitAndExit(EntityType::Solid(solid)) + let local: Vec = self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); + let solid = Self::solid_from_four_points(&local); + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid))) } else { CmdResult::NeedPoint } @@ -121,9 +134,13 @@ impl CadCommand for Solid2dCommand { fn on_enter(&mut self) -> CmdResult { if self.points.len() == 3 { - let p = &self.points; + let p: Vec = self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); let solid = Solid::triangle(Self::v3(p[0]), Self::v3(p[1]), Self::v3(p[2])); - CmdResult::CommitAndExit(EntityType::Solid(solid)) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Solid(solid))) } else { CmdResult::Cancel } diff --git a/src/modules/draw/draw/trace.rs b/src/modules/draw/draw/trace.rs index 4c842a26c..15b10f4da 100644 --- a/src/modules/draw/draw/trace.rs +++ b/src/modules/draw/draw/trace.rs @@ -16,7 +16,7 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; @@ -45,6 +45,7 @@ pub struct TraceCommand { /// Every point picked so far, in world space. `points[0]` fixes the band's /// elevation (its Z). points: Vec, + plane: WorkingPlane, } impl TraceCommand { @@ -53,6 +54,7 @@ impl TraceCommand { width: 1.0, width_set: false, points: Vec::new(), + plane: WorkingPlane::default(), } } @@ -62,13 +64,17 @@ impl TraceCommand { if self.points.len() < 2 { return None; } - let elevation = self.points[0].z; + let local: Vec = self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); + let elevation = local[0].z; let mut pl = LwPolyline::new(); pl.is_closed = false; pl.constant_width = self.width; pl.elevation = elevation; - pl.vertices = self - .points + pl.vertices = local .iter() .map(|p| { let mut v = LwVertex::new(Vector2::new(p.x, p.y)); @@ -78,11 +84,15 @@ impl TraceCommand { }) .collect(); pl.normal = Vector3::UNIT_Z; - Some(EntityType::LwPolyline(pl)) + Some(self.plane.place_entity(EntityType::LwPolyline(pl))) } } impl CadCommand for TraceCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "TRACE" } diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 42968feae..7a0a3976b 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -6,7 +6,7 @@ use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdOption, CmdResult}; +use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -25,6 +25,7 @@ pub struct WipeoutCommand { mode: WipeoutMode, first: Option, points: Vec, + plane: WorkingPlane, } #[derive(Clone, Copy, PartialEq)] @@ -40,6 +41,7 @@ impl WipeoutCommand { mode: WipeoutMode::Draw, first: None, points: Vec::new(), + plane: WorkingPlane::default(), } } @@ -48,6 +50,7 @@ impl WipeoutCommand { mode: WipeoutMode::Polyline, first: None, points: Vec::new(), + plane: WorkingPlane::default(), } } @@ -57,12 +60,18 @@ impl WipeoutCommand { mode: WipeoutMode::Rectangular, first: None, points: Vec::new(), + plane: WorkingPlane::default(), } } fn finish_draw(&self) -> CmdResult { - match make_poly_wipeout(&self.points) { - Some(entity) => CmdResult::CommitAndExit(entity), + let local: Vec = self + .points + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); + match make_poly_wipeout(&local) { + Some(entity) => CmdResult::CommitAndExit(self.plane.place_entity(entity)), None => CmdResult::NeedPoint, } } @@ -74,6 +83,10 @@ impl WipeoutCommand { } impl CadCommand for WipeoutCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "WIPEOUT" } @@ -131,7 +144,11 @@ impl CadCommand for WipeoutCommand { } WipeoutMode::Rectangular => { if let Some(first) = self.first { - CmdResult::CommitAndExit(make_rect_wipeout(first, point)) + let first = self.plane.to_local(first); + let point = self.plane.to_local(point); + CmdResult::CommitAndExit( + self.plane.place_entity(make_rect_wipeout(first, point)), + ) } else { self.first = Some(point); CmdResult::NeedPoint @@ -228,15 +245,21 @@ impl CadCommand for WipeoutCommand { } WipeoutMode::Rectangular => { let first = self.first?; + let corners = { + let first_local = self.plane.to_local(first); + let point_local = self.plane.to_local(point); + [ + first_local, + DVec3::new(point_local.x, first_local.y, first_local.z), + DVec3::new(point_local.x, point_local.y, first_local.z), + DVec3::new(first_local.x, point_local.y, first_local.z), + first_local, + ] + .map(|corner| self.plane.to_world(corner)) + }; Some(WireModel::solid_f64( "wipeout_preview".into(), - vec![ - [first.x, first.y, first.z], - [point.x, first.y, first.z], - [point.x, point.y, first.z], - [first.x, point.y, first.z], - [first.x, first.y, first.z], - ], + corners.iter().map(|p| [p.x, p.y, p.z]).collect(), WireModel::CYAN, false, )) diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 4008c1db7..17c2c21f7 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -3,7 +3,7 @@ use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::scene::model::wire_model::WireModel; pub struct DistCommand { @@ -11,15 +11,23 @@ pub struct DistCommand { // loses ~0.03–0.06 units at survey-scale coordinates (e.g. eastings near // 5e5), which made snapped-endpoint measurements read off by that much. first: Option, + plane: WorkingPlane, } impl DistCommand { pub fn new() -> Self { - Self { first: None } + Self { + first: None, + plane: WorkingPlane::default(), + } } } impl CadCommand for DistCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "DIST" } @@ -34,11 +42,11 @@ impl CadCommand for DistCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Some(p1) = self.first { - let delta = pt - p1; + let delta = self.plane.vector_to_local(pt - p1); let dist = delta.length(); let dx = delta.x; - let dy = delta.y; // drawing plane is world XY - let dz = delta.z; // elevation + let dy = delta.y; + let dz = delta.z; // Angle in XY plane — degrees from +X let angle_xy = dy.atan2(dx).to_degrees(); diff --git a/src/modules/draw/inquiry/id.rs b/src/modules/draw/inquiry/id.rs index a2db2f2fa..ef2af86ce 100644 --- a/src/modules/draw/inquiry/id.rs +++ b/src/modules/draw/inquiry/id.rs @@ -3,17 +3,25 @@ use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; -pub struct IdCommand; +pub struct IdCommand { + plane: WorkingPlane, +} impl IdCommand { pub fn new() -> Self { - Self + Self { + plane: WorkingPlane::default(), + } } } impl CadCommand for IdCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ID" } @@ -23,10 +31,10 @@ impl CadCommand for IdCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { - // Drawing plane is world XY (z = elevation). - let x = pt.x; - let y = pt.y; - let z = pt.z; + let local = self.plane.to_local(pt); + let x = local.x; + let y = local.y; + let z = local.z; let x_s = format!("{x:.4}"); let y_s = format!("{y:.4}"); let z_s = format!("{z:.4}"); diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 209b16711..6e20ee273 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -18,7 +18,7 @@ use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -54,6 +54,7 @@ pub struct MeasureGeomCommand { points: Vec, /// The picked entity for RADIUS, injected before `on_entity_pick`. picked: Option, + plane: WorkingPlane, } impl MeasureGeomCommand { @@ -62,6 +63,7 @@ impl MeasureGeomCommand { mode: Mode::Choose, points: vec![], picked: None, + plane: WorkingPlane::default(), } } @@ -107,12 +109,14 @@ impl MeasureGeomCommand { } /// DISTANCE readout for the two collected points. - fn distance_msg(p1: DVec3, p2: DVec3) -> String { + fn distance_msg(plane: WorkingPlane, p1: DVec3, p2: DVec3) -> String { + let p1 = plane.to_local(p1); + let p2 = plane.to_local(p2); let delta = p2 - p1; let dist = delta.length(); let dx = delta.x; - let dy = delta.y; // drawing plane is world XY - let dz = delta.z; // elevation + let dy = delta.y; + let dz = delta.z; let angle_xy = dy.atan2(dx).to_degrees(); let dist_s = format!("{dist:.4}"); let angle_xy_s = format!("{angle_xy:.4}"); @@ -131,7 +135,8 @@ impl MeasureGeomCommand { } /// AREA readout: shoelace area (f64, relative to first vertex) + perimeter. - fn area_msg(points: &[DVec3]) -> String { + fn area_msg(plane: WorkingPlane, points: &[DVec3]) -> String { + let points = points.iter().map(|point| plane.to_local(*point)).collect::>(); let n = points.len(); let origin = points[0]; let mut area_sum = 0.0f64; @@ -207,6 +212,10 @@ impl CadCommand for MeasureGeomCommand { } } + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn wants_text_input(&self) -> bool { // Only the opening mode-keyword step reads a typed token. self.mode == Mode::Choose @@ -267,7 +276,11 @@ impl CadCommand for MeasureGeomCommand { Mode::Distance => { self.points.push(pt); if self.points.len() == 2 { - CmdResult::Measurement(Self::distance_msg(self.points[0], self.points[1])) + CmdResult::Measurement(Self::distance_msg( + self.plane, + self.points[0], + self.points[1], + )) } else { CmdResult::NeedPoint } @@ -299,7 +312,7 @@ impl CadCommand for MeasureGeomCommand { if self.points.len() < 3 { CmdResult::Cancel } else { - CmdResult::Measurement(Self::area_msg(&self.points)) + CmdResult::Measurement(Self::area_msg(self.plane, &self.points)) } } _ => CmdResult::Cancel, diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index 4295d8b64..707306105 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -14,10 +14,10 @@ // 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text) use acadrust::Handle; -use glam::{DVec3, Mat4}; +use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult, EntityTransform}; +use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane}; use crate::modules::draw::defaults; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; @@ -63,7 +63,7 @@ pub struct ArrayRectCommand { default_cols: u32, default_row_sp: f64, default_col_sp: f64, - ucs: Mat4, + plane: WorkingPlane, } impl ArrayRectCommand { @@ -76,18 +76,17 @@ impl ArrayRectCommand { default_cols: defaults::get_array_cols() as u32, default_row_sp: defaults::get_array_row_sp(), default_col_sp: defaults::get_array_col_sp(), - ucs: Mat4::IDENTITY, + plane: WorkingPlane::default(), } } - /// Row/column offsets run along the active UCS axes (`ucs` rotates the - /// world-frame grid step), identity = world. + /// Row/column offsets run along the active coordinate axes. fn build_transforms( rows: u32, cols: u32, row_sp: f64, col_sp: f64, - ucs: Mat4, + plane: WorkingPlane, ) -> Vec { let mut t = Vec::new(); for r in 0..rows { @@ -95,9 +94,11 @@ impl ArrayRectCommand { if r == 0 && c == 0 { continue; } - t.push(EntityTransform::Translate(ucs.as_dmat4().transform_vector3( - glam::DVec3::new(col_sp * c as f64, row_sp * r as f64, 0.0), - ))); + t.push(EntityTransform::Translate(plane.vector_to_world(DVec3::new( + col_sp * c as f64, + row_sp * r as f64, + 0.0, + )))); } } t @@ -109,8 +110,8 @@ impl CadCommand for ArrayRectCommand { "ARRAYRECT" } - fn set_ucs(&mut self, ucs: Mat4) { - self.ucs = ucs; + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; } fn prompt(&self) -> String { @@ -194,7 +195,7 @@ impl CadCommand for ArrayRectCommand { }; Some(CmdResult::BatchCopy( self.handles.clone(), - Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs), + Self::build_transforms(rows, cols, row_sp, col_sp, self.plane), )) } } @@ -219,7 +220,7 @@ impl CadCommand for ArrayRectCommand { } RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp), }; - Self::build_transforms(rows, cols, row_sp, col_sp, self.ucs) + Self::build_transforms(rows, cols, row_sp, col_sp, self.plane) .iter() .flat_map(|t| { if let EntityTransform::Translate(delta) = t { @@ -262,6 +263,7 @@ pub struct ArrayPolarCommand { step: PolarStep, default_count: u32, default_angle: f64, + plane: WorkingPlane, } impl ArrayPolarCommand { @@ -272,11 +274,16 @@ impl ArrayPolarCommand { step: PolarStep::Center, default_count: defaults::get_array_p_count() as u32, default_angle: defaults::get_array_p_angle(), + plane: WorkingPlane::default(), } } } impl CadCommand for ArrayPolarCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ARRAYPOLAR" } @@ -342,6 +349,7 @@ impl CadCommand for ArrayPolarCommand { let transforms = (1..count) .map(|n| EntityTransform::Rotate { center, + axis: self.plane.z, angle_rad: step_rad * n as f64, }) .collect(); @@ -363,12 +371,13 @@ impl CadCommand for ArrayPolarCommand { } }; let step_rad = total_deg.to_radians() / count as f64; + let axis = self.plane.z.as_vec3(); let mut out: Vec = (1..count) .flat_map(|n| { let angle_rad = (step_rad * n as f64) as f32; self.wire_models .iter() - .map(move |w| w.rotated(center, angle_rad)) + .map(move |wire| wire.rotated_about_axis(center, axis, angle_rad)) }) .collect(); // Rubber-band from center to cursor while picking the center point. @@ -675,6 +684,7 @@ impl ArrayPathCommand { let center = Self::rigid_center(p0, p, dth); EntityTransform::Rotate { center, + axis: DVec3::Z, angle_rad: dth, } } @@ -814,7 +824,7 @@ impl CadCommand for ArrayPathCommand { .iter() .map(|w| w.translated(delta.as_vec3())) .collect::>(), - EntityTransform::Rotate { center, angle_rad } => self + EntityTransform::Rotate { center, angle_rad, .. } => self .wire_models .iter() .map(|w| w.rotated(center.as_vec3(), *angle_rad as f32)) diff --git a/src/modules/draw/modify/mirror.rs b/src/modules/draw/modify/mirror.rs index 8ed57cf68..4b7621e38 100644 --- a/src/modules/draw/modify/mirror.rs +++ b/src/modules/draw/modify/mirror.rs @@ -12,7 +12,7 @@ use acadrust::Handle; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult, EntityTransform}; +use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -41,6 +41,7 @@ pub struct MirrorCommand { text_ghosts: Vec<(WireModel, DVec3)>, mirror_text: bool, step: Step, + plane: WorkingPlane, } impl MirrorCommand { @@ -56,11 +57,16 @@ impl MirrorCommand { text_ghosts, mirror_text, step: Step::P1, + plane: WorkingPlane::default(), } } } impl CadCommand for MirrorCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "MIRROR" } @@ -147,18 +153,32 @@ impl CadCommand for MirrorCommand { let mut out: Vec = self .wire_models .iter() - .map(|w| w.mirrored(p1.as_vec3(), p2.as_vec3())) + .map(|w| { + w.mirrored_in_plane( + p1.as_vec3(), + p2.as_vec3(), + self.plane.z.as_vec3(), + ) + }) .collect(); // Text ghosts honour MIRRTEXT: on → true glyph mirror (full reflection); // off → keep glyphs readable, relocate to the mirror-symmetric position // by reflecting the box centre and translating. for (w, center) in &self.text_ghosts { if self.mirror_text { - out.push(w.mirrored(p1.as_vec3(), p2.as_vec3())); + out.push(w.mirrored_in_plane( + p1.as_vec3(), + p2.as_vec3(), + self.plane.z.as_vec3(), + )); } else { - let (mut cx, mut cy) = (center.x, center.y); - crate::scene::view::transform::reflect_xy_point(&mut cx, &mut cy, p1, p2); - let delta = glam::Vec3::new((cx - center.x) as f32, (cy - center.y) as f32, 0.0); + let reflected = crate::scene::view::transform::reflected_point( + *center, + p1, + p2, + self.plane.z, + ); + let delta = (reflected - *center).as_vec3(); out.push(w.translated(delta)); } } @@ -180,7 +200,11 @@ impl MirrorCommand { /// Commit the mirror. `erase` true flips the originals in place; false /// keeps them and adds a mirrored copy. Either way the command ends. fn finish(&self, p1: DVec3, p2: DVec3, erase: bool) -> CmdResult { - let xform = EntityTransform::Mirror { p1, p2 }; + let xform = EntityTransform::Mirror { + p1, + p2, + working_normal: self.plane.z, + }; if erase { CmdResult::TransformSelected(self.handles.clone(), xform) } else { diff --git a/src/modules/draw/modify/rotate.rs b/src/modules/draw/modify/rotate.rs index 3ad38847b..4ea51cb30 100644 --- a/src/modules/draw/modify/rotate.rs +++ b/src/modules/draw/modify/rotate.rs @@ -13,7 +13,7 @@ use acadrust::Handle; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult, DynField, EntityTransform}; +use crate::command::{CadCommand, CmdResult, DynField, EntityTransform, WorkingPlane}; use crate::modules::draw::defaults; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -44,6 +44,7 @@ pub struct RotateCommand { wire_models: Vec, step: Step, default_angle: f64, // degrees + plane: WorkingPlane, } impl RotateCommand { @@ -53,6 +54,7 @@ impl RotateCommand { wire_models, step: Step::Center, default_angle: defaults::get_rotate_angle(), + plane: WorkingPlane::default(), } } @@ -60,12 +62,20 @@ impl RotateCommand { defaults::set_rotate_angle(angle_rad.to_degrees()); CmdResult::TransformSelected( self.handles.clone(), - EntityTransform::Rotate { center, angle_rad }, + EntityTransform::Rotate { + center, + axis: self.plane.z, + angle_rad, + }, ) } } impl CadCommand for RotateCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "ROTATE" } @@ -110,11 +120,9 @@ impl CadCommand for RotateCommand { } Step::Angle { center } => { let center = *center; - let direction = pt - center; - if direction.x.hypot(direction.y) <= f64::EPSILON { + let Some(angle_rad) = self.plane.angle(center, pt) else { return CmdResult::NeedPoint; - } - let angle_rad = direction.y.atan2(direction.x); + }; self.commit(center, angle_rad) } Step::RefFirst { center } => { @@ -123,22 +131,18 @@ impl CadCommand for RotateCommand { CmdResult::NeedPoint } Step::RefSecond { center, first } => { - let direction = pt - *first; - if direction.x.hypot(direction.y) <= f64::EPSILON { - return CmdResult::NeedPoint; - } let center = *center; - let ref_angle = direction.y.atan2(direction.x); + let Some(ref_angle) = self.plane.angle(*first, pt) else { + return CmdResult::NeedPoint; + }; self.step = Step::RefNew { center, ref_angle }; CmdResult::NeedPoint } Step::RefNew { center, ref_angle } => { let center = *center; - let direction = pt - center; - if direction.x.hypot(direction.y) <= f64::EPSILON { + let Some(new_angle) = self.plane.angle(center, pt) else { return CmdResult::NeedPoint; - } - let new_angle = direction.y.atan2(direction.x); + }; self.commit(center, new_angle - *ref_angle) } } @@ -192,11 +196,10 @@ impl CadCommand for RotateCommand { fn on_preview_wires(&mut self, pt: DVec3) -> Vec { let (center, angle_rad) = match &self.step { Step::Angle { center } => { - let direction = pt - *center; - if direction.x.hypot(direction.y) <= f64::EPSILON { + let Some(angle) = self.plane.angle(*center, pt) else { return vec![]; - } - (*center, direction.y.atan2(direction.x)) + }; + (*center, angle) } Step::RefSecond { first, .. } => { return vec![WireModel::solid( @@ -210,11 +213,10 @@ impl CadCommand for RotateCommand { )]; } Step::RefNew { center, ref_angle } => { - let direction = pt - *center; - if direction.x.hypot(direction.y) <= f64::EPSILON { + let Some(angle) = self.plane.angle(*center, pt) else { return vec![]; - } - (*center, direction.y.atan2(direction.x) - *ref_angle) + }; + (*center, angle - *ref_angle) } _ => return vec![], }; @@ -222,7 +224,13 @@ impl CadCommand for RotateCommand { // drawn by the dynamic-input overlay (polar guide), not here. self.wire_models .iter() - .map(|w| w.rotated(center.as_vec3(), angle_rad as f32)) + .map(|w| { + w.rotated_about_axis( + center.as_vec3(), + self.plane.z.as_vec3(), + angle_rad as f32, + ) + }) .collect() } diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index 320284521..9a1171096 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -4,7 +4,7 @@ use acadrust::EntityType; use glam::{DVec3, Vec3}; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -59,6 +59,7 @@ pub struct InsertBlockCommand { /// is measured from, so `on_preview_wires` can rubber-band it to the /// cursor. Set by paste-as-block; empty for a plain INSERT. preview: Option<(Vec, Vec3)>, + plane: WorkingPlane, } impl InsertBlockCommand { @@ -72,6 +73,7 @@ impl InsertBlockCommand { awaiting: None, pending_insert: None, preview: None, + plane: WorkingPlane::default(), } } @@ -89,11 +91,16 @@ impl InsertBlockCommand { awaiting: None, pending_insert: None, preview: Some((preview_wires, base)), + plane: WorkingPlane::default(), } } } impl CadCommand for InsertBlockCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "INSERT" } @@ -146,10 +153,15 @@ impl CadCommand for InsertBlockCommand { match &self.step { Step::Name => CmdResult::NeedPoint, Step::Point { name } => { - let mut ins = Insert::new(name.clone(), Vector3::new(pt.x, pt.y, pt.z)); + let point = self.plane.to_local(pt); + let mut ins = Insert::new( + name.clone(), + Vector3::new(point.x, point.y, point.z), + ); ins.set_x_scale(self.x_scale); ins.set_y_scale(self.y_scale); ins.rotation = self.rotation_rad; + ins.apply_transform(&self.plane.to_world_transform()); let block_name = name.clone(); self.pending_insert = Some(ins); // Signal the host to check for attdefs. diff --git a/src/modules/insert/minsert.rs b/src/modules/insert/minsert.rs index caafdcbab..d913018c1 100644 --- a/src/modules/insert/minsert.rs +++ b/src/modules/insert/minsert.rs @@ -12,7 +12,7 @@ use acadrust::EntityType; use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; #[allow(dead_code)] @@ -67,6 +67,7 @@ pub struct MinsertCommand { columns: u16, row_spacing: f64, column_spacing: f64, + plane: WorkingPlane, } impl MinsertCommand { @@ -78,6 +79,7 @@ impl MinsertCommand { columns: 1, row_spacing: 0.0, column_spacing: 0.0, + plane: WorkingPlane::default(), } } @@ -88,7 +90,7 @@ impl MinsertCommand { ins.column_count = self.columns.max(1); ins.row_spacing = self.row_spacing; ins.column_spacing = self.column_spacing; - CmdResult::CommitAndExit(EntityType::Insert(ins)) + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Insert(ins))) } /// Advance from the parameter currently at `idx` to the next, or build the @@ -109,6 +111,10 @@ impl MinsertCommand { } impl CadCommand for MinsertCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "MINSERT" } @@ -160,7 +166,8 @@ impl CadCommand for MinsertCommand { Step::Name => CmdResult::NeedPoint, Step::Point { name } => { let name = name.clone(); - let point = Vector3::new(pt.x, pt.y, pt.z); + let point = self.plane.to_local(pt); + let point = Vector3::new(point.x, point.y, point.z); self.step = Step::Params { name, point, diff --git a/src/modules/insert/xattach.rs b/src/modules/insert/xattach.rs index 63c90cab7..c8c45d5cf 100644 --- a/src/modules/insert/xattach.rs +++ b/src/modules/insert/xattach.rs @@ -16,7 +16,7 @@ use glam::DVec3; use crate::t; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::scene::Scene; @@ -33,17 +33,26 @@ pub fn tool() -> ToolDef { pub struct XAttachCommand { path: String, block_name: String, + plane: WorkingPlane, } impl XAttachCommand { /// Create an XATTACH command with a path already filled in (from file-picker). pub fn with_path(path: String) -> Self { let block_name = path_to_block_name(&path); - Self { path, block_name } + Self { + path, + block_name, + plane: WorkingPlane::default(), + } } } impl CadCommand for XAttachCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "XATTACH" } @@ -59,10 +68,11 @@ impl CadCommand for XAttachCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { // We return the INSERT entity; the command handler in commands.rs // calls `prepare_xref_block` on the scene before committing. - CmdResult::CommitAndExit(EntityType::Insert(Insert::new( + let point = self.plane.to_local(pt); + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Insert(Insert::new( self.block_name.clone(), - Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64), - ))) + Vector3::new(point.x, point.y, point.z), + )))) } fn on_enter(&mut self) -> CmdResult { diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index eff03f3a3..1a78fd456 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -13,7 +13,7 @@ use glam::DVec3; use crate::t; use truck_modeling::Solid; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; @@ -67,6 +67,7 @@ pub struct PrimitiveCommand { pts: Vec, /// True once the footprint is set and we are collecting the height. height_step: bool, + plane: WorkingPlane, } impl PrimitiveCommand { @@ -75,6 +76,7 @@ impl PrimitiveCommand { shape: Shape::from_id(id).unwrap_or(Shape::Box), pts: Vec::new(), height_step: false, + plane: WorkingPlane::default(), } } @@ -110,13 +112,17 @@ impl PrimitiveCommand { return None; } if self.shape == Shape::Box { - let center = [(a.x + b.x) / 2.0, (a.y + b.y) / 2.0, height / 2.0]; + let center = [ + (a.x + b.x) / 2.0, + (a.y + b.y) / 2.0, + a.z + height / 2.0, + ]; ( primitives::build_box(center, length, width, height), solid_model::box_solid(center, length, width, height), ) } else { - let origin = [a.x.min(b.x), a.y.min(b.y), 0.0]; + let origin = [a.x.min(b.x), a.y.min(b.y), a.z]; ( primitives::build_wedge(origin, length, width, height), solid_model::wedge_solid(origin, length, width, height), @@ -129,7 +135,7 @@ impl PrimitiveCommand { if r < 1e-6 || height < 1e-6 { return None; } - let center = [c.x, c.y, 0.0]; + let center = [c.x, c.y, c.z]; if self.shape == Shape::Cylinder { ( primitives::build_cylinder(center, r, height), @@ -148,7 +154,7 @@ impl PrimitiveCommand { if r < 1e-6 { return None; } - let center = [c.x, c.y, 0.0]; + let center = [c.x, c.y, c.z]; ( primitives::build_sphere(center, r), solid_model::sphere_solid(center, r), @@ -161,7 +167,7 @@ impl PrimitiveCommand { if major < 1e-6 || minor < 1e-6 { return None; } - let center = [c.x, c.y, 0.0]; + let center = [c.x, c.y, c.z]; ( primitives::build_torus(center, major, minor), solid_model::torus_solid(center, major, minor), @@ -178,16 +184,40 @@ impl PrimitiveCommand { fn commit(&self, height: f64) -> CmdResult { match self.build(height) { - Some((entity, solid)) => CmdResult::CommitSolid { - entity, - solid: Box::new(solid), - }, + Some((entity, solid)) => { + let matrix = truck_modeling::Matrix4::new( + self.plane.x.x, + self.plane.x.y, + self.plane.x.z, + 0.0, + self.plane.y.x, + self.plane.y.y, + self.plane.y.z, + 0.0, + self.plane.z.x, + self.plane.z.y, + self.plane.z.z, + 0.0, + self.plane.origin.x, + self.plane.origin.y, + self.plane.origin.z, + 1.0, + ); + CmdResult::CommitSolid { + entity: self.plane.place_entity(entity), + solid: Box::new(truck_modeling::builder::transformed(&solid, matrix)), + } + } None => CmdResult::Cancel, } } } impl CadCommand for PrimitiveCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { self.shape.name() } @@ -210,10 +240,10 @@ impl CadCommand for PrimitiveCommand { if self.height_step { // A click in the ground plane has no Z; use its distance from the // footprint centre as the height magnitude. - let h = (pt - self.pts[0]).length(); + let h = (self.plane.to_local(pt) - self.pts[0]).length(); return self.commit(h.max(1e-6)); } - self.pts.push(pt); + self.pts.push(self.plane.to_local(pt)); if self.pts.len() < self.footprint_pts() { return CmdResult::NeedPoint; } @@ -255,8 +285,23 @@ impl CadCommand for PrimitiveCommand { return None; } let mut foot = self.pts.clone(); - foot.push(pt); - Some(footprint_wire(self.shape, &foot)) + foot.push(self.plane.to_local(pt)); + let mut preview = footprint_wire(self.shape, &foot); + preview.points = preview + .points + .iter() + .map(|point| { + if point[0].is_nan() { + *point + } else { + self.plane + .to_world(glam::Vec3::from_array(*point).as_dvec3()) + .as_vec3() + .to_array() + } + }) + .collect(); + Some(preview) } } @@ -277,11 +322,11 @@ fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel { } else { let (a, b) = (pts[0], pts[1]); points.extend_from_slice(&[ - [a.x as f32, a.y as f32, 0.0], - [b.x as f32, a.y as f32, 0.0], - [b.x as f32, b.y as f32, 0.0], - [a.x as f32, b.y as f32, 0.0], - [a.x as f32, a.y as f32, 0.0], + [a.x as f32, a.y as f32, a.z as f32], + [b.x as f32, a.y as f32, a.z as f32], + [b.x as f32, b.y as f32, a.z as f32], + [a.x as f32, b.y as f32, a.z as f32], + [a.x as f32, a.y as f32, a.z as f32], ]); } wire("primitive_preview", points) @@ -294,7 +339,7 @@ fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) { out.push([ (c.x + r * t.cos()) as f32, (c.y + r * t.sin()) as f32, - 0.0, + c.z as f32, ]); } } diff --git a/src/modules/view/ucs_cmd.rs b/src/modules/view/ucs_cmd.rs index c142c04b5..d7d58ed87 100644 --- a/src/modules/view/ucs_cmd.rs +++ b/src/modules/view/ucs_cmd.rs @@ -16,6 +16,7 @@ use crate::t; pub struct UcsCommand { /// The chosen option keyword (uppercased), once entered; `None` until then. option: Option, + points: Vec, } impl UcsCommand { @@ -25,12 +26,12 @@ impl UcsCommand { /// Options that take no further argument and execute immediately. fn is_zero_arg(opt: &str) -> bool { - matches!(opt, "W" | "WORLD" | "LIST" | "?") + matches!(opt, "W" | "WORLD" | "VIEW" | "V" | "LIST" | "?") } /// Options whose argument is a coordinate (so a click is accepted too). fn takes_point(opt: &str) -> bool { - matches!(opt, "ORIGIN" | "O") + matches!(opt, "ORIGIN" | "O" | "3POINT" | "3P") } /// Options that expect one more typed argument (angle or name). @@ -49,11 +50,16 @@ impl CadCommand for UcsCommand { fn prompt(&self) -> String { match self.option.as_deref() { - None => t!("UCS option [World/Z/X/Y/Origin/Save/Delete] or name:").into_owned(), + None => t!("UCS option [World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:").into_owned(), Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(), Some("X") => t!("UCS rotation angle about X (degrees):").into_owned(), Some("Y") => t!("UCS rotation angle about Y (degrees):").into_owned(), Some("ORIGIN") | Some("O") => t!("UCS new origin point:").into_owned(), + Some("3POINT") | Some("3P") => match self.points.len() { + 0 => t!("UCS specify new origin:").into_owned(), + 1 => t!("UCS specify point on positive X axis:").into_owned(), + _ => t!("UCS specify point in positive XY plane:").into_owned(), + }, Some("SAVE") | Some("S") => t!("UCS name to save current UCS as:").into_owned(), Some("DELETE") | Some("DEL") | Some("D") => { t!("UCS name of UCS to delete:").into_owned() @@ -68,6 +74,11 @@ impl CadCommand for UcsCommand { fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); + if matches!(self.option.as_deref(), Some("3POINT") | Some("3P")) + && crate::app::helpers::parse_coord(t).is_some() + { + return None; + } match self.option.take() { // First token: the option keyword (or a named UCS to restore). None => { @@ -78,7 +89,7 @@ impl CadCommand for UcsCommand { let up = t.to_uppercase(); if Self::is_zero_arg(&up) { Some(CmdResult::Dispatch(format!("UCS {up}"))) - } else if Self::takes_value(&up) { + } else if Self::takes_value(&up) || Self::takes_point(&up) { // Needs a value next; keep the command active and re-prompt. self.option = Some(up); Some(CmdResult::NeedPoint) @@ -93,11 +104,31 @@ impl CadCommand for UcsCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { + if matches!(self.option.as_deref(), Some("3POINT") | Some("3P")) { + self.points.push(pt); + if self.points.len() < 3 { + return CmdResult::NeedPoint; + } + let points = std::mem::take(&mut self.points); + self.option = None; + return CmdResult::Dispatch(format!( + "UCS 3POINTW {},{},{}|{},{},{}|{},{},{}", + points[0].x, + points[0].y, + points[0].z, + points[1].x, + points[1].y, + points[1].z, + points[2].x, + points[2].y, + points[2].z, + )); + } // A clicked point only makes sense for the Origin option; otherwise a // stray click is ignored (keep waiting for the typed keyword / value). if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) { - let opt = self.option.take().unwrap_or_default(); - return CmdResult::Dispatch(format!("UCS {opt} {},{},{}", pt.x, pt.y, pt.z)); + self.option.take(); + return CmdResult::Dispatch(format!("UCS ORIGINW {},{},{}", pt.x, pt.y, pt.z)); } CmdResult::NeedPoint } diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 7bcd31fe9..50e9467c0 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -245,23 +245,40 @@ impl WireModel { /// Return a clone with every point rotated around `center` by `angle_rad`. pub fn rotated(&self, center: glam::Vec3, angle_rad: f32) -> Self { + self.rotated_about_axis(center, glam::Vec3::Z, angle_rad) + } + + /// Return a clone rotated about an arbitrary world-space axis. + pub fn rotated_about_axis( + &self, + center: glam::Vec3, + axis: glam::Vec3, + angle_rad: f32, + ) -> Self { + let rotation = glam::Quat::from_axis_angle(axis.normalize_or_zero(), angle_rad); let (s, c) = angle_rad.sin_cos(); let mut out = self.clone(); out.name = format!("preview_{}", self.name); out.color = Self::CYAN; out.selected = false; for p in &mut out.points { - let dx = p[0] - center.x; - let dy = p[1] - center.y; - p[0] = center.x + dx * c - dy * s; - p[1] = center.y + dx * s + dy * c; + let mapped = center + rotation * (glam::Vec3::from_array(*p) - center); + *p = mapped.to_array(); + } + for p in &mut out.points_low { + *p = (rotation * glam::Vec3::from_array(*p)).to_array(); } if !out.text_verts.is_empty() { - let (cx, cy) = (center.x as f64, center.y as f64); + let center = center.as_dvec3(); + let axis = axis.as_dvec3().normalize_or_zero(); let (s, c) = (s as f64, c as f64); out.text_verts = map_text_verts(&self.text_verts, |x, y, z| { - let (dx, dy) = (x - cx, y - cy); - (cx + dx * c - dy * s, cy + dx * s + dy * c, z) + let v = glam::DVec3::new(x, y, z) - center; + let mapped = center + + v * c + + axis.cross(v) * s + + axis * axis.dot(v) * (1.0 - c); + (mapped.x, mapped.y, mapped.z) }); } out @@ -322,41 +339,48 @@ impl WireModel { /// Return a clone mirrored across the line through `p1`→`p2`. pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self { - let ax = p2.x - p1.x; - let ay = p2.y - p1.y; - let len2 = ax * ax + ay * ay; + self.mirrored_in_plane(p1, p2, glam::Vec3::Z) + } + + /// Return a clone reflected through the plane containing the picked line + /// and the supplied working-plane normal. + pub fn mirrored_in_plane( + &self, + p1: glam::Vec3, + p2: glam::Vec3, + working_normal: glam::Vec3, + ) -> Self { + let plane_normal = (p2 - p1) + .cross(working_normal.normalize_or_zero()) + .normalize_or_zero(); let mut out = self.clone(); out.name = format!("preview_{}", self.name); out.color = Self::CYAN; out.selected = false; - if len2 < 1e-12 { + if plane_normal.length_squared() < 1e-12 { return out; } for p in &mut out.points { - let dx = p[0] - p1.x; - let dy = p[1] - p1.y; - let t = (dx * ax + dy * ay) / len2; - p[0] = p1.x + 2.0 * t * ax - dx; - p[1] = p1.y + 2.0 * t * ay - dy; + let point = glam::Vec3::from_array(*p); + *p = (point - 2.0 * plane_normal.dot(point - p1) * plane_normal).to_array(); } // World position is the double-single sum `points + points_low` (text / // UTM wires split it), so the residual must reflect too — as a direction // (linear reflection about the axis, no `p1` offset). for p in &mut out.points_low { - let t = (p[0] * ax + p[1] * ay) / len2; - p[0] = 2.0 * t * ax - p[0]; - p[1] = 2.0 * t * ay - p[1]; + let point = glam::Vec3::from_array(*p); + *p = (point - 2.0 * plane_normal.dot(point) * plane_normal).to_array(); } // Glyph quads reflect wholesale (true mirror) — the caller only routes // text through here for MIRRTEXT-on; MIRRTEXT-off relocates via // `translated` so glyphs stay readable. if !out.text_verts.is_empty() { - let (ax, ay, len2) = (ax as f64, ay as f64, len2 as f64); - let (p1x, p1y) = (p1.x as f64, p1.y as f64); + let p1 = p1.as_dvec3(); + let normal = plane_normal.as_dvec3(); out.text_verts = map_text_verts(&self.text_verts, |x, y, z| { - let (dx, dy) = (x - p1x, y - p1y); - let t = (dx * ax + dy * ay) / len2; - (p1x + 2.0 * t * ax - dx, p1y + 2.0 * t * ay - dy, z) + let point = glam::DVec3::new(x, y, z); + let mapped = point - 2.0 * normal.dot(point - p1) * normal; + (mapped.x, mapped.y, mapped.z) }); } out diff --git a/src/scene/mspace.rs b/src/scene/mspace.rs index 1b6d210e6..31542838e 100644 --- a/src/scene/mspace.rs +++ b/src/scene/mspace.rs @@ -433,6 +433,15 @@ impl Scene { self.camera.borrow().rotation * glam::Vec3::Z } + pub fn active_camera_rotation(&self) -> glam::Quat { + if let Some(handle) = self.active_viewport { + if let Some(camera) = self.camera_for_viewport(handle) { + return camera.rotation; + } + } + self.camera.borrow().rotation + } + /// View-rotation matrix for the active viewport (MSPACE), or the /// paper-space camera's matrix when not in MSPACE. /// Used by ViewCube hit-testing so clicks map to the correct camera. diff --git a/src/scene/view/dispatch.rs b/src/scene/view/dispatch.rs index b7deff19a..dfeaccbf7 100644 --- a/src/scene/view/dispatch.rs +++ b/src/scene/view/dispatch.rs @@ -83,6 +83,39 @@ pub fn properties_sectioned( sections } +pub fn entity_in_working_plane( + entity: &EntityType, + plane: crate::command::WorkingPlane, +) -> EntityType { + let mut local = entity.clone(); + if !plane.is_identity() { + apply_transform( + &mut local, + &EntityTransform::Affine(plane.to_local_transform()), + ); + } + local +} + +pub fn apply_geom_prop_in_working_plane( + entity: &mut EntityType, + field: &str, + value: &str, + plane: crate::command::WorkingPlane, +) { + if plane.is_identity() { + apply_geom_prop(entity, field, value); + return; + } + let mut local = entity_in_working_plane(entity, plane); + apply_geom_prop(&mut local, field, value); + apply_transform( + &mut local, + &EntityTransform::Affine(plane.to_world_transform()), + ); + *entity = local; +} + pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) { let e = entity.as_entity_mut(); match field { diff --git a/src/scene/view/transform.rs b/src/scene/view/transform.rs index 72a68f662..481665745 100644 --- a/src/scene/view/transform.rs +++ b/src/scene/view/transform.rs @@ -8,13 +8,12 @@ fn to_v3(v: DVec3) -> Vector3 { Vector3::new(v.x, v.y, v.z) } -pub fn apply_standard_transform(entity: &mut T, center: DVec3, angle_rad: f64) +pub fn apply_standard_transform(entity: &mut T, center: DVec3, axis: DVec3, angle_rad: f64) where T: acadrust::Entity, { - let z_axis = Vector3::new(0.0, 0.0, 1.0); let t = Transform::from_translation(to_v3(-center)) - .then(&Transform::from_rotation(z_axis, angle_rad)) + .then(&Transform::from_rotation(to_v3(axis.normalize_or(DVec3::Z)), angle_rad)) .then(&Transform::from_translation(to_v3(center))); entity.apply_transform(&t); } @@ -35,11 +34,25 @@ where { match t { EntityTransform::Translate(d) => entity.translate(to_v3(*d)), - EntityTransform::Rotate { center, angle_rad } => { - apply_standard_transform(entity, *center, *angle_rad) + EntityTransform::Rotate { center, axis, angle_rad } => { + apply_standard_transform(entity, *center, *axis, *angle_rad) } EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor), - EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2), + EntityTransform::Mirror { + p1, + p2, + working_normal, + } => { + if working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) { + mirror(entity, *p1, *p2) + } else { + entity.apply_transform(&reflection_about_working_line( + *p1, + *p2, + *working_normal, + )); + } + } EntityTransform::Affine(transform) => entity.apply_transform(transform), } } @@ -49,19 +62,46 @@ where /// `apply_transform` paths (which handle direction flags, stored-angle /// conventions and bulges themselves). Degenerate line → identity. pub fn reflection_about_xy_line(p1: DVec3, p2: DVec3) -> acadrust::types::Transform { + reflection_about_working_line(p1, p2, DVec3::Z) +} + +/// Reflection through the plane that contains the picked line and is +/// perpendicular to the active working plane. +pub fn reflection_about_working_line( + p1: DVec3, + p2: DVec3, + working_normal: DVec3, +) -> acadrust::types::Transform { use acadrust::types::{Matrix4, Transform}; - let dx = (p2.x - p1.x) as f64; - let dy = (p2.y - p1.y) as f64; - if dx * dx + dy * dy < 1e-12 { + let line = p2 - p1; + let plane_normal = line + .cross(working_normal.normalize_or(DVec3::Z)) + .normalize_or(DVec3::ZERO); + if plane_normal.length_squared() < 1e-12 { return Transform::identity(); } - let ang = dy.atan2(dx); - let m = Matrix4::translation(p1.x as f64, p1.y as f64, 0.0) - * Matrix4::rotation_z(ang) - * Matrix4::scaling(1.0, -1.0, 1.0) - * Matrix4::rotation_z(-ang) - * Matrix4::translation(-(p1.x as f64), -(p1.y as f64), 0.0); - Transform::from_matrix(m) + let n = plane_normal; + let d = 2.0 * n.dot(p1); + Transform::from_matrix(Matrix4 { + m: [ + [1.0 - 2.0 * n.x * n.x, -2.0 * n.x * n.y, -2.0 * n.x * n.z, d * n.x], + [-2.0 * n.y * n.x, 1.0 - 2.0 * n.y * n.y, -2.0 * n.y * n.z, d * n.y], + [-2.0 * n.z * n.x, -2.0 * n.z * n.y, 1.0 - 2.0 * n.z * n.z, d * n.z], + [0.0, 0.0, 0.0, 1.0], + ], + }) +} + +pub fn reflected_point(p: DVec3, p1: DVec3, p2: DVec3, working_normal: DVec3) -> DVec3 { + let line = p2 - p1; + let normal = line + .cross(working_normal.normalize_or(DVec3::Z)) + .normalize_or(DVec3::ZERO); + if normal.length_squared() < 1e-12 { + p + } else { + p - 2.0 * normal.dot(p - p1) * normal + } } pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) { @@ -80,11 +120,6 @@ pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) { *y = p1.y as f64 + my; } -pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: DVec3, p2: DVec3) { - reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2); - reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2); -} - /// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS /// for a given entity normal vector. /// @@ -190,6 +225,7 @@ mod mirror_delegation_tests { h.apply_transform(&EntityTransform::Mirror { p1: DVec3::new(5.0, 0.0, 0.0), p2: DVec3::new(5.0, 1.0, 0.0), + working_normal: DVec3::Z, }); let edges = &h.paths[0].edges;