forked from 1b-agent/opencadstudio-upstream
Support dynamic rectangle resizing via grip editing
This commit is contained in:
1 parent
f30b490831
commit
913705106e
8 files changed
+282
-41
No files matched your search
+101
-14
@@ -423,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.
|
||||
//
|
||||
@@ -506,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 {
|
||||
@@ -523,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]
|
||||
@@ -1340,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
|
||||
@@ -1397,12 +1444,52 @@ 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 corner = |id| {
|
||||
self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, grip)| **owner == popup.handle && grip.id == id)
|
||||
.map(|(_, grip)| grip.world)
|
||||
};
|
||||
if let (Some(p0), Some(p1), Some(p2), Some(opposite)) = (
|
||||
corner(0),
|
||||
corner(1),
|
||||
corner(2),
|
||||
corner((popup.grip_id + 2) % 4),
|
||||
) {
|
||||
if let (Some(width_axis), Some(height_axis)) =
|
||||
((p1 - p0).try_normalize(), (p2 - p1).try_normalize())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -93,13 +97,20 @@ impl OpenCADStudio {
|
||||
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()
|
||||
@@ -115,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;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
@@ -1596,9 +1621,41 @@ impl OpenCADStudio {
|
||||
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
|
||||
|
||||
@@ -1004,6 +1004,44 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
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 (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,
|
||||
))
|
||||
};
|
||||
Some((project(width_center)?, project(height_center)?))
|
||||
});
|
||||
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
||||
.dyn_fields
|
||||
.iter()
|
||||
@@ -1011,6 +1049,14 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.map(|(idx, f)| {
|
||||
let value = match (&f.buffer, live) {
|
||||
(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
|
||||
// (ARC span / direction) shows it in degrees.
|
||||
(None, Some(lv)) if f.component == DynComponent::Angle => {
|
||||
@@ -1039,6 +1085,13 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
active: idx == tab.dyn_active,
|
||||
locked: f.locked(),
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user