feat(select): Ctrl+click picks one segment of a polyline

Ctrl+click on a polyline selects the segment under the cursor (again to
drop it); several segments can be picked. Only the picked segments are
highlighted, and MOVE, ROTATE, SCALE and MIRROR then change just their
vertices, so the neighbouring segments stretch to follow. Any other
selected objects are transformed whole as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 22:14:56 +03:00
1 parent 11f68a1185
commit fcf1cedf52
6 files changed
+220 -1

No files matched your search

+3
View File
@@ -24,6 +24,9 @@ impl OpenCADStudio {
// component so each member keeps its own dimensions. Other
// transforms retain their selected-object behavior.
let pending = self.begin_undo(i, label, handles.len(), true);
let handles = self.tabs[i]
.scene
.transform_picked_segments(&handles, &transform);
self.tabs[i].scene.transform_entities(&handles, &transform);
self.apply_inferred_constraints(i, &handles);
self.tabs[i].dirty = true;
+33
View File
@@ -4014,6 +4014,39 @@ impl OpenCADStudio {
}
}
// Ctrl+click on a polyline picks the segment under the cursor; the
// edit commands then move just that part.
if self.ctrl_down && self.tabs[i].active_cmd.is_none() {
let (view_rot, eye) = {
let cam = self.tabs[i].scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
};
let wires = self.tabs[i].scene.hit_test_wires();
let hit = scene::pick::hit_test::click_hit(
p,
&*wires,
view_rot,
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
&self.tabs[i].scene.draw_depth_map(),
)
.and_then(Scene::handle_from_wire_name)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
drop(wires);
let point = self.tabs[i].last_cursor_world;
if hit.is_some_and(|handle| self.tabs[i].scene.toggle_sub_segment(handle, point)) {
self.tabs[i]
.scene
.selection
.borrow_mut()
.clear_left_selection_gesture();
self.refresh_properties();
return Task::none();
}
}
// Ctrl+click on a composite solid picks the solid of it under the
// cursor — the one the Properties palette and grips then edit.
if self.ctrl_down
+5
View File
@@ -9,6 +9,7 @@ pub mod annotative;
pub mod assoc_array;
pub mod compare;
pub mod section_cull;
pub mod sub_select;
pub mod block_service;
pub mod cache;
pub mod convert;
@@ -2124,6 +2125,9 @@ pub struct Scene {
lighting_cache: RefCell<HashMap<(Handle, u64), Vec<SceneLight>>>,
/// Currently selected entity handles.
pub selected: HashSet<Handle>,
/// Polylines only part of which is selected (Ctrl+click): the picked
/// segment indices.
pub sub_segments: HashMap<Handle, std::collections::BTreeSet<usize>>,
selected_order: Vec<Handle>,
/// The parametric-constraint glyph currently selected in the viewport.
/// Entity selection changes clear it so the two selections stay exclusive.
@@ -2764,6 +2768,7 @@ impl Scene {
object_data_cache: crate::entities::object_data::ObjectDataCache::default(),
lighting_cache: RefCell::new(HashMap::default()),
selected: HashSet::default(),
sub_segments: HashMap::default(),
selected_order: Vec::new(),
selected_constraint: None,
object_isolation: ObjectIsolationState::default(),
+1
View File
@@ -165,6 +165,7 @@ impl Scene {
pub fn deselect_all(&mut self) {
self.selected_constraint = None;
self.sub_segments.clear();
if self.selected.is_empty() {
return;
}
+159
View File
@@ -0,0 +1,159 @@
//! Picking part of an object: Ctrl+click on a polyline selects the segment
//! under the cursor. The polyline joins the selection, only its picked
//! segments are highlighted, and MOVE / ROTATE / SCALE / MIRROR then change
//! just the vertices of those segments, stretching the neighbours.
use std::collections::BTreeSet;
use codec::entities::LwPolyline;
use codec::{EntityType, Handle};
use glam::DVec3;
use super::Scene;
use crate::command::EntityTransform;
/// Segment `index` of `polyline` on its own: its two vertices, with the
/// bulge of the first.
fn segment(polyline: &LwPolyline, index: usize) -> Option<LwPolyline> {
let count = polyline.vertices.len();
let next = if index + 1 < count {
index + 1
} else if polyline.is_closed && count > 2 {
0
} else {
return None;
};
let mut part = polyline.clone();
part.vertices = vec![
polyline.vertices[index].clone(),
polyline.vertices[next].clone(),
];
part.vertices[1].bulge = 0.0;
part.is_closed = false;
Some(part)
}
fn segment_points(polyline: &LwPolyline, index: usize) -> Vec<[f64; 3]> {
segment(polyline, index)
.and_then(|part| crate::entities::curve::lwpolyline_curve(&part))
.map(|curve| crate::entities::curve::curve_points(&curve))
.unwrap_or_default()
}
impl Scene {
/// Ctrl+click: toggle the segment of polyline `handle` nearest `point`
/// and keep the polyline selected while any of its segments is. `false`
/// when `handle` is not a polyline.
pub fn toggle_sub_segment(&mut self, handle: Handle, point: DVec3) -> bool {
let Some(EntityType::LwPolyline(polyline)) = self.document.get_entity(handle) else {
return false;
};
let nearest = (0..polyline.vertices.len())
.filter_map(|index| {
let distance = segment_points(polyline, index)
.windows(2)
.map(|pair| {
let a = DVec3::from_array(pair[0]).truncate();
let b = DVec3::from_array(pair[1]).truncate();
let p = point.truncate();
let t = ((p - a).dot(b - a) / (b - a).length_squared().max(1e-300))
.clamp(0.0, 1.0);
p.distance(a + (b - a) * t)
})
.fold(f64::INFINITY, f64::min);
distance.is_finite().then_some((index, distance))
})
.min_by(|a, b| a.1.total_cmp(&b.1));
let Some((index, _)) = nearest else {
return false;
};
if !self.selected.contains(&handle) {
self.sub_segments.remove(&handle);
}
let segments = self.sub_segments.entry(handle).or_default();
if !segments.remove(&index) {
segments.insert(index);
}
if segments.is_empty() {
self.sub_segments.remove(&handle);
self.deselect_entity(handle);
} else {
self.select_entity(handle, false);
}
self.bump_selection_set();
true
}
/// The picked segments of a selected polyline, if only part of it is.
pub(crate) fn picked_segments(&self, handle: Handle) -> Option<&BTreeSet<usize>> {
self.selected
.contains(&handle)
.then(|| self.sub_segments.get(&handle))
.flatten()
}
/// World points of the picked segments of `handle`, NaN-separated, for
/// the selection highlight.
pub(crate) fn picked_segment_points(&self, handle: Handle) -> Option<Vec<[f64; 3]>> {
let segments = self.picked_segments(handle)?;
let Some(EntityType::LwPolyline(polyline)) = self.document.get_entity(handle) else {
return None;
};
let mut points = Vec::new();
for &index in segments {
points.extend(segment_points(polyline, index));
points.push([f64::NAN; 3]);
}
Some(points)
}
/// Apply `transform` to just the vertices of the picked segments of each
/// partly selected polyline in `handles`. Returns the handles left to
/// transform whole.
pub fn transform_picked_segments(
&mut self,
handles: &[Handle],
transform: &EntityTransform,
) -> Vec<Handle> {
let mut whole = Vec::with_capacity(handles.len());
for &handle in handles {
let (Some(segments), Some(EntityType::LwPolyline(polyline))) = (
self.picked_segments(handle),
self.document.get_entity(handle),
) else {
whole.push(handle);
continue;
};
let count = polyline.vertices.len();
let vertices: BTreeSet<usize> = segments
.iter()
.flat_map(|&index| [index, (index + 1) % count])
.collect();
let Some(plane) = crate::entities::curve::lwpolyline_curve(polyline)
.map(|curve| curve.plane)
else {
whole.push(handle);
continue;
};
let mut moved = polyline.clone();
for index in vertices {
let vertex = &mut moved.vertices[index];
let world = plane.point_at([vertex.location.x, vertex.location.y]);
let mut marker = EntityType::Point(codec::entities::Point::at(
codec::types::Vector3::new(world[0], world[1], world[2]),
));
super::view::dispatch::apply_transform(&mut marker, transform);
let EntityType::Point(marker) = marker else {
continue;
};
let location = marker.location;
if let Some([x, y]) = plane.project([location.x, location.y, location.z]) {
vertex.location.x = x;
vertex.location.y = y;
}
}
self.update_entity(EntityType::LwPolyline(moved));
}
whole
}
}
+19 -1
View File
@@ -4056,6 +4056,18 @@ impl Scene {
} else {
WireModel::HOVER
};
if let Some(points) = selected
.then(|| self.picked_segment_points(handle))
.flatten()
{
wires.push(WireModel::solid_f64(
handle.value().to_string(),
points,
tint,
true,
));
continue;
}
for set in interaction_meshes
.iter()
.filter(|set| set.entity_handle() == Some(handle))
@@ -4918,7 +4930,13 @@ impl Scene {
wire_content_id,
wire_patch,
selected_handles: Arc::new(if show_interaction {
self.selected.iter().copied().collect()
// A partly picked polyline lights only its picked segments,
// drawn with the highlight wires.
self.selected
.iter()
.copied()
.filter(|handle| self.picked_segments(*handle).is_none())
.collect()
} else {
rustc_hash::FxHashSet::default()
}),