From 2e397aec99b3b16096a6bd00b127419b45eb00f5 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:22:44 +0300 Subject: [PATCH] VPCLIP arc options, visible linked clip boundaries, XCLIP depth rejection - VPCLIP arc mode takes Angle, CEnter, Direction, Radius and Second pt with the reference prompts, and CLose closes with a tangent arc; the arc math is shared with PLINE. - Viewport clip boundaries from MVIEW and VPCLIP stay visible; picking or erasing a boundary takes its viewport along. - XCLIP Clipdepth: a rejected front plane keeps the old front plane and clears the back plane. Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/viewport.rs | 18 +- src/modules/draw/draw/polyline.rs | 295 +++++++++++++++-------------- src/modules/insert/xclip.rs | 5 +- src/modules/layout/mview.rs | 161 +++++++++++++++- src/scene/selection.rs | 13 ++ 5 files changed, 329 insertions(+), 163 deletions(-) diff --git a/src/app/command_driver/viewport.rs b/src/app/command_driver/viewport.rs index e4f548c96..327d1256b 100644 --- a/src/app/command_driver/viewport.rs +++ b/src/app/command_driver/viewport.rs @@ -82,10 +82,7 @@ impl OpenCADStudio { let old = vp.clip_boundary_handle; let scene = &mut self.tabs[i].scene; let clip = match boundary { - Some(mut entity) => { - entity.common_mut().invisible = true; - scene.add_entity(entity) - } + Some(entity) => scene.add_entity(entity), None => boundary_handle, }; let mut vp = vp; @@ -116,7 +113,6 @@ impl OpenCADStudio { vp.clip_boundary_handle = clip; if let Some(entity) = scene.document.get_entity_mut(clip) { let common = entity.common_mut(); - common.invisible = true; if !common.reactors.contains(&viewport) { common.reactors.push(viewport); } @@ -182,13 +178,10 @@ impl OpenCADStudio { let label = self.history_label_from_active_cmd(i, "MVIEW"); let pending = self.begin_undo(i, label, touched, true); let clip_handle = match boundary { - Some(mut boundary) => { - // A non-rectangular viewport owns a helper boundary - // entity through `clip_boundary_handle`. Keep that - // helper in the document for DWG compatibility and - // stencil clipping, but do not expose it as a separate - // selectable polyline. - boundary.common_mut().invisible = true; + Some(boundary) => { + // A non-rectangular viewport owns its boundary through + // `clip_boundary_handle`: a visible paper-space object that + // is picked and erased together with the viewport. match self.commit_entity_handle(boundary) { Some(handle) => handle, None => { @@ -242,7 +235,6 @@ impl OpenCADStudio { } if let Some(boundary) = self.tabs[i].scene.document.get_entity_mut(clip_handle) { let common = boundary.common_mut(); - common.invisible = true; if !common.reactors.contains(&viewport_handle) { common.reactors.push(viewport_handle); } diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index d507a6089..bd87604cc 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -47,7 +47,7 @@ enum SegMode { /// sub-step returns to `None` once it has produced a vertex, or when Undo / /// Enter / Escape backs out of it. #[derive(Clone, Copy, PartialEq, Debug)] -enum Sub { +pub(crate) enum Sub { None, /// Width / Halfwidth: starting width, then ending width. `half` scales the /// prompt and the typed value by two. @@ -81,7 +81,7 @@ enum Sub { impl Sub { /// The step reads a single typed value (width, length, angle, radius). - fn is_scalar(self) -> bool { + pub(crate) fn is_scalar(self) -> bool { matches!( self, Sub::WidthStart { .. } @@ -97,7 +97,7 @@ impl Sub { } /// The typed value is an angle (degrees) rather than a length. - fn is_angle(self) -> bool { + pub(crate) fn is_angle(self) -> bool { matches!( self, Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } @@ -249,149 +249,8 @@ impl PlineCommand { .unwrap_or(DVec2::X) } - /// Sign for an arc whose direction is not fixed by its definition - /// (CEnter / Radius endpoints, chord length): bend the way the previous - /// segment was heading so the polyline flows on, else counter-clockwise. - fn free_arc_sign(&self, a: DVec2, e: DVec2) -> f64 { - let chord = e - a; - let cross = match self.last_tangent { - Some(t) => t.as_dvec2().perp_dot(chord), - None => 1.0, - }; - if cross < 0.0 { - -1.0 - } else { - 1.0 - } - } - - /// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when - /// the points are collinear or coincident. - fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option { - let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y)); - if d.abs() < 1e-12 { - return None; - } - let a2 = a.length_squared(); - let s2 = s.length_squared(); - let e2 = e.length_squared(); - let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d; - let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d; - let c = DVec2::new(cx, cy); - let start = (a - c).y.atan2((a - c).x); - let end = (e - c).y.atan2((e - c).x); - // The arc bends towards `s`: with `s` right of the chord a → e the - // arc runs counter-clockwise, left of it clockwise. - let ccw = (e - a).perp_dot(s - a) < 0.0; - let mut sweep = end - start; - if ccw { - if sweep <= 0.0 { - sweep += std::f64::consts::TAU; - } - } else if sweep >= 0.0 { - sweep -= std::f64::consts::TAU; - } - Some((sweep / 4.0).tan()) - } - - /// Endpoint of the arc about `c` starting at `a` with signed `sweep`. - fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 { - let (sn, cs) = sweep.sin_cos(); - let v = a - c; - c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs) - } - - /// Resolve the arc a sub-step describes for a candidate `cursor`/point: - /// `(endpoint, bulge)`. `None` when the geometry is degenerate for that - /// input (e.g. a chord longer than the diameter). fn arc_for(&self, sub: Sub, a: DVec2, p: DVec2) -> Option<(DVec2, f64)> { - let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU; - match sub { - Sub::ArcAngleEnd { angle } if sweep_ok(angle) => { - if (p - a).length_squared() < 1e-12 { - return None; - } - Some((p, (angle / 4.0).tan())) - } - Sub::ArcAngleCenter { angle } if sweep_ok(angle) => { - if (a - p).length_squared() < 1e-12 { - return None; - } - Some((Self::rotate_about(p, a, angle), (angle / 4.0).tan())) - } - Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => { - let dir = (p - a).try_normalize()?; - let chord = 2.0 * r * (angle.abs() * 0.5).sin(); - Some((a + dir * chord, (angle / 4.0).tan())) - } - Sub::ArcCenterEnd { c } => { - let r = (a - c).length(); - if r < 1e-9 { - return None; - } - let dir = (p - c).try_normalize()?; - let e = c + dir * r; - let start = (a - c).y.atan2((a - c).x); - let end = (e - c).y.atan2((e - c).x); - let sign = self.free_arc_sign(a, e); - let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU); - if sign < 0.0 { - sweep -= std::f64::consts::TAU; - } - if !sweep_ok(sweep) { - return None; - } - Some((e, (sweep / 4.0).tan())) - } - Sub::ArcCenterAngle { c } => { - // `p` carries the typed angle in `x`. - let angle = p.x; - if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) { - return None; - } - Some((Self::rotate_about(c, a, angle), (angle / 4.0).tan())) - } - Sub::ArcCenterLength { c } => { - // `p` carries the typed chord length in `x`. - let len = p.x; - let r = (a - c).length(); - if r < 1e-9 || len <= 0.0 || len > 2.0 * r { - return None; - } - let sweep = 2.0 * (len / (2.0 * r)).asin(); - // Minor arc; bend on the side the previous segment favours. - let e_ccw = Self::rotate_about(c, a, sweep); - let sign = self.free_arc_sign(a, e_ccw); - let sweep = sweep * sign; - Some((Self::rotate_about(c, a, sweep), (sweep / 4.0).tan())) - } - Sub::ArcDirectionEnd { dir } => { - if (p - a).length_squared() < 1e-12 { - return None; - } - Some((p, compute_bulge(a, dir, p))) - } - Sub::ArcRadiusEnd { r } if r > 0.0 => { - let chord = (p - a).length(); - if chord < 1e-9 || chord > 2.0 * r { - return None; - } - let sweep = 2.0 * (chord / (2.0 * r)).asin() * self.free_arc_sign(a, p); - Some((p, (sweep / 4.0).tan())) - } - Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => { - let dir = (p - a).try_normalize()?; - let chord = 2.0 * r * (angle.abs() * 0.5).sin(); - Some((a + dir * chord, (angle / 4.0).tan())) - } - Sub::ArcSecondEnd { s } => { - if (p - a).length_squared() < 1e-12 { - return None; - } - Some((p, Self::bulge_through(a, s, p)?)) - } - _ => None, - } + arc_for(sub, a, p, self.last_tangent) } /// Land a segment ending at local `e` with `bulge`, publish it, and @@ -582,6 +441,152 @@ impl PlineCommand { // ── Arc geometry helpers ─────────────────────────────────────────────────── +/// Sign for an arc whose direction is not fixed by its definition +/// (CEnter / Radius endpoints, chord length): bend the way the previous +/// segment was heading so the polyline flows on, else counter-clockwise. +fn free_arc_sign(last_tangent: Option, a: DVec2, e: DVec2) -> f64 { + let chord = e - a; + let cross = match last_tangent { + Some(t) => t.as_dvec2().perp_dot(chord), + None => 1.0, + }; + if cross < 0.0 { + -1.0 + } else { + 1.0 + } +} + +/// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when +/// the points are collinear or coincident. +fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option { + let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y)); + if d.abs() < 1e-12 { + return None; + } + let a2 = a.length_squared(); + let s2 = s.length_squared(); + let e2 = e.length_squared(); + let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d; + let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d; + let c = DVec2::new(cx, cy); + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + // The arc bends towards `s`: with `s` right of the chord a → e the + // arc runs counter-clockwise, left of it clockwise. + let ccw = (e - a).perp_dot(s - a) < 0.0; + let mut sweep = end - start; + if ccw { + if sweep <= 0.0 { + sweep += std::f64::consts::TAU; + } + } else if sweep >= 0.0 { + sweep -= std::f64::consts::TAU; + } + Some((sweep / 4.0).tan()) +} + +/// Endpoint of the arc about `c` starting at `a` with signed `sweep`. +fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 { + let (sn, cs) = sweep.sin_cos(); + let v = a - c; + c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs) +} + +/// Resolve the arc an arc sub-step describes (PLINE, VPCLIP) from `a` for a +/// candidate `cursor`/point `p`, after a segment leaving along +/// `last_tangent`: `(endpoint, bulge)`. `None` when the geometry is +/// degenerate for that input (e.g. a chord longer than the diameter). +pub(crate) fn arc_for(sub: Sub, a: DVec2, p: DVec2, last_tangent: Option) -> Option<(DVec2, f64)> { + let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU; + match sub { + Sub::ArcAngleEnd { angle } if sweep_ok(angle) => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, (angle / 4.0).tan())) + } + Sub::ArcAngleCenter { angle } if sweep_ok(angle) => { + if (a - p).length_squared() < 1e-12 { + return None; + } + Some((rotate_about(p, a, angle), (angle / 4.0).tan())) + } + Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcCenterEnd { c } => { + let r = (a - c).length(); + if r < 1e-9 { + return None; + } + let dir = (p - c).try_normalize()?; + let e = c + dir * r; + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + let sign = free_arc_sign(last_tangent, a, e); + let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU); + if sign < 0.0 { + sweep -= std::f64::consts::TAU; + } + if !sweep_ok(sweep) { + return None; + } + Some((e, (sweep / 4.0).tan())) + } + Sub::ArcCenterAngle { c } => { + // `p` carries the typed angle in `x`. + let angle = p.x; + if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) { + return None; + } + Some((rotate_about(c, a, angle), (angle / 4.0).tan())) + } + Sub::ArcCenterLength { c } => { + // `p` carries the typed chord length in `x`. + let len = p.x; + let r = (a - c).length(); + if r < 1e-9 || len <= 0.0 || len > 2.0 * r { + return None; + } + let sweep = 2.0 * (len / (2.0 * r)).asin(); + // Minor arc; bend on the side the previous segment favours. + let e_ccw = rotate_about(c, a, sweep); + let sign = free_arc_sign(last_tangent, a, e_ccw); + let sweep = sweep * sign; + Some((rotate_about(c, a, sweep), (sweep / 4.0).tan())) + } + Sub::ArcDirectionEnd { dir } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, compute_bulge(a, dir, p))) + } + Sub::ArcRadiusEnd { r } if r > 0.0 => { + let chord = (p - a).length(); + if chord < 1e-9 || chord > 2.0 * r { + return None; + } + let sweep = 2.0 * (chord / (2.0 * r)).asin() * free_arc_sign(last_tangent, a, p); + Some((p, (sweep / 4.0).tan())) + } + Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcSecondEnd { s } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, bulge_through(a, s, p)?)) + } + _ => None, + } +} + /// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`. /// Returns 0.0 if the points are coincident or the tangent is parallel to the chord. pub(crate) fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 { diff --git a/src/modules/insert/xclip.rs b/src/modules/insert/xclip.rs index e0c7206dc..dcbca18e1 100644 --- a/src/modules/insert/xclip.rs +++ b/src/modules/insert/xclip.rs @@ -220,10 +220,11 @@ impl XclipCommand { self.step = Step::Back; CmdResult::NeedPoint } - // A front plane behind the back one changes nothing. + // A front plane behind the back one is not applied; the back + // plane is cleared and the old front one kept. _ if self.front.is_some_and(|front| front < d) => self.act(XclipAction::Depth { front: None, - back: None, + back: Some(None), rejected: true, }), _ => self.act(XclipAction::Depth { front: Some(self.front), back: Some(Some(d)), rejected: false }), diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 46ec2d9e4..99dffedef 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -11,6 +11,8 @@ // Enter an arc boundary option // [Angle/CEnter/CLose/Direction/Line/Radius/Second pt/Undo/Endpoint of arc] : // Specify length of line: +// and the arc options' prompts (Angle, CEnter, Direction, Radius, Second pt) +// as the reference words them; CLose in arc mode closes with a tangent arc. use codec::entities::{LwPolyline, LwVertex, Viewport}; use codec::tables::View; @@ -20,7 +22,7 @@ use crate::t; use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; use crate::modules::draw::draw::polyline::{ - arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc, + arc_for, arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc, Sub, }; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -78,6 +80,8 @@ pub struct MviewCommand { /// already has a boundary. clip: Option<(Handle, bool)>, picked: Option, + /// VPCLIP arc mode: the arc option being answered. + arc_sub: Sub, } impl MviewCommand { @@ -99,6 +103,7 @@ impl MviewCommand { original_layout, clip: None, picked: None, + arc_sub: Sub::None, } } @@ -122,7 +127,129 @@ impl MviewCommand { self.clip.map_or(Handle::NULL, |(handle, _)| handle) } + /// VPCLIP: an arc segment from the last vertex to `e`. + fn push_arc(&mut self, e: DVec2, bulge: f64) -> CmdResult { + let Some(last) = self.polygon.last().copied() else { + return CmdResult::NeedPoint; + }; + let end = DVec3::new(e.x, e.y, last.z); + let index = self.polygon.len() - 1; + self.polygon_bulges[index] = bulge; + self.polygon_last_tangent = seg_exit_tangent(last, end, bulge); + self.arc_sub = Sub::None; + self.polygon.push(end); + self.polygon_bulges.push(0.0); + CmdResult::NeedPoint + } + + fn last2(&self) -> Option { + self.polygon.last().map(|p| DVec2::new(p.x, p.y)) + } + + /// VPCLIP arc option answered with a typed value. + fn arc_value(&mut self, value: f64) -> Option { + let a = self.last2()?; + let tangent = self.polygon_last_tangent; + let angle_ok = |r: f64| r.abs() > 1e-9 && r.abs() < std::f64::consts::TAU; + let next = match self.arc_sub { + Sub::ArcAngle if angle_ok(value.to_radians()) => Sub::ArcAngleEnd { angle: value.to_radians() }, + Sub::ArcAngleRadius { angle } if value > 0.0 => Sub::ArcAngleRadiusDir { angle, r: value }, + Sub::ArcRadius if value > 0.0 => Sub::ArcRadiusEnd { r: value }, + Sub::ArcRadiusAngle { r } if angle_ok(value.to_radians()) => { + Sub::ArcRadiusAngleDir { r, angle: value.to_radians() } + } + sub @ Sub::ArcCenterAngle { .. } => { + let (e, bulge) = arc_for(sub, a, DVec2::new(value.to_radians(), 0.0), tangent)?; + return Some(self.push_arc(e, bulge)); + } + sub @ Sub::ArcCenterLength { .. } => { + let (e, bulge) = arc_for(sub, a, DVec2::new(value, 0.0), tangent)?; + return Some(self.push_arc(e, bulge)); + } + _ => return None, + }; + self.arc_sub = next; + Some(CmdResult::NeedPoint) + } + + /// VPCLIP arc option answered with a point. + fn arc_point(&mut self, pt: DVec3) -> CmdResult { + let Some(a) = self.last2() else { + return CmdResult::NeedPoint; + }; + let p = DVec2::new(pt.x, pt.y); + let sub = self.arc_sub; + if sub.is_scalar() { + let value = if sub.is_angle() { (p - a).y.atan2((p - a).x).to_degrees() } else { (p - a).length() }; + return self.arc_value(value).unwrap_or(CmdResult::NeedPoint); + } + match sub { + Sub::ArcCenter if (p - a).length_squared() > 1e-12 => self.arc_sub = Sub::ArcCenterEnd { c: p }, + Sub::ArcDirection => { + if let Some(dir) = (p - a).try_normalize() { + self.arc_sub = Sub::ArcDirectionEnd { dir }; + } + } + Sub::ArcSecond if (p - a).length_squared() > 1e-12 => self.arc_sub = Sub::ArcSecondEnd { s: p }, + _ => { + if let Some((e, bulge)) = arc_for(sub, a, p, self.polygon_last_tangent) { + return self.push_arc(e, bulge); + } + } + } + CmdResult::NeedPoint + } + + /// VPCLIP CLose in arc mode: a tangent arc back to the start. + fn close_with_arc(&mut self) -> CmdResult { + let (Some(last), Some(first)) = (self.polygon.last().copied(), self.polygon.first().copied()) else { + return CmdResult::NeedPoint; + }; + let tangent = self.polygon_last_tangent.map_or(DVec2::X, |t| t.as_dvec2()); + let index = self.polygon.len() - 1; + self.polygon_bulges[index] = compute_bulge(DVec2::new(last.x, last.y), tangent, DVec2::new(first.x, first.y)); + self.finish_polygon() + } + + fn arc_prompt(&self) -> Option<&'static str> { + Some(match self.arc_sub { + Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } => "Specify included angle:", + Sub::ArcAngleEnd { .. } => "Specify endpoint of arc (hold Ctrl to switch direction) or [CEnter/Radius]:", + Sub::ArcCenter | Sub::ArcAngleCenter { .. } => "Specify center point of arc:", + Sub::ArcCenterEnd { .. } => "Specify endpoint of arc (hold Ctrl to switch direction) or [Angle/Length]:", + Sub::ArcCenterLength { .. } => "Specify length of chord:", + Sub::ArcDirection => "Specify the tangent direction for the start point of arc:", + Sub::ArcDirectionEnd { .. } => "Specify endpoint of the arc (hold Ctrl to switch direction):", + Sub::ArcRadius | Sub::ArcAngleRadius { .. } => "Specify radius of arc:", + Sub::ArcRadiusEnd { .. } => "Specify endpoint of arc (hold Ctrl to switch direction) or [Angle]:", + Sub::ArcAngleRadiusDir { .. } | Sub::ArcRadiusAngleDir { .. } => "Specify direction of chord for arc:", + Sub::ArcSecond => "Specify second point on arc:", + Sub::ArcSecondEnd { .. } => "Specify end point of arc:", + _ => return None, + }) + } + fn clip_text(&mut self, upper: &str) -> Option { + if self.step == Step::Polygon && self.arc_sub != Sub::None { + let next = match (self.arc_sub, upper) { + (Sub::ArcAngleEnd { angle }, "CE" | "CENTER" | "CENTRE") => Sub::ArcAngleCenter { angle }, + (Sub::ArcAngleEnd { angle }, "R" | "RADIUS") => Sub::ArcAngleRadius { angle }, + (Sub::ArcCenterEnd { c }, "A" | "ANGLE") => Sub::ArcCenterAngle { c }, + (Sub::ArcCenterEnd { c }, "L" | "LENGTH") => Sub::ArcCenterLength { c }, + (Sub::ArcRadiusEnd { r }, "A" | "ANGLE") => Sub::ArcRadiusAngle { r }, + (sub, text) if sub.is_scalar() => { + let value = if sub.is_angle() { + crate::entities::common::parse_typed_angle(text).map(f64::to_degrees) + } else { + crate::entities::common::parse_typed_length(text) + }; + return value.and_then(|v| self.arc_value(v)); + } + _ => return None, + }; + self.arc_sub = next; + return Some(CmdResult::NeedPoint); + } match self.step { Step::ClipChoice => match upper { "" | "P" | "POLYGONAL" => { @@ -146,7 +273,18 @@ impl MviewCommand { Some(self.on_point(last + DVec3::new(direction.x, direction.y, 0.0) * length)) } Step::Polygon if self.polygon_mode == PolygonMode::Arc => match upper { - "CL" | "CLOSE" if self.polygon.len() >= 2 => Some(self.finish_polygon()), + "CL" | "CLOSE" if self.polygon.len() >= 2 => Some(self.close_with_arc()), + "A" | "ANGLE" | "CE" | "CENTER" | "CENTRE" | "D" | "DIRECTION" | "R" | "RADIUS" | "S" + | "SECOND" | "SECOND PT" => { + self.arc_sub = match upper { + "A" | "ANGLE" => Sub::ArcAngle, + "CE" | "CENTER" | "CENTRE" => Sub::ArcCenter, + "D" | "DIRECTION" => Sub::ArcDirection, + "R" | "RADIUS" => Sub::ArcRadius, + _ => Sub::ArcSecond, + }; + Some(CmdResult::NeedPoint) + } "L" | "LINE" => { self.polygon_mode = PolygonMode::Line; Some(CmdResult::NeedPoint) @@ -367,6 +505,7 @@ impl CadCommand for MviewCommand { Step::ClipChoice => "Select clipping object or [Polygonal] :", Step::ClipLength => "Specify length of line:", Step::Polygon if self.polygon.is_empty() => "Specify start point:", + Step::Polygon if self.arc_prompt().is_some() => self.arc_prompt().unwrap_or_default(), Step::Polygon if self.polygon_mode == PolygonMode::Arc => { "Enter an arc boundary option\n[Angle/CEnter/CLose/Direction/Line/Radius/Second pt/Undo/Endpoint of arc] :" } @@ -418,9 +557,20 @@ impl CadCommand for MviewCommand { if self.clip.is_some() { return match self.step { Step::ClipChoice => vec![CmdOption::new("Polygonal", "P")], + Step::Polygon if self.arc_sub != Sub::None => match self.arc_sub { + Sub::ArcAngleEnd { .. } => vec![CmdOption::new("CEnter", "CE"), CmdOption::new("Radius", "R")], + Sub::ArcCenterEnd { .. } => vec![CmdOption::new("Angle", "A"), CmdOption::new("Length", "L")], + Sub::ArcRadiusEnd { .. } => vec![CmdOption::new("Angle", "A")], + _ => Vec::new(), + }, Step::Polygon if self.polygon_mode == PolygonMode::Arc => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("CEnter", "CE"), CmdOption::new("CLose", "CL"), + CmdOption::new("Direction", "D"), CmdOption::new("Line", "L"), + CmdOption::new("Radius", "R"), + CmdOption::new("Second pt", "S"), CmdOption::new("Undo", "U"), ], Step::Polygon if !self.polygon.is_empty() => { @@ -479,6 +629,7 @@ impl CadCommand for MviewCommand { }, None => CmdResult::NeedPoint, }, + Step::Polygon if self.arc_sub != Sub::None => self.arc_point(pt), Step::Polygon => { if let Some(last) = self.polygon.last().copied() { let last_index = self.polygon.len() - 1; @@ -620,7 +771,7 @@ impl CadCommand for MviewCommand { } fn input_kind(&self) -> InputKind { - if self.step == Step::ClipLength { + if self.step == Step::ClipLength || (self.step == Step::Polygon && self.arc_sub.is_scalar()) { InputKind::FreeText } else if self.step == Step::ChooseView { InputKind::FreeText @@ -707,6 +858,10 @@ impl CadCommand for MviewCommand { self.step = Step::Polygon; return Some(CmdResult::NeedPoint); } + if self.arc_sub != Sub::None { + self.arc_sub = Sub::None; + return Some(CmdResult::NeedPoint); + } if self.step == Step::Polygon && !self.polygon.is_empty() { Some(self.undo_polygon()) } else { diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 942593097..097c28bc3 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -121,6 +121,19 @@ impl Scene { handles: &[Handle], ) -> Vec { let mut expanded = self.expanded_with_leaders(handles); + // A viewport's clip boundary goes with its viewport: picking the + // boundary selects both, erasing it erases the viewport. + let viewports: Vec = expanded + .iter() + .filter_map(|h| self.document.get_entity(*h).map(|e| (*h, e))) + .flat_map(|(h, e)| { + e.common().reactors.iter().copied().filter(move |r| { + matches!(self.document.get_entity(*r), + Some(EntityType::Viewport(vp)) if vp.clip_boundary_handle == h) + }) + }) + .collect(); + expanded.extend(viewports); expanded.sort_unstable_by_key(|handle| handle.value()); expanded.dedup(); expanded