From 1cf2b700ce164f5219d4fcee06dcda181cbb96e7 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 31 Jul 2026 23:36:07 +0300 Subject: [PATCH] fix(polyline): place added vertices live Keep new vertices in grip placement until click and restore the original entity on Escape. Correct reverse direction and segment metadata across polyline variants.\n\nRefs #610 --- src/app/command_driver.rs | 4 +- src/app/properties.rs | 15 +++ src/app/update/command.rs | 130 ++++++++++++++++++++++ src/app/update/viewport.rs | 116 +++++++++++++++++-- src/entities/lwpolyline.rs | 172 +++++++++++++++++++++++++++-- src/entities/polyline.rs | 95 ++++++++++++---- src/modules/draw/modify/reverse.rs | 71 ++++++++++-- 7 files changed, 547 insertions(+), 56 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index eb8ba74f4..88e05d7e1 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -56,7 +56,9 @@ impl OpenCADStudio { let handles = std::mem::take(&mut self.grip_preview_handles); let originals = std::mem::take(&mut self.grip_originals); + let mut changed_handles: rustc_hash::FxHashSet<_> = handles.iter().copied().collect(); for (handle, original) in originals { + changed_handles.insert(handle); if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { *entity = original; } @@ -64,7 +66,7 @@ impl OpenCADStudio { for &handle in &handles { self.tabs[i].scene.preview_hidden.remove(&handle); } - let changes: Vec<_> = handles + let changes: Vec<_> = changed_handles .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); diff --git a/src/app/properties.rs b/src/app/properties.rs index 97e310f20..8046f9745 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -136,6 +136,21 @@ impl OpenCADStudio { } else { 0 }; + let prop_vertex = if cur_handles.len() == 1 { + let vertex_count = self.tabs[i] + .scene + .document + .get_entity(cur_handles[0]) + .and_then(|entity| match entity { + acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()), + acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), + acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()), + _ => None, + }); + vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1))) + } else { + prop_vertex + }; let prop_vertex_indicator_active = if cur_handles == prev_handles { self.tabs[i].properties.prop_vertex_indicator_active } else { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 32ee5b1c8..f370b0a69 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -21,6 +21,49 @@ use acadrust::{EntityType as AcadEntityType, Handle}; use iced::time::Instant; use iced::{mouse, Point, Task}; +/// Return `(new vertex grip id, old vertex count)` for polyline Add Vertex. +/// LWPolyline segment grips live after the vertex grips, so translate their id +/// back to the segment before selecting the inserted vertex. +fn polyline_add_vertex_target(entity: &AcadEntityType, grip_id: usize) -> Option<(usize, usize)> { + match entity { + AcadEntityType::LwPolyline(polyline) => { + let n = polyline.vertices.len(); + if n == 0 { + None + } else if grip_id < n { + Some((grip_id + 1, n)) + } else { + let segment = grip_id - n; + let segment_count = if polyline.is_closed { + n + } else { + n.saturating_sub(1) + }; + (segment < segment_count).then_some((segment + 1, n)) + } + } + AcadEntityType::Polyline(polyline) if grip_id < polyline.vertices.len() => { + Some((grip_id + 1, polyline.vertices.len())) + } + AcadEntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => { + Some((grip_id + 1, polyline.vertices.len())) + } + AcadEntityType::Polyline3D(polyline) if grip_id < polyline.vertices.len() => { + Some((grip_id + 1, polyline.vertices.len())) + } + _ => None, + } +} + +fn polyline_vertex_count(entity: &AcadEntityType) -> Option { + match entity { + AcadEntityType::LwPolyline(polyline) => Some(polyline.vertices.len()), + AcadEntityType::Polyline(polyline) => Some(polyline.vertices.len()), + AcadEntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), + AcadEntityType::Polyline3D(polyline) => Some(polyline.vertices.len()), + _ => None, + } +} impl OpenCADStudio { pub(super) fn begin_tab_close_queue(&mut self, tab_ids: Vec) -> Task { @@ -966,6 +1009,93 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } return Task::none(); } + // Polyline Add Vertex is an interactive placement, not an + // immediate midpoint edit. Seed the undo snapshot before the + // provisional vertex exists, then engage its new grip so the + // regular snap/ortho/polar preview follows the cursor. One + // click commits the whole append+move; Escape restores the + // original entity. + if matches!(item.action, GripMenuAction::AddVertex) { + let placement = self.tabs[i] + .scene + .document + .get_entity(popup.handle) + .cloned() + .and_then(|original| { + polyline_add_vertex_target(&original, popup.grip_id) + .map(|(new_gid, old_len)| (original, new_gid, old_len)) + }); + if let Some((original, new_gid, old_len)) = placement { + let dirty_before = self.tabs[i].dirty; + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(popup.handle) + { + entity.apply_grip_menu(popup.grip_id, item.action); + } + let inserted = self.tabs[i] + .scene + .document + .get_entity(popup.handle) + .and_then(polyline_vertex_count) + .is_some_and(|len| len == old_len + 1); + if !inserted { + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(popup.handle) + { + *entity = original; + } + self.tabs[i].scene.bump_entities(&[ + (popup.handle, crate::scene::ChangeKind::Modified), + ]); + self.tabs[i].dirty = dirty_before; + self.refresh_selected_grips(); + self.refresh_properties(); + self.command_line.push_error("Cannot add a vertex here."); + return Task::none(); + } + self.tabs[i] + .scene + .bump_entities(&[(popup.handle, crate::scene::ChangeKind::Modified)]); + self.tabs[i].dirty = true; + self.refresh_selected_grips(); + self.refresh_properties(); + let grip_world = self.tabs[i] + .selected_grip_handles + .iter() + .zip(self.tabs[i].selected_grips.iter()) + .find(|(owner, grip)| { + **owner == popup.handle && grip.id == new_gid + }) + .map(|(_, grip)| grip.world); + if let Some(grip_world) = grip_world { + self.grip_originals = vec![(popup.handle, original)]; + self.grip_dirty_before = Some(dirty_before); + self.tabs[i].active_grip = Some(GripEdit::single( + popup.handle, + new_gid, + false, + grip_world, + )); + self.command_line + .push_info("Specify new vertex location:"); + } else { + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(popup.handle) + { + *entity = original; + } + self.tabs[i].scene.bump_entities(&[ + (popup.handle, crate::scene::ChangeKind::Modified), + ]); + self.tabs[i].dirty = dirty_before; + self.refresh_selected_grips(); + self.refresh_properties(); + self.command_line + .push_error("Cannot place the new vertex."); + } + return Task::none(); + } + } // One-shot action — apply immediately. self.push_undo_snapshot(i, item.label); // For Add Leader, the new arrow becomes the last grip; remember diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index f209d4c48..ecbb07fbb 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -34,6 +34,51 @@ fn pt_pt_d2(a: Point, b: Point) -> f32 { (a.x - b.x).powi(2) + (a.y - b.y).powi(2) } +fn is_added_polyline_vertex( + original: &AcadEntityType, + current: &AcadEntityType, + vertex_id: usize, +) -> bool { + match (original, current) { + (AcadEntityType::LwPolyline(before), AcadEntityType::LwPolyline(after)) => { + after.vertices.len() == before.vertices.len() + 1 + && vertex_id < after.vertices.len() + } + (AcadEntityType::Polyline2D(before), AcadEntityType::Polyline2D(after)) => { + after.vertices.len() == before.vertices.len() + 1 + && vertex_id < after.vertices.len() + } + _ => false, + } +} + +/// Return the source bulge when `vertex_id` is the provisional point inserted +/// into an arc segment. Straight segments stay straight during placement. +fn added_arc_bulge( + original: &AcadEntityType, + current: &AcadEntityType, + vertex_id: usize, +) -> Option { + if !is_added_polyline_vertex(original, current, vertex_id) { + return None; + } + let prev = vertex_id.checked_sub(1)?; + let (n, closed, bulge) = match original { + AcadEntityType::LwPolyline(polyline) => ( + polyline.vertices.len(), + polyline.is_closed, + polyline.vertices.get(prev)?.bulge, + ), + AcadEntityType::Polyline2D(polyline) => ( + polyline.vertices.len(), + polyline.is_closed(), + polyline.vertices.get(prev)?.bulge, + ), + _ => return None, + }; + ((closed || prev + 1 < n) && bulge.abs() >= 1e-9).then_some(bulge) +} + /// Squared distance from `p` to the segment `a`–`b`. fn pt_seg_d2(p: Point, a: Point, b: Point) -> f32 { let (vx, vy) = (b.x - a.x, b.y - a.y); @@ -820,17 +865,22 @@ impl OpenCADStudio { for handle in std::mem::take(&mut self.grip_preview_handles) { self.tabs[i].scene.preview_hidden.remove(&handle); } - self.grip_originals = edited_handles - .iter() - .filter_map(|&handle| { - self.tabs[i] - .scene - .document - .get_entity(handle) - .cloned() - .map(|entity| (handle, entity)) - }) - .collect(); + // Interactive Add Vertex seeds this with the entity from + // before insertion so append + placement is one undo step. + // Normal grip drags still snapshot their current entities here. + if self.grip_originals.is_empty() { + self.grip_originals = edited_handles + .iter() + .filter_map(|&handle| { + self.tabs[i] + .scene + .document + .get_entity(handle) + .cloned() + .map(|entity| (handle, entity)) + }) + .collect(); + } for &handle in &edited_handles { self.tabs[i].scene.preview_hidden.insert(handle); } @@ -960,9 +1010,35 @@ impl OpenCADStudio { (target.handle, target.grip_id, apply) }) .collect(); + let refit_arc_targets: Vec<_> = grip + .targets + .iter() + .filter_map(|target| { + let original = self + .grip_originals + .iter() + .find(|(handle, _)| *handle == target.handle) + .map(|(_, entity)| entity)?; + let current = self.tabs[i] + .scene + .document + .get_entity(target.handle)?; + added_arc_bulge(original, current, target.grip_id) + .map(|bulge| (target.handle, target.grip_id, bulge)) + }) + .collect(); for (handle, grip_id, apply) in actions { self.tabs[i].scene.apply_grip(handle, grip_id, apply); } + for (handle, vertex_id, original_bulge) in refit_arc_targets { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::entities::lwpolyline::refit_added_arc_vertex( + entity, + vertex_id, + original_bulge, + ); + } + } self.tabs[i].scene.set_preview_hatches(&edited_handles); self.tabs[i].dirty = true; if let Some(active) = self.tabs[i].active_grip.as_mut() { @@ -2253,6 +2329,19 @@ impl OpenCADStudio { // Engaging click — stay hot, wait for the placement click. return Task::none(); } + let added_vertex_focus = grip.targets.iter().find_map(|target| { + let original = self + .grip_originals + .iter() + .find(|(handle, _)| *handle == target.handle) + .map(|(_, entity)| entity)?; + let current = self.tabs[i] + .scene + .document + .get_entity(target.handle)?; + is_added_polyline_vertex(original, current, target.grip_id) + .then_some(target.grip_id) + }); self.tabs[i].active_grip = None; // Commit the grip drag as one undoable group, then put every // edited entity back into the resident tessellation. @@ -2287,6 +2376,11 @@ impl OpenCADStudio { // Placement confirmed — keep the just-added leader. self.grip_add_provisional = None; self.tabs[i].snap_result = None; + if let Some(vertex_id) = added_vertex_focus { + self.tabs[i].properties.prop_vertex = vertex_id; + self.tabs[i].properties.prop_vertex_indicator_active = true; + crate::scene::view::dispatch::set_prop_current_vertex(vertex_id); + } self.refresh_properties(); return Task::none(); } diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index f5fd00de3..e32813db2 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -62,6 +62,113 @@ fn bulge_from_midpoint(p0: [f64; 2], p1: [f64; 2], mid: [f64; 2]) -> Option Some(if cross > 0.0 { bulge } else { -bulge }) } +/// Bulges for the two consecutive arcs `p0 → mid` and `mid → p1` on the +/// circle through all three points. Used while placing a vertex inserted into +/// an arc segment so both halves keep meeting at the cursor. +fn split_bulges_from_point( + p0: [f64; 2], + mid: [f64; 2], + p1: [f64; 2], + original_bulge: f64, +) -> (f64, f64) { + let d0 = (mid[0] - p0[0]).hypot(mid[1] - p0[1]); + let d1 = (p1[0] - mid[0]).hypot(p1[1] - mid[1]); + if d0 < 1e-12 { + return (0.0, original_bulge); + } + if d1 < 1e-12 { + return (original_bulge, 0.0); + } + let ax = 2.0 * (mid[0] - p0[0]); + let ay = 2.0 * (mid[1] - p0[1]); + let bx = 2.0 * (p1[0] - p0[0]); + let by = 2.0 * (p1[1] - p0[1]); + let ca = mid[0] * mid[0] + mid[1] * mid[1] - p0[0] * p0[0] - p0[1] * p0[1]; + let cb = p1[0] * p1[0] + p1[1] * p1[1] - p0[0] * p0[0] - p0[1] * p0[1]; + let det = ax * by - ay * bx; + if det.abs() < 1e-12 { + return (0.0, 0.0); + } + let cx = (ca * by - cb * ay) / det; + let cy = (ax * cb - bx * ca) / det; + let a0 = (p0[1] - cy).atan2(p0[0] - cx); + let am = (mid[1] - cy).atan2(mid[0] - cx); + let a1 = (p1[1] - cy).atan2(p1[0] - cx); + let cross = + (mid[0] - p0[0]) * (p1[1] - mid[1]) - (mid[1] - p0[1]) * (p1[0] - mid[0]); + let bulge = |from: f64, to: f64| { + if cross > 0.0 { + ((to - from).rem_euclid(TAU) / 4.0).tan() + } else { + -((from - to).rem_euclid(TAU) / 4.0).tan() + } + }; + (bulge(a0, am), bulge(am, a1)) +} + +/// Refit the two arc segments around a newly inserted vertex. The caller +/// ensures this placement originated from an arc, so straight-segment Add +/// Vertex remains two straight segments when the cursor moves off the chord. +pub(crate) fn refit_added_arc_vertex( + entity: &mut acadrust::EntityType, + vertex_id: usize, + original_bulge: f64, +) { + match entity { + acadrust::EntityType::LwPolyline(polyline) => { + let n = polyline.vertices.len(); + if vertex_id == 0 || vertex_id >= n { + return; + } + let next = if vertex_id + 1 < n { + vertex_id + 1 + } else if polyline.is_closed { + 0 + } else { + return; + }; + let prev = vertex_id - 1; + let p0 = polyline.vertices[prev].location; + let mid = polyline.vertices[vertex_id].location; + let p1 = polyline.vertices[next].location; + let (first, second) = split_bulges_from_point( + [p0.x, p0.y], + [mid.x, mid.y], + [p1.x, p1.y], + original_bulge, + ); + polyline.vertices[prev].bulge = first.clamp(-1e6, 1e6); + polyline.vertices[vertex_id].bulge = second.clamp(-1e6, 1e6); + } + acadrust::EntityType::Polyline2D(polyline) => { + let n = polyline.vertices.len(); + if vertex_id == 0 || vertex_id >= n { + return; + } + let next = if vertex_id + 1 < n { + vertex_id + 1 + } else if polyline.is_closed() { + 0 + } else { + return; + }; + let prev = vertex_id - 1; + let p0 = &polyline.vertices[prev].location; + let mid = &polyline.vertices[vertex_id].location; + let p1 = &polyline.vertices[next].location; + let (first, second) = split_bulges_from_point( + [p0.x, p0.y], + [mid.x, mid.y], + [p1.x, p1.y], + original_bulge, + ); + polyline.vertices[prev].bulge = first.clamp(-1e6, 1e6); + polyline.vertices[vertex_id].bulge = second.clamp(-1e6, 1e6); + } + _ => {} + } +} + /// Tessellate a thick polyline segment list into NaN-separated Lines geometry. /// Shared by LwPolyline and Polyline2D thickness paths. fn thick_segments( @@ -774,10 +881,23 @@ impl crate::entities::traits::Grippable for LwPolyline { match action { A::Stretch => {} A::AddVertex => { - // Insert a new vertex at the chord midpoint between the - // hovered vertex (or segment) and its neighbour. The new - // vertex inherits the previous vertex's bulge so an - // existing arc is split into two arcs of the same chord. + if n == 0 { + return; + } + // The last vertex of an open polyline extends the path. Start + // the provisional vertex on the endpoint; the grip driver then + // moves it with the cursor until the placement click. + if grip_id == n - 1 && !self.is_closed { + self.vertices[grip_id].bulge = 0.0; + let mut new_v = self.vertices[grip_id].clone(); + new_v.bulge = 0.0; + new_v.vertex_id = 0; + self.vertices.push(new_v); + return; + } + // Insert a provisional vertex midway along the following + // segment. Arc segments are split into two arcs; the grip + // driver refits both halves while the cursor moves. let (i0, i1) = if grip_id < n { let i0 = grip_id; let i1 = (grip_id + 1) % n; @@ -789,14 +909,42 @@ impl crate::entities::traits::Grippable for LwPolyline { if i1 == 0 && !self.is_closed { return; } - let v0 = &self.vertices[i0]; - let v1 = &self.vertices[i1]; - let mx = (v0.location.x + v1.location.x) * 0.5; - let my = (v0.location.y + v1.location.y) * 0.5; - let inherited = v0.clone(); - let mut new_v = inherited.clone(); - new_v.location.x = mx; - new_v.location.y = my; + let v0 = self.vertices[i0]; + let v1 = self.vertices[i1]; + let midpoint = if v0.bulge.abs() >= 1e-9 { + arc_midpoint( + [v0.location.x, v0.location.y], + [v1.location.x, v1.location.y], + v0.bulge, + ) + } else { + [ + (v0.location.x + v1.location.x) * 0.5, + (v0.location.y + v1.location.y) * 0.5, + ] + }; + let mut new_v = v0; + new_v.location.x = midpoint[0]; + new_v.location.y = midpoint[1]; + new_v.vertex_id = 0; + let effective_start = if v0.start_width > 1e-9 { + v0.start_width + } else { + self.constant_width + }; + let effective_end = if v0.end_width > 1e-9 { + v0.end_width + } else { + self.constant_width + }; + let middle_width = (effective_start + effective_end) * 0.5; + self.vertices[i0].end_width = middle_width; + new_v.start_width = middle_width; + if v0.bulge.abs() >= 1e-9 { + let half_bulge = (v0.bulge.atan() * 0.5).tan(); + self.vertices[i0].bulge = half_bulge; + new_v.bulge = half_bulge; + } let insert_at = (i0 + 1).min(self.vertices.len()); self.vertices.insert(insert_at, new_v); } diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index ca8c96f75..b11cde3ea 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -95,12 +95,13 @@ impl Grippable for Polyline { let n = self.vertices.len(); match action { A::AddVertex if grip_id < n => { - let i1 = (grip_id + 1) % n; - if i1 == 0 && grip_id + 1 == n { + if grip_id == n - 1 && !self.is_closed() { + self.vertices.push(self.vertices[grip_id].clone()); return; } - let v0 = &self.vertices[grip_id]; - let v1 = &self.vertices[i1]; + let i1 = (grip_id + 1) % n; + let v0 = self.vertices[grip_id].clone(); + let v1 = self.vertices[i1].clone(); let mx = (v0.location.x + v1.location.x) * 0.5; let my = (v0.location.y + v1.location.y) * 0.5; let mz = (v0.location.z + v1.location.z) * 0.5; @@ -383,14 +384,23 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { fn tapered_band_verts_2d( pl: &acadrust::entities::Polyline2D, ) -> Option> { - let d = pl.start_width.max(pl.end_width); + let default_start = pl.start_width; + let default_end = pl.end_width; let filtered = drawn_vertices2d(pl); let verts: &[acadrust::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices); let band: Vec<([f64; 2], f64, f64, f64)> = verts .iter() .map(|v| { - let sw = if v.start_width > 1e-9 { v.start_width } else { d }; - let ew = if v.end_width > 1e-9 { v.end_width } else { d }; + let sw = if v.start_width > 1e-9 { + v.start_width + } else { + default_start + }; + let ew = if v.end_width > 1e-9 { + v.end_width + } else { + default_end + }; ([v.location.x, v.location.y], v.bulge, sw, ew) }) .collect(); @@ -460,19 +470,59 @@ impl Grippable for Polyline2D { let elev = self.elevation; match action { A::AddVertex if grip_id < n => { - let i1 = (grip_id + 1) % n; - if i1 == 0 && grip_id + 1 == n { + if grip_id == n - 1 && !self.is_closed() { + self.vertices[grip_id].bulge = 0.0; + let mut new_v = self.vertices[grip_id].clone(); + new_v.bulge = 0.0; + new_v.id = 0; + self.vertices.push(new_v); return; } - let v0 = &self.vertices[grip_id]; - let v1 = &self.vertices[i1]; - let mx = (v0.location.x + v1.location.x) * 0.5; - let my = (v0.location.y + v1.location.y) * 0.5; + let i1 = (grip_id + 1) % n; + let v0 = self.vertices[grip_id].clone(); + let v1 = self.vertices[i1].clone(); + let midpoint = if v0.bulge.abs() >= 1e-9 { + crate::entities::common::BulgeArc::from_bulge( + [v0.location.x, v0.location.y], + [v1.location.x, v1.location.y], + v0.bulge, + ) + .map(|arc| arc.sample(0.5)) + .unwrap_or([ + (v0.location.x + v1.location.x) * 0.5, + (v0.location.y + v1.location.y) * 0.5, + ]) + } else { + [ + (v0.location.x + v1.location.x) * 0.5, + (v0.location.y + v1.location.y) * 0.5, + ] + }; let mut new_v = v0.clone(); - new_v.location.x = mx; - new_v.location.y = my; + new_v.location.x = midpoint[0]; + new_v.location.y = midpoint[1]; new_v.location.z = elev; + new_v.id = 0; + let effective_start = if v0.start_width > 1e-9 { + v0.start_width + } else { + self.start_width + }; + let effective_end = if v0.end_width > 1e-9 { + v0.end_width + } else { + self.end_width + }; + let middle_width = (effective_start + effective_end) * 0.5; + self.vertices[grip_id].end_width = middle_width; + new_v.start_width = middle_width; + if v0.bulge.abs() >= 1e-9 { + new_v.bulge = (v0.bulge.atan() * 0.5).tan(); + } let insert_at = (grip_id + 1).min(self.vertices.len()); + if v0.bulge.abs() >= 1e-9 { + self.vertices[grip_id].bulge = new_v.bulge; + } self.vertices.insert(insert_at, new_v); } A::RemoveVertex if grip_id < n && n > 2 => { @@ -743,10 +793,13 @@ impl Grippable for Polyline3D { let n = self.vertices.len(); match action { A::AddVertex if grip_id < n => { - let i1 = (grip_id + 1) % n; - if i1 == 0 && grip_id + 1 == n { + if grip_id == n - 1 && !self.is_closed() { + let mut new_v = self.vertices[grip_id].clone(); + new_v.handle = acadrust::Handle::NULL; + self.vertices.push(new_v); return; } + let i1 = (grip_id + 1) % n; let v0 = &self.vertices[grip_id]; let v1 = &self.vertices[i1]; let mx = (v0.position.x + v1.position.x) * 0.5; @@ -756,6 +809,7 @@ impl Grippable for Polyline3D { new_v.position.x = mx; new_v.position.y = my; new_v.position.z = mz; + new_v.handle = acadrust::Handle::NULL; let insert_at = (grip_id + 1).min(self.vertices.len()); self.vertices.insert(insert_at, new_v); } @@ -864,7 +918,8 @@ impl Transformable for Polyline3D { pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> ([f64; 2], Vec>) { // The stored widths are the band's FULL width and `polyline_segment_fill` // offsets ±hw about the centreline — halve them. See `lwpolyline::wide_fills`. - let hw_default = pl.start_width.max(pl.end_width) as f32 * 0.5; + let hw_default_start = pl.start_width as f32 * 0.5; + let hw_default_end = pl.end_width as f32 * 0.5; let filtered = drawn_vertices2d(pl); let verts: &[acadrust::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices); let n = verts.len(); @@ -880,12 +935,12 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> ([f64; 2], Vec< let hw0 = if v0.start_width > 1e-9 { v0.start_width as f32 * 0.5 } else { - hw_default + hw_default_start }; let hw1 = if v0.end_width > 1e-9 { v0.end_width as f32 * 0.5 } else { - hw_default + hw_default_end }; if hw0 < 1e-6 && hw1 < 1e-6 { continue; diff --git a/src/modules/draw/modify/reverse.rs b/src/modules/draw/modify/reverse.rs index e1cf908c8..e5d6e8c8e 100644 --- a/src/modules/draw/modify/reverse.rs +++ b/src/modules/draw/modify/reverse.rs @@ -5,13 +5,14 @@ // and swaps it in via `CmdResult::ReplaceEntity`. Supported curve types: // // * Line — swap start / end. -// * LwPolyline — reverse the vertex list. The bulge stored at a vertex +// * LwPolyline / Polyline2D — reverse the vertex list. The bulge stored at a +// vertex // describes the arc on the segment *starting* at that vertex, // so a plain `vertices.reverse()` would attach each bulge to // the wrong segment (and with the wrong sense). We therefore // recompute every bulge from the original segment bulges (see // `reverse_lwpolyline`). -// * Polyline3D — reverse the vertex list (no bulges to reconcile). +// * Polyline / Polyline3D — reverse the vertex list (no bulges to reconcile). // * Spline — reverse control points and fit points, then regenerate the // clamped knot vector (mirrors the REVERSE branch of // `splinedit::apply_spline_op`). @@ -60,6 +61,14 @@ impl ReverseCommand { Some(EntityType::Line(out)) } EntityType::LwPolyline(pl) => Some(EntityType::LwPolyline(reverse_lwpolyline(pl))), + EntityType::Polyline2D(pl) => { + Some(EntityType::Polyline2D(reverse_polyline2d(pl))) + } + EntityType::Polyline(pl) => { + let mut out = pl.clone(); + out.vertices.reverse(); + Some(EntityType::Polyline(out)) + } EntityType::Polyline3D(pl) => { let mut out = pl.clone(); out.vertices.reverse(); @@ -91,8 +100,8 @@ impl ReverseCommand { /// For a closed polyline the closing segment wraps, so the general modular form /// is used: new seg `i` (from new vertex `i`) corresponds to original segment /// starting at original vertex `(n-1-i-1).rem_euclid(n)`. The widths follow the -/// vertex they are attached to and are carried along with the reversed order; -/// they are not segment-relative, so they need no shift. +/// Widths are segment-relative. Reversing a segment swaps its start and end +/// widths in addition to moving them to the corresponding reversed segment. fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline { let mut out = pl.clone(); let n = pl.vertices.len(); @@ -100,13 +109,9 @@ fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline { return out; } - // Reverse vertex positions and per-vertex widths by cloning in reverse. - for i in 0..n { - let src = &pl.vertices[n - 1 - i]; - out.vertices[i].location = src.location; - out.vertices[i].start_width = src.start_width; - out.vertices[i].end_width = src.end_width; - } + // Reverse the complete vertex payload first. Live segment attributes are + // remapped below; the open terminal vertex keeps its unused payload. + out.vertices = pl.vertices.iter().rev().copied().collect(); // Recompute bulges from the original segment bulges. if pl.is_closed { @@ -114,13 +119,18 @@ fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline { for i in 0..n { let src_seg_start = (n - 1 - i + n - 1) % n; // = (2n - 2 - i) % n out.vertices[i].bulge = -pl.vertices[src_seg_start].bulge; + out.vertices[i].start_width = pl.vertices[src_seg_start].end_width; + out.vertices[i].end_width = pl.vertices[src_seg_start].start_width; } } else { // n - 1 segments. New segment i (for i in 0..n-1) maps to original // segment starting at vertex n-2-i. The trailing vertex's bulge is // unused for an open polyline; clear it for cleanliness. for i in 0..n - 1 { - out.vertices[i].bulge = -pl.vertices[n - 2 - i].bulge; + let src_seg_start = n - 2 - i; + out.vertices[i].bulge = -pl.vertices[src_seg_start].bulge; + out.vertices[i].start_width = pl.vertices[src_seg_start].end_width; + out.vertices[i].end_width = pl.vertices[src_seg_start].start_width; } out.vertices[n - 1].bulge = 0.0; } @@ -128,6 +138,43 @@ fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline { out } +/// Reverse a heavy 2D polyline with the same segment semantics as LWPolyline. +fn reverse_polyline2d(pl: &acadrust::entities::Polyline2D) -> acadrust::entities::Polyline2D { + let mut out = pl.clone(); + let n = pl.vertices.len(); + if n < 2 { + return out; + } + + out.vertices = pl.vertices.iter().rev().cloned().collect(); + out.start_width = pl.end_width; + out.end_width = pl.start_width; + for vertex in &mut out.vertices { + let tangent_flag = acadrust::entities::VertexFlags::CURVE_FIT_TANGENT.bits(); + if vertex.flags.bits() & tangent_flag != 0 { + vertex.curve_tangent = (vertex.curve_tangent + std::f64::consts::PI) + .rem_euclid(std::f64::consts::TAU); + } + } + if pl.is_closed() { + for i in 0..n { + let src_seg_start = (n - 1 - i + n - 1) % n; + out.vertices[i].bulge = -pl.vertices[src_seg_start].bulge; + out.vertices[i].start_width = pl.vertices[src_seg_start].end_width; + out.vertices[i].end_width = pl.vertices[src_seg_start].start_width; + } + } else { + for i in 0..n - 1 { + let src_seg_start = n - 2 - i; + out.vertices[i].bulge = -pl.vertices[src_seg_start].bulge; + out.vertices[i].start_width = pl.vertices[src_seg_start].end_width; + out.vertices[i].end_width = pl.vertices[src_seg_start].start_width; + } + out.vertices[n - 1].bulge = 0.0; + } + out +} + /// Reverse a Spline: flip control points and fit points, regenerate the /// clamped knot vector. Mirrors `splinedit::apply_spline_op`'s REVERSE branch. fn reverse_spline(sp: &Spline) -> Spline {