Files
OneCAD-Draft/src/entities/hatch.rs
T
Hakan SevenandClaude Opus 4.8 b9cebab951 feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.

Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00

612 lines
26 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::{FallbackTess, Grippable, PropertyEditable, Transformable};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry};
use crate::scene::model::wire_model::SnapHint;
/// Signed area of every boundary path, summed via the shoelace formula over
/// the OCS boundary vertices (bulge/curved edges approximated by their end
/// points). Returns the absolute total area.
fn boundary_area(h: &Hatch) -> f64 {
let mut area = 0.0;
for path in &h.paths {
let mut ring: Vec<[f64; 2]> = Vec::new();
for edge in &path.edges {
match edge {
BoundaryEdge::Polyline(poly) => {
for v in &poly.vertices {
ring.push([v.x, v.y]);
}
}
BoundaryEdge::Line(l) => {
ring.push([l.start.x, l.start.y]);
ring.push([l.end.x, l.end.y]);
}
BoundaryEdge::CircularArc(a) => ring.push([a.center.x, a.center.y]),
BoundaryEdge::EllipticArc(e) => ring.push([e.center.x, e.center.y]),
BoundaryEdge::Spline(s) => {
let src = if !s.fit_points.is_empty() {
s.fit_points.iter().map(|p| [p.x, p.y]).collect::<Vec<_>>()
} else {
s.control_points
.iter()
.map(|p| [p.x, p.y])
.collect::<Vec<_>>()
};
ring.extend(src);
}
}
}
let n = ring.len();
if n < 3 {
continue;
}
let mut acc = 0.0;
for i in 0..n {
let a = ring[i];
let b = ring[(i + 1) % n];
acc += a[0] * b[1] - b[0] * a[1];
}
area += acc.abs() * 0.5;
}
area
}
/// Format a color for a read-only row (`ByLayer` / `ByBlock` / index / RGB).
fn color_str(c: acadrust::types::Color) -> String {
c.to_string()
}
fn properties(h: &Hatch) -> Vec<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 area = boundary_area(h);
let g = &h.gradient_color;
let grad_c1 = g.colors.first().map(|e| color_str(e.color)).unwrap_or_default();
let grad_c2 = g.colors.get(1).map(|e| color_str(e.color)).unwrap_or_default();
if g.enabled {
// ── Gradient fill ──────────────────────────────────────────────────
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" };
return vec![
PropSection {
title: "Pattern".into(),
props: vec![
ro("Type", "fill_type", grad_type),
ro("Gradient name", "gradient_name", g.name.clone()),
ro("Color 1", "gradient_color_1", grad_c1),
ro("Color 2", "gradient_color_2", grad_c2),
edit("Angle", "pattern_angle", g.angle.to_degrees()),
ro("Centered", "gradient_centered", centered),
],
},
PropSection {
title: "Geometry".into(),
props: vec![
edit("Elevation", "elevation", h.elevation),
ro("Origin X", "origin_x", String::new()),
ro("Origin Y", "origin_y", String::new()),
ro("Area", "area", format!("{:.4}", area)),
ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
],
},
PropSection {
title: "Misc".into(),
props: vec![
ro(
"Associative",
"associative",
if h.is_associative { "Yes" } else { "No" },
),
ro("Annotative", "annotative", String::new()),
ro("Island detection style", "style", style),
ro("Background color", "background_color", String::new()),
],
},
];
}
// ── Hatch (pattern / solid) ────────────────────────────────────────────
let hatch_type = if h.is_solid { "Solid" } else { "Pattern" };
vec![
PropSection {
title: "Pattern".into(),
props: vec![
ro("Type", "pattern_type_label", hatch_type),
Property {
label: "Pattern name".into(),
field: "pattern_name",
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
},
ro("Color", "color", color_str(h.common.color)),
ro("Background color", "background_color", String::new()),
ro(
"Transparency",
"transparency",
format!("{}%", (h.common.transparency.as_percent() * 100.0).round() as i32),
),
edit("Angle", "pattern_angle", h.pattern_angle.to_degrees()),
edit("Scale", "pattern_scale", h.pattern_scale),
ro("Origin X", "origin_x", String::new()),
ro("Origin Y", "origin_y", String::new()),
ro("Annotative", "annotative", String::new()),
ro(
"Associative",
"associative",
if h.is_associative { "Yes" } else { "No" },
),
ro("Layer override", "layer_override", String::new()),
ro("Double", "double", if h.is_double { "Yes" } else { "No" }),
ro("Spacing", "spacing", String::new()),
ro("ISO pen width", "iso_pen_width", String::new()),
ro("Gradient colors", "gradient_colors", g.colors.len().to_string()),
ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)),
ro("Gradient color 1", "gradient_color_1", grad_c1),
ro("Gradient color 2", "gradient_color_2", grad_c2),
ro("Pattern type", "pattern_type", pattern_type),
],
},
PropSection {
title: "Geometry".into(),
props: vec![
edit("Elevation", "elevation", h.elevation),
ro("Area", "area", format!("{:.4}", area)),
ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
],
},
PropSection {
title: "Misc".into(),
props: vec![
ro("Boundary retention", "boundary_retention", String::new()),
ro("Island detection style", "style", style),
],
},
]
}
fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
let Some(v) = parse_f64(value) else {
return;
};
match field {
"pattern_angle" if h.gradient_color.enabled => h.gradient_color.angle = v.to_radians(),
"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) {
crate::scene::view::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
// Delegate the mirror to acadrust's transform_hatch (via the Entity
// trait): it flips the boundary-arc direction flags, re-mirrors the
// stored angles and preserves the stored sweep — including the
// wrap-encoded end angles above 2π that AutoCAD writes. The old
// hand-rolled angle-swap here was only valid for ccw boundary arcs on
// an axis-aligned mirror line and went stale the moment those
// conventions were fixed upstream.
let t = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
acadrust::entities::Entity::apply_transform(entity, &t);
});
}
impl PropertyEditable for Hatch {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<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;
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, glam::DVec3::new(v.x, v.y, elev)));
id += 1;
}
}
BoundaryEdge::Line(l) => {
out.push(center_grip(
id,
glam::DVec3::new(l.start.x, l.start.y, elev),
));
id += 1;
out.push(center_grip(id, glam::DVec3::new(l.end.x, l.end.y, elev)));
id += 1;
}
BoundaryEdge::CircularArc(a) => {
out.push(center_grip(
id,
glam::DVec3::new(a.center.x, a.center.y, elev),
));
id += 1;
}
BoundaryEdge::EllipticArc(e) => {
out.push(center_grip(
id,
glam::DVec3::new(e.center.x, e.center.y, 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, glam::DVec3::new(x, y, 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.as_dvec3() + *d,
};
(p.x, p.y)
}
'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;
}
}
}
}
}
}
}
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
vec![
GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
},
GripMenuItem {
label: "Origin Point",
action: GripMenuAction::OriginPoint,
},
GripMenuItem {
label: "Hatch Angle",
action: GripMenuAction::HatchAngle,
},
GripMenuItem {
label: "Hatch Scale",
action: GripMenuAction::HatchScale,
},
]
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
// Origin / Angle / Scale need a follow-up value — handled by
// `apply_grip_menu_value`.
}
fn grip_menu_value_prompt(
&self,
_grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) -> Option<&'static str> {
use crate::scene::model::object::GripMenuAction as A;
match action {
A::HatchAngle => Some("Angle (deg)"),
A::HatchScale => Some("Scale"),
_ => None,
}
}
fn apply_grip_menu_value(
&mut self,
_grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
value: f64,
) {
use crate::scene::model::object::GripMenuAction as A;
match action {
A::HatchAngle => {
self.pattern_angle = value.to_radians();
}
A::HatchScale => {
if value > 0.0 {
self.pattern_scale = value;
}
}
_ => {}
}
}
}
impl FallbackTess for Hatch {
fn fallback_geometry(&self) -> FallbackGeometry {
let normal = (self.normal.x, self.normal.y, self.normal.z);
// Convert a 2D OCS hatch boundary point to absolute WCS.
let to_wcs = |x: f64, y: f64| -> [f64; 3] {
let (wx, wy, wz) =
crate::scene::view::transform::ocs_point_to_wcs((x, y, self.elevation), normal);
[wx, wy, wz]
};
// Snap point at a world (f64) location, cast to the f32 snap buffer.
let snap_at = |w: [f64; 3]| Vec3::new(w[0] as f32, w[1] as f32, w[2] as f32);
let mut pts: Vec<[f64; 3]> = Vec::new();
let mut key_verts: Vec<[f64; 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([f64::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([f64::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([f64::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((
snap_at(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([f64::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((
snap_at(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)
}
}