Phase 1 of bringing the grip vocabulary in line with the standard CAD convention: drop the per-position-type shape encoding (Diamond for translate, Circle for parameter) and use a plain square for every endpoint / vertex / centre grip. The behavioural midpoint vs vertex distinction stays — it lives on `GripDef::is_midpoint` and drives `Translate` vs `Absolute` in the grip-edit dispatcher — only the marker shape collapses. - `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond / Circle are gone. - `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction vector consumed by `Rectangle` to orient the box along its segment. - `entities::common` exposes `square_grip`, `center_grip` (same square marker, flagged as translate), `rectangle_grip(id, world, dir)` for oriented mid-segment handles, and a kept-for-Phase-2 `triangle_grip`. - `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir` through to the canvas painter; the Rectangle path rotates the box around the grip centre using the world-XY direction (flipping the sin for screen-Y). Every existing `diamond_grip(id, w)` call becomes `center_grip(id, w)` — the visual change is square-replacing-diamond at line midpoints / arc midpoints / circle centres / ellipse centres / viewport / hatch loop centroids / dimension grips / image corners / multileader / ray / underlay / ole2frame / solid3d. `LwPolyline` grows a stretch handle on every segment (straight or arc), drawn as the new oriented rectangle. The grip-edit path translates both segment endpoints for straight segments and adjusts the arc bulge from the new midpoint for arc segments.
130 lines
4.2 KiB
Rust
130 lines
4.2 KiB
Rust
//! OpenCADStudio-style grip editing.
|
|
|
|
use acadrust::Handle;
|
|
use glam::{Mat4, Vec3};
|
|
use iced::{Point, Rectangle};
|
|
|
|
use super::object::{GripDef, GripShape};
|
|
|
|
/// Pixel radius for grip hit-detection.
|
|
pub const GRIP_THRESHOLD_PX: f32 = 8.0;
|
|
/// Half-size of the rendered grip square / diamond in pixels.
|
|
pub const GRIP_HALF_PX: f32 = 5.0;
|
|
|
|
// ── Active drag state ─────────────────────────────────────────────────────
|
|
|
|
/// Stored on `OpenCADStudio` while a grip is being dragged.
|
|
#[derive(Clone, Debug)]
|
|
pub struct GripEdit {
|
|
/// Handle of the entity being edited.
|
|
pub handle: Handle,
|
|
/// Index into the entity's grip list.
|
|
pub grip_id: usize,
|
|
/// `true` → midpoint / translate grip; `false` → endpoint / absolute grip.
|
|
pub is_translate: bool,
|
|
/// World-space position of the grip when the drag started (ortho/polar base).
|
|
pub origin_world: Vec3,
|
|
/// Last world-space cursor position (needed for incremental delta on translate drags).
|
|
pub last_world: Vec3,
|
|
}
|
|
|
|
// ── Screen-space helpers ───────────────────────────────────────────────────
|
|
|
|
/// Project a slice of `GripDef`s to screen space.
|
|
/// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip.
|
|
pub fn grips_to_screen(
|
|
grips: &[GripDef],
|
|
view_proj: Mat4,
|
|
bounds: Rectangle,
|
|
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
|
grips
|
|
.iter()
|
|
.map(|g| {
|
|
let ndc = view_proj.project_point3(g.world);
|
|
let screen = Point::new(
|
|
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
|
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
|
);
|
|
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Paper-space variant: project grips using the 2-D linear `to_px` transform.
|
|
/// Parameters match the `to_px` closure in `paper_canvas.rs`.
|
|
pub fn grips_to_screen_paper(
|
|
grips: &[GripDef],
|
|
tx: f32,
|
|
ty: f32,
|
|
half_w: f32,
|
|
half_h: f32,
|
|
bounds: Rectangle,
|
|
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
|
grips
|
|
.iter()
|
|
.map(|g| {
|
|
let screen = Point::new(
|
|
(g.world.x - tx + half_w) / (2.0 * half_w) * bounds.width,
|
|
(ty + half_h - g.world.y) / (2.0 * half_h) * bounds.height,
|
|
);
|
|
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Paper-space hit-test variant (mirrors `find_hit_grip` but uses 2-D projection).
|
|
pub fn find_hit_grip_paper(
|
|
cursor: Point,
|
|
grips: &[GripDef],
|
|
tx: f32,
|
|
ty: f32,
|
|
half_w: f32,
|
|
half_h: f32,
|
|
bounds: Rectangle,
|
|
) -> Option<(usize, bool, Vec3)> {
|
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
|
let mut best: Option<(usize, bool, Vec3)> = None;
|
|
|
|
for g in grips {
|
|
let screen = Point::new(
|
|
(g.world.x - tx + half_w) / (2.0 * half_w) * bounds.width,
|
|
(ty + half_h - g.world.y) / (2.0 * half_h) * bounds.height,
|
|
);
|
|
let dx = screen.x - cursor.x;
|
|
let dy = screen.y - cursor.y;
|
|
let d = (dx * dx + dy * dy).sqrt();
|
|
if d < best_dist {
|
|
best_dist = d;
|
|
best = Some((g.id, g.is_midpoint, g.world));
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
/// Find the closest grip within `GRIP_THRESHOLD_PX` pixels of `cursor`.
|
|
/// Returns `(grip_id, is_translate, world_pos)` if found, else `None`.
|
|
pub fn find_hit_grip(
|
|
cursor: Point,
|
|
grips: &[GripDef],
|
|
view_proj: Mat4,
|
|
bounds: Rectangle,
|
|
) -> Option<(usize, bool, Vec3)> {
|
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
|
let mut best: Option<(usize, bool, Vec3)> = None;
|
|
|
|
for g in grips {
|
|
let ndc = view_proj.project_point3(g.world);
|
|
let screen = Point::new(
|
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
|
);
|
|
let dx = screen.x - cursor.x;
|
|
let dy = screen.y - cursor.y;
|
|
let d = (dx * dx + dy * dy).sqrt();
|
|
if d < best_dist {
|
|
best_dist = d;
|
|
best = Some((g.id, g.is_midpoint, g.world));
|
|
}
|
|
}
|
|
best
|
|
}
|