forked from 1b-agent/opencadstudio-upstream
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.
496 lines
21 KiB
Rust
496 lines
21 KiB
Rust
use acadrust::entities::{BoundaryEdge, Hatch};
|
|
use glam::Vec3;
|
|
|
|
use crate::command::EntityTransform;
|
|
use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro};
|
|
use crate::entities::traits::{Grippable, FallbackTess, PropertyEditable, Transformable};
|
|
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
use crate::scene::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry};
|
|
use crate::scene::wire_model::SnapHint;
|
|
|
|
fn properties(h: &Hatch) -> PropSection {
|
|
let pattern_type = match h.pattern_type {
|
|
acadrust::entities::HatchPatternType::Predefined => "Predefined",
|
|
acadrust::entities::HatchPatternType::UserDefined => "User Defined",
|
|
acadrust::entities::HatchPatternType::Custom => "Custom",
|
|
};
|
|
let style = match h.style {
|
|
acadrust::entities::HatchStyleType::Normal => "Normal",
|
|
acadrust::entities::HatchStyleType::Outer => "Outer",
|
|
acadrust::entities::HatchStyleType::Ignore => "Ignore",
|
|
};
|
|
let fill_type = if h.gradient_color.enabled {
|
|
format!("Gradient ({})", h.gradient_color.name)
|
|
} else if h.is_solid {
|
|
"Solid".into()
|
|
} else {
|
|
format!("Pattern ({})", h.pattern.name)
|
|
};
|
|
let boundary_count: usize = h
|
|
.paths
|
|
.iter()
|
|
.map(|p| {
|
|
p.edges
|
|
.iter()
|
|
.map(|e| match e {
|
|
BoundaryEdge::Polyline(poly) => poly.vertices.len(),
|
|
_ => 1,
|
|
})
|
|
.sum::<usize>()
|
|
})
|
|
.sum();
|
|
PropSection {
|
|
title: "Geometry".into(),
|
|
props: vec![
|
|
ro("Fill Type", "fill_type", fill_type),
|
|
Property {
|
|
label: "Pattern Name".into(),
|
|
field: "pattern_name",
|
|
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
|
|
},
|
|
ro("Pattern Type", "pattern_type", pattern_type),
|
|
edit(
|
|
"Pattern Angle",
|
|
"pattern_angle",
|
|
h.pattern_angle.to_degrees(),
|
|
),
|
|
edit("Pattern Scale", "pattern_scale", h.pattern_scale),
|
|
ro("Style", "style", style),
|
|
ro("Boundary Paths", "path_count", h.paths.len().to_string()),
|
|
ro("Boundary Verts", "vert_count", boundary_count.to_string()),
|
|
ro("Double", "double", if h.is_double { "Yes" } else { "No" }),
|
|
ro(
|
|
"Associative",
|
|
"associative",
|
|
if h.is_associative { "Yes" } else { "No" },
|
|
),
|
|
edit("Elevation", "elevation", h.elevation),
|
|
ro("Seed Points", "seed_count", h.seed_points.len().to_string()),
|
|
ro(
|
|
"Pixel Size",
|
|
"pixel_size",
|
|
format!("{:.6}", h.pixel_size),
|
|
),
|
|
ro(
|
|
"Normal",
|
|
"normal",
|
|
format!("{:.3}, {:.3}, {:.3}", h.normal.x, h.normal.y, h.normal.z),
|
|
),
|
|
],
|
|
}
|
|
}
|
|
|
|
fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|
let Some(v) = parse_f64(value) else {
|
|
return;
|
|
};
|
|
match field {
|
|
"pattern_angle" => h.pattern_angle = v.to_radians(),
|
|
"pattern_scale" if v > 0.0 => h.pattern_scale = v,
|
|
"elevation" => h.elevation = v,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
|
use crate::scene::transform::reflect_xy_point as reflect;
|
|
crate::scene::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
|
let dx = (p2.x - p1.x) as f64;
|
|
let dy = (p2.y - p1.y) as f64;
|
|
let len2 = dx * dx + dy * dy;
|
|
if len2 < 1e-12 {
|
|
return;
|
|
}
|
|
let line_angle = dy.atan2(dx);
|
|
for path in &mut entity.paths {
|
|
for edge in &mut path.edges {
|
|
match edge {
|
|
BoundaryEdge::Line(l) => {
|
|
reflect(&mut l.start.x, &mut l.start.y, p1, p2);
|
|
reflect(&mut l.end.x, &mut l.end.y, p1, p2);
|
|
}
|
|
BoundaryEdge::CircularArc(a) => {
|
|
reflect(&mut a.center.x, &mut a.center.y, p1, p2);
|
|
let tmp = a.start_angle;
|
|
a.start_angle = 2.0 * line_angle - a.end_angle;
|
|
a.end_angle = 2.0 * line_angle - tmp;
|
|
}
|
|
BoundaryEdge::EllipticArc(e) => {
|
|
reflect(&mut e.center.x, &mut e.center.y, p1, p2);
|
|
let ax = dx;
|
|
let ay = dy;
|
|
let rx = e.major_axis_endpoint.x;
|
|
let ry = e.major_axis_endpoint.y;
|
|
let dot = rx * ax + ry * ay;
|
|
e.major_axis_endpoint.x = 2.0 * dot * ax / len2 - rx;
|
|
e.major_axis_endpoint.y = 2.0 * dot * ay / len2 - ry;
|
|
let tmp = e.start_angle;
|
|
e.start_angle = 2.0 * line_angle - e.end_angle;
|
|
e.end_angle = 2.0 * line_angle - tmp;
|
|
}
|
|
BoundaryEdge::Spline(s) => {
|
|
for cp in &mut s.control_points {
|
|
reflect(&mut cp.x, &mut cp.y, p1, p2);
|
|
}
|
|
for fp in &mut s.fit_points {
|
|
reflect(&mut fp.x, &mut fp.y, p1, p2);
|
|
}
|
|
}
|
|
BoundaryEdge::Polyline(p) => {
|
|
for v in &mut p.vertices {
|
|
reflect(&mut v.x, &mut v.y, p1, p2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
impl PropertyEditable for Hatch {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
|
properties(self)
|
|
}
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
apply_geom_prop(self, field, value);
|
|
}
|
|
}
|
|
|
|
impl Transformable for Hatch {
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
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(center_grip(id, Vec3::new(v.x as f32, v.y as f32, elev)));
|
|
id += 1;
|
|
}
|
|
}
|
|
BoundaryEdge::Line(l) => {
|
|
out.push(center_grip(
|
|
id,
|
|
Vec3::new(l.start.x as f32, l.start.y as f32, elev),
|
|
));
|
|
id += 1;
|
|
out.push(center_grip(
|
|
id,
|
|
Vec3::new(l.end.x as f32, l.end.y as f32, elev),
|
|
));
|
|
id += 1;
|
|
}
|
|
BoundaryEdge::CircularArc(a) => {
|
|
out.push(center_grip(
|
|
id,
|
|
Vec3::new(a.center.x as f32, a.center.y as f32, elev),
|
|
));
|
|
id += 1;
|
|
}
|
|
BoundaryEdge::EllipticArc(e) => {
|
|
out.push(center_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(center_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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FallbackTess for Hatch {
|
|
fn fallback_geometry(&self, world_offset: [f64; 3]) -> FallbackGeometry {
|
|
let [ox, oy, oz] = world_offset;
|
|
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
|
// Convert a 2D OCS hatch boundary point to WCS, then subtract world_offset.
|
|
let to_wcs = |x: f64, y: f64| -> [f32; 3] {
|
|
let (wx, wy, wz) =
|
|
crate::scene::transform::ocs_point_to_wcs((x, y, self.elevation), normal);
|
|
[(wx - ox) as f32, (wy - oy) as f32, (wz - oz) as f32]
|
|
};
|
|
let mut pts: Vec<[f32; 3]> = Vec::new();
|
|
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
|
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
|
for path in &self.paths {
|
|
for edge in &path.edges {
|
|
match edge {
|
|
BoundaryEdge::Polyline(poly) => {
|
|
// Hatch-boundary polyline vertices encode bulge in
|
|
// `Vector3.z`; straight segments emit just the
|
|
// start vertex, bulged segments tessellate the arc
|
|
// between v0 → v1.
|
|
let verts = &poly.vertices;
|
|
let count = verts.len();
|
|
if count == 0 {
|
|
continue;
|
|
}
|
|
// Break the wire between this polyline and whatever
|
|
// preceded it — without the separator the renderer
|
|
// draws a ghost segment from the previous edge / path
|
|
// straight to this polyline's first vertex, which
|
|
// shows up as a stray boundary line between hatch
|
|
// regions.
|
|
if !pts.is_empty() {
|
|
pts.push([f32::NAN; 3]);
|
|
}
|
|
let start_idx = pts.len();
|
|
let seg_count = if poly.is_closed {
|
|
count
|
|
} else {
|
|
count.saturating_sub(1)
|
|
};
|
|
for i in 0..seg_count {
|
|
let v0 = &verts[i];
|
|
let v1 = &verts[(i + 1) % count];
|
|
let bulge = v0.z;
|
|
let arc = if bulge.abs() < 1e-9 {
|
|
None
|
|
} else {
|
|
crate::entities::common::BulgeArc::from_bulge(
|
|
[v0.x, v0.y],
|
|
[v1.x, v1.y],
|
|
bulge,
|
|
)
|
|
};
|
|
let Some(arc) = arc else {
|
|
let p = to_wcs(v0.x, v0.y);
|
|
pts.push(p);
|
|
key_verts.push(p);
|
|
continue;
|
|
};
|
|
let segs = arc_segments(arc.radius, arc.sweep.abs(), wire_chord_tol(arc.radius));
|
|
for j in 0..segs {
|
|
let s = arc.sample(j as f64 / segs as f64);
|
|
let p = to_wcs(s[0], s[1]);
|
|
pts.push(p);
|
|
if j == 0 {
|
|
key_verts.push(p);
|
|
}
|
|
}
|
|
}
|
|
// Close the loop visually for closed polylines by
|
|
// returning to the first emitted point.
|
|
if poly.is_closed {
|
|
if let Some(first) = pts.get(start_idx).cloned() {
|
|
if first[0].is_finite() {
|
|
pts.push(first);
|
|
}
|
|
}
|
|
} else if let Some(last) = verts.last() {
|
|
let p = to_wcs(last.x, last.y);
|
|
pts.push(p);
|
|
key_verts.push(p);
|
|
}
|
|
}
|
|
BoundaryEdge::Line(ln) => {
|
|
let p0 = to_wcs(ln.start.x, ln.start.y);
|
|
let p1 = to_wcs(ln.end.x, ln.end.y);
|
|
if !pts.is_empty() {
|
|
pts.push([f32::NAN; 3]);
|
|
}
|
|
pts.push(p0);
|
|
pts.push(p1);
|
|
key_verts.push(p0);
|
|
key_verts.push(p1);
|
|
}
|
|
BoundaryEdge::CircularArc(arc) => {
|
|
let (sa, span) =
|
|
arc_signed_span(arc.start_angle, arc.end_angle, arc.counter_clockwise);
|
|
let segs = arc_segments(arc.radius, span.abs(), wire_chord_tol(arc.radius));
|
|
if !pts.is_empty() {
|
|
pts.push([f32::NAN; 3]);
|
|
}
|
|
for i in 0..=segs {
|
|
let t = sa + span * (i as f64 / segs as f64);
|
|
let p = to_wcs(
|
|
arc.center.x + arc.radius * t.cos(),
|
|
arc.center.y + arc.radius * t.sin(),
|
|
);
|
|
pts.push(p);
|
|
if i == 0 || i == segs {
|
|
key_verts.push(p);
|
|
}
|
|
}
|
|
snap_pts.push((
|
|
Vec3::from(to_wcs(arc.center.x, arc.center.y)),
|
|
SnapHint::Center,
|
|
));
|
|
}
|
|
BoundaryEdge::EllipticArc(ell) => {
|
|
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
|
|
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
|
|
.sqrt();
|
|
let r_min = r_maj * ell.minor_axis_ratio;
|
|
let rot = ell
|
|
.major_axis_endpoint
|
|
.y
|
|
.atan2(ell.major_axis_endpoint.x);
|
|
let (sa, span) =
|
|
arc_signed_span(ell.start_angle, ell.end_angle, ell.counter_clockwise);
|
|
let segs = arc_segments(r_maj, span.abs(), wire_chord_tol(r_maj));
|
|
if !pts.is_empty() {
|
|
pts.push([f32::NAN; 3]);
|
|
}
|
|
let (cr, sr) = (rot.cos(), rot.sin());
|
|
for i in 0..=segs {
|
|
let t = sa + span * (i as f64 / segs as f64);
|
|
let lx = r_maj * t.cos();
|
|
let ly = r_min * t.sin();
|
|
let p = to_wcs(
|
|
ell.center.x + lx * cr - ly * sr,
|
|
ell.center.y + lx * sr + ly * cr,
|
|
);
|
|
pts.push(p);
|
|
if i == 0 || i == segs {
|
|
key_verts.push(p);
|
|
}
|
|
}
|
|
snap_pts.push((
|
|
Vec3::from(to_wcs(ell.center.x, ell.center.y)),
|
|
SnapHint::Center,
|
|
));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
if pts.is_empty() {
|
|
pts = vec![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
|
|
}
|
|
(pts, snap_pts, vec![], key_verts)
|
|
}
|
|
}
|