2026-03-22 14:56:27 +03:00
|
|
|
use acadrust::entities::{BoundaryEdge, Hatch};
|
2026-08-09 01:44:04 +03:00
|
|
|
use acadrust::kernel::geom2d::{
|
|
|
|
|
Arc as KernelArc, Curve as KernelCurve, Ellipse as KernelEllipse,
|
|
|
|
|
EllipseArc as KernelEllipseArc, Line as KernelLine, NurbsCurve as KernelNurbs,
|
|
|
|
|
Parameterization, Polyline as KernelPolyline, PolylineVertex as KernelVertex,
|
|
|
|
|
};
|
2026-04-03 19:16:17 +03:00
|
|
|
use glam::Vec3;
|
2026-08-02 22:09:25 +03:00
|
|
|
use crate::t;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
fix(props): angle rows format via AUNITS, not LUNITS (#297)
Every angle row went through the linear formatter, so switching LUNITS
to Architectural showed a block's Rotation as feet-and-inches — and the
string wouldn't parse back, wedging the field until it was cleared and
retyped. Only the decimal/fractional LUNITS survived by accident.
New edit_angle row helper formats angles with format_angle
(AUNITS/AUPREC: decimal degrees, DMS, grads or radians), applied to all
17 angle rows (rotations, obliquing, arc start/end, hatch pattern
angle, multileader text rotation). parse_angle_deg converts every
AUNITS display form back to degrees ("30°"/"30d", DMS 30°15'20",
33.33g, 0.52r) and parse_f64 falls back to it, so the displayed value
round-trips through an edit.
Closes #297
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:42:14 +03:00
|
|
|
use crate::entities::common::{center_grip, circle_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro};
|
2026-06-11 00:04:37 +03:00
|
|
|
use crate::entities::traits::{FallbackTess, Grippable, PropertyEditable, Transformable};
|
2026-06-16 11:33:46 +03:00
|
|
|
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;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
2026-08-09 01:44:04 +03:00
|
|
|
/// The area the hatch's boundary paths enclose.
|
|
|
|
|
///
|
|
|
|
|
/// Summed edge by edge through the kernel, which measures what each edge
|
|
|
|
|
/// actually encloses rather than what a polygon through some of its points
|
|
|
|
|
/// would. The version this replaced pushed an arc's *centre* into the ring
|
|
|
|
|
/// and a spline's control points — neither of which is on the boundary — so
|
|
|
|
|
/// the number it produced was not the area of anything.
|
|
|
|
|
///
|
|
|
|
|
/// Outer paths and their holes both contribute; the sign of a loop says
|
|
|
|
|
/// which it is, so the magnitude of the sum is the region's own area.
|
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
|
|
|
fn boundary_area(h: &Hatch) -> f64 {
|
|
|
|
|
let mut area = 0.0;
|
|
|
|
|
for path in &h.paths {
|
2026-08-09 01:44:04 +03:00
|
|
|
let mut path_area = 0.0;
|
|
|
|
|
let mut ends: Vec<[f64; 2]> = Vec::new();
|
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
|
|
|
for edge in &path.edges {
|
2026-08-09 01:44:04 +03:00
|
|
|
let Some(curve) = edge_curve(edge) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
path_area += curve.enclosed_area();
|
|
|
|
|
ends.push(curve.point_at(0.0));
|
|
|
|
|
ends.push(curve.point_at(1.0));
|
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
|
|
|
}
|
2026-08-09 01:44:04 +03:00
|
|
|
// Edges are stored as separate pieces, so the chain has to be closed
|
|
|
|
|
// by the chord from the last end back to the first — the same closing
|
|
|
|
|
// an open polyline gets.
|
|
|
|
|
if let (Some(first), Some(last)) = (ends.first(), ends.last()) {
|
|
|
|
|
path_area += 0.5 * (last[0] * first[1] - first[0] * last[1]);
|
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
|
|
|
}
|
2026-08-09 01:44:04 +03:00
|
|
|
area += path_area.abs();
|
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
|
|
|
}
|
|
|
|
|
area
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 01:44:04 +03:00
|
|
|
/// A hatch boundary edge as a kernel curve, in the hatch's own OCS.
|
|
|
|
|
fn edge_curve(edge: &BoundaryEdge) -> Option<KernelCurve> {
|
|
|
|
|
Some(match edge {
|
|
|
|
|
BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine {
|
|
|
|
|
start: [l.start.x, l.start.y],
|
|
|
|
|
end: [l.end.x, l.end.y],
|
|
|
|
|
}),
|
|
|
|
|
BoundaryEdge::CircularArc(a) => {
|
|
|
|
|
// A clockwise edge is the same arc walked the other way, so the
|
|
|
|
|
// stored angles swap rather than the sweep going negative.
|
|
|
|
|
let (start, end) = if a.counter_clockwise {
|
|
|
|
|
(a.start_angle, a.end_angle)
|
|
|
|
|
} else {
|
|
|
|
|
(a.end_angle, a.start_angle)
|
|
|
|
|
};
|
|
|
|
|
KernelCurve::Arc(KernelArc {
|
|
|
|
|
centre: [a.center.x, a.center.y],
|
|
|
|
|
radius: a.radius,
|
|
|
|
|
start_angle: start,
|
|
|
|
|
end_angle: end,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::EllipticArc(e) => {
|
|
|
|
|
let major = (e.major_axis_endpoint.x, e.major_axis_endpoint.y);
|
|
|
|
|
let radius = major.0.hypot(major.1);
|
|
|
|
|
if radius < 1e-12 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let (start, end) = if e.counter_clockwise {
|
|
|
|
|
(e.start_angle, e.end_angle)
|
|
|
|
|
} else {
|
|
|
|
|
(e.end_angle, e.start_angle)
|
|
|
|
|
};
|
|
|
|
|
KernelCurve::Ellipse(KernelEllipseArc {
|
|
|
|
|
ellipse: KernelEllipse {
|
|
|
|
|
centre: [e.center.x, e.center.y],
|
|
|
|
|
major_radius: radius,
|
|
|
|
|
minor_radius: radius * e.minor_axis_ratio,
|
|
|
|
|
major_axis: [major.0 / radius, major.1 / radius],
|
|
|
|
|
},
|
|
|
|
|
start_parameter: start,
|
|
|
|
|
end_parameter: end,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::Polyline(poly) => {
|
|
|
|
|
let vertices: Vec<KernelVertex> = poly
|
|
|
|
|
.vertices
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|v| KernelVertex {
|
|
|
|
|
position: [v.x, v.y],
|
|
|
|
|
// A boundary polyline stores its bulge in the vertex's
|
|
|
|
|
// third component.
|
|
|
|
|
bulge: v.z,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if vertices.len() < 2 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
KernelCurve::Polyline(KernelPolyline {
|
|
|
|
|
vertices,
|
|
|
|
|
closed: poly.is_closed,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::Spline(s) => {
|
|
|
|
|
let control: Vec<[f64; 2]> = s.control_points.iter().map(|p| [p.x, p.y]).collect();
|
|
|
|
|
// A boundary spline's control points carry their weight in the
|
|
|
|
|
// third component; for a polynomial one it is unset and the
|
|
|
|
|
// kernel's own default of all-ones applies.
|
|
|
|
|
let weights = s
|
|
|
|
|
.rational
|
|
|
|
|
.then(|| s.control_points.iter().map(|p| p.z).collect::<Vec<f64>>());
|
|
|
|
|
let curve = KernelNurbs::new(
|
|
|
|
|
s.degree.max(1) as usize,
|
|
|
|
|
control,
|
|
|
|
|
s.knots.clone(),
|
|
|
|
|
weights,
|
|
|
|
|
)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
// A fit-point boundary spline, interpolated the same way a
|
|
|
|
|
// SPLINE entity's is.
|
|
|
|
|
let fit: Vec<[f64; 2]> = s.fit_points.iter().map(|p| [p.x, p.y]).collect();
|
|
|
|
|
KernelNurbs::interpolate(&fit, None, None, Parameterization::Chord)
|
|
|
|
|
})?;
|
|
|
|
|
KernelCurve::Nurbs(curve)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 22:48:33 +03:00
|
|
|
/// Mean of every boundary edge point (OCS) — the centroid used to place the
|
|
|
|
|
/// pattern-origin grip on the hatch body. `None` when there are no boundary
|
|
|
|
|
/// points.
|
|
|
|
|
fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> {
|
|
|
|
|
let mut sx = 0.0;
|
|
|
|
|
let mut sy = 0.0;
|
|
|
|
|
let mut n = 0.0;
|
|
|
|
|
let mut add = |x: f64, y: f64| {
|
|
|
|
|
sx += x;
|
|
|
|
|
sy += y;
|
|
|
|
|
n += 1.0;
|
|
|
|
|
};
|
|
|
|
|
for path in &h.paths {
|
|
|
|
|
for edge in &path.edges {
|
|
|
|
|
match edge {
|
|
|
|
|
BoundaryEdge::Polyline(p) => {
|
|
|
|
|
for v in &p.vertices {
|
|
|
|
|
add(v.x, v.y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::Line(l) => {
|
|
|
|
|
add(l.start.x, l.start.y);
|
|
|
|
|
add(l.end.x, l.end.y);
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::CircularArc(a) => add(a.center.x, a.center.y),
|
|
|
|
|
BoundaryEdge::EllipticArc(e) => add(e.center.x, e.center.y),
|
|
|
|
|
BoundaryEdge::Spline(s) => {
|
|
|
|
|
if !s.fit_points.is_empty() {
|
|
|
|
|
for p in &s.fit_points {
|
|
|
|
|
add(p.x, p.y);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for p in &s.control_points {
|
|
|
|
|
add(p.x, p.y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(n > 0.0).then(|| (sx / n, sy / n))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 16:44:31 +03:00
|
|
|
/// Scale the stored, final hatch-pattern geometry around the pattern origin.
|
|
|
|
|
/// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata
|
|
|
|
|
/// scale is not applied again by the renderer.
|
|
|
|
|
pub(crate) fn scale_pattern_geometry(
|
|
|
|
|
pattern: &mut acadrust::entities::HatchPattern,
|
|
|
|
|
factor: f64,
|
|
|
|
|
) {
|
|
|
|
|
let (origin_x, origin_y) = pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|line| (line.base_point.x, line.base_point.y))
|
|
|
|
|
.unwrap_or((0.0, 0.0));
|
|
|
|
|
for line in pattern.lines.iter_mut() {
|
|
|
|
|
line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor;
|
|
|
|
|
line.base_point.y = origin_y + (line.base_point.y - origin_y) * factor;
|
|
|
|
|
line.offset.x *= factor;
|
|
|
|
|
line.offset.y *= factor;
|
|
|
|
|
for dash in line.dash_lengths.iter_mut() {
|
|
|
|
|
*dash *= factor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rotate the stored, final hatch-pattern geometry around the pattern origin.
|
|
|
|
|
/// This keeps the line angle, base point and world-space offset in sync.
|
|
|
|
|
pub(crate) fn rotate_pattern_geometry(
|
|
|
|
|
pattern: &mut acadrust::entities::HatchPattern,
|
|
|
|
|
angle: f64,
|
|
|
|
|
) {
|
|
|
|
|
let (sin, cos) = angle.sin_cos();
|
|
|
|
|
let (origin_x, origin_y) = pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|line| (line.base_point.x, line.base_point.y))
|
|
|
|
|
.unwrap_or((0.0, 0.0));
|
|
|
|
|
for line in pattern.lines.iter_mut() {
|
|
|
|
|
line.angle += angle;
|
|
|
|
|
let (x, y) = (
|
|
|
|
|
line.base_point.x - origin_x,
|
|
|
|
|
line.base_point.y - origin_y,
|
|
|
|
|
);
|
|
|
|
|
line.base_point.x = origin_x + x * cos - y * sin;
|
|
|
|
|
line.base_point.y = origin_y + x * sin + y * cos;
|
|
|
|
|
let (x, y) = (line.offset.x, line.offset.y);
|
|
|
|
|
line.offset.x = x * cos - y * sin;
|
|
|
|
|
line.offset.y = x * sin + y * cos;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Move every stored line by the same amount, preserving the pattern phase.
|
|
|
|
|
pub(crate) fn translate_pattern_geometry(
|
|
|
|
|
pattern: &mut acadrust::entities::HatchPattern,
|
|
|
|
|
dx: f64,
|
|
|
|
|
dy: f64,
|
|
|
|
|
) {
|
|
|
|
|
for line in pattern.lines.iter_mut() {
|
|
|
|
|
line.base_point.x += dx;
|
|
|
|
|
line.base_point.y += dy;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:35:27 +03:00
|
|
|
/// Read the hatch background colour from its `HATCHBACKGROUNDCOLOR` extended
|
|
|
|
|
/// data (group 1071, packed true colour: high byte = colour method, low three =
|
|
|
|
|
/// RGB). `None` when unset or not a true-colour value.
|
|
|
|
|
pub fn background_color(h: &Hatch) -> Option<acadrust::types::Color> {
|
|
|
|
|
let rec = h.common.extended_data.get_record("HATCHBACKGROUNDCOLOR")?;
|
|
|
|
|
let raw = rec.values.iter().find_map(|v| match v {
|
|
|
|
|
acadrust::xdata::XDataValue::Integer32(n) => Some(*n as u32),
|
|
|
|
|
_ => None,
|
|
|
|
|
})?;
|
|
|
|
|
match (raw >> 24) & 0xFF {
|
2026-07-21 12:22:15 +03:00
|
|
|
// Colour-method byte: C0 = ByLayer, C1 = ByBlock, C2 = true colour,
|
|
|
|
|
// C3 = ACI index.
|
|
|
|
|
0xC0 => Some(acadrust::types::Color::ByLayer),
|
|
|
|
|
0xC1 => Some(acadrust::types::Color::ByBlock),
|
|
|
|
|
0xC2 => Some(acadrust::types::Color::Rgb {
|
2026-07-08 23:35:27 +03:00
|
|
|
r: ((raw >> 16) & 0xFF) as u8,
|
|
|
|
|
g: ((raw >> 8) & 0xFF) as u8,
|
|
|
|
|
b: (raw & 0xFF) as u8,
|
|
|
|
|
}),
|
2026-07-21 12:22:15 +03:00
|
|
|
0xC3 => Some(acadrust::types::Color::Index((raw & 0xFF) as u8)),
|
2026-07-08 23:35:27 +03:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pack an RGB colour into a `HATCHBACKGROUNDCOLOR` group-1071 true-colour word.
|
|
|
|
|
pub fn pack_background_color(c: &acadrust::types::Color) -> i32 {
|
2026-07-21 12:22:15 +03:00
|
|
|
use acadrust::types::Color;
|
|
|
|
|
(match c {
|
|
|
|
|
Color::ByLayer => 0xC0000000u32,
|
|
|
|
|
Color::ByBlock => 0xC1000000u32,
|
|
|
|
|
Color::Index(i) => 0xC3000000u32 | (*i as u8 as u32),
|
|
|
|
|
_ => {
|
|
|
|
|
let (r, g, b) = c.rgb().unwrap_or((255, 255, 255));
|
|
|
|
|
0xC2000000u32 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
|
|
|
|
|
}
|
|
|
|
|
}) as i32
|
2026-07-08 23:35:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Replace (or insert) the hatch's `HATCHBACKGROUNDCOLOR` extended-data record,
|
|
|
|
|
/// preserving every other application's records.
|
|
|
|
|
pub fn set_background_color(h: &mut Hatch, c: &acadrust::types::Color) {
|
|
|
|
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
|
|
|
|
const APP: &str = "HATCHBACKGROUNDCOLOR";
|
|
|
|
|
let kept: Vec<ExtendedDataRecord> = h
|
|
|
|
|
.common
|
|
|
|
|
.extended_data
|
|
|
|
|
.records()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|r| r.application_name != APP)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
h.common.extended_data.clear();
|
|
|
|
|
for r in kept {
|
|
|
|
|
h.common.extended_data.add_record(r);
|
|
|
|
|
}
|
|
|
|
|
let mut rec = ExtendedDataRecord::new(APP);
|
|
|
|
|
rec.add_value(XDataValue::Integer32(pack_background_color(c)));
|
|
|
|
|
h.common.extended_data.add_record(rec);
|
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
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:22:15 +03:00
|
|
|
/// Remove the hatch's `HATCHBACKGROUNDCOLOR` extended-data record (background
|
|
|
|
|
/// off), preserving every other application's records.
|
|
|
|
|
pub fn clear_background_color(h: &mut Hatch) {
|
|
|
|
|
use acadrust::xdata::ExtendedDataRecord;
|
|
|
|
|
const APP: &str = "HATCHBACKGROUNDCOLOR";
|
|
|
|
|
let kept: Vec<ExtendedDataRecord> = h
|
|
|
|
|
.common
|
|
|
|
|
.extended_data
|
|
|
|
|
.records()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|r| r.application_name != APP)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
h.common.extended_data.clear();
|
|
|
|
|
for r in kept {
|
|
|
|
|
h.common.extended_data.add_record(r);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 22:46:19 +03:00
|
|
|
fn properties(h: &Hatch) -> Vec<PropSection> {
|
2026-03-22 14:56:27 +03:00
|
|
|
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",
|
|
|
|
|
};
|
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
|
|
|
let area = boundary_area(h);
|
|
|
|
|
let g = &h.gradient_color;
|
2026-07-08 23:35:27 +03:00
|
|
|
let default_col = acadrust::types::Color::Index(7);
|
|
|
|
|
let grad_c1 = g.colors.first().map(|e| e.color).unwrap_or(default_col);
|
|
|
|
|
let grad_c2 = g.colors.get(1).map(|e| e.color).unwrap_or(default_col);
|
2026-07-21 12:22:15 +03:00
|
|
|
let bg_on = background_color(h).is_some();
|
2026-07-08 23:35:27 +03:00
|
|
|
let bg_col = background_color(h).unwrap_or(default_col);
|
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
|
|
|
|
|
|
|
|
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" };
|
2026-07-21 12:22:15 +03:00
|
|
|
let mut sections = vec![
|
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
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Pattern").into_owned(),
|
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
|
|
|
props: vec![
|
2026-07-08 23:35:27 +03:00
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Type").into_owned(),
|
2026-07-08 23:35:27 +03:00
|
|
|
field: "fill_type",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: grad_type.to_string(),
|
|
|
|
|
options: vec!["Two color".into(), "One color".into()],
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-21 12:51:08 +03:00
|
|
|
{
|
|
|
|
|
let (kind, invert) =
|
|
|
|
|
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
|
|
|
|
|
let _ = invert;
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Gradient type").into_owned(),
|
2026-07-21 12:51:08 +03:00
|
|
|
field: "gradient_type",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: kind.label().to_string(),
|
|
|
|
|
options: crate::scene::model::hatch_model::GradientKind::ALL
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|k| k.label().to_string())
|
|
|
|
|
.collect(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Invert").into_owned(),
|
2026-07-21 12:51:08 +03:00
|
|
|
field: "gradient_invert",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "gradient_invert",
|
|
|
|
|
value: crate::scene::model::hatch_model::GradientKind::from_name(
|
|
|
|
|
&g.name,
|
|
|
|
|
)
|
|
|
|
|
.1,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-08 23:35:27 +03:00
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Color 1").into_owned(),
|
2026-07-08 23:35:27 +03:00
|
|
|
field: "gradient_color_1",
|
|
|
|
|
value: PropValue::ColorChoice(grad_c1),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Color 2").into_owned(),
|
2026-07-08 23:35:27 +03:00
|
|
|
field: "gradient_color_2",
|
|
|
|
|
value: PropValue::ColorChoice(grad_c2),
|
|
|
|
|
},
|
2026-08-02 22:09:25 +03:00
|
|
|
edit_angle(t!("Angle").as_ref(), "pattern_angle", g.angle.to_degrees()),
|
|
|
|
|
ro(t!("Centered").as_ref(), "gradient_centered", centered),
|
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
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Geometry").into_owned(),
|
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
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
edit(t!("Elevation").as_ref(), "elevation", h.elevation),
|
|
|
|
|
ro(t!("Area").as_ref(), "area", format!("{:.4}", area)),
|
|
|
|
|
ro(t!("Cumulative area").as_ref(), "cumulative_area", format!("{:.4}", area)),
|
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
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Misc").into_owned(),
|
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
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
ro(t!("Associative").as_ref(),
|
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
|
|
|
"associative",
|
|
|
|
|
if h.is_associative { "Yes" } else { "No" },
|
|
|
|
|
),
|
2026-08-02 22:09:25 +03:00
|
|
|
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
|
|
|
|
ro(t!("Island detection style").as_ref(), "style", style),
|
2026-07-08 23:35:27 +03:00
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Background").into_owned(),
|
2026-07-21 12:22:15 +03:00
|
|
|
field: "bg_enabled",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "bg_enabled",
|
|
|
|
|
value: bg_on,
|
|
|
|
|
},
|
|
|
|
|
},
|
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
|
|
|
],
|
2026-03-22 14:56:27 +03:00
|
|
|
},
|
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
|
|
|
];
|
2026-07-21 12:22:15 +03:00
|
|
|
if bg_on {
|
|
|
|
|
if let Some(sec) = sections.last_mut() {
|
|
|
|
|
sec.props.push(Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Background color").into_owned(),
|
2026-07-21 12:22:15 +03:00
|
|
|
field: "background_color",
|
|
|
|
|
value: PropValue::ColorChoice(bg_col),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sections;
|
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
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:22:15 +03:00
|
|
|
|
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
|
|
|
// ── Hatch (pattern / solid) ────────────────────────────────────────────
|
2026-07-08 22:48:33 +03:00
|
|
|
// "Type" = pattern definition source (Predefined / User Defined / Custom).
|
|
|
|
|
let type_row = Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Type").into_owned(),
|
2026-07-08 22:48:33 +03:00
|
|
|
field: "pattern_type_label",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: pattern_type.to_string(),
|
|
|
|
|
options: vec!["Predefined".into(), "User Defined".into(), "Custom".into()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let pattern_name_row = Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Pattern name").into_owned(),
|
2026-07-08 22:48:33 +03:00
|
|
|
field: "pattern_name",
|
|
|
|
|
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
|
|
|
|
|
};
|
|
|
|
|
let associative_row = Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Associative").into_owned(),
|
2026-07-08 22:48:33 +03:00
|
|
|
field: "associative",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "associative",
|
|
|
|
|
value: h.is_associative,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let double_row = Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Double").into_owned(),
|
2026-07-08 22:48:33 +03:00
|
|
|
field: "double",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "double",
|
|
|
|
|
value: h.is_double,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let island_row = Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Island detection style").into_owned(),
|
2026-07-08 22:48:33 +03:00
|
|
|
field: "style",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: style.to_string(),
|
|
|
|
|
options: vec!["Normal".into(), "Outer".into(), "Ignore".into()],
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-08-02 22:09:25 +03:00
|
|
|
let spacing_row = edit(t!("Spacing").as_ref(),
|
2026-07-08 22:48:33 +03:00
|
|
|
"spacing",
|
|
|
|
|
h.pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|l| l.offset.length())
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
);
|
|
|
|
|
// Pattern tiling origin: the base point the pattern lines are anchored to.
|
|
|
|
|
let (origin_x, origin_y) = h
|
|
|
|
|
.pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|l| (l.base_point.x, l.base_point.y))
|
|
|
|
|
.unwrap_or((0.0, 0.0));
|
|
|
|
|
|
2026-07-21 12:22:15 +03:00
|
|
|
let mut sections = vec![
|
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
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Pattern").into_owned(),
|
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
|
|
|
props: vec![
|
2026-07-08 22:48:33 +03:00
|
|
|
type_row,
|
|
|
|
|
pattern_name_row,
|
2026-08-02 22:09:25 +03:00
|
|
|
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
|
|
|
|
edit_angle(t!("Angle").as_ref(), "pattern_angle", h.pattern_angle.to_degrees()),
|
|
|
|
|
edit(t!("Scale").as_ref(), "pattern_scale", h.pattern_scale),
|
|
|
|
|
edit(t!("Origin X").as_ref(), "origin_x", origin_x),
|
|
|
|
|
edit(t!("Origin Y").as_ref(), "origin_y", origin_y),
|
2026-07-08 22:48:33 +03:00
|
|
|
spacing_row,
|
2026-08-02 22:09:25 +03:00
|
|
|
ro(t!("ISO pen width").as_ref(), "iso_pen_width", String::new()),
|
2026-07-08 22:48:33 +03:00
|
|
|
double_row,
|
|
|
|
|
associative_row,
|
|
|
|
|
island_row,
|
2026-07-08 23:35:27 +03:00
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Background").into_owned(),
|
2026-07-21 12:22:15 +03:00
|
|
|
field: "bg_enabled",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "bg_enabled",
|
|
|
|
|
value: bg_on,
|
|
|
|
|
},
|
2026-07-08 23:35:27 +03:00
|
|
|
},
|
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
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Geometry").into_owned(),
|
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
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
edit(t!("Elevation").as_ref(), "elevation", h.elevation),
|
|
|
|
|
ro(t!("Area").as_ref(), "area", format!("{:.4}", area)),
|
|
|
|
|
ro(t!("Cumulative area").as_ref(), "cumulative_area", format!("{:.4}", area)),
|
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
|
|
|
],
|
|
|
|
|
},
|
2026-07-21 12:22:15 +03:00
|
|
|
];
|
|
|
|
|
if bg_on {
|
|
|
|
|
if let Some(sec) = sections.first_mut() {
|
|
|
|
|
sec.props.push(Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Background color").into_owned(),
|
2026-07-21 12:22:15 +03:00
|
|
|
field: "background_color",
|
|
|
|
|
value: PropValue::ColorChoice(bg_col),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sections
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
2026-07-08 22:48:33 +03:00
|
|
|
use acadrust::entities::{HatchPatternType, HatchStyleType};
|
|
|
|
|
// Non-numeric fields (bool toggles / enum choices) — handled before the
|
|
|
|
|
// f64 parse below, which would otherwise reject their string values.
|
|
|
|
|
match field {
|
2026-07-21 12:22:15 +03:00
|
|
|
// Background on/off (#415): off removes the HATCHBACKGROUNDCOLOR
|
|
|
|
|
// record entirely (no background), on seeds a white one for the
|
|
|
|
|
// colour picker to change.
|
|
|
|
|
"bg_enabled" => {
|
|
|
|
|
if background_color(h).is_some() {
|
|
|
|
|
clear_background_color(h);
|
|
|
|
|
} else {
|
|
|
|
|
set_background_color(h, &acadrust::types::Color::ByLayer);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-08 22:48:33 +03:00
|
|
|
"double" => {
|
|
|
|
|
h.is_double = if value == "toggle" {
|
|
|
|
|
!h.is_double
|
|
|
|
|
} else {
|
|
|
|
|
value == "true"
|
|
|
|
|
};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"associative" => {
|
|
|
|
|
h.is_associative = if value == "toggle" {
|
|
|
|
|
!h.is_associative
|
|
|
|
|
} else {
|
|
|
|
|
value == "true"
|
|
|
|
|
};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"pattern_type_label" => {
|
|
|
|
|
h.pattern_type = match value {
|
|
|
|
|
"Predefined" => HatchPatternType::Predefined,
|
|
|
|
|
"User Defined" => HatchPatternType::UserDefined,
|
|
|
|
|
"Custom" => HatchPatternType::Custom,
|
|
|
|
|
_ => h.pattern_type,
|
|
|
|
|
};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"style" => {
|
|
|
|
|
h.style = match value {
|
|
|
|
|
"Normal" => HatchStyleType::Normal,
|
|
|
|
|
"Outer" => HatchStyleType::Outer,
|
|
|
|
|
"Ignore" => HatchStyleType::Ignore,
|
|
|
|
|
_ => h.style,
|
|
|
|
|
};
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-08 23:35:27 +03:00
|
|
|
"fill_type" => {
|
|
|
|
|
h.gradient_color.is_single_color = value == "One color";
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-21 12:51:08 +03:00
|
|
|
// Gradient shape selection + stop inversion (#415). Both re-derive the
|
|
|
|
|
// standard DXF gradient name; Linear has no INV name, so inverting a
|
|
|
|
|
// linear swaps the colour stops instead.
|
|
|
|
|
"gradient_type" => {
|
|
|
|
|
use crate::scene::model::hatch_model::GradientKind;
|
|
|
|
|
let (_, invert) = GradientKind::from_name(&h.gradient_color.name);
|
|
|
|
|
if let Some(kind) = GradientKind::from_label(value) {
|
|
|
|
|
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
"gradient_invert" => {
|
|
|
|
|
use crate::scene::model::hatch_model::GradientKind;
|
|
|
|
|
let (kind, invert) = GradientKind::from_name(&h.gradient_color.name);
|
|
|
|
|
let invert = !invert;
|
|
|
|
|
if matches!(kind, GradientKind::Linear) {
|
|
|
|
|
if h.gradient_color.colors.len() >= 2 {
|
|
|
|
|
h.gradient_color.colors.swap(0, 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-08 22:48:33 +03:00
|
|
|
_ => {}
|
2026-07-03 08:06:00 +03:00
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
let Some(v) = parse_f64(value) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
match field {
|
2026-07-27 16:44:31 +03:00
|
|
|
"pattern_angle" if h.gradient_color.enabled => {
|
|
|
|
|
h.gradient_color.angle = v.to_radians();
|
|
|
|
|
h.pattern_angle = v.to_radians();
|
|
|
|
|
}
|
|
|
|
|
"pattern_angle" => {
|
|
|
|
|
let angle = v.to_radians();
|
|
|
|
|
let delta = angle - h.pattern_angle;
|
|
|
|
|
rotate_pattern_geometry(&mut h.pattern, delta);
|
|
|
|
|
h.pattern_angle = angle;
|
|
|
|
|
}
|
2026-07-21 12:22:15 +03:00
|
|
|
// The stored pattern lines are the FINAL rendered geometry (offsets,
|
|
|
|
|
// base points and dashes in world units — see the prebaked path in
|
|
|
|
|
// hatch_model_from_dxf), so changing the scale must rescale them by
|
|
|
|
|
// the ratio or the edit has no visible effect (#415).
|
|
|
|
|
"pattern_scale" if v > 0.0 => {
|
|
|
|
|
let old = h.pattern_scale;
|
|
|
|
|
if old > 1e-12 {
|
|
|
|
|
let k = v / old;
|
2026-07-27 16:44:31 +03:00
|
|
|
scale_pattern_geometry(&mut h.pattern, k);
|
2026-07-21 12:22:15 +03:00
|
|
|
}
|
|
|
|
|
h.pattern_scale = v;
|
|
|
|
|
}
|
2026-07-08 22:48:33 +03:00
|
|
|
// Scale every pattern line's offset so the first line's spacing = v,
|
|
|
|
|
// preserving the relative spacing between lines.
|
|
|
|
|
"spacing" if v > 0.0 => {
|
|
|
|
|
let cur = h
|
|
|
|
|
.pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|l| l.offset.length())
|
|
|
|
|
.unwrap_or(0.0);
|
|
|
|
|
if cur > 1e-9 {
|
|
|
|
|
let s = v / cur;
|
|
|
|
|
for line in h.pattern.lines.iter_mut() {
|
|
|
|
|
line.offset.x *= s;
|
|
|
|
|
line.offset.y *= s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Move the pattern origin: shift every line's base point by the delta
|
|
|
|
|
// from the current origin (first line), preserving their relative offsets.
|
|
|
|
|
"origin_x" => {
|
|
|
|
|
if let Some(cur) = h.pattern.lines.first().map(|l| l.base_point.x) {
|
|
|
|
|
let d = v - cur;
|
|
|
|
|
for line in h.pattern.lines.iter_mut() {
|
|
|
|
|
line.base_point.x += d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"origin_y" => {
|
|
|
|
|
if let Some(cur) = h.pattern.lines.first().map(|l| l.base_point.y) {
|
|
|
|
|
let d = v - cur;
|
|
|
|
|
for line in h.pattern.lines.iter_mut() {
|
|
|
|
|
line.base_point.y += d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
"elevation" => h.elevation = v,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
2026-06-11 09:03:18 +03:00
|
|
|
// 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.
|
2026-06-16 11:33:46 +03:00
|
|
|
let t = crate::scene::view::transform::reflection_about_xy_line(p1, p2);
|
2026-06-11 09:03:18 +03:00
|
|
|
acadrust::entities::Entity::apply_transform(entity, &t);
|
2026-03-22 14:56:27 +03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PropertyEditable for Hatch {
|
2026-07-02 22:46:19 +03:00
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
2026-03-22 14:56:27 +03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-03 19:16:17 +03:00
|
|
|
|
|
|
|
|
// ── 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> {
|
2026-06-11 00:04:37 +03:00
|
|
|
let elev = self.elevation;
|
2026-04-03 19:16:17 +03:00
|
|
|
let mut out = Vec::new();
|
|
|
|
|
let mut id = 0usize;
|
2026-07-08 22:48:33 +03:00
|
|
|
// Grip 0 = pattern-origin handle, shown at the hatch centroid (only when a
|
|
|
|
|
// pattern exists). Dragging it moves the pattern tiling origin; boundary
|
|
|
|
|
// grips follow from the next id.
|
|
|
|
|
if let Some(l0) = self.pattern.lines.first() {
|
|
|
|
|
let (gx, gy) =
|
|
|
|
|
boundary_centroid(self).unwrap_or((l0.base_point.x, l0.base_point.y));
|
|
|
|
|
out.push(circle_grip(id, glam::DVec3::new(gx, gy, elev)));
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
2026-04-03 19:16:17 +03:00
|
|
|
for path in &self.paths {
|
|
|
|
|
for edge in &path.edges {
|
|
|
|
|
match edge {
|
|
|
|
|
BoundaryEdge::Polyline(p) => {
|
|
|
|
|
for v in &p.vertices {
|
2026-06-11 00:04:37 +03:00
|
|
|
out.push(center_grip(id, glam::DVec3::new(v.x, v.y, elev)));
|
2026-04-03 19:16:17 +03:00
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::Line(l) => {
|
refactor(grip): square-only marker set + oriented stretch handles
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.
2026-05-29 02:07:34 +03:00
|
|
|
out.push(center_grip(
|
2026-05-12 10:36:15 +03:00
|
|
|
id,
|
2026-06-11 00:04:37 +03:00
|
|
|
glam::DVec3::new(l.start.x, l.start.y, elev),
|
2026-05-12 10:36:15 +03:00
|
|
|
));
|
2026-04-03 19:16:17 +03:00
|
|
|
id += 1;
|
2026-06-11 00:04:37 +03:00
|
|
|
out.push(center_grip(id, glam::DVec3::new(l.end.x, l.end.y, elev)));
|
2026-04-03 19:16:17 +03:00
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::CircularArc(a) => {
|
refactor(grip): square-only marker set + oriented stretch handles
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.
2026-05-29 02:07:34 +03:00
|
|
|
out.push(center_grip(
|
2026-05-12 10:36:15 +03:00
|
|
|
id,
|
2026-06-11 00:04:37 +03:00
|
|
|
glam::DVec3::new(a.center.x, a.center.y, elev),
|
2026-05-12 10:36:15 +03:00
|
|
|
));
|
2026-04-03 19:16:17 +03:00
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::EllipticArc(e) => {
|
refactor(grip): square-only marker set + oriented stretch handles
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.
2026-05-29 02:07:34 +03:00
|
|
|
out.push(center_grip(
|
2026-05-12 10:36:15 +03:00
|
|
|
id,
|
2026-06-11 00:04:37 +03:00
|
|
|
glam::DVec3::new(e.center.x, e.center.y, elev),
|
2026-05-12 10:36:15 +03:00
|
|
|
));
|
2026-04-03 19:16:17 +03:00
|
|
|
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 {
|
2026-06-11 00:04:37 +03:00
|
|
|
out.push(center_grip(id, glam::DVec3::new(x, y, elev)));
|
2026-04-03 19:16:17 +03:00
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
|
|
|
let elev = self.elevation as f32;
|
|
|
|
|
|
|
|
|
|
fn resolve(apply: &GripApply, cur: Vec3) -> (f64, f64) {
|
|
|
|
|
let p = match apply {
|
|
|
|
|
GripApply::Absolute(p) => *p,
|
feat(f64): command layer compiles on DVec3
Whole command/snap/grip/cursor chain now builds on f64 DVec3 end to end,
so picked points, snaps and grip drags stay precise at UTM-scale
coordinates instead of quantizing to ~0.5 m through f32.
- All ~80 command files migrated: draw (line/circle/arc/ellipse/shapes
in full f64; others via point methods), modify, annotate, insert, view,
inquiry, layers, groups, clipboard, layout.
- Entity apply_grip impls updated for GripApply::{Absolute,Translate}(DVec3).
- Construction sites (EntityTransform/CmdResult/DynAnchor) emit DVec3;
wire-preview boundaries cast to f32 only at the GPU edge.
- Modify/annotate commands keep internal Vec3 geometry where precision is
non-critical, casting at the DVec3 boundary; key draw commands store f64.
Builds clean (no warnings). Visual verification of crosshair/snap/grip at
extreme UTM zoom pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:51:50 +03:00
|
|
|
GripApply::Translate(d) => cur.as_dvec3() + *d,
|
2026-04-03 19:16:17 +03:00
|
|
|
};
|
feat(f64): command layer compiles on DVec3
Whole command/snap/grip/cursor chain now builds on f64 DVec3 end to end,
so picked points, snaps and grip drags stay precise at UTM-scale
coordinates instead of quantizing to ~0.5 m through f32.
- All ~80 command files migrated: draw (line/circle/arc/ellipse/shapes
in full f64; others via point methods), modify, annotate, insert, view,
inquiry, layers, groups, clipboard, layout.
- Entity apply_grip impls updated for GripApply::{Absolute,Translate}(DVec3).
- Construction sites (EntityTransform/CmdResult/DynAnchor) emit DVec3;
wire-preview boundaries cast to f32 only at the GPU edge.
- Modify/annotate commands keep internal Vec3 geometry where precision is
non-critical, casting at the DVec3 boundary; key draw commands store f64.
Builds clean (no warnings). Visual verification of crosshair/snap/grip at
extreme UTM zoom pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:51:50 +03:00
|
|
|
(p.x, p.y)
|
2026-04-03 19:16:17 +03:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 22:48:33 +03:00
|
|
|
let mut id = 0usize;
|
|
|
|
|
// Grip 0 = pattern origin: shift every line's base point by the delta,
|
|
|
|
|
// preserving relative offsets (mirrors the origin_x / origin_y edits).
|
|
|
|
|
if let Some((ox, oy)) = self
|
|
|
|
|
.pattern
|
|
|
|
|
.lines
|
|
|
|
|
.first()
|
|
|
|
|
.map(|l| (l.base_point.x, l.base_point.y))
|
|
|
|
|
{
|
|
|
|
|
if grip_id == id {
|
|
|
|
|
let (nx, ny) = resolve(&apply, Vec3::new(ox as f32, oy as f32, elev));
|
|
|
|
|
let (dx, dy) = (nx - ox, ny - oy);
|
|
|
|
|
for line in self.pattern.lines.iter_mut() {
|
|
|
|
|
line.base_point.x += dx;
|
|
|
|
|
line.base_point.y += dy;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:16:17 +03:00
|
|
|
'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 {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) =
|
|
|
|
|
resolve(&apply, Vec3::new(v.x as f32, v.y as f32, elev));
|
2026-04-03 19:16:17 +03:00
|
|
|
v.x = nx;
|
|
|
|
|
v.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::Line(l) => {
|
|
|
|
|
if id == grip_id {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) = resolve(
|
|
|
|
|
&apply,
|
|
|
|
|
Vec3::new(l.start.x as f32, l.start.y as f32, elev),
|
|
|
|
|
);
|
2026-04-03 19:16:17 +03:00
|
|
|
l.start.x = nx;
|
|
|
|
|
l.start.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
if id == grip_id {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) =
|
|
|
|
|
resolve(&apply, Vec3::new(l.end.x as f32, l.end.y as f32, elev));
|
2026-04-03 19:16:17 +03:00
|
|
|
l.end.x = nx;
|
|
|
|
|
l.end.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::CircularArc(a) => {
|
|
|
|
|
if id == grip_id {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) = resolve(
|
|
|
|
|
&apply,
|
|
|
|
|
Vec3::new(a.center.x as f32, a.center.y as f32, elev),
|
|
|
|
|
);
|
2026-04-03 19:16:17 +03:00
|
|
|
a.center.x = nx;
|
|
|
|
|
a.center.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
BoundaryEdge::EllipticArc(e) => {
|
|
|
|
|
if id == grip_id {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) = resolve(
|
|
|
|
|
&apply,
|
|
|
|
|
Vec3::new(e.center.x as f32, e.center.y as f32, elev),
|
|
|
|
|
);
|
2026-04-03 19:16:17 +03:00
|
|
|
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 {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) =
|
|
|
|
|
resolve(&apply, Vec3::new(fp.x as f32, fp.y as f32, elev));
|
2026-04-03 19:16:17 +03:00
|
|
|
fp.x = nx;
|
|
|
|
|
fp.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for cp in &mut s.control_points {
|
|
|
|
|
if id == grip_id {
|
2026-05-12 10:36:15 +03:00
|
|
|
let (nx, ny) =
|
|
|
|
|
resolve(&apply, Vec3::new(cp.x as f32, cp.y as f32, elev));
|
2026-04-03 19:16:17 +03:00
|
|
|
cp.x = nx;
|
|
|
|
|
cp.y = ny;
|
|
|
|
|
break 'outer;
|
|
|
|
|
}
|
|
|
|
|
id += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
|
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
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,
|
|
|
|
|
},
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
// Origin / Angle / Scale need a follow-up value — handled by
|
|
|
|
|
// `apply_grip_menu_value`.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn grip_menu_value_prompt(
|
|
|
|
|
&self,
|
|
|
|
|
_grip_id: usize,
|
2026-06-16 11:33:46 +03:00
|
|
|
action: crate::scene::model::object::GripMenuAction,
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
) -> Option<&'static str> {
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
match action {
|
|
|
|
|
A::HatchAngle => Some("Angle (deg)"),
|
|
|
|
|
A::HatchScale => Some("Scale"),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip_menu_value(
|
|
|
|
|
&mut self,
|
|
|
|
|
_grip_id: usize,
|
2026-06-16 11:33:46 +03:00
|
|
|
action: crate::scene::model::object::GripMenuAction,
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
value: f64,
|
|
|
|
|
) {
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
match action {
|
|
|
|
|
A::HatchAngle => {
|
2026-07-27 16:44:31 +03:00
|
|
|
let angle = value.to_radians();
|
|
|
|
|
let delta = angle - self.pattern_angle;
|
|
|
|
|
rotate_pattern_geometry(&mut self.pattern, delta);
|
|
|
|
|
self.pattern_angle = angle;
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
}
|
|
|
|
|
A::HatchScale => {
|
|
|
|
|
if value > 0.0 {
|
2026-07-27 16:44:31 +03:00
|
|
|
if self.pattern_scale > 1e-12 {
|
|
|
|
|
let factor = value / self.pattern_scale;
|
|
|
|
|
scale_pattern_geometry(&mut self.pattern, factor);
|
|
|
|
|
}
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
self.pattern_scale = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-03 19:16:17 +03:00
|
|
|
}
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
|
2026-05-23 23:16:57 +03:00
|
|
|
impl FallbackTess for Hatch {
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
fn fallback_geometry(&self) -> FallbackGeometry {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
// Convert a 2D OCS hatch boundary point to absolute WCS.
|
2026-06-24 10:09:19 +03:00
|
|
|
let to_wcs = |x: f64, y: f64| -> [f64; 3] {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
let (wx, wy, wz) =
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::transform::ocs_point_to_wcs((x, y, self.elevation), normal);
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
[wx, wy, wz]
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
};
|
2026-06-24 10:09:19 +03:00
|
|
|
// 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();
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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();
|
2026-05-26 18:16:40 +03:00
|
|
|
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() {
|
2026-06-24 10:09:19 +03:00
|
|
|
pts.push([f64::NAN; 3]);
|
2026-05-26 18:16:40 +03:00
|
|
|
}
|
|
|
|
|
let start_idx = pts.len();
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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;
|
refactor(common): unify bulge → arc math into BulgeArc
The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.
Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.
Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.
array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:42:26 +03:00
|
|
|
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 {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
let p = to_wcs(v0.x, v0.y);
|
|
|
|
|
pts.push(p);
|
|
|
|
|
key_verts.push(p);
|
|
|
|
|
continue;
|
refactor(common): unify bulge → arc math into BulgeArc
The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.
Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.
Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.
array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:42:26 +03:00
|
|
|
};
|
2026-06-11 00:04:37 +03:00
|
|
|
let segs = arc_segments(
|
|
|
|
|
arc.radius,
|
|
|
|
|
arc.sweep.abs(),
|
|
|
|
|
wire_chord_tol(arc.radius),
|
|
|
|
|
);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
for j in 0..segs {
|
refactor(common): unify bulge → arc math into BulgeArc
The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.
Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.
Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.
array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:42:26 +03:00
|
|
|
let s = arc.sample(j as f64 / segs as f64);
|
|
|
|
|
let p = to_wcs(s[0], s[1]);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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() {
|
2026-06-24 10:09:19 +03:00
|
|
|
pts.push([f64::NAN; 3]);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
}
|
|
|
|
|
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() {
|
2026-06-24 10:09:19 +03:00
|
|
|
pts.push([f64::NAN; 3]);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
}
|
|
|
|
|
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((
|
2026-06-24 10:09:19 +03:00
|
|
|
snap_at(to_wcs(arc.center.x, arc.center.y)),
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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;
|
2026-06-11 00:04:37 +03:00
|
|
|
let rot = ell.major_axis_endpoint.y.atan2(ell.major_axis_endpoint.x);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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() {
|
2026-06-24 10:09:19 +03:00
|
|
|
pts.push([f64::NAN; 3]);
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
}
|
|
|
|
|
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((
|
2026-06-24 10:09:19 +03:00
|
|
|
snap_at(to_wcs(ell.center.x, ell.center.y)),
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|