feat(ellipse): add complete grip editing
This commit is contained in:
commit
0f250bb485
5 changed files with 193 additions and 92 deletions
|
|
@ -462,7 +462,7 @@ pub fn rectangle_grip(id: usize, world: glam::DVec3, dir: [f32; 2]) -> GripDef {
|
||||||
world,
|
world,
|
||||||
is_midpoint: true,
|
is_midpoint: true,
|
||||||
shape: GripShape::Rectangle,
|
shape: GripShape::Rectangle,
|
||||||
dir: Some(dir),
|
dir: Some(glam::DVec3::new(dir[0] as f64, dir[1] as f64, 0.0)),
|
||||||
axis: None,
|
axis: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -479,6 +479,18 @@ pub fn triangle_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Triangle grip pointing along a world-space direction.
|
||||||
|
pub fn oriented_triangle_grip(id: usize, world: glam::DVec3, dir: glam::DVec3) -> GripDef {
|
||||||
|
GripDef {
|
||||||
|
id,
|
||||||
|
world,
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: GripShape::Triangle,
|
||||||
|
dir: Some(dir),
|
||||||
|
axis: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Editable ANGLE row: displays via AUNITS/AUPREC. Angle rows used the
|
/// Editable ANGLE row: displays via AUNITS/AUPREC. Angle rows used the
|
||||||
/// LINEAR formatter, so LUNITS=Architectural showed a block rotation as
|
/// LINEAR formatter, so LUNITS=Architectural showed a block rotation as
|
||||||
/// feet-and-inches and the string wouldn't parse back (#297). Value in
|
/// feet-and-inches and the string wouldn't parse back (#297). Value in
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
use acadrust::entities::Ellipse;
|
use acadrust::entities::Ellipse;
|
||||||
|
use cadkernel::geom2d::{
|
||||||
|
Curve as KernelCurve, EllipseArc as KernelEllipseArc, Tolerance, Vec2 as KernelVec2,
|
||||||
|
};
|
||||||
|
use cadkernel::space::PlanarCurve;
|
||||||
|
|
||||||
use crate::entities::curve::CurveSnap;
|
use crate::entities::curve::CurveSnap;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
center_grip, edit_prop as edit, oriented_triangle_grip, parse_f64, ro_prop as ro,
|
||||||
|
square_grip,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::RenderConvertible;
|
use crate::entities::traits::RenderConvertible;
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||||
|
|
@ -58,33 +63,56 @@ fn to_render(ell: &Ellipse) -> RenderEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A curve through every one of `points` in order.
|
fn ellipse_curves(ell: &Ellipse) -> Option<(PlanarCurve, PlanarCurve)> {
|
||||||
///
|
let arc = crate::entities::curve::ellipse_curve(ell)?;
|
||||||
/// Degree one, so the control polygon *is* the curve — the same thing the
|
let KernelCurve::Ellipse(piece) = &arc.curve else {
|
||||||
/// hand-built sampling produced, without restating how a B-spline is put
|
return None;
|
||||||
/// together at each site.
|
};
|
||||||
|
let full = PlanarCurve::new(
|
||||||
|
arc.plane,
|
||||||
|
KernelCurve::Ellipse(KernelEllipseArc::full(piece.ellipse)),
|
||||||
|
);
|
||||||
|
Some((arc, full))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_closed(ell: &Ellipse) -> bool {
|
||||||
|
crate::entities::curve::ellipse_curve(ell).is_some_and(|curve| curve.curve.is_closed())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameter_at_point(ell: &Ellipse, point: glam::DVec3) -> Option<f64> {
|
||||||
|
let (_, full) = ellipse_curves(ell)?;
|
||||||
|
Some(
|
||||||
|
(full.parameter_at(point.to_array())? * std::f64::consts::TAU)
|
||||||
|
.rem_euclid(std::f64::consts::TAU),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn grips(ell: &Ellipse) -> Vec<GripDef> {
|
fn grips(ell: &Ellipse) -> Vec<GripDef> {
|
||||||
let ctr = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z);
|
let fallback = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z);
|
||||||
let maj = glam::DVec3::new(
|
let Some((arc, full)) = ellipse_curves(ell) else {
|
||||||
ell.center.x + ell.major_axis.x,
|
return vec![center_grip(0, fallback)];
|
||||||
ell.center.y + ell.major_axis.y,
|
|
||||||
ell.center.z + ell.major_axis.z,
|
|
||||||
);
|
|
||||||
let major_xy =
|
|
||||||
((ell.major_axis.x * ell.major_axis.x + ell.major_axis.y * ell.major_axis.y) as f64).sqrt();
|
|
||||||
let (px, py) = if major_xy > 1e-10 {
|
|
||||||
let s = ell.major_axis_length() * ell.minor_axis_ratio / major_xy;
|
|
||||||
(-ell.major_axis.y * s, ell.major_axis.x * s)
|
|
||||||
} else {
|
|
||||||
(0.0, ell.major_axis_length() * ell.minor_axis_ratio)
|
|
||||||
};
|
};
|
||||||
let min = glam::DVec3::new(ell.center.x + px, ell.center.y + py, ell.center.z);
|
let KernelCurve::Ellipse(piece) = &full.curve else {
|
||||||
vec![
|
return vec![center_grip(0, fallback)];
|
||||||
|
};
|
||||||
|
let ctr = glam::DVec3::from_array(full.plane.point_at(piece.ellipse.centre));
|
||||||
|
let axes = [0.0, 0.25, 0.5, 0.75].map(|t| glam::DVec3::from_array(full.point_at(t)));
|
||||||
|
let mut grips = vec![
|
||||||
center_grip(0, ctr),
|
center_grip(0, ctr),
|
||||||
square_grip(1, maj),
|
square_grip(1, axes[0]),
|
||||||
square_grip(2, min),
|
square_grip(2, axes[1]),
|
||||||
]
|
square_grip(3, axes[2]),
|
||||||
|
square_grip(4, axes[3]),
|
||||||
|
];
|
||||||
|
if !arc.curve.is_closed() {
|
||||||
|
let start = glam::DVec3::from_array(arc.point_at(0.0));
|
||||||
|
let end = glam::DVec3::from_array(arc.point_at(1.0));
|
||||||
|
let start_outward = -glam::DVec3::from_array(arc.tangent_at(0.0));
|
||||||
|
let end_outward = glam::DVec3::from_array(arc.tangent_at(1.0));
|
||||||
|
grips.push(oriented_triangle_grip(5, start, start_outward));
|
||||||
|
grips.push(oriented_triangle_grip(6, end, end_outward));
|
||||||
|
}
|
||||||
|
grips
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(ell: &Ellipse) -> Vec<PropSection> {
|
fn properties(ell: &Ellipse) -> Vec<PropSection> {
|
||||||
|
|
@ -114,9 +142,7 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
|
||||||
let minor_vec = v * r_minor;
|
let minor_vec = v * r_minor;
|
||||||
|
|
||||||
let start_angle = ell.start_parameter.to_degrees().rem_euclid(360.0);
|
let start_angle = ell.start_parameter.to_degrees().rem_euclid(360.0);
|
||||||
let end_angle = if crate::entities::curve::ellipse_curve(ell)
|
let end_angle = if is_closed(ell) {
|
||||||
.is_some_and(|curve| curve.curve.is_closed())
|
|
||||||
{
|
|
||||||
360.0
|
360.0
|
||||||
} else {
|
} else {
|
||||||
ell.end_parameter.to_degrees().rem_euclid(360.0)
|
ell.end_parameter.to_degrees().rem_euclid(360.0)
|
||||||
|
|
@ -188,6 +214,73 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_axis_grip(ell: &mut Ellipse, grip_id: usize, point: glam::DVec3) {
|
||||||
|
let Some((_, full)) = ellipse_curves(ell) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let KernelCurve::Ellipse(piece) = &full.curve else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(projected) = full.plane.project(point.to_array()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let center = KernelVec2::from(piece.ellipse.centre);
|
||||||
|
let mut dragged = KernelVec2::from(projected) - center;
|
||||||
|
let tolerance = Tolerance::default().linear();
|
||||||
|
|
||||||
|
if matches!(grip_id, 1 | 3) {
|
||||||
|
if grip_id == 3 {
|
||||||
|
dragged = -dragged;
|
||||||
|
}
|
||||||
|
if dragged.length() <= tolerance {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let axis = full.plane.vector_at(dragged.to_array());
|
||||||
|
ell.major_axis.x = axis[0];
|
||||||
|
ell.major_axis.y = axis[1];
|
||||||
|
ell.major_axis.z = axis[2];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if grip_id == 4 {
|
||||||
|
dragged = -dragged;
|
||||||
|
}
|
||||||
|
let major_dir = KernelVec2::from(piece.ellipse.major_axis);
|
||||||
|
let minor_dir = major_dir.perpendicular();
|
||||||
|
let major_len = piece.ellipse.major_radius.abs();
|
||||||
|
let minor_len = piece.ellipse.minor_radius.abs();
|
||||||
|
if major_len <= tolerance || minor_len <= tolerance {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let along_major = dragged.dot(major_dir).abs();
|
||||||
|
let along_minor = dragged.dot(minor_dir).abs();
|
||||||
|
let (drag_dir, dragged_len, fixed_dir, fixed_len) = if along_minor >= along_major {
|
||||||
|
(minor_dir, along_minor, major_dir, major_len)
|
||||||
|
} else {
|
||||||
|
(major_dir, along_major, minor_dir, minor_len)
|
||||||
|
};
|
||||||
|
if fixed_len <= tolerance {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (axis, ratio) = if dragged_len >= fixed_len {
|
||||||
|
(
|
||||||
|
drag_dir * dragged_len,
|
||||||
|
(fixed_len / dragged_len).clamp(0.001, 1.0),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
fixed_dir * fixed_len,
|
||||||
|
(dragged_len / fixed_len).clamp(0.001, 1.0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let axis = full.plane.vector_at(axis.to_array());
|
||||||
|
ell.major_axis.x = axis[0];
|
||||||
|
ell.major_axis.y = axis[1];
|
||||||
|
ell.major_axis.z = axis[2];
|
||||||
|
ell.minor_axis_ratio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
||||||
match (grip_id, apply) {
|
match (grip_id, apply) {
|
||||||
(0, GripApply::Translate(d)) => {
|
(0, GripApply::Translate(d)) => {
|
||||||
|
|
@ -200,54 +293,15 @@ fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
||||||
ell.center.y = p.y as f64;
|
ell.center.y = p.y as f64;
|
||||||
ell.center.z = p.z as f64;
|
ell.center.z = p.z as f64;
|
||||||
}
|
}
|
||||||
(1, GripApply::Absolute(p)) => {
|
(1 | 2 | 3 | 4, GripApply::Absolute(p)) => apply_axis_grip(ell, grip_id, p),
|
||||||
ell.major_axis.x = p.x as f64 - ell.center.x;
|
(5 | 6, GripApply::Absolute(p)) => {
|
||||||
ell.major_axis.y = p.y as f64 - ell.center.y;
|
let Some(parameter) = parameter_at_point(ell, p) else {
|
||||||
ell.major_axis.z = p.z as f64 - ell.center.z;
|
|
||||||
}
|
|
||||||
(2, GripApply::Absolute(p)) => {
|
|
||||||
// Minor-axis grip. The two axes are always perpendicular, so this
|
|
||||||
// grip stretches one of them along its fixed direction while the
|
|
||||||
// other stays put. Work in the ellipse's view plane (XY).
|
|
||||||
let mx = ell.major_axis.x;
|
|
||||||
let my = ell.major_axis.y;
|
|
||||||
let major_len = (mx * mx + my * my).sqrt();
|
|
||||||
if major_len <= 1e-10 {
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
let major_unit = (mx / major_len, my / major_len);
|
|
||||||
let minor_unit = (-major_unit.1, major_unit.0);
|
|
||||||
let minor_len = major_len * ell.minor_axis_ratio;
|
|
||||||
|
|
||||||
let dx = p.x as f64 - ell.center.x;
|
|
||||||
let dy = p.y as f64 - ell.center.y;
|
|
||||||
// Which stored axis is actually under the cursor? Below a circle it
|
|
||||||
// is the minor direction; once the drag pushes the minor past the
|
|
||||||
// major and the axes swap, the grabbed endpoint sits on the major
|
|
||||||
// direction. Decide by cursor alignment — NOT by recomputing a perp
|
|
||||||
// from the just-swapped major every frame — so a continuous drag
|
|
||||||
// stays stable instead of flipping the major 90° each move (which
|
|
||||||
// made both axes balloon once the minor reached the major). The
|
|
||||||
// perpendicular (non-dragged) axis keeps its length; the longer axis
|
|
||||||
// is stored as the major so the ratio stays <= 1 per the file format.
|
|
||||||
let along_major = (dx * major_unit.0 + dy * major_unit.1).abs();
|
|
||||||
let along_minor = (dx * minor_unit.0 + dy * minor_unit.1).abs();
|
|
||||||
let (drag_dir, dragged_len, fixed_dir, fixed_len) = if along_minor >= along_major {
|
|
||||||
(minor_unit, along_minor, major_unit, major_len)
|
|
||||||
} else {
|
|
||||||
(major_unit, along_major, minor_unit, minor_len)
|
|
||||||
};
|
};
|
||||||
if fixed_len <= 1e-10 {
|
if grip_id == 5 {
|
||||||
return;
|
ell.start_parameter = parameter;
|
||||||
}
|
|
||||||
if dragged_len >= fixed_len {
|
|
||||||
ell.major_axis.x = drag_dir.0 * dragged_len;
|
|
||||||
ell.major_axis.y = drag_dir.1 * dragged_len;
|
|
||||||
ell.minor_axis_ratio = (fixed_len / dragged_len).clamp(0.001, 1.0);
|
|
||||||
} else {
|
} else {
|
||||||
ell.major_axis.x = fixed_dir.0 * fixed_len;
|
ell.end_parameter = parameter;
|
||||||
ell.major_axis.y = fixed_dir.1 * fixed_len;
|
|
||||||
ell.minor_axis_ratio = (dragged_len / fixed_len).clamp(0.001, 1.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,8 @@ pub struct GripDef {
|
||||||
pub is_midpoint: bool,
|
pub is_midpoint: bool,
|
||||||
/// Visual marker shape for the grip.
|
/// Visual marker shape for the grip.
|
||||||
pub shape: GripShape,
|
pub shape: GripShape,
|
||||||
/// World-XY direction vector used to orient a `Rectangle` grip
|
/// World-space marker direction, projected with the grip position.
|
||||||
/// along its segment. `None` for shapes that don't need rotation
|
pub dir: Option<glam::DVec3>,
|
||||||
/// (Square, Triangle in non-directional contexts).
|
|
||||||
pub dir: Option<[f32; 2]>,
|
|
||||||
/// World-space axis that constrains this grip's drag.
|
/// World-space axis that constrains this grip's drag.
|
||||||
pub axis: Option<glam::DVec3>,
|
pub axis: Option<glam::DVec3>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ impl GripEdit {
|
||||||
|
|
||||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn marker_direction(from: Vec2, to: Vec2) -> [f32; 2] {
|
||||||
|
[to.x - from.x, from.y - to.y]
|
||||||
|
}
|
||||||
|
|
||||||
/// Project a slice of `GripDef`s to screen space.
|
/// Project a slice of `GripDef`s to screen space.
|
||||||
/// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip.
|
/// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip.
|
||||||
pub fn grips_to_screen(
|
pub fn grips_to_screen(
|
||||||
|
|
@ -91,11 +95,18 @@ pub fn grips_to_screen(
|
||||||
// Project from the f64 grip position via the relative-to-eye path so
|
// Project from the f64 grip position via the relative-to-eye path so
|
||||||
// the grip stays glued to the wire at UTM-scale coordinates (an
|
// the grip stays glued to the wire at UTM-scale coordinates (an
|
||||||
// `as_vec3` cast first would quantize it ~0.5 m off at high zoom).
|
// `as_vec3` cast first would quantize it ~0.5 m off at high zoom).
|
||||||
let screen = match camera.project(g.world, bounds) {
|
let projected = camera.project(g.world, bounds);
|
||||||
|
let screen = match projected {
|
||||||
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
|
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
|
||||||
None => Point::new(f32::NAN, f32::NAN),
|
None => Point::new(f32::NAN, f32::NAN),
|
||||||
};
|
};
|
||||||
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
let dir = g.dir.and_then(|dir| {
|
||||||
|
Some(marker_direction(
|
||||||
|
projected?,
|
||||||
|
camera.project(g.world + dir, bounds)?,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
(g.id, screen, g.is_midpoint, g.shape, dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -110,14 +121,24 @@ pub fn grips_to_screen_paper(
|
||||||
half_h: f32,
|
half_h: f32,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
||||||
|
let project = |world: DVec3| {
|
||||||
|
Vec2::new(
|
||||||
|
(world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
||||||
|
(ty + half_h - world.y as f32) / (2.0 * half_h) * bounds.height,
|
||||||
|
)
|
||||||
|
};
|
||||||
grips
|
grips
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| {
|
.map(|g| {
|
||||||
let screen = Point::new(
|
let projected = project(g.world);
|
||||||
(g.world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
let screen = Point::new(projected.x, projected.y);
|
||||||
(ty + half_h - g.world.y as f32) / (2.0 * half_h) * bounds.height,
|
let dir = g.dir.map(|dir| {
|
||||||
);
|
[
|
||||||
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
dir.x as f32 / (2.0 * half_w) * bounds.width,
|
||||||
|
dir.y as f32 / (2.0 * half_h) * bounds.height,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
(g.id, screen, g.is_midpoint, g.shape, dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -207,11 +228,18 @@ pub fn grips_to_screen_rte(
|
||||||
grips
|
grips
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| {
|
.map(|g| {
|
||||||
let screen = match project_rte(g.world, view_rot, eye, bounds) {
|
let projected = project_rte(g.world, view_rot, eye, bounds);
|
||||||
|
let screen = match projected {
|
||||||
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
|
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
|
||||||
None => Point::new(f32::NAN, f32::NAN),
|
None => Point::new(f32::NAN, f32::NAN),
|
||||||
};
|
};
|
||||||
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
let dir = g.dir.and_then(|dir| {
|
||||||
|
Some(marker_direction(
|
||||||
|
projected?,
|
||||||
|
project_rte(g.world + dir, view_rot, eye, bounds)?,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
(g.id, screen, g.is_midpoint, g.shape, dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,7 @@ pub struct GripMarker {
|
||||||
pub is_hot: bool,
|
pub is_hot: bool,
|
||||||
/// True → pointer is over this grip (drawn with the hover fill).
|
/// True → pointer is over this grip (drawn with the hover fill).
|
||||||
pub is_hovered: bool,
|
pub is_hovered: bool,
|
||||||
/// World-XY direction vector — only consumed by the `Rectangle`
|
/// Screen-plane direction with positive Y pointing up.
|
||||||
/// shape to orient the box along its segment. `None` for grips
|
|
||||||
/// that don't need rotation.
|
|
||||||
pub dir: Option<[f32; 2]>,
|
pub dir: Option<[f32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,9 +384,20 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
GripShape::Triangle => canvas::Path::new(|b| {
|
GripShape::Triangle => canvas::Path::new(|b| {
|
||||||
b.move_to(Point::new(sp.x, sp.y - h));
|
let (forward_x, forward_y) = match grip.dir {
|
||||||
b.line_to(Point::new(sp.x + h, sp.y + h));
|
Some([dx, dy]) if (dx * dx + dy * dy) > 1e-12 => {
|
||||||
b.line_to(Point::new(sp.x - h, sp.y + h));
|
let length = (dx * dx + dy * dy).sqrt();
|
||||||
|
(dx / length, -dy / length)
|
||||||
|
}
|
||||||
|
_ => (0.0, -1.0),
|
||||||
|
};
|
||||||
|
let side_x = -forward_y;
|
||||||
|
let side_y = forward_x;
|
||||||
|
let tip = Point::new(sp.x + forward_x * h, sp.y + forward_y * h);
|
||||||
|
let base = Point::new(sp.x - forward_x * h, sp.y - forward_y * h);
|
||||||
|
b.move_to(tip);
|
||||||
|
b.line_to(Point::new(base.x + side_x * h, base.y + side_y * h));
|
||||||
|
b.line_to(Point::new(base.x - side_x * h, base.y - side_y * h));
|
||||||
b.close();
|
b.close();
|
||||||
}),
|
}),
|
||||||
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
|
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue