forked from 1b-agent/opencadstudio-upstream
Merge PR #1215: add rectangle grip dimensions
This commit is contained in:
commit
7c5a388f19
12 files changed
+559
-64
No files matched your search
Generated
+2
-2
@@ -895,7 +895,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6f3842eca010637ec7eb16688b6bd5ef3628ad83#6f3842eca010637ec7eb16688b6bd5ef3628ad83"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a3185be8189d78d6e93b4ea5b9238b184da4f3cf#a3185be8189d78d6e93b4ea5b9238b184da4f3cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
@@ -906,7 +906,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel-constraints"
|
name = "cadkernel-constraints"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6f3842eca010637ec7eb16688b6bd5ef3628ad83#6f3842eca010637ec7eb16688b6bd5ef3628ad83"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a3185be8189d78d6e93b4ea5b9238b184da4f3cf#a3185be8189d78d6e93b4ea5b9238b184da4f3cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -31,8 +31,8 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6f3842eca010637ec7eb16688b6bd5ef3628ad83", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a3185be8189d78d6e93b4ea5b9238b184da4f3cf", features = ["acis", "offset"] }
|
||||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6f3842eca010637ec7eb16688b6bd5ef3628ad83" }
|
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a3185be8189d78d6e93b4ea5b9238b184da4f3cf" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
+140
-14
@@ -209,6 +209,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
GripEditMode::Lengthen
|
GripEditMode::Lengthen
|
||||||
| GripEditMode::Radius
|
| GripEditMode::Radius
|
||||||
| GripEditMode::ArcLength
|
| GripEditMode::ArcLength
|
||||||
|
| GripEditMode::RectangleWidth
|
||||||
|
| GripEditMode::RectangleHeight
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
self.command_line.input.push_str(&s);
|
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::Lengthen
|
||||||
| GripEditMode::Radius
|
| GripEditMode::Radius
|
||||||
| GripEditMode::ArcLength
|
| GripEditMode::ArcLength
|
||||||
|
| GripEditMode::RectangleWidth
|
||||||
|
| GripEditMode::RectangleHeight
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
self.command_line.input.pop();
|
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 => {
|
crate::scene::model::object::GripMenuAction::ArcLength => {
|
||||||
Some(GripEditMode::ArcLength)
|
Some(GripEditMode::ArcLength)
|
||||||
}
|
}
|
||||||
|
crate::scene::model::object::GripMenuAction::RectangleWidth => {
|
||||||
|
Some(GripEditMode::RectangleWidth)
|
||||||
|
}
|
||||||
|
crate::scene::model::object::GripMenuAction::RectangleHeight => {
|
||||||
|
Some(GripEditMode::RectangleHeight)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
expected_mode.is_some_and(|mode| {
|
expected_mode.is_some_and(|mode| {
|
||||||
@@ -368,6 +378,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
) | (
|
) | (
|
||||||
GripEditMode::ArcLength,
|
GripEditMode::ArcLength,
|
||||||
crate::scene::model::object::GripMenuAction::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
|
&& 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;
|
let i = self.active_tab;
|
||||||
|
|
||||||
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
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]
|
let dyn_locked = self.tabs[i]
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
.any(|field| field.buffer.is_some());
|
.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:
|
// Distance only:
|
||||||
// keep the cursor's current direction.
|
// 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 delta = target - grip.last_world;
|
||||||
|
|
||||||
let actions: Vec<_> = grip
|
let actions: Vec<_> = if let Some((opposite, width_axis, height_axis)) = grip.rectangle_frame {
|
||||||
.targets
|
let opposite_id = (grip.grip_id + 2) % 4;
|
||||||
.iter()
|
let mut edits = Vec::with_capacity(3);
|
||||||
.map(|target_grip| {
|
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 {
|
let apply = if target_grip.is_translate {
|
||||||
GripApply::Translate(delta)
|
GripApply::Translate(delta)
|
||||||
} else {
|
} else {
|
||||||
@@ -507,8 +569,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
target_grip.grip_id,
|
target_grip.grip_id,
|
||||||
apply,
|
apply,
|
||||||
)
|
)
|
||||||
})
|
}).collect()
|
||||||
.collect();
|
};
|
||||||
|
|
||||||
for (handle, grip_id, apply) in actions {
|
for (handle, grip_id, apply) in actions {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
@@ -1324,6 +1386,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
if matches!(
|
if matches!(
|
||||||
item.action,
|
item.action,
|
||||||
GripMenuAction::Stretch
|
GripMenuAction::Stretch
|
||||||
|
| GripMenuAction::RectangleResize
|
||||||
| GripMenuAction::MoveWithText
|
| GripMenuAction::MoveWithText
|
||||||
| GripMenuAction::MoveWithDimLine
|
| GripMenuAction::MoveWithDimLine
|
||||||
| GripMenuAction::MoveWithLeader
|
| GripMenuAction::MoveWithLeader
|
||||||
@@ -1381,12 +1444,59 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
|| (!is_dimension && g.is_midpoint),
|
|| (!is_dimension && g.is_midpoint),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
if matches!(item.action, GripMenuAction::RectangleResize) {
|
||||||
popup.handle,
|
let rectangle = self.tabs[i]
|
||||||
grip_id,
|
.scene
|
||||||
is_translate,
|
.document
|
||||||
g.world,
|
.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();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -1410,6 +1520,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
GripMenuAction::Lengthen
|
GripMenuAction::Lengthen
|
||||||
| GripMenuAction::Radius
|
| GripMenuAction::Radius
|
||||||
| GripMenuAction::ArcLength
|
| GripMenuAction::ArcLength
|
||||||
|
| GripMenuAction::RectangleWidth
|
||||||
|
| GripMenuAction::RectangleHeight
|
||||||
) {
|
) {
|
||||||
if let Some((_, grip)) = self.tabs[i]
|
if let Some((_, grip)) = self.tabs[i]
|
||||||
.selected_grip_handles
|
.selected_grip_handles
|
||||||
@@ -1439,6 +1551,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
popup.grip_id,
|
popup.grip_id,
|
||||||
grip.world,
|
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(
|
_ => GripEdit::lengthen(
|
||||||
popup.handle,
|
popup.handle,
|
||||||
popup.grip_id,
|
popup.grip_id,
|
||||||
@@ -1460,6 +1582,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
crate::t!("Specify point or enter arc length:").as_ref(),
|
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 {
|
} else {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
crate::t!("Specify point or enter distance:").as_ref(),
|
crate::t!("Specify point or enter distance:").as_ref(),
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ impl OpenCADStudio {
|
|||||||
//
|
//
|
||||||
// Grip editing is not an `active_cmd`, so handle it before the normal
|
// Grip editing is not an `active_cmd`, so handle it before the normal
|
||||||
// command-only path below.
|
// 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]
|
let grip_input = self.tabs[i]
|
||||||
.active_grip
|
.active_grip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -87,13 +91,26 @@ impl OpenCADStudio {
|
|||||||
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
||||||
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
(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 {
|
if let Some((origin, scalar_role)) = grip_input {
|
||||||
let wanted_roles: Vec<crate::command::DynRole> = scalar_role.map_or_else(
|
let wanted_roles: Vec<crate::command::DynRole> = if rectangle_frame.is_some() {
|
||||||
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|
vec![crate::command::DynRole::Width, crate::command::DynRole::Height]
|
||||||
|role| vec![role],
|
} 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]
|
let current: Vec<crate::command::DynRole> = self.tabs[i]
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -109,12 +126,14 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].dyn_active = 0;
|
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
|
crate::command::DynGuide::Radius
|
||||||
} else {
|
} else {
|
||||||
crate::command::DynGuide::Polar
|
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;
|
self.tabs[i].dyn_ref = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1591,6 +1591,8 @@ impl OpenCADStudio {
|
|||||||
crate::scene::pick::grip::GripEditMode::Lengthen
|
crate::scene::pick::grip::GripEditMode::Lengthen
|
||||||
| crate::scene::pick::grip::GripEditMode::Radius
|
| crate::scene::pick::grip::GripEditMode::Radius
|
||||||
| crate::scene::pick::grip::GripEditMode::ArcLength
|
| 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()
|
&& !self.tabs[i].dyn_fields.is_empty()
|
||||||
|
|||||||
@@ -649,6 +649,7 @@ impl OpenCADStudio {
|
|||||||
mode: GripEditMode::Stretch,
|
mode: GripEditMode::Stretch,
|
||||||
axis,
|
axis,
|
||||||
targets,
|
targets,
|
||||||
|
rectangle_frame: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1579,6 +1580,30 @@ impl OpenCADStudio {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let apply_started = Instant::now();
|
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 delta = snapped - grip.last_world;
|
||||||
let menu_action = match grip.mode {
|
let menu_action = match grip.mode {
|
||||||
GripEditMode::Lengthen => {
|
GripEditMode::Lengthen => {
|
||||||
@@ -1590,9 +1615,47 @@ impl OpenCADStudio {
|
|||||||
GripEditMode::ArcLength => {
|
GripEditMode::ArcLength => {
|
||||||
Some(crate::scene::model::object::GripMenuAction::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,
|
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()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
grip.targets
|
grip.targets
|
||||||
@@ -3268,6 +3331,7 @@ impl OpenCADStudio {
|
|||||||
if matches!(
|
if matches!(
|
||||||
grip.mode,
|
grip.mode,
|
||||||
GripEditMode::Lengthen | GripEditMode::Radius | GripEditMode::ArcLength
|
GripEditMode::Lengthen | GripEditMode::Radius | GripEditMode::ArcLength
|
||||||
|
| GripEditMode::RectangleWidth | GripEditMode::RectangleHeight
|
||||||
) {
|
) {
|
||||||
self.grip_pending = None;
|
self.grip_pending = None;
|
||||||
self.command_line.input.clear();
|
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::pick::grip::GripEditMode::ArcLength => {
|
||||||
crate::scene::model::object::GripMenuAction::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,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let original = self
|
let original = self
|
||||||
@@ -998,6 +1004,64 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
w,
|
w,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
let rectangle_values = tab
|
||||||
|
.active_grip
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|grip| grip.rectangle_frame)
|
||||||
|
.map(|(opposite, width_axis, height_axis)| {
|
||||||
|
let delta = w - opposite;
|
||||||
|
(delta.dot(width_axis).abs(), delta.dot(height_axis).abs())
|
||||||
|
});
|
||||||
|
let rectangle_label_screens = tab
|
||||||
|
.active_grip
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|grip| grip.rectangle_frame)
|
||||||
|
.and_then(|(opposite, width_axis, height_axis)| {
|
||||||
|
let delta = w - opposite;
|
||||||
|
let width = delta.dot(width_axis);
|
||||||
|
let height = delta.dot(height_axis);
|
||||||
|
let width_center = opposite + width_axis * (width * 0.5);
|
||||||
|
let height_center = opposite
|
||||||
|
+ width_axis * width
|
||||||
|
+ height_axis * (height * 0.5);
|
||||||
|
let rectangle_center = opposite
|
||||||
|
+ width_axis * (width * 0.5)
|
||||||
|
+ height_axis * (height * 0.5);
|
||||||
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||||
|
let (camera, bounds) = tab
|
||||||
|
.scene
|
||||||
|
.viewport_edit_frame((vw, vh))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
(
|
||||||
|
tab.scene.camera.borrow().clone(),
|
||||||
|
tab.scene.active_model_tile_bounds(vw, vh),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let project = |point| {
|
||||||
|
camera.project(point, bounds).map(|screen| iced::Point::new(
|
||||||
|
bounds.x + screen.x,
|
||||||
|
bounds.y + screen.y,
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let center = project(rectangle_center)?;
|
||||||
|
let offset_from_center = |side: iced::Point, pixels: f32| {
|
||||||
|
let dx = side.x - center.x;
|
||||||
|
let dy = side.y - center.y;
|
||||||
|
let length = dx.hypot(dy);
|
||||||
|
if length > 1.0e-3 {
|
||||||
|
iced::Point::new(
|
||||||
|
side.x + dx / length * pixels,
|
||||||
|
side.y + dy / length * pixels,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
side
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
offset_from_center(project(width_center)?, 14.0),
|
||||||
|
offset_from_center(project(height_center)?, 18.0),
|
||||||
|
))
|
||||||
|
});
|
||||||
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1005,6 +1069,14 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
.map(|(idx, f)| {
|
.map(|(idx, f)| {
|
||||||
let value = match (&f.buffer, live) {
|
let value = match (&f.buffer, live) {
|
||||||
(Some(b), _) => b.clone(),
|
(Some(b), _) => b.clone(),
|
||||||
|
(None, _) if rectangle_values.is_some() => {
|
||||||
|
let (width, height) = rectangle_values.unwrap();
|
||||||
|
match f.role {
|
||||||
|
crate::command::DynRole::Width => format!("{width:.4}"),
|
||||||
|
crate::command::DynRole::Height => format!("{height:.4}"),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
// An angle step with a command-supplied live value
|
// An angle step with a command-supplied live value
|
||||||
// (ARC span / direction) shows it in degrees.
|
// (ARC span / direction) shows it in degrees.
|
||||||
(None, Some(lv)) if f.component == DynComponent::Angle => {
|
(None, Some(lv)) if f.component == DynComponent::Angle => {
|
||||||
@@ -1033,6 +1105,13 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
active: idx == tab.dyn_active,
|
active: idx == tab.dyn_active,
|
||||||
locked: f.locked(),
|
locked: f.locked(),
|
||||||
role: f.role,
|
role: f.role,
|
||||||
|
center: rectangle_label_screens.and_then(|(width, height)| {
|
||||||
|
match f.role {
|
||||||
|
crate::command::DynRole::Width => Some(width),
|
||||||
|
crate::command::DynRole::Height => Some(height),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1772,6 +1851,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
crate::scene::pick::grip::GripEditMode::Lengthen
|
crate::scene::pick::grip::GripEditMode::Lengthen
|
||||||
| crate::scene::pick::grip::GripEditMode::Radius
|
| crate::scene::pick::grip::GripEditMode::Radius
|
||||||
| crate::scene::pick::grip::GripEditMode::ArcLength
|
| crate::scene::pick::grip::GripEditMode::ArcLength
|
||||||
|
| crate::scene::pick::grip::GripEditMode::RectangleWidth
|
||||||
|
| crate::scene::pick::grip::GripEditMode::RectangleHeight
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let dyn_capturing =
|
let dyn_capturing =
|
||||||
|
|||||||
+192
-31
@@ -1,5 +1,7 @@
|
|||||||
use acadrust::entities::{LwPolyline, LwVertex};
|
use acadrust::entities::{LwPolyline, LwVertex};
|
||||||
use cadkernel::geom2d::{signed_area, Curve, Polyline, PolylineVertex, Vec2};
|
use cadkernel::geom2d::{
|
||||||
|
signed_area, Curve, Polyline, PolylineVertex, RectangleFrame, Tolerance, Vec2,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
@@ -884,31 +886,19 @@ fn set_revision_cloud_arc_length(pline: &mut LwPolyline, requested: f64) {
|
|||||||
pline.vertices = cloud_vertices(&points, magnitude * sign, width_ratios);
|
pline.vertices = cloud_vertices(&points, magnitude * sign, width_ratios);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rectangle_frame(
|
||||||
|
pline: &LwPolyline,
|
||||||
|
) -> Option<(RectangleFrame, cadkernel::space::Plane)> {
|
||||||
|
let planar = crate::entities::curve::lwpolyline_curve(pline)?;
|
||||||
|
let Curve::Polyline(polyline) = &planar.curve else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let rectangle = polyline.rectangle_frame(Tolerance::default())?;
|
||||||
|
Some((rectangle, planar.plane))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_rectangle(pline: &LwPolyline) -> bool {
|
pub(crate) fn is_rectangle(pline: &LwPolyline) -> bool {
|
||||||
if pline.common.extended_data.get_record("OCS_RECTANGLE").is_some() {
|
rectangle_frame(pline).is_some()
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if !pline.is_closed
|
|
||||||
|| pline.vertices.len() != 4
|
|
||||||
|| pline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1.0e-9)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let edges: Vec<Vec2> = (0..4)
|
|
||||||
.map(|index| {
|
|
||||||
let from = pline.vertices[index].location;
|
|
||||||
let to = pline.vertices[(index + 1) % 4].location;
|
|
||||||
Vec2::new(to.x - from.x, to.y - from.y)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let lengths: Vec<f64> = edges.iter().map(|edge| edge.length()).collect();
|
|
||||||
if lengths.iter().any(|length| *length <= 1.0e-12) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let tolerance = 1.0e-9;
|
|
||||||
edges[0].dot(edges[1]).abs() <= tolerance * lengths[0] * lengths[1]
|
|
||||||
&& edges[0].cross(edges[2]).abs() <= tolerance * lengths[0] * lengths[2]
|
|
||||||
&& edges[1].cross(edges[3]).abs() <= tolerance * lengths[1] * lengths[3]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
||||||
@@ -1203,7 +1193,14 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
|||||||
// Vertex grip. Break only where a split is possible: any vertex
|
// Vertex grip. Break only where a split is possible: any vertex
|
||||||
// of a closed polyline, interior vertices of an open one.
|
// of a closed polyline, interior vertices of an open one.
|
||||||
let breakable = n >= 3 && (self.is_closed || (grip_id > 0 && grip_id < n - 1));
|
let breakable = n >= 3 && (self.is_closed || (grip_id > 0 && grip_id < n - 1));
|
||||||
let mut items = vec![
|
let mut items = Vec::new();
|
||||||
|
if is_rectangle(self) {
|
||||||
|
items.push(GripMenuItem {
|
||||||
|
label: "Resize",
|
||||||
|
action: GripMenuAction::RectangleResize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.extend([
|
||||||
GripMenuItem {
|
GripMenuItem {
|
||||||
label: "Stretch",
|
label: "Stretch",
|
||||||
action: GripMenuAction::Stretch,
|
action: GripMenuAction::Stretch,
|
||||||
@@ -1216,7 +1213,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
|||||||
label: "Remove Vertex",
|
label: "Remove Vertex",
|
||||||
action: GripMenuAction::RemoveVertex,
|
action: GripMenuAction::RemoveVertex,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
if breakable {
|
if breakable {
|
||||||
items.push(GripMenuItem {
|
items.push(GripMenuItem {
|
||||||
label: "Break",
|
label: "Break",
|
||||||
@@ -1242,7 +1239,19 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
|||||||
action: GripMenuAction::ConvertToArc,
|
action: GripMenuAction::ConvertToArc,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
vec![
|
let mut items = Vec::new();
|
||||||
|
if is_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 {
|
GripMenuItem {
|
||||||
label: "Stretch",
|
label: "Stretch",
|
||||||
action: GripMenuAction::Stretch,
|
action: GripMenuAction::Stretch,
|
||||||
@@ -1252,7 +1261,93 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
|||||||
action: GripMenuAction::AddVertex,
|
action: GripMenuAction::AddVertex,
|
||||||
},
|
},
|
||||||
convert,
|
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_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;
|
||||||
|
let (frame, plane) = rectangle_frame(self)?;
|
||||||
|
let seg = grip_id.checked_sub(4)?;
|
||||||
|
if seg >= 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let axis = match (seg % 2, action) {
|
||||||
|
(0, A::RectangleHeight) => Vec2::from(frame.height_axis),
|
||||||
|
(1, A::RectangleWidth) => Vec2::from(frame.width_axis),
|
||||||
|
_ => 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 point = Vec2::from(plane.project(point.to_array())?);
|
||||||
|
let value = (point - opposite_mid).dot(axis).abs();
|
||||||
|
(value > Tolerance::default().linear()).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 value <= Tolerance::default().linear() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some((frame, _)) = rectangle_frame(self) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(seg) = grip_id.checked_sub(4) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if seg >= 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let axis = match (seg % 2, action) {
|
||||||
|
(0, A::RectangleHeight) => frame.height_axis,
|
||||||
|
(1, A::RectangleWidth) => frame.width_axis,
|
||||||
|
_ => 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 signed_distance = delta.x * axis[0] + delta.y * axis[1];
|
||||||
|
if signed_distance.abs() <= Tolerance::default().linear() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let direction = acadrust::types::Vector2::new(axis[0], axis[1])
|
||||||
|
* signed_distance.signum();
|
||||||
|
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) {
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||||
use crate::scene::model::object::GripMenuAction as A;
|
use crate::scene::model::object::GripMenuAction as A;
|
||||||
@@ -1440,9 +1535,10 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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::entities::{LwPolyline, LwVertex};
|
||||||
use acadrust::Vector2;
|
use acadrust::{Vector2, Vector3};
|
||||||
|
|
||||||
fn make_test_lwpolyline(count: usize, constant_width: f64) -> LwPolyline {
|
fn make_test_lwpolyline(count: usize, constant_width: f64) -> LwPolyline {
|
||||||
let mut pl = LwPolyline::default();
|
let mut pl = LwPolyline::default();
|
||||||
@@ -1453,6 +1549,71 @@ mod tests {
|
|||||||
pl
|
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_corner_offers_resize_before_stretch() {
|
||||||
|
let pl = make_test_rectangle();
|
||||||
|
let corner = pl.grip_menu(0);
|
||||||
|
assert_eq!(corner[0].action, GripMenuAction::RectangleResize);
|
||||||
|
assert_eq!(corner[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_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 rectangle_point_preview_uses_entity_plane() {
|
||||||
|
let mut pl = make_test_rectangle();
|
||||||
|
pl.normal = Vector3::new(0.0, 1.0, 0.0);
|
||||||
|
pl.elevation = 5.0;
|
||||||
|
let (_, plane) = rectangle_frame(&pl).expect("rectangle frame");
|
||||||
|
let point = glam::DVec3::from_array(plane.point_at([14.0, 2.5]));
|
||||||
|
|
||||||
|
let value = pl.grip_menu_point_value(5, GripMenuAction::RectangleWidth, point);
|
||||||
|
|
||||||
|
assert_eq!(value, Some(14.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lwpolyline_uniform_width_shows_global_width() {
|
fn test_lwpolyline_uniform_width_shows_global_width() {
|
||||||
let pl = make_test_lwpolyline(3, 5.0);
|
let pl = make_test_lwpolyline(3, 5.0);
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ fn make_rect_pline(
|
|||||||
style: RectStyle,
|
style: RectStyle,
|
||||||
) -> Option<EntityType> {
|
) -> Option<EntityType> {
|
||||||
let (geometry, elevation) = rectangle_polyline(corners, plane, style)?;
|
let (geometry, elevation) = rectangle_polyline(corners, plane, style)?;
|
||||||
let mut polyline = LwPolyline {
|
let polyline = LwPolyline {
|
||||||
vertices: geometry
|
vertices: geometry
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
@@ -289,9 +289,6 @@ fn make_rect_pline(
|
|||||||
thickness: style.thickness,
|
thickness: style.thickness,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut marker = acadrust::xdata::ExtendedDataRecord::new("OCS_RECTANGLE");
|
|
||||||
marker.add_value(acadrust::xdata::XDataValue::Integer16(1));
|
|
||||||
polyline.common.extended_data.add_record(marker);
|
|
||||||
Some(plane.place_entity(EntityType::LwPolyline(polyline)))
|
Some(plane.place_entity(EntityType::LwPolyline(polyline)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,9 @@ pub enum GripMenuAction {
|
|||||||
Lengthen,
|
Lengthen,
|
||||||
Radius,
|
Radius,
|
||||||
ArcLength,
|
ArcLength,
|
||||||
|
RectangleWidth,
|
||||||
|
RectangleHeight,
|
||||||
|
RectangleResize,
|
||||||
AddVertex,
|
AddVertex,
|
||||||
RemoveVertex,
|
RemoveVertex,
|
||||||
ConvertToArc,
|
ConvertToArc,
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ pub struct GripEdit {
|
|||||||
pub axis: Option<DVec3>,
|
pub axis: Option<DVec3>,
|
||||||
/// Every hot grip moved by this edit. A normal grip edit contains one target.
|
/// Every hot grip moved by this edit. A normal grip edit contains one target.
|
||||||
pub targets: Vec<GripTarget>,
|
pub targets: Vec<GripTarget>,
|
||||||
|
/// Opposite corner and local width/height axes for rectangle corner resize.
|
||||||
|
pub rectangle_frame: Option<(DVec3, DVec3, DVec3)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -56,6 +58,9 @@ pub enum GripEditMode {
|
|||||||
Lengthen,
|
Lengthen,
|
||||||
Radius,
|
Radius,
|
||||||
ArcLength,
|
ArcLength,
|
||||||
|
RectangleWidth,
|
||||||
|
RectangleHeight,
|
||||||
|
RectangleResize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -86,6 +91,7 @@ impl GripEdit {
|
|||||||
is_translate,
|
is_translate,
|
||||||
last_world: world,
|
last_world: world,
|
||||||
}],
|
}],
|
||||||
|
rectangle_frame: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +112,32 @@ impl GripEdit {
|
|||||||
edit.mode = GripEditMode::ArcLength;
|
edit.mode = GripEditMode::ArcLength;
|
||||||
edit
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rectangle_resize(
|
||||||
|
handle: Handle,
|
||||||
|
grip_id: usize,
|
||||||
|
world: DVec3,
|
||||||
|
opposite: DVec3,
|
||||||
|
width_axis: DVec3,
|
||||||
|
height_axis: DVec3,
|
||||||
|
) -> Self {
|
||||||
|
let mut edit = Self::single(handle, grip_id, false, world);
|
||||||
|
edit.mode = GripEditMode::RectangleResize;
|
||||||
|
edit.rectangle_frame = Some((opposite, width_axis, height_axis));
|
||||||
|
edit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||||
|
|||||||
+14
-4
@@ -2685,6 +2685,8 @@ pub struct DynBox {
|
|||||||
/// User has typed a value (the box no longer tracks the cursor).
|
/// User has typed a value (the box no longer tracks the cursor).
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
pub role: DynRole,
|
pub role: DynRole,
|
||||||
|
/// Optional exact viewport position, used by rotated rectangle dimensions.
|
||||||
|
pub center: Option<Point>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dynamic_input_overlay<'a>(
|
pub fn dynamic_input_overlay<'a>(
|
||||||
@@ -3038,7 +3040,7 @@ impl DynInputCanvas {
|
|||||||
|
|
||||||
// ── Box placement by role ──
|
// ── Box placement by role ──
|
||||||
for b in &self.boxes {
|
for b in &self.boxes {
|
||||||
let center = match b.role {
|
let center = b.center.unwrap_or_else(|| match b.role {
|
||||||
DynRole::Angle => self.label_screen.unwrap_or_else(|| {
|
DynRole::Angle => self.label_screen.unwrap_or_else(|| {
|
||||||
let a_mid = a_ref + sweep * 0.5;
|
let a_mid = a_ref + sweep * 0.5;
|
||||||
let r = (len - DYN_BOX_H * 2.0).max(len * 0.5);
|
let r = (len - DYN_BOX_H * 2.0).max(len * 0.5);
|
||||||
@@ -3049,10 +3051,18 @@ impl DynInputCanvas {
|
|||||||
}),
|
}),
|
||||||
DynRole::X | DynRole::Width => Point {
|
DynRole::X | DynRole::Width => Point {
|
||||||
x: (base.x + cursor.x) * 0.5,
|
x: (base.x + cursor.x) * 0.5,
|
||||||
y: base.y + 14.0,
|
y: if self.guide == DynGuide::RectSides {
|
||||||
|
base.y - (cursor.y - base.y).signum() * 14.0
|
||||||
|
} else {
|
||||||
|
base.y + 14.0
|
||||||
|
},
|
||||||
},
|
},
|
||||||
DynRole::Y | DynRole::Height => Point {
|
DynRole::Y | DynRole::Height => Point {
|
||||||
x: corner.x + 18.0,
|
x: if self.guide == DynGuide::RectSides {
|
||||||
|
corner.x + (cursor.x - base.x).signum() * 18.0
|
||||||
|
} else {
|
||||||
|
corner.x + 18.0
|
||||||
|
},
|
||||||
y: (base.y + cursor.y) * 0.5,
|
y: (base.y + cursor.y) * 0.5,
|
||||||
},
|
},
|
||||||
// Perpendicular measure: on the measured segment / dim line.
|
// Perpendicular measure: on the measured segment / dim line.
|
||||||
@@ -3071,7 +3081,7 @@ impl DynInputCanvas {
|
|||||||
x: base.x + dx * len * 0.5 + nx * 16.0,
|
x: base.x + dx * len * 0.5 + nx * 16.0,
|
||||||
y: base.y + dy * len * 0.5 + ny * 16.0,
|
y: base.y + dy * len * 0.5 + ny * 16.0,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
Self::draw_box(frame, b, center, bounds, theme);
|
Self::draw_box(frame, b, center, bounds, theme);
|
||||||
}
|
}
|
||||||
// Keep the tracking hint near the crosshair in guided layouts.
|
// Keep the tracking hint near the crosshair in guided layouts.
|
||||||
|
|||||||
Reference in new issue
Block a user