forked from 1b-agent/opencadstudio-upstream
Keep rotation angle label on guide arc
This commit is contained in:
1 parent
833af72eb2
commit
7edf268eef
4 files changed
+109
-57
No files matched your search
@@ -836,6 +836,25 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
let w = tab.last_cursor_world;
|
let w = tab.last_cursor_world;
|
||||||
let base = tab.dyn_anchor.or(self.last_point);
|
let base = tab.dyn_anchor.or(self.last_point);
|
||||||
|
let label_screen = tab
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|command| command.dyn_label_point(w))
|
||||||
|
.and_then(|world| {
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
camera.project(world, bounds).map(|point| {
|
||||||
|
iced::Point::new(bounds.x + point.x, bounds.y + point.y)
|
||||||
|
})
|
||||||
|
});
|
||||||
// A command may drive a typed scalar by mouse (e.g. a
|
// A command may drive a typed scalar by mouse (e.g. a
|
||||||
// perpendicular distance to a picked object); show that live
|
// perpendicular distance to a picked object); show that live
|
||||||
// value in the box until the user types over it.
|
// value in the box until the user types over it.
|
||||||
@@ -898,6 +917,7 @@ impl OpenCADStudio {
|
|||||||
tab.last_cursor_screen,
|
tab.last_cursor_screen,
|
||||||
tab.last_point_screen,
|
tab.last_point_screen,
|
||||||
tab.dyn_ref_screen,
|
tab.dyn_ref_screen,
|
||||||
|
label_screen,
|
||||||
tab.dyn_guide,
|
tab.dyn_guide,
|
||||||
boxes,
|
boxes,
|
||||||
prompt,
|
prompt,
|
||||||
|
|||||||
@@ -2179,6 +2179,12 @@ pub trait CadCommand: Send {
|
|||||||
fn dyn_live_value(&self, _cursor: DVec3) -> Option<f64> {
|
fn dyn_live_value(&self, _cursor: DVec3) -> Option<f64> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional world-space point where the dynamic value box should be
|
||||||
|
/// centred. The view projects it with the active camera.
|
||||||
|
fn dyn_label_point(&self, _cursor: DVec3) -> Option<DVec3> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -73,6 +73,19 @@ impl RotateCommand {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn angle_arc(
|
||||||
|
center: [f64; 2],
|
||||||
|
radius: f64,
|
||||||
|
angle: f64,
|
||||||
|
) -> Option<(geom2d::Arc, bool)> {
|
||||||
|
let (start, end, reverse) = if angle > 0.0 {
|
||||||
|
(0.0, angle, false)
|
||||||
|
} else {
|
||||||
|
(angle, 0.0, true)
|
||||||
|
};
|
||||||
|
geom2d::bounded_arc(center, radius, start, end).map(|arc| (arc, reverse))
|
||||||
|
}
|
||||||
|
|
||||||
fn angle_guide(&self, center: DVec3, radius: f64, angle: f64) -> Vec<WireModel> {
|
fn angle_guide(&self, center: DVec3, radius: f64, angle: f64) -> Vec<WireModel> {
|
||||||
if radius <= f64::EPSILON {
|
if radius <= f64::EPSILON {
|
||||||
return vec![];
|
return vec![];
|
||||||
@@ -95,13 +108,11 @@ impl RotateCommand {
|
|||||||
return guides;
|
return guides;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (start, end, reverse) = if angle > 0.0 {
|
let Some((arc, reverse)) = Self::angle_arc(
|
||||||
(0.0, angle, false)
|
[center_local.x, center_local.y],
|
||||||
} else {
|
radius,
|
||||||
(angle, 0.0, true)
|
angle,
|
||||||
};
|
) else {
|
||||||
let Some(arc) = geom2d::bounded_arc([center_local.x, center_local.y], radius, start, end)
|
|
||||||
else {
|
|
||||||
return guides;
|
return guides;
|
||||||
};
|
};
|
||||||
let mut points = KernelCurve::Arc(arc).tessellate_angle(TAU / 64.0);
|
let mut points = KernelCurve::Arc(arc).tessellate_angle(TAU / 64.0);
|
||||||
@@ -327,4 +338,54 @@ impl CadCommand for RotateCommand {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dyn_label_point(&self, cursor: DVec3) -> Option<DVec3> {
|
||||||
|
let center = match self.step {
|
||||||
|
Step::Angle { center } | Step::RefNew { center, .. } => center,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let center_local = self.plane.to_local(center);
|
||||||
|
let direction = self.plane.vector_to_local(cursor - center);
|
||||||
|
let radius = direction.truncate().length();
|
||||||
|
if radius <= f64::EPSILON {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let angle = direction.y.atan2(direction.x);
|
||||||
|
let point = if angle.abs() <= f64::EPSILON {
|
||||||
|
[center_local.x + radius, center_local.y]
|
||||||
|
} else {
|
||||||
|
let (arc, _) = Self::angle_arc(
|
||||||
|
[center_local.x, center_local.y],
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
)?;
|
||||||
|
KernelCurve::Arc(arc).point_at(0.5)
|
||||||
|
};
|
||||||
|
Some(
|
||||||
|
self.plane
|
||||||
|
.to_world(DVec3::new(point[0], point[1], center_local.z)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn angle_label_uses_the_working_plane_arc_midpoint() {
|
||||||
|
let plane = WorkingPlane::new(DVec3::ZERO, DVec3::Y, DVec3::Z);
|
||||||
|
let mut command = RotateCommand::new(vec![], vec![]);
|
||||||
|
command.set_working_plane(plane);
|
||||||
|
command.step = Step::Angle {
|
||||||
|
center: DVec3::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = command
|
||||||
|
.dyn_label_point(plane.y * 10.0)
|
||||||
|
.expect("label point");
|
||||||
|
let expected = (plane.x + plane.y) * (50.0_f64).sqrt();
|
||||||
|
|
||||||
|
assert!(label.abs_diff_eq(expected, 1e-9));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+15
-50
@@ -2301,6 +2301,7 @@ pub fn dynamic_input_overlay<'a>(
|
|||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
base_screen: Option<Point>,
|
base_screen: Option<Point>,
|
||||||
ref_screen: Option<Point>,
|
ref_screen: Option<Point>,
|
||||||
|
label_screen: Option<Point>,
|
||||||
guide: DynGuide,
|
guide: DynGuide,
|
||||||
boxes: Vec<DynBox>,
|
boxes: Vec<DynBox>,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@@ -2310,6 +2311,7 @@ pub fn dynamic_input_overlay<'a>(
|
|||||||
cursor_screen,
|
cursor_screen,
|
||||||
base_screen,
|
base_screen,
|
||||||
ref_screen,
|
ref_screen,
|
||||||
|
label_screen,
|
||||||
guide,
|
guide,
|
||||||
boxes,
|
boxes,
|
||||||
prompt,
|
prompt,
|
||||||
@@ -2327,6 +2329,8 @@ struct DynInputCanvas {
|
|||||||
base_screen: Option<Point>,
|
base_screen: Option<Point>,
|
||||||
/// Far end of the reference line (projected `dyn_ref`) — for `Perp`.
|
/// Far end of the reference line (projected `dyn_ref`) — for `Perp`.
|
||||||
ref_screen: Option<Point>,
|
ref_screen: Option<Point>,
|
||||||
|
/// Command-supplied world-space label point projected by the active camera.
|
||||||
|
label_screen: Option<Point>,
|
||||||
guide: DynGuide,
|
guide: DynGuide,
|
||||||
boxes: Vec<DynBox>,
|
boxes: Vec<DynBox>,
|
||||||
/// The active command's current prompt, drawn just above the boxes.
|
/// The active command's current prompt, drawn just above the boxes.
|
||||||
@@ -2359,21 +2363,6 @@ impl DynInputCanvas {
|
|||||||
(Self::box_content(b).len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0
|
(Self::box_content(b).len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn box_center_near_cursor(b: &DynBox, cursor: Point, bounds: iced::Rectangle) -> Point {
|
|
||||||
let width = Self::box_width(b);
|
|
||||||
let x = if cursor.x + DYN_OFFSET_X + width <= bounds.width {
|
|
||||||
cursor.x + DYN_OFFSET_X + width * 0.5
|
|
||||||
} else {
|
|
||||||
cursor.x - DYN_OFFSET_X - width * 0.5
|
|
||||||
};
|
|
||||||
let y = if cursor.y + DYN_OFFSET_X + DYN_BOX_H <= bounds.height {
|
|
||||||
cursor.y + DYN_OFFSET_X + DYN_BOX_H * 0.5
|
|
||||||
} else {
|
|
||||||
cursor.y - DYN_OFFSET_X - DYN_BOX_H * 0.5
|
|
||||||
};
|
|
||||||
Point { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a value box centred at `center`, clamped inside `bounds`.
|
/// Draw a value box centred at `center`, clamped inside `bounds`.
|
||||||
fn draw_box(
|
fn draw_box(
|
||||||
frame: &mut canvas::Frame,
|
frame: &mut canvas::Frame,
|
||||||
@@ -2660,7 +2649,14 @@ 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 = match b.role {
|
||||||
DynRole::Angle => Self::box_center_near_cursor(b, cursor_raw, bounds),
|
DynRole::Angle => self.label_screen.unwrap_or_else(|| {
|
||||||
|
let a_mid = a_ref + sweep * 0.5;
|
||||||
|
let r = (len - DYN_BOX_H * 2.0).max(len * 0.5);
|
||||||
|
Point {
|
||||||
|
x: base.x + a_mid.cos() * r - nx * 18.0,
|
||||||
|
y: base.y + a_mid.sin() * r - ny * 18.0,
|
||||||
|
}
|
||||||
|
}),
|
||||||
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: base.y + 14.0,
|
||||||
@@ -2854,6 +2850,9 @@ impl canvas::Program<Message> for DynInputCanvas {
|
|||||||
|
|
||||||
// Guided layouts need the anchor; without it fall back to a cursor row.
|
// Guided layouts need the anchor; without it fall back to a cursor row.
|
||||||
match (self.guide, self.base_screen) {
|
match (self.guide, self.base_screen) {
|
||||||
|
(DynGuide::None, Some(base)) if self.label_screen.is_some() => {
|
||||||
|
self.draw_guided(&mut frame, bounds, base, theme)
|
||||||
|
}
|
||||||
(DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds, theme),
|
(DynGuide::None, _) | (_, None) => self.draw_row(&mut frame, bounds, theme),
|
||||||
(_, Some(base)) => self.draw_guided(&mut frame, bounds, base, theme),
|
(_, Some(base)) => self.draw_guided(&mut frame, bounds, base, theme),
|
||||||
}
|
}
|
||||||
@@ -2861,40 +2860,6 @@ impl canvas::Program<Message> for DynInputCanvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod dynamic_input_position_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn angle_box_follows_cursor_and_flips_inside_viewport_edges() {
|
|
||||||
let angle = DynBox {
|
|
||||||
label: String::new(),
|
|
||||||
value: "163.7".to_string(),
|
|
||||||
active: true,
|
|
||||||
locked: false,
|
|
||||||
role: DynRole::Angle,
|
|
||||||
};
|
|
||||||
let bounds = iced::Rectangle {
|
|
||||||
x: 0.0,
|
|
||||||
y: 0.0,
|
|
||||||
width: 800.0,
|
|
||||||
height: 600.0,
|
|
||||||
};
|
|
||||||
let width = DynInputCanvas::box_width(&angle);
|
|
||||||
|
|
||||||
let cursor = Point::new(400.0, 300.0);
|
|
||||||
let center = DynInputCanvas::box_center_near_cursor(&angle, cursor, bounds);
|
|
||||||
assert!((center.x - width * 0.5 - cursor.x - DYN_OFFSET_X).abs() < 0.01);
|
|
||||||
assert!((center.y - DYN_BOX_H * 0.5 - cursor.y - DYN_OFFSET_X).abs() < 0.01);
|
|
||||||
|
|
||||||
let cursor = Point::new(795.0, 595.0);
|
|
||||||
let center = DynInputCanvas::box_center_near_cursor(&angle, cursor, bounds);
|
|
||||||
assert!(center.x + width * 0.5 <= bounds.width);
|
|
||||||
assert!(center.y + DYN_BOX_H * 0.5 <= bounds.height);
|
|
||||||
assert!(center.x < cursor.x && center.y < cursor.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod clip_tests {
|
mod clip_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in new issue
Block a user