diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 1ebb3e54a..deb8d1351 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -1,9 +1,10 @@ use acadrust::entities::{BoundaryEdge, Hatch}; +use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro}; -use crate::entities::traits::{PropertyEditable, Transformable}; -use crate::scene::object::{PropSection, PropValue, Property}; +use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; +use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; fn properties(h: &Hatch) -> PropSection { let pattern_type = match h.pattern_type { @@ -167,3 +168,146 @@ impl Transformable for Hatch { 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 { + 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; + } + } + } + } + } + } + } +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index cf9462c29..3b3c8e0fb 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -95,6 +95,7 @@ impl EntityTypeOps for EntityType { EntityType::Leader(leader) => Grippable::grips(leader), EntityType::MultiLeader(ml) => Grippable::grips(ml), EntityType::Dimension(dim) => Grippable::grips(dim), + EntityType::Hatch(hatch) => Grippable::grips(hatch), _ => vec![], } } @@ -290,6 +291,7 @@ impl EntityTypeOps for EntityType { EntityType::Leader(leader) => Grippable::apply_grip(leader, grip_id, apply), EntityType::MultiLeader(ml) => Grippable::apply_grip(ml, grip_id, apply), EntityType::Dimension(dim) => Grippable::apply_grip(dim, grip_id, apply), + EntityType::Hatch(hatch) => Grippable::apply_grip(hatch, grip_id, apply), _ => {} } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 856a18712..e26bc3627 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1660,6 +1660,15 @@ impl Scene { if let Some(entity) = self.document.get_entity_mut(handle) { 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 ──────────────────────────