Support dynamic rectangle resizing via grip editing

This commit is contained in:
Marcel committed 2026-09-11 20:52:33 +02:00
1 parent f30b490831
commit 913705106e
8 files changed
+282 -41

No files matched your search

+101 -14
View File
@@ -423,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.
// //
@@ -506,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 {
@@ -523,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]
@@ -1340,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
@@ -1397,12 +1444,52 @@ 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 corner = |id| {
grip_id, self.tabs[i]
is_translate, .selected_grip_handles
g.world, .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(); return Task::none();
} }
+19 -6
View File
@@ -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()
@@ -93,13 +97,20 @@ impl OpenCADStudio {
crate::scene::pick::grip::GripEditMode::RectangleHeight => { crate::scene::pick::grip::GripEditMode::RectangleHeight => {
(grip.origin_world, Some(crate::command::DynRole::Height)) (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()
@@ -115,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;
} }
+58 -1
View File
@@ -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 => {
@@ -1596,9 +1621,41 @@ impl OpenCADStudio {
GripEditMode::RectangleHeight => { GripEditMode::RectangleHeight => {
Some(crate::scene::model::object::GripMenuAction::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
+53
View File
@@ -1004,6 +1004,44 @@ 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 (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 let boxes: Vec<crate::ui::overlay::DynBox> = tab
.dyn_fields .dyn_fields
.iter() .iter()
@@ -1011,6 +1049,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 => {
@@ -1039,6 +1085,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();
+28 -18
View File
@@ -885,16 +885,6 @@ fn set_revision_cloud_arc_length(pline: &mut LwPolyline, requested: f64) {
} }
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() {
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 if !pline.is_closed
|| pline.vertices.len() != 4 || pline.vertices.len() != 4
|| pline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1.0e-9) || pline.vertices.iter().any(|vertex| vertex.bulge.abs() > 1.0e-9)
@@ -918,6 +908,11 @@ fn is_resizable_rectangle(pline: &LwPolyline) -> bool {
&& edges[1].cross(edges[3]).abs() <= tolerance * lengths[1] * lengths[3] && edges[1].cross(edges[3]).abs() <= tolerance * lengths[1] * lengths[3]
} }
fn is_rectangle_command_entity(pline: &LwPolyline) -> bool {
pline.common.extended_data.get_record("OCS_RECTANGLE").is_some()
|| is_rectangle(pline)
}
fn properties(pline: &LwPolyline) -> Vec<PropSection> { fn properties(pline: &LwPolyline) -> Vec<PropSection> {
let n = pline.vertices.len(); let n = pline.vertices.len();
// The panel's Current Vertex focus, clamped to this polyline's range. // The panel's Current Vertex focus, clamped to this polyline's range.
@@ -978,7 +973,7 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
edit(t!("Vertex Y").as_ref(), "vertex_y", vy), edit(t!("Vertex Y").as_ref(), "vertex_y", vy),
edit_scalar(t!("Bulge").as_ref(), "bulge", v.map_or(0.0, |vertex| vertex.bulge)), edit_scalar(t!("Bulge").as_ref(), "bulge", v.map_or(0.0, |vertex| vertex.bulge)),
]; ];
if !is_rectangle(pline) { if !is_rectangle_command_entity(pline) {
geometry_props.push(edit(t!("Start segment width").as_ref(), "start_width", start_w)); geometry_props.push(edit(t!("Start segment width").as_ref(), "start_width", start_w));
geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w)); geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w));
} }
@@ -1210,7 +1205,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,
@@ -1223,7 +1225,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",
@@ -1250,7 +1252,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
} }
}; };
let mut items = Vec::new(); let mut items = Vec::new();
if is_resizable_rectangle(self) && seg < 4 { if is_rectangle(self) && seg < 4 {
// Moving an edge changes the dimension perpendicular to it. // Moving an edge changes the dimension perpendicular to it.
items.push(GripMenuItem { items.push(GripMenuItem {
label: if seg % 2 == 0 { "Height" } else { "Width" }, label: if seg % 2 == 0 { "Height" } else { "Width" },
@@ -1282,7 +1284,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
) -> Option<&'static str> { ) -> Option<&'static str> {
use crate::scene::model::object::GripMenuAction as A; use crate::scene::model::object::GripMenuAction as A;
let n = self.vertices.len(); let n = self.vertices.len();
(is_resizable_rectangle(self) (is_rectangle(self)
&& n == 4 && n == 4
&& (n..n + 4).contains(&grip_id) && (n..n + 4).contains(&grip_id)
&& matches!(action, A::RectangleWidth | A::RectangleHeight)) && matches!(action, A::RectangleWidth | A::RectangleHeight))
@@ -1299,7 +1301,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
point: glam::DVec3, point: glam::DVec3,
) -> Option<f64> { ) -> Option<f64> {
use crate::scene::model::object::GripMenuAction as A; use crate::scene::model::object::GripMenuAction as A;
if !is_resizable_rectangle(self) if !is_rectangle(self)
|| !matches!(action, A::RectangleWidth | A::RectangleHeight) || !matches!(action, A::RectangleWidth | A::RectangleHeight)
{ {
return None; return None;
@@ -1334,7 +1336,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
value: f64, value: f64,
) { ) {
use crate::scene::model::object::GripMenuAction as A; use crate::scene::model::object::GripMenuAction as A;
if !is_resizable_rectangle(self) || value <= 1.0e-9 if !is_rectangle(self) || value <= 1.0e-9
|| !matches!(action, A::RectangleWidth | A::RectangleHeight) || !matches!(action, A::RectangleWidth | A::RectangleHeight)
{ {
return; return;
@@ -1582,6 +1584,14 @@ mod tests {
assert_eq!(right[1].action, GripMenuAction::Stretch); 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] #[test]
fn rectangle_dimension_edit_keeps_opposite_edge_fixed() { fn rectangle_dimension_edit_keeps_opposite_edge_fixed() {
let mut pl = make_test_rectangle(); let mut pl = make_test_rectangle();
@@ -1590,7 +1600,7 @@ mod tests {
assert_eq!(pl.vertices[1].location, Vector2::new(10.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[2].location, Vector2::new(10.0, 8.0));
assert_eq!(pl.vertices[3].location, Vector2::new(0.0, 8.0)); assert_eq!(pl.vertices[3].location, Vector2::new(0.0, 8.0));
assert!(is_resizable_rectangle(&pl)); assert!(is_rectangle(&pl));
} }
#[test] #[test]
+1
View File
@@ -177,6 +177,7 @@ pub enum GripMenuAction {
ArcLength, ArcLength,
RectangleWidth, RectangleWidth,
RectangleHeight, RectangleHeight,
RectangleResize,
AddVertex, AddVertex,
RemoveVertex, RemoveVertex,
ConvertToArc, ConvertToArc,
+18
View File
@@ -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)]
@@ -58,6 +60,7 @@ pub enum GripEditMode {
ArcLength, ArcLength,
RectangleWidth, RectangleWidth,
RectangleHeight, RectangleHeight,
RectangleResize,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -88,6 +91,7 @@ impl GripEdit {
is_translate, is_translate,
last_world: world, last_world: world,
}], }],
rectangle_frame: None,
} }
} }
@@ -120,6 +124,20 @@ impl GripEdit {
edit.mode = GripEditMode::RectangleHeight; edit.mode = GripEditMode::RectangleHeight;
edit 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 ───────────────────────────────────────────────────
+4 -2
View File
@@ -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);
@@ -3071,7 +3073,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.