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:
parent
36d51286d4
commit
749f3b99dc
3 changed files with 158 additions and 3 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ──────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue