forked from 1b-agent/opencadstudio-upstream
Add rectangle width and height grip editing
This commit is contained in:
1 parent
798379fbe0
commit
f30b490831
9 files changed
+260
-5
No files matched your search
@@ -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
|
||||
@@ -1410,6 +1426,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 +1457,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 +1488,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(),
|
||||
|
||||
@@ -87,6 +87,12 @@ 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))
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((origin, scalar_role)) = grip_input {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1590,6 +1590,12 @@ 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::Stretch => None,
|
||||
};
|
||||
let actions: Vec<_> = if menu_action.is_some() {
|
||||
@@ -3268,6 +3274,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();
|
||||
|
||||
@@ -984,6 +984,12 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
||||
crate::scene::model::object::GripMenuAction::ArcLength
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::RectangleWidth => {
|
||||
crate::scene::model::object::GripMenuAction::RectangleWidth
|
||||
}
|
||||
crate::scene::pick::grip::GripEditMode::RectangleHeight => {
|
||||
crate::scene::model::object::GripMenuAction::RectangleHeight
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let original = self
|
||||
@@ -1772,6 +1778,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
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
|
||||
)
|
||||
});
|
||||
let dyn_capturing =
|
||||
|
||||
+154
-3
@@ -888,6 +888,13 @@ pub(crate) fn is_rectangle(pline: &LwPolyline) -> bool {
|
||||
if pline.common.extended_data.get_record("OCS_RECTANGLE").is_some() {
|
||||
return true;
|
||||
}
|
||||
is_resizable_rectangle(pline)
|
||||
}
|
||||
|
||||
/// Geometry check used for constrained rectangle grips. Unlike the semantic
|
||||
/// marker, this must stop matching as soon as an unrestricted vertex edit has
|
||||
/// distorted the four-corner rectangle.
|
||||
fn is_resizable_rectangle(pline: &LwPolyline) -> bool {
|
||||
if !pline.is_closed
|
||||
|| pline.vertices.len() != 4
|
||||
|| pline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1.0e-9)
|
||||
@@ -1242,7 +1249,19 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
||||
action: GripMenuAction::ConvertToArc,
|
||||
}
|
||||
};
|
||||
vec![
|
||||
let mut items = Vec::new();
|
||||
if is_resizable_rectangle(self) && seg < 4 {
|
||||
// Moving an edge changes the dimension perpendicular to it.
|
||||
items.push(GripMenuItem {
|
||||
label: if seg % 2 == 0 { "Height" } else { "Width" },
|
||||
action: if seg % 2 == 0 {
|
||||
GripMenuAction::RectangleHeight
|
||||
} else {
|
||||
GripMenuAction::RectangleWidth
|
||||
},
|
||||
});
|
||||
}
|
||||
items.extend([
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action: GripMenuAction::Stretch,
|
||||
@@ -1252,7 +1271,94 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
||||
action: GripMenuAction::AddVertex,
|
||||
},
|
||||
convert,
|
||||
]
|
||||
]);
|
||||
items
|
||||
}
|
||||
|
||||
fn grip_menu_value_prompt(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
) -> Option<&'static str> {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
let n = self.vertices.len();
|
||||
(is_resizable_rectangle(self)
|
||||
&& n == 4
|
||||
&& (n..n + 4).contains(&grip_id)
|
||||
&& matches!(action, A::RectangleWidth | A::RectangleHeight))
|
||||
.then_some(match action {
|
||||
A::RectangleWidth => "New width",
|
||||
_ => "New height",
|
||||
})
|
||||
}
|
||||
|
||||
fn grip_menu_point_value(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if !is_resizable_rectangle(self)
|
||||
|| !matches!(action, A::RectangleWidth | A::RectangleHeight)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let seg = grip_id.checked_sub(4)?;
|
||||
if seg >= 4 {
|
||||
return None;
|
||||
}
|
||||
let opposite_mid = {
|
||||
let a = self.vertices[(seg + 2) % 4].location;
|
||||
let b = self.vertices[(seg + 3) % 4].location;
|
||||
Vec2::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5)
|
||||
};
|
||||
let edge = {
|
||||
let a = self.vertices[seg].location;
|
||||
let b = self.vertices[(seg + 1) % 4].location;
|
||||
Vec2::new(b.x - a.x, b.y - a.y)
|
||||
};
|
||||
let edge_length = edge.length();
|
||||
if edge_length <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let normal = Vec2::new(-edge.y / edge_length, edge.x / edge_length);
|
||||
let value = (Vec2::new(point.x, point.y) - opposite_mid).dot(normal).abs();
|
||||
(value > 1.0e-9).then_some(value)
|
||||
}
|
||||
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
value: f64,
|
||||
) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if !is_resizable_rectangle(self) || value <= 1.0e-9
|
||||
|| !matches!(action, A::RectangleWidth | A::RectangleHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(seg) = grip_id.checked_sub(4) else {
|
||||
return;
|
||||
};
|
||||
if seg >= 4 {
|
||||
return;
|
||||
}
|
||||
let i0 = seg;
|
||||
let i1 = (seg + 1) % 4;
|
||||
let o0 = (seg + 3) % 4;
|
||||
let o1 = (seg + 2) % 4;
|
||||
let selected_mid = (self.vertices[i0].location + self.vertices[i1].location) * 0.5;
|
||||
let opposite_mid = (self.vertices[o0].location + self.vertices[o1].location) * 0.5;
|
||||
let delta = selected_mid - opposite_mid;
|
||||
let distance = (delta.x * delta.x + delta.y * delta.y).sqrt();
|
||||
if distance <= 1.0e-9 {
|
||||
return;
|
||||
}
|
||||
let direction = acadrust::types::Vector2::new(delta.x / distance, delta.y / distance);
|
||||
self.vertices[i0].location = self.vertices[o0].location + direction * value;
|
||||
self.vertices[i1].location = self.vertices[o1].location + direction * value;
|
||||
}
|
||||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
@@ -1440,7 +1546,8 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entities::traits::PropertyEditable;
|
||||
use crate::entities::traits::{Grippable, PropertyEditable};
|
||||
use crate::scene::model::object::GripMenuAction;
|
||||
use acadrust::entities::{LwPolyline, LwVertex};
|
||||
use acadrust::Vector2;
|
||||
|
||||
@@ -1453,6 +1560,50 @@ mod tests {
|
||||
pl
|
||||
}
|
||||
|
||||
fn make_test_rectangle() -> LwPolyline {
|
||||
let mut pl = LwPolyline::default();
|
||||
pl.is_closed = true;
|
||||
pl.vertices = [(0.0, 0.0), (10.0, 0.0), (10.0, 5.0), (0.0, 5.0)]
|
||||
.into_iter()
|
||||
.map(|(x, y)| LwVertex::new(Vector2::new(x, y)))
|
||||
.collect();
|
||||
pl
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rectangle_midpoint_offers_dimension_before_stretch() {
|
||||
let pl = make_test_rectangle();
|
||||
let bottom = pl.grip_menu(4);
|
||||
assert_eq!(bottom[0].action, GripMenuAction::RectangleHeight);
|
||||
assert_eq!(bottom[1].action, GripMenuAction::Stretch);
|
||||
|
||||
let right = pl.grip_menu(5);
|
||||
assert_eq!(right[0].action, GripMenuAction::RectangleWidth);
|
||||
assert_eq!(right[1].action, GripMenuAction::Stretch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rectangle_dimension_edit_keeps_opposite_edge_fixed() {
|
||||
let mut pl = make_test_rectangle();
|
||||
pl.apply_grip_menu_value(6, GripMenuAction::RectangleHeight, 8.0);
|
||||
assert_eq!(pl.vertices[0].location, Vector2::new(0.0, 0.0));
|
||||
assert_eq!(pl.vertices[1].location, Vector2::new(10.0, 0.0));
|
||||
assert_eq!(pl.vertices[2].location, Vector2::new(10.0, 8.0));
|
||||
assert_eq!(pl.vertices[3].location, Vector2::new(0.0, 8.0));
|
||||
assert!(is_resizable_rectangle(&pl));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rectangle_point_preview_reports_full_dimension() {
|
||||
let pl = make_test_rectangle();
|
||||
let value = pl.grip_menu_point_value(
|
||||
5,
|
||||
GripMenuAction::RectangleWidth,
|
||||
glam::DVec3::new(14.0, 2.5, 0.0),
|
||||
);
|
||||
assert_eq!(value, Some(14.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lwpolyline_uniform_width_shows_global_width() {
|
||||
let pl = make_test_lwpolyline(3, 5.0);
|
||||
|
||||
@@ -86,6 +86,20 @@ fn make_pline(points: &[DVec3], plane: WorkingPlane) -> EntityType {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Plain four-corner rectangles still use the lightweight polyline entity,
|
||||
/// but carry the same semantic hint as rectangles made by the full RECT
|
||||
/// command. Keep this separate from `make_pline`, which is also used by
|
||||
/// polygon commands.
|
||||
fn make_basic_rect(points: &[DVec3; 4], plane: WorkingPlane) -> EntityType {
|
||||
let mut entity = make_pline(points, plane);
|
||||
if let EntityType::LwPolyline(polyline) = &mut entity {
|
||||
let mut marker = acadrust::xdata::ExtendedDataRecord::new("OCS_RECTANGLE");
|
||||
marker.add_value(acadrust::xdata::XDataValue::Integer16(1));
|
||||
polyline.common.extended_data.add_record(marker);
|
||||
}
|
||||
entity
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RectStyle {
|
||||
chamfer_first: f64,
|
||||
@@ -894,7 +908,7 @@ impl CadCommand for RectRotCommand {
|
||||
let c = b + perp * h;
|
||||
let d = a + perp * h;
|
||||
let corners = [a, b, c, d].map(|point| self.plane.to_world(point));
|
||||
CmdResult::CommitAndExit(make_pline(&corners, self.plane))
|
||||
CmdResult::CommitAndExit(make_basic_rect(&corners, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -998,7 +1012,7 @@ impl CadCommand for RectCenCommand {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let q = ucs_box_around_center(c, pt, self.plane);
|
||||
CmdResult::CommitAndExit(make_pline(&q, self.plane))
|
||||
CmdResult::CommitAndExit(make_basic_rect(&q, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1478,6 +1492,25 @@ fn edge_poly_params(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_rectangle_constructor_adds_semantic_marker() {
|
||||
let plane = WorkingPlane::default();
|
||||
let corners = [
|
||||
DVec3::new(0.0, 0.0, 0.0),
|
||||
DVec3::new(10.0, 0.0, 0.0),
|
||||
DVec3::new(10.0, 5.0, 0.0),
|
||||
DVec3::new(0.0, 5.0, 0.0),
|
||||
];
|
||||
let EntityType::LwPolyline(polyline) = make_basic_rect(&corners, plane) else {
|
||||
panic!("rectangle constructor must create an LwPolyline");
|
||||
};
|
||||
assert!(polyline
|
||||
.common
|
||||
.extended_data
|
||||
.get_record("OCS_RECTANGLE")
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygon_variants_share_last_valid_side_count() {
|
||||
let mut inscribed = PolyCommand::new();
|
||||
|
||||
@@ -175,6 +175,8 @@ pub enum GripMenuAction {
|
||||
Lengthen,
|
||||
Radius,
|
||||
ArcLength,
|
||||
RectangleWidth,
|
||||
RectangleHeight,
|
||||
AddVertex,
|
||||
RemoveVertex,
|
||||
ConvertToArc,
|
||||
|
||||
@@ -56,6 +56,8 @@ pub enum GripEditMode {
|
||||
Lengthen,
|
||||
Radius,
|
||||
ArcLength,
|
||||
RectangleWidth,
|
||||
RectangleHeight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -106,6 +108,18 @@ impl GripEdit {
|
||||
edit.mode = GripEditMode::ArcLength;
|
||||
edit
|
||||
}
|
||||
|
||||
pub fn rectangle_width(handle: Handle, grip_id: usize, world: DVec3) -> Self {
|
||||
let mut edit = Self::single(handle, grip_id, false, world);
|
||||
edit.mode = GripEditMode::RectangleWidth;
|
||||
edit
|
||||
}
|
||||
|
||||
pub fn rectangle_height(handle: Handle, grip_id: usize, world: DVec3) -> Self {
|
||||
let mut edit = Self::single(handle, grip_id, false, world);
|
||||
edit.mode = GripEditMode::RectangleHeight;
|
||||
edit
|
||||
}
|
||||
}
|
||||
|
||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user