forked from 1b-agent/opencadstudio-upstream
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
This commit is contained in:
1 parent
f0c1be0db1
commit
1cf2b700ce
7 files changed
+547
-56
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<usize> {
|
||||
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<u64>) -> Task<Message> {
|
||||
@@ -966,6 +1009,93 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
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
|
||||
|
||||
+105
-11
@@ -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<f64> {
|
||||
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();
|
||||
}
|
||||
|
||||
+160
-12
@@ -62,6 +62,113 @@ fn bulge_from_midpoint(p0: [f64; 2], p1: [f64; 2], mid: [f64; 2]) -> Option<f64>
|
||||
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);
|
||||
}
|
||||
|
||||
+75
-20
@@ -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<Vec<([f64; 2], f64, f64, f64)>> {
|
||||
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<Vec<[f32; 2]>>) {
|
||||
// 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user