Feat: hatch boundary grip editing — all edge types supported

Polyline, Line, CircularArc, EllipticArc, and Spline boundary edges
all get diamond grips at their control vertices. Dragging a grip updates
the underlying DXF boundary vertex and immediately rebuilds the GPU
hatch model via Scene::apply_grip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-03 19:16:17 +03:00
commit 749f3b99dc
3 changed files with 158 additions and 3 deletions

View file

@ -1,9 +1,10 @@
use acadrust::entities::{BoundaryEdge, Hatch}; use acadrust::entities::{BoundaryEdge, Hatch};
use glam::Vec3;
use crate::command::EntityTransform; use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro}; use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro};
use crate::entities::traits::{PropertyEditable, Transformable}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
use crate::scene::object::{PropSection, PropValue, Property}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
fn properties(h: &Hatch) -> PropSection { fn properties(h: &Hatch) -> PropSection {
let pattern_type = match h.pattern_type { let pattern_type = match h.pattern_type {
@ -167,3 +168,146 @@ impl Transformable for Hatch {
apply_transform(self, t); apply_transform(self, t);
} }
} }
// ── Grip editing ───────────────────────────────────────────────────────────
/// Assign sequential grip IDs across all boundary paths and edges.
/// Exposed control points per edge type:
/// Polyline → each vertex (x, y)
/// Line → start, end
/// CircularArc → center
/// EllipticArc → center
/// Spline → fit points if present, else control points (x, y)
impl Grippable for Hatch {
fn grips(&self) -> Vec<GripDef> {
let elev = self.elevation as f32;
let mut out = Vec::new();
let mut id = 0usize;
for path in &self.paths {
for edge in &path.edges {
match edge {
BoundaryEdge::Polyline(p) => {
for v in &p.vertices {
out.push(diamond_grip(id, Vec3::new(v.x as f32, v.y as f32, elev)));
id += 1;
}
}
BoundaryEdge::Line(l) => {
out.push(diamond_grip(id, Vec3::new(l.start.x as f32, l.start.y as f32, elev)));
id += 1;
out.push(diamond_grip(id, Vec3::new(l.end.x as f32, l.end.y as f32, elev)));
id += 1;
}
BoundaryEdge::CircularArc(a) => {
out.push(diamond_grip(id, Vec3::new(a.center.x as f32, a.center.y as f32, elev)));
id += 1;
}
BoundaryEdge::EllipticArc(e) => {
out.push(diamond_grip(id, Vec3::new(e.center.x as f32, e.center.y as f32, elev)));
id += 1;
}
BoundaryEdge::Spline(s) => {
let pts: Vec<[f64; 2]> = if !s.fit_points.is_empty() {
s.fit_points.iter().map(|p| [p.x, p.y]).collect()
} else {
s.control_points.iter().map(|p| [p.x, p.y]).collect()
};
for [x, y] in pts {
out.push(diamond_grip(id, Vec3::new(x as f32, y as f32, elev)));
id += 1;
}
}
}
}
}
out
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
let elev = self.elevation as f32;
let mut id = 0usize;
fn resolve(apply: &GripApply, cur: Vec3) -> (f64, f64) {
let p = match apply {
GripApply::Absolute(p) => *p,
GripApply::Translate(d) => cur + *d,
};
(p.x as f64, p.y as f64)
}
'outer: for path in &mut self.paths {
for edge in &mut path.edges {
match edge {
BoundaryEdge::Polyline(p) => {
for v in &mut p.vertices {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(v.x as f32, v.y as f32, elev));
v.x = nx;
v.y = ny;
break 'outer;
}
id += 1;
}
}
BoundaryEdge::Line(l) => {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(l.start.x as f32, l.start.y as f32, elev));
l.start.x = nx;
l.start.y = ny;
break 'outer;
}
id += 1;
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(l.end.x as f32, l.end.y as f32, elev));
l.end.x = nx;
l.end.y = ny;
break 'outer;
}
id += 1;
}
BoundaryEdge::CircularArc(a) => {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(a.center.x as f32, a.center.y as f32, elev));
a.center.x = nx;
a.center.y = ny;
break 'outer;
}
id += 1;
}
BoundaryEdge::EllipticArc(e) => {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(e.center.x as f32, e.center.y as f32, elev));
e.center.x = nx;
e.center.y = ny;
break 'outer;
}
id += 1;
}
BoundaryEdge::Spline(s) => {
if !s.fit_points.is_empty() {
for fp in &mut s.fit_points {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(fp.x as f32, fp.y as f32, elev));
fp.x = nx;
fp.y = ny;
break 'outer;
}
id += 1;
}
} else {
for cp in &mut s.control_points {
if id == grip_id {
let (nx, ny) = resolve(&apply, Vec3::new(cp.x as f32, cp.y as f32, elev));
cp.x = nx;
cp.y = ny;
break 'outer;
}
id += 1;
}
}
}
}
}
}
}
}

View file

@ -95,6 +95,7 @@ impl EntityTypeOps for EntityType {
EntityType::Leader(leader) => Grippable::grips(leader), EntityType::Leader(leader) => Grippable::grips(leader),
EntityType::MultiLeader(ml) => Grippable::grips(ml), EntityType::MultiLeader(ml) => Grippable::grips(ml),
EntityType::Dimension(dim) => Grippable::grips(dim), EntityType::Dimension(dim) => Grippable::grips(dim),
EntityType::Hatch(hatch) => Grippable::grips(hatch),
_ => vec![], _ => vec![],
} }
} }
@ -290,6 +291,7 @@ impl EntityTypeOps for EntityType {
EntityType::Leader(leader) => Grippable::apply_grip(leader, grip_id, apply), EntityType::Leader(leader) => Grippable::apply_grip(leader, grip_id, apply),
EntityType::MultiLeader(ml) => Grippable::apply_grip(ml, grip_id, apply), EntityType::MultiLeader(ml) => Grippable::apply_grip(ml, grip_id, apply),
EntityType::Dimension(dim) => Grippable::apply_grip(dim, grip_id, apply), EntityType::Dimension(dim) => Grippable::apply_grip(dim, grip_id, apply),
EntityType::Hatch(hatch) => Grippable::apply_grip(hatch, grip_id, apply),
_ => {} _ => {}
} }
} }

View file

@ -1660,6 +1660,15 @@ impl Scene {
if let Some(entity) = self.document.get_entity_mut(handle) { if let Some(entity) = self.document.get_entity_mut(handle) {
dispatch::apply_grip(entity, grip_id, apply); dispatch::apply_grip(entity, grip_id, apply);
} }
// Rebuild the GPU hatch model when a hatch boundary vertex moves.
if let Some(EntityType::Hatch(dxf)) = self.document.get_entity(handle) {
let color = tessellate::aci_to_rgba(&dxf.common.color);
if let Some(model) = Self::hatch_model_from_dxf(dxf, color) {
self.hatches.insert(handle, model);
} else {
self.hatches.remove(&handle);
}
}
} }
// ── Hit-test convenience: wire name → Handle ────────────────────────── // ── Hit-test convenience: wire name → Handle ──────────────────────────