forked from 1b-agent/opencadstudio-upstream
feat(leader): dynamic MLEADER text placement, grips, and consistent arrows
Reworks MLEADER text/leader geometry and grip editing, and aligns LEADER to MLEADER's conventions (kept as separate entities). Text & landing (render path): - Text is side-anchored on the leader-facing edge and reads outward; the side, alignment and the horizontal landing are recomputed every frame from the text grip's position relative to the leader, so moving the arrow or the text re-mirrors the whole layout and the landing stays attached to the text's near edge. - Fixes #49: text was centred on the leader endpoint regardless of side. Grips: - Text-location grip plus an MTEXT-style triangle wrap-width grip at the box's far edge; dragging it sets the MText wrap width. - Multifunction grip menu restored and made functional: leader-line vertices offer Add Leader / Remove Leader (Add grabs the new arrow so it follows the cursor — click places, Esc removes it); the text grip offers Move Independent (text only) and Move with Leader (translates the whole multileader via a move-all sentinel grip). Arrows: - LEADER and MLEADER arrowheads now render at the same size (text height), matching previews. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6f64d8d0d9
commit
8b3d083789
6 files changed
+395
-162
No files matched your search
@@ -205,6 +205,10 @@ pub(super) struct OpenCADStudio {
|
||||
/// until dismissed (click outside, ESC, cursor leaves the grip).
|
||||
grip_popup: Option<GripPopup>,
|
||||
grip_pending: Option<GripPendingValue>,
|
||||
/// A leader line just added via the "Add Leader" grip menu whose arrow is
|
||||
/// being placed (follows the cursor). `(entity handle, new-arrow grip id)`.
|
||||
/// Esc before the placement click removes it again.
|
||||
grip_add_provisional: Option<(acadrust::Handle, usize)>,
|
||||
/// Open Quick Select panel state. `None` = panel closed. Filters are
|
||||
/// applied via `Message::QSelectApply`; the panel is dismissed on
|
||||
/// Apply / Cancel / Esc / outside-click.
|
||||
@@ -980,6 +984,7 @@ impl OpenCADStudio {
|
||||
grip_hover: None,
|
||||
grip_popup: None,
|
||||
grip_pending: None,
|
||||
grip_add_provisional: None,
|
||||
qselect: None,
|
||||
show_ucs_icon: true,
|
||||
show_viewcube: true,
|
||||
|
||||
+71
-6
@@ -1283,6 +1283,19 @@ impl OpenCADStudio {
|
||||
// A hot grip (click-move-click placement in progress) ends on
|
||||
// Escape, leaving the entity at its last previewed position.
|
||||
if self.tabs[self.active_tab].active_grip.take().is_some() {
|
||||
// An Add-Leader arrow being placed: Esc removes it again.
|
||||
if let Some((h, gid)) = self.grip_add_provisional.take() {
|
||||
let i = self.active_tab;
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
||||
e.apply_grip_menu(
|
||||
gid,
|
||||
crate::scene::object::GripMenuAction::RemoveLeader,
|
||||
);
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.refresh_selected_grips();
|
||||
}
|
||||
self.tabs[self.active_tab].snap_result = None;
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
@@ -2516,6 +2529,8 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip = None;
|
||||
// Placement confirmed — keep the just-added leader.
|
||||
self.grip_add_provisional = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
@@ -3310,20 +3325,33 @@ impl OpenCADStudio {
|
||||
};
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
use crate::scene::object::GripMenuAction;
|
||||
if matches!(item.action, GripMenuAction::Stretch) {
|
||||
// Stretch = move this grip. Engage it so the next click
|
||||
// places it (click-move-click) — same as picking the grip
|
||||
// directly in the viewport. Without this the menu just
|
||||
if matches!(
|
||||
item.action,
|
||||
GripMenuAction::Stretch
|
||||
| GripMenuAction::MoveWithLeader
|
||||
| GripMenuAction::MoveIndependent
|
||||
) {
|
||||
// Stretch / Move = grab this grip. Engage it so the next
|
||||
// click places it (click-move-click) — same as picking the
|
||||
// grip directly in the viewport. Without this the menu just
|
||||
// closed and the grip never became hot (issue #48).
|
||||
if let Some(g) = self.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.find(|g| g.id == popup.grip_id)
|
||||
{
|
||||
// "Move with Leader" drags the whole multileader; the
|
||||
// others move just the picked grip.
|
||||
let (grip_id, is_translate) =
|
||||
if matches!(item.action, GripMenuAction::MoveWithLeader) {
|
||||
(crate::entities::multileader::MOVE_ALL_GRIP, true)
|
||||
} else {
|
||||
(popup.grip_id, g.is_midpoint)
|
||||
};
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
handle: popup.handle,
|
||||
grip_id: popup.grip_id,
|
||||
is_translate: g.is_midpoint,
|
||||
grip_id,
|
||||
is_translate,
|
||||
origin_world: g.world,
|
||||
last_world: g.world,
|
||||
});
|
||||
@@ -3351,6 +3379,27 @@ impl OpenCADStudio {
|
||||
}
|
||||
// One-shot action — apply immediately.
|
||||
self.push_undo_snapshot(i, item.label);
|
||||
// For Add Leader, the new arrow becomes the last grip; remember
|
||||
// its id so we can grab it for placement right after.
|
||||
let add_leader_gid = if matches!(item.action, GripMenuAction::AddLeader) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(popup.handle)
|
||||
.and_then(|e| match e {
|
||||
acadrust::EntityType::MultiLeader(ml) => Some(
|
||||
ml.context
|
||||
.leader_roots
|
||||
.iter()
|
||||
.flat_map(|r| r.lines.iter())
|
||||
.map(|l| l.points.len())
|
||||
.sum::<usize>(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(entity) = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
@@ -3363,6 +3412,22 @@ impl OpenCADStudio {
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_selected_grips();
|
||||
self.refresh_properties();
|
||||
// Grab the new arrow so it follows the cursor (click places it,
|
||||
// Esc removes it).
|
||||
if let Some(new_gid) = add_leader_gid {
|
||||
if let Some(g) =
|
||||
self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid)
|
||||
{
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
handle: popup.handle,
|
||||
grip_id: new_gid,
|
||||
is_translate: false,
|
||||
origin_world: g.world,
|
||||
last_world: g.world,
|
||||
});
|
||||
self.grip_add_provisional = Some((popup.handle, new_gid));
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user