Merge PR #1215: add rectangle grip dimensions

This commit is contained in:
Hakan Seven committed 2026-09-12 14:53:37 +03:00
commit 7c5a388f19
12 files changed
+559 -64

No files matched your search

+140 -14
View File
@@ -209,6 +209,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
GripEditMode::Lengthen
| GripEditMode::Radius
| GripEditMode::ArcLength
| GripEditMode::RectangleWidth
| GripEditMode::RectangleHeight
)
}) {
self.command_line.input.push_str(&s);
@@ -262,6 +264,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
GripEditMode::Lengthen
| GripEditMode::Radius
| GripEditMode::ArcLength
| GripEditMode::RectangleWidth
| GripEditMode::RectangleHeight
)
}) {
self.command_line.input.pop();
@@ -316,6 +320,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
crate::scene::model::object::GripMenuAction::ArcLength => {
Some(GripEditMode::ArcLength)
}
crate::scene::model::object::GripMenuAction::RectangleWidth => {
Some(GripEditMode::RectangleWidth)
}
crate::scene::model::object::GripMenuAction::RectangleHeight => {
Some(GripEditMode::RectangleHeight)
}
_ => None,
};
expected_mode.is_some_and(|mode| {
@@ -368,6 +378,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
) | (
GripEditMode::ArcLength,
crate::scene::model::object::GripMenuAction::ArcLength,
) | (
GripEditMode::RectangleWidth,
crate::scene::model::object::GripMenuAction::RectangleWidth,
) | (
GripEditMode::RectangleHeight,
crate::scene::model::object::GripMenuAction::RectangleHeight,
)
)
&& grip.handle == pending.handle
@@ -407,13 +423,37 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
let i = self.active_tab;
if let Some(grip) = self.tabs[i].active_grip.clone() {
if grip.mode == GripEditMode::Stretch {
if matches!(grip.mode, GripEditMode::Stretch | GripEditMode::RectangleResize) {
let dyn_locked = self.tabs[i]
.dyn_fields
.iter()
.any(|field| field.buffer.is_some());
let target = if dyn_locked {
let target = if matches!(grip.mode, GripEditMode::RectangleResize) {
grip.rectangle_frame.map(|(opposite, width_axis, height_axis)| {
let cursor_delta = self.tabs[i].last_cursor_world - opposite;
let mut width = cursor_delta.dot(width_axis);
let mut height = cursor_delta.dot(height_axis);
for field in &self.tabs[i].dyn_fields {
let value = field
.buffer
.as_ref()
.and_then(|buffer| crate::app::expr_eval::eval_number(buffer));
if let Some(value) = value {
match field.role {
crate::command::DynRole::Width => {
width = value.abs().copysign(width);
}
crate::command::DynRole::Height => {
height = value.abs().copysign(height);
}
_ => {}
}
}
}
opposite + width_axis * width + height_axis * height
})
} else if dyn_locked {
// Distance only:
// keep the cursor's current direction.
//
@@ -490,10 +530,32 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
let delta = target - grip.last_world;
let actions: Vec<_> = grip
.targets
.iter()
.map(|target_grip| {
let actions: Vec<_> = if let Some((opposite, width_axis, height_axis)) = grip.rectangle_frame {
let opposite_id = (grip.grip_id + 2) % 4;
let mut edits = Vec::with_capacity(3);
for adjacent_id in [(opposite_id + 1) % 4, (opposite_id + 3) % 4] {
if let Some(original) = self.tabs[i]
.selected_grip_handles
.iter()
.zip(self.tabs[i].selected_grips.iter())
.find(|(owner, candidate)| **owner == grip.handle && candidate.id == adjacent_id)
.map(|(_, candidate)| candidate.world)
{
let d = original - opposite;
let axis = if d.dot(width_axis).abs() >= d.dot(height_axis).abs() {
width_axis
} else {
height_axis
};
edits.push((grip.handle, adjacent_id, GripApply::Absolute(
opposite + axis * (target - opposite).dot(axis),
)));
}
}
edits.push((grip.handle, grip.grip_id, GripApply::Absolute(target)));
edits
} else {
grip.targets.iter().map(|target_grip| {
let apply = if target_grip.is_translate {
GripApply::Translate(delta)
} else {
@@ -507,8 +569,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
target_grip.grip_id,
apply,
)
})
.collect();
}).collect()
};
for (handle, grip_id, apply) in actions {
self.tabs[i]
@@ -1324,6 +1386,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if matches!(
item.action,
GripMenuAction::Stretch
| GripMenuAction::RectangleResize
| GripMenuAction::MoveWithText
| GripMenuAction::MoveWithDimLine
| GripMenuAction::MoveWithLeader
@@ -1381,12 +1444,59 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|| (!is_dimension && g.is_midpoint),
)
};
self.tabs[i].active_grip = Some(GripEdit::single(
popup.handle,
grip_id,
is_translate,
g.world,
));
if matches!(item.action, GripMenuAction::RectangleResize) {
let rectangle = self.tabs[i]
.scene
.document
.get_entity(popup.handle)
.and_then(|entity| {
let AcadEntityType::LwPolyline(polyline) = entity else {
return None;
};
let (frame, plane) =
crate::entities::lwpolyline::rectangle_frame(polyline)?;
let opposite = polyline
.vertices
.get((popup.grip_id + 2) % 4)?
.location;
Some((
glam::DVec3::from_array(
plane.point_at([opposite.x, opposite.y]),
),
glam::DVec3::from_array(plane.vector_at(frame.width_axis))
.try_normalize()?,
glam::DVec3::from_array(plane.vector_at(frame.height_axis))
.try_normalize()?,
))
});
if let Some((opposite, width_axis, height_axis)) = rectangle {
if self.grip_originals.is_empty() {
self.grip_originals = self.tabs[i]
.scene
.document
.get_entity(popup.handle)
.cloned()
.map(|entity| vec![(popup.handle, entity)])
.unwrap_or_default();
}
self.tabs[i].active_grip = Some(GripEdit::rectangle_resize(
popup.handle,
popup.grip_id,
g.world,
opposite,
width_axis,
height_axis,
));
self.sync_dyn_fields();
}
} else {
self.tabs[i].active_grip = Some(GripEdit::single(
popup.handle,
grip_id,
is_translate,
g.world,
));
}
}
return Task::none();
}
@@ -1410,6 +1520,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
GripMenuAction::Lengthen
| GripMenuAction::Radius
| GripMenuAction::ArcLength
| GripMenuAction::RectangleWidth
| GripMenuAction::RectangleHeight
) {
if let Some((_, grip)) = self.tabs[i]
.selected_grip_handles
@@ -1439,6 +1551,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
popup.grip_id,
grip.world,
),
GripMenuAction::RectangleWidth => GripEdit::rectangle_width(
popup.handle,
popup.grip_id,
grip.world,
),
GripMenuAction::RectangleHeight => GripEdit::rectangle_height(
popup.handle,
popup.grip_id,
grip.world,
),
_ => GripEdit::lengthen(
popup.handle,
popup.grip_id,
@@ -1460,6 +1582,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.command_line.push_info(
crate::t!("Specify point or enter arc length:").as_ref(),
);
} else if matches!(item.action, GripMenuAction::RectangleWidth) {
self.command_line.push_info("Specify point or enter width:");
} else if matches!(item.action, GripMenuAction::RectangleHeight) {
self.command_line.push_info("Specify point or enter height:");
} else {
self.command_line.push_info(
crate::t!("Specify point or enter distance:").as_ref(),
+25 -6
View File
@@ -71,6 +71,10 @@ impl OpenCADStudio {
//
// Grip editing is not an `active_cmd`, so handle it before the normal
// command-only path below.
let rectangle_frame = self.tabs[i]
.active_grip
.as_ref()
.and_then(|grip| grip.rectangle_frame);
let grip_input = self.tabs[i]
.active_grip
.as_ref()
@@ -87,13 +91,26 @@ impl OpenCADStudio {
crate::scene::pick::grip::GripEditMode::ArcLength => {
(grip.origin_world, Some(crate::command::DynRole::Distance))
}
crate::scene::pick::grip::GripEditMode::RectangleWidth => {
(grip.origin_world, Some(crate::command::DynRole::Width))
}
crate::scene::pick::grip::GripEditMode::RectangleHeight => {
(grip.origin_world, Some(crate::command::DynRole::Height))
}
crate::scene::pick::grip::GripEditMode::RectangleResize => {
(grip.origin_world, None)
}
});
if let Some((origin, scalar_role)) = grip_input {
let wanted_roles: Vec<crate::command::DynRole> = scalar_role.map_or_else(
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|role| vec![role],
);
let wanted_roles: Vec<crate::command::DynRole> = if rectangle_frame.is_some() {
vec![crate::command::DynRole::Width, crate::command::DynRole::Height]
} else {
scalar_role.map_or_else(
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|role| vec![role],
)
};
let current: Vec<crate::command::DynRole> = self.tabs[i]
.dyn_fields
.iter()
@@ -109,12 +126,14 @@ impl OpenCADStudio {
self.tabs[i].dyn_active = 0;
}
self.tabs[i].dyn_guide = if scalar_role.is_some() {
self.tabs[i].dyn_guide = if rectangle_frame.is_some() {
crate::command::DynGuide::RectSides
} else if scalar_role.is_some() {
crate::command::DynGuide::Radius
} else {
crate::command::DynGuide::Polar
};
self.tabs[i].dyn_anchor = Some(origin);
self.tabs[i].dyn_anchor = rectangle_frame.map(|frame| frame.0).or(Some(origin));
self.tabs[i].dyn_ref = None;
return;
}
+2
View File
@@ -1591,6 +1591,8 @@ impl OpenCADStudio {
crate::scene::pick::grip::GripEditMode::Lengthen
| crate::scene::pick::grip::GripEditMode::Radius
| crate::scene::pick::grip::GripEditMode::ArcLength
| crate::scene::pick::grip::GripEditMode::RectangleWidth
| crate::scene::pick::grip::GripEditMode::RectangleHeight
)
})
&& !self.tabs[i].dyn_fields.is_empty()
+65 -1
View File
@@ -649,6 +649,7 @@ impl OpenCADStudio {
mode: GripEditMode::Stretch,
axis,
targets,
rectangle_frame: None,
}
}
@@ -1579,6 +1580,30 @@ impl OpenCADStudio {
));
let apply_started = Instant::now();
if let Some((opposite, width_axis, height_axis)) = grip.rectangle_frame {
let delta = snapped - opposite;
let mut width = delta.dot(width_axis);
let mut height = delta.dot(height_axis);
for field in &self.tabs[i].dyn_fields {
let Some(buffer) = field.buffer.as_ref() else {
continue;
};
let Some(value) = crate::app::expr_eval::eval_number(buffer) else {
continue;
};
match field.role {
crate::command::DynRole::Width => {
width = value.abs().copysign(width);
}
crate::command::DynRole::Height => {
height = value.abs().copysign(height);
}
_ => {}
}
}
snapped = opposite + width_axis * width + height_axis * height;
self.tabs[i].last_cursor_world = snapped;
}
let delta = snapped - grip.last_world;
let menu_action = match grip.mode {
GripEditMode::Lengthen => {
@@ -1590,9 +1615,47 @@ impl OpenCADStudio {
GripEditMode::ArcLength => {
Some(crate::scene::model::object::GripMenuAction::ArcLength)
}
GripEditMode::RectangleWidth => {
Some(crate::scene::model::object::GripMenuAction::RectangleWidth)
}
GripEditMode::RectangleHeight => {
Some(crate::scene::model::object::GripMenuAction::RectangleHeight)
}
GripEditMode::RectangleResize => None,
GripEditMode::Stretch => None,
};
let actions: Vec<_> = if menu_action.is_some() {
let actions: Vec<_> = if matches!(grip.mode, GripEditMode::RectangleResize) {
let Some((opposite, width_axis, height_axis)) = grip.rectangle_frame else {
return Task::none();
};
let opposite_id = (grip.grip_id + 2) % 4;
let adjacent_ids = [(opposite_id + 1) % 4, (opposite_id + 3) % 4];
let mut edits = Vec::with_capacity(3);
for adjacent_id in adjacent_ids {
let original = self.tabs[i]
.selected_grip_handles
.iter()
.zip(self.tabs[i].selected_grips.iter())
.find(|(owner, candidate)| {
**owner == grip.handle && candidate.id == adjacent_id
})
.map(|(_, candidate)| candidate.world);
if let Some(original) = original {
let original_delta = original - opposite;
let axis = if original_delta.dot(width_axis).abs()
>= original_delta.dot(height_axis).abs()
{
width_axis
} else {
height_axis
};
let point = opposite + axis * (snapped - opposite).dot(axis);
edits.push((grip.handle, adjacent_id, GripApply::Absolute(point)));
}
}
edits.push((grip.handle, grip.grip_id, GripApply::Absolute(snapped)));
edits
} else if menu_action.is_some() {
Vec::new()
} else {
grip.targets
@@ -3268,6 +3331,7 @@ impl OpenCADStudio {
if matches!(
grip.mode,
GripEditMode::Lengthen | GripEditMode::Radius | GripEditMode::ArcLength
| GripEditMode::RectangleWidth | GripEditMode::RectangleHeight
) {
self.grip_pending = None;
self.command_line.input.clear();