feat(grips): improve direct editing
Refit arcs from fixed control points and highlight hovered grips. Let Lengthen preview from the cursor or accept a typed distance.
This commit is contained in:
parent
b744d4bf2d
commit
539d83de47
10 changed files with 421 additions and 59 deletions
|
|
@ -85,9 +85,9 @@ pub struct GripPopup {
|
|||
}
|
||||
|
||||
/// Pending follow-up value for grip-menu actions that need a number
|
||||
/// (Lengthen / Radius / Arc Length / Rotate Text). The next number
|
||||
/// typed in the command line is parsed and routed into
|
||||
/// `apply_grip_menu_value` for `(handle, grip_id, action)`.
|
||||
/// (Lengthen / Radius / Arc Length / Rotate Text). Lengthen can also resolve
|
||||
/// from a viewport point; otherwise the next typed number is parsed and routed
|
||||
/// into `apply_grip_menu_value` for `(handle, grip_id, action)`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GripPendingValue {
|
||||
pub handle: acadrust::Handle,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ use crate::app::helpers::{
|
|||
};
|
||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::pick::grip::{
|
||||
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode,
|
||||
};
|
||||
use crate::scene::model::object::GripApply;
|
||||
use crate::scene::{
|
||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
|
|
@ -242,9 +244,21 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
"{}: expected a number, got \"{raw}\"",
|
||||
pending.label
|
||||
).as_ref());
|
||||
return Task::none();
|
||||
self.grip_pending = Some(pending);
|
||||
return self.focus_cmd_input();
|
||||
};
|
||||
let i = self.active_tab;
|
||||
let interactive_lengthen = self.tabs[i]
|
||||
.active_grip
|
||||
.as_ref()
|
||||
.is_some_and(|grip| {
|
||||
grip.mode == GripEditMode::Lengthen
|
||||
&& grip.handle == pending.handle
|
||||
&& grip.grip_id == pending.grip_id
|
||||
});
|
||||
if interactive_lengthen {
|
||||
self.cancel_active_grip_edit();
|
||||
}
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
self.push_undo_snapshot(i, pending.label);
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(pending.handle)
|
||||
|
|
@ -576,6 +590,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
if self.visibility_popup.take().is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
if self.tabs[self.active_tab].active_grip.is_some() {
|
||||
self.grip_pending = None;
|
||||
self.command_line.input.clear();
|
||||
if self.cancel_active_grip_edit() {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if self.grip_pending.take().is_some() {
|
||||
self.command_line.input.clear();
|
||||
return Task::none();
|
||||
|
|
@ -987,7 +1008,26 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
action: item.action,
|
||||
label,
|
||||
});
|
||||
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
||||
if matches!(item.action, GripMenuAction::Lengthen) {
|
||||
if let Some((_, grip)) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, grip)| {
|
||||
**owner == popup.handle && grip.id == popup.grip_id
|
||||
})
|
||||
{
|
||||
self.tabs[i].active_grip = Some(GripEdit::lengthen(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
grip.world,
|
||||
));
|
||||
}
|
||||
self.command_line
|
||||
.push_info(crate::t!("Specify point or enter distance:").as_ref());
|
||||
} else {
|
||||
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
||||
}
|
||||
return self.focus_cmd_input();
|
||||
}
|
||||
// Break at vertex replaces the entity with the split pieces —
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
|||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::model::object::GripApply;
|
||||
use crate::scene::pick::grip::{
|
||||
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripTarget,
|
||||
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode, GripTarget,
|
||||
};
|
||||
use crate::scene::{
|
||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
|
|
@ -344,6 +344,7 @@ impl OpenCADStudio {
|
|||
grip_id,
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
mode: GripEditMode::Stretch,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
|
|
@ -1003,18 +1004,91 @@ impl OpenCADStudio {
|
|||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let apply_started = Instant::now();
|
||||
let delta = snapped - grip.last_world;
|
||||
let actions: Vec<_> = grip
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let apply = if target.is_translate {
|
||||
GripApply::Translate(delta)
|
||||
} else {
|
||||
GripApply::Absolute(target.last_world + delta)
|
||||
};
|
||||
(target.handle, target.grip_id, apply)
|
||||
})
|
||||
.collect();
|
||||
let lengthen = grip.mode == GripEditMode::Lengthen;
|
||||
let actions: Vec<_> = if lengthen {
|
||||
Vec::new()
|
||||
} else {
|
||||
grip.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let apply = if target.is_translate {
|
||||
GripApply::Translate(delta)
|
||||
} else {
|
||||
GripApply::Absolute(target.last_world + delta)
|
||||
};
|
||||
(target.handle, target.grip_id, apply)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if lengthen {
|
||||
let original = self
|
||||
.grip_originals
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == grip.handle)
|
||||
.map(|(_, entity)| entity.clone());
|
||||
if let Some(original) = original {
|
||||
let action = crate::scene::model::object::GripMenuAction::Lengthen;
|
||||
let value = crate::scene::view::dispatch::grip_menu_point_value(
|
||||
&original,
|
||||
grip.grip_id,
|
||||
action,
|
||||
snapped,
|
||||
);
|
||||
if let Some(value) = value {
|
||||
if let Some(current) =
|
||||
self.tabs[i].scene.document.get_entity_mut(grip.handle)
|
||||
{
|
||||
*current = original;
|
||||
crate::entities::traits::EntityTypeOps::apply_grip_menu_value(
|
||||
current,
|
||||
grip.grip_id,
|
||||
action,
|
||||
value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Arc point grips are coupled: moving one point must rebuild the
|
||||
// circle from the drag-start start/middle/end set, otherwise the
|
||||
// supposedly fixed points drift a little on every mouse event.
|
||||
let mut arc_grip_edits: rustc_hash::FxHashMap<
|
||||
Handle,
|
||||
Vec<(usize, glam::DVec3)>,
|
||||
> = rustc_hash::FxHashMap::default();
|
||||
for (handle, grip_id, apply) in &actions {
|
||||
if let GripApply::Absolute(point) = apply {
|
||||
if (1..=3).contains(grip_id) {
|
||||
arc_grip_edits
|
||||
.entry(*handle)
|
||||
.or_default()
|
||||
.push((*grip_id, *point));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rebuilt_arcs = rustc_hash::FxHashSet::default();
|
||||
for (handle, edits) in arc_grip_edits {
|
||||
let original = self
|
||||
.grip_originals
|
||||
.iter()
|
||||
.find(|(original_handle, _)| *original_handle == handle)
|
||||
.map(|(_, entity)| entity.clone());
|
||||
let Some(original) = original else {
|
||||
continue;
|
||||
};
|
||||
let Some(current) = self.tabs[i].scene.document.get_entity_mut(handle) else {
|
||||
continue;
|
||||
};
|
||||
if crate::scene::view::dispatch::refit_arc_grips(
|
||||
current,
|
||||
&original,
|
||||
&edits,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
rebuilt_arcs.insert(handle);
|
||||
}
|
||||
}
|
||||
let refit_arc_targets: Vec<_> = grip
|
||||
.targets
|
||||
.iter()
|
||||
|
|
@ -1033,7 +1107,9 @@ impl OpenCADStudio {
|
|||
})
|
||||
.collect();
|
||||
for (handle, grip_id, apply) in actions {
|
||||
self.tabs[i].scene.apply_grip(handle, grip_id, apply);
|
||||
if !rebuilt_arcs.contains(&handle) {
|
||||
self.tabs[i].scene.apply_grip(handle, grip_id, apply);
|
||||
}
|
||||
}
|
||||
for (handle, vertex_id, original_bulge) in refit_arc_targets {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
|
|
@ -2334,6 +2410,10 @@ impl OpenCADStudio {
|
|||
// Engaging click — stay hot, wait for the placement click.
|
||||
return Task::none();
|
||||
}
|
||||
if grip.mode == GripEditMode::Lengthen {
|
||||
self.grip_pending = None;
|
||||
self.command_line.input.clear();
|
||||
}
|
||||
let added_vertex_focus = grip.targets.iter().find_map(|target| {
|
||||
let original = self
|
||||
.grip_originals
|
||||
|
|
|
|||
|
|
@ -451,10 +451,16 @@ impl OpenCADStudio {
|
|||
|| (Some(handle) == sel_h
|
||||
&& Some(grip_id) == current_vertex_grip)
|
||||
});
|
||||
let is_hovered = owner.is_some_and(|handle| {
|
||||
self.grip_hover.as_ref().is_some_and(|hover| {
|
||||
hover.handle == handle && hover.grip_id == grip_id
|
||||
})
|
||||
});
|
||||
crate::ui::overlay::GripMarker {
|
||||
pos: screen,
|
||||
shape,
|
||||
is_hot,
|
||||
is_hovered,
|
||||
dir,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use truck_modeling::{builder, Point3};
|
|||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
};
|
||||
use crate::entities::traits::TruckConvertible;
|
||||
use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject};
|
||||
|
|
@ -103,26 +104,114 @@ fn to_truck(arc: &Arc) -> TruckEntity {
|
|||
}
|
||||
}
|
||||
|
||||
fn angle_span(start: f32, end: f32) -> f32 {
|
||||
let mut span = end - start;
|
||||
if span < 0.0 {
|
||||
span += std::f32::consts::TAU;
|
||||
fn control_points(arc: &Arc) -> [glam::DVec3; 3] {
|
||||
let center = glam::DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
let sweep = (arc.end_angle - arc.start_angle).rem_euclid(TAU);
|
||||
let point = |angle: f64| {
|
||||
center
|
||||
+ glam::DVec3::new(
|
||||
arc.radius * angle.cos(),
|
||||
arc.radius * angle.sin(),
|
||||
0.0,
|
||||
)
|
||||
};
|
||||
[
|
||||
point(arc.start_angle),
|
||||
point(arc.start_angle + sweep * 0.5),
|
||||
point(arc.end_angle),
|
||||
]
|
||||
}
|
||||
|
||||
fn circumcircle(
|
||||
a: glam::DVec3,
|
||||
b: glam::DVec3,
|
||||
c: glam::DVec3,
|
||||
) -> Option<(glam::DVec3, f64)> {
|
||||
// Work relative to the first point so large drawing coordinates do not
|
||||
// lose the small differences that define the circle.
|
||||
let ab = b - a;
|
||||
let ac = c - a;
|
||||
let bc = c - b;
|
||||
let scale2 = ab
|
||||
.length_squared()
|
||||
.max(ac.length_squared())
|
||||
.max(bc.length_squared());
|
||||
if scale2 < 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
span
|
||||
let det = 2.0 * (ab.x * ac.y - ab.y * ac.x);
|
||||
if det.abs() <= scale2 * 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let ab2 = ab.x * ab.x + ab.y * ab.y;
|
||||
let ac2 = ac.x * ac.x + ac.y * ac.y;
|
||||
let center = a
|
||||
+ glam::DVec3::new(
|
||||
(ab2 * ac.y - ac2 * ab.y) / det,
|
||||
(ab.x * ac2 - ac.x * ab2) / det,
|
||||
0.0,
|
||||
);
|
||||
let radius = center.distance(a);
|
||||
radius.is_finite().then_some((center, radius))
|
||||
}
|
||||
|
||||
pub(crate) fn refit_grips(
|
||||
arc: &mut Arc,
|
||||
original: &Arc,
|
||||
edits: &[(usize, glam::DVec3)],
|
||||
) -> bool {
|
||||
let mut points = control_points(original);
|
||||
let mut changed = false;
|
||||
for &(grip_id, point) in edits {
|
||||
let index = match grip_id {
|
||||
1 => 0,
|
||||
2 => 2,
|
||||
3 => 1,
|
||||
_ => continue,
|
||||
};
|
||||
points[index] = point;
|
||||
changed = true;
|
||||
}
|
||||
if !changed {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some((center, radius)) = circumcircle(points[0], points[1], points[2]) else {
|
||||
return false;
|
||||
};
|
||||
if radius <= 1.0e-9 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let start = (points[0].y - center.y).atan2(points[0].x - center.x);
|
||||
let middle = (points[1].y - center.y).atan2(points[1].x - center.x);
|
||||
let end = (points[2].y - center.y).atan2(points[2].x - center.x);
|
||||
let sweep = (end - start).rem_euclid(TAU);
|
||||
let middle_sweep = (middle - start).rem_euclid(TAU);
|
||||
// Crossing the two fixed points makes the three-point definition
|
||||
// degenerate before it reverses. Keep the last valid preview instead of
|
||||
// swapping the start and end grip identities under the cursor.
|
||||
if sweep <= 1.0e-9 || middle_sweep > sweep + 1.0e-9 {
|
||||
return false;
|
||||
}
|
||||
|
||||
arc.center.x = center.x;
|
||||
arc.center.y = center.y;
|
||||
arc.center.z = original.center.z;
|
||||
arc.radius = radius;
|
||||
arc.start_angle = start;
|
||||
arc.end_angle = end;
|
||||
true
|
||||
}
|
||||
|
||||
fn grips(arc: &Arc) -> Vec<GripDef> {
|
||||
let ctr = glam::DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
||||
let r = arc.radius;
|
||||
let sa = arc.start_angle as f32;
|
||||
let ea = arc.end_angle as f32;
|
||||
let ma = (sa + angle_span(sa, ea) * 0.5) as f64;
|
||||
let (sa, ea) = (arc.start_angle, arc.end_angle);
|
||||
let [start, middle, end] = control_points(arc);
|
||||
vec![
|
||||
center_grip(0, ctr),
|
||||
square_grip(1, ctr + glam::DVec3::new(r * sa.cos(), r * sa.sin(), 0.0)),
|
||||
square_grip(2, ctr + glam::DVec3::new(r * ea.cos(), r * ea.sin(), 0.0)),
|
||||
center_grip(3, ctr + glam::DVec3::new(r * ma.cos(), r * ma.sin(), 0.0)),
|
||||
square_grip(1, start),
|
||||
square_grip(2, end),
|
||||
square_grip(3, middle),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -204,31 +293,9 @@ fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) {
|
|||
arc.center.y = p.y;
|
||||
arc.center.z = p.z;
|
||||
}
|
||||
(1, GripApply::Absolute(p)) => {
|
||||
let dx = p.x - arc.center.x;
|
||||
let dy = p.y - arc.center.y;
|
||||
arc.start_angle = dy.atan2(dx);
|
||||
}
|
||||
(2, GripApply::Absolute(p)) => {
|
||||
let dx = p.x - arc.center.x;
|
||||
let dy = p.y - arc.center.y;
|
||||
arc.end_angle = dy.atan2(dx);
|
||||
}
|
||||
(3, GripApply::Translate(d)) => {
|
||||
let sa = arc.start_angle;
|
||||
let ea = arc.end_angle;
|
||||
let span = angle_span(sa as f32, ea as f32) as f64;
|
||||
let mid_a = sa + span * 0.5;
|
||||
let current_mid_x = arc.center.x + arc.radius * mid_a.cos();
|
||||
let current_mid_y = arc.center.y + arc.radius * mid_a.sin();
|
||||
let new_mid_x = current_mid_x + d.x;
|
||||
let new_mid_y = current_mid_y + d.y;
|
||||
let dx = new_mid_x - arc.center.x;
|
||||
let dy = new_mid_y - arc.center.y;
|
||||
let new_r = (dx * dx + dy * dy).sqrt();
|
||||
if new_r > 1e-6 {
|
||||
arc.radius = new_r;
|
||||
}
|
||||
(1..=3, GripApply::Absolute(p)) => {
|
||||
let original = arc.clone();
|
||||
let _ = refit_grips(arc, &original, &[(grip_id, p)]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -316,6 +383,29 @@ impl crate::entities::traits::Grippable for Arc {
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if !matches!(action, A::Lengthen) || self.radius <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
let cursor_angle = (point.y - self.center.y).atan2(point.x - self.center.x);
|
||||
let current_sweep = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
||||
let desired_sweep = match grip_id {
|
||||
1 => (self.end_angle - cursor_angle).rem_euclid(TAU),
|
||||
2 => (cursor_angle - self.start_angle).rem_euclid(TAU),
|
||||
_ => return None,
|
||||
};
|
||||
if desired_sweep <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
Some((desired_sweep - current_sweep) * self.radius)
|
||||
}
|
||||
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
|
|
|
|||
|
|
@ -210,6 +210,34 @@ impl crate::entities::traits::Grippable for Line {
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if !matches!(action, A::Lengthen) {
|
||||
return None;
|
||||
}
|
||||
let direction = glam::DVec3::new(
|
||||
self.end.x - self.start.x,
|
||||
self.end.y - self.start.y,
|
||||
self.end.z - self.start.z,
|
||||
);
|
||||
let length = direction.length();
|
||||
if length < 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let unit = direction / length;
|
||||
let value = match grip_id {
|
||||
0 => (glam::DVec3::new(self.start.x, self.start.y, self.start.z) - point).dot(unit),
|
||||
1 => (point - glam::DVec3::new(self.end.x, self.end.y, self.end.z)).dot(unit),
|
||||
_ => return None,
|
||||
};
|
||||
(length + value > 1.0e-9).then_some(value)
|
||||
}
|
||||
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,17 @@ pub trait Grippable {
|
|||
) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
/// Convert an interactive cursor point into the numeric value consumed by
|
||||
/// `apply_grip_menu_value`. Actions without a point-driven form return
|
||||
/// `None`.
|
||||
fn grip_menu_point_value(
|
||||
&self,
|
||||
_grip_id: usize,
|
||||
_action: GripMenuAction,
|
||||
_point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropertyEditable {
|
||||
|
|
@ -163,6 +174,12 @@ pub trait EntityTypeOps {
|
|||
grip_id: usize,
|
||||
action: GripMenuAction,
|
||||
) -> Option<&'static str>;
|
||||
fn grip_menu_point_value(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
action: GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64>;
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
|
|
@ -405,6 +422,28 @@ impl EntityTypeOps for EntityType {
|
|||
)
|
||||
}
|
||||
|
||||
fn grip_menu_point_value(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
action: GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
dispatch!(self,
|
||||
|e| Grippable::grip_menu_point_value(e, grip_id, action, point),
|
||||
[
|
||||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => None,
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
|
|
|
|||
|
|
@ -24,10 +24,18 @@ pub struct GripEdit {
|
|||
pub origin_world: DVec3,
|
||||
/// Last world-space cursor position (needed for incremental delta on translate drags).
|
||||
pub last_world: DVec3,
|
||||
/// How cursor movement modifies the selected grip.
|
||||
pub mode: GripEditMode,
|
||||
/// Every hot grip moved by this edit. A normal grip edit contains one target.
|
||||
pub targets: Vec<GripTarget>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum GripEditMode {
|
||||
Stretch,
|
||||
Lengthen,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GripTarget {
|
||||
pub handle: Handle,
|
||||
|
|
@ -48,6 +56,7 @@ impl GripEdit {
|
|||
grip_id,
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
mode: GripEditMode::Stretch,
|
||||
targets: vec![GripTarget {
|
||||
handle,
|
||||
grip_id,
|
||||
|
|
@ -56,6 +65,12 @@ impl GripEdit {
|
|||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lengthen(handle: Handle, grip_id: usize, world: DVec3) -> Self {
|
||||
let mut edit = Self::single(handle, grip_id, false, world);
|
||||
edit.mode = GripEditMode::Lengthen;
|
||||
edit
|
||||
}
|
||||
}
|
||||
|
||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -245,6 +245,57 @@ pub fn apply_grip(entity: &mut EntityType, grip_id: usize, apply: crate::scene::
|
|||
EntityTypeOps::apply_grip(entity, grip_id, apply);
|
||||
}
|
||||
|
||||
/// Rebuild an arc from its drag-start control points and the point grips moved
|
||||
/// in the current frame. Returns `None` for non-arc entities and `Some(false)`
|
||||
/// when the requested points are temporarily degenerate.
|
||||
pub fn refit_arc_grips(
|
||||
entity: &mut EntityType,
|
||||
original: &EntityType,
|
||||
edits: &[(usize, glam::DVec3)],
|
||||
) -> Option<bool> {
|
||||
let EntityType::Arc(arc) = entity else {
|
||||
return None;
|
||||
};
|
||||
let EntityType::Arc(original_arc) = original else {
|
||||
return None;
|
||||
};
|
||||
let edits = match planar_ocs_normal(original) {
|
||||
Some(normal) => edits
|
||||
.iter()
|
||||
.map(|&(grip_id, point)| {
|
||||
let (x, y, z) =
|
||||
super::transform::wcs_point_to_ocs((point.x, point.y, point.z), normal);
|
||||
(grip_id, glam::DVec3::new(x, y, z))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
None => edits.to_vec(),
|
||||
};
|
||||
Some(crate::entities::arc::refit_grips(
|
||||
arc,
|
||||
original_arc,
|
||||
&edits,
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert a world-space cursor point into the value expected by an
|
||||
/// interactive grip-menu action.
|
||||
pub fn grip_menu_point_value(
|
||||
entity: &EntityType,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
let point = match planar_ocs_normal(entity) {
|
||||
Some(normal) => {
|
||||
let (x, y, z) =
|
||||
super::transform::wcs_point_to_ocs((point.x, point.y, point.z), normal);
|
||||
glam::DVec3::new(x, y, z)
|
||||
}
|
||||
None => point,
|
||||
};
|
||||
EntityTypeOps::grip_menu_point_value(entity, grip_id, action, point)
|
||||
}
|
||||
|
||||
pub fn apply_transform(entity: &mut EntityType, t: &EntityTransform) {
|
||||
EntityTypeOps::apply_transform(entity, t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ pub struct GripMarker {
|
|||
pub shape: GripShape,
|
||||
/// True → grip is currently being dragged (drawn filled red).
|
||||
pub is_hot: bool,
|
||||
/// True → pointer is over this grip (drawn with the hover fill).
|
||||
pub is_hovered: bool,
|
||||
/// World-XY direction vector — only consumed by the `Rectangle`
|
||||
/// shape to orient the box along its segment. `None` for grips
|
||||
/// that don't need rotation.
|
||||
|
|
@ -338,6 +340,17 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
|||
|
||||
if grip.is_hot {
|
||||
frame.fill(&path, theme.palette().danger.base.color);
|
||||
} else if grip.is_hovered {
|
||||
let pair = theme.palette().primary.strong;
|
||||
frame.fill(&path, pair.color);
|
||||
frame.stroke(
|
||||
&path,
|
||||
canvas::Stroke {
|
||||
width: 1.5,
|
||||
style: canvas::Style::Solid(pair.text),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let palette = theme.palette();
|
||||
let color = palette.primary.base.color;
|
||||
|
|
|
|||
Loading…
Reference in a new issue