2026-03-22 14:56:27 +03:00
|
|
|
|
use acadrust::entities::Ellipse;
|
|
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire};
|
|
|
|
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
|
|
|
|
|
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
refactor(entities): collapse trait wrapper boilerplate via macro
Every entity file ended with the same three-trait wrapper block:
impl Grippable for T {
fn grips(&self) -> Vec<GripDef> { grips(self) }
fn apply_grip(&mut self, ...) { apply_grip(self, ...) }
}
impl PropertyEditable for T { ... }
impl Transformable for T { ... }
— 23 lines of pure delegation around five same-named free functions.
Replace with `crate::impl_entity_basics!(T);` (a macro in
entities/traits.rs that expands to the three trait impls).
Applied where the file already follows the free-fn delegation
convention (Arc, Circle, Ellipse, Insert, Leader, Line, LwPolyline,
MultiLeader, Ole2Frame, Point, Spline, Viewport). Files that inlined
the trait method bodies directly (Ray, XLine, Shape, Solid, Tolerance,
MLine, Mesh, Solid3D/Region/Body, Table, RasterImage, Wipeout,
Underlay, Hatch, Polyline, Polyline2D, Polyline3D, Dimension) left
alone — they already skip the indirection. Text/MText/Attribute use
the with-text-style flavour and will move once that variant is needed
beyond the existing two manual impls.
A second macro `impl_entity_basics_with_text_styles!` is included for
the text-like flavour but no entity uses it yet.
Net: -204 lines.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:16:06 +03:00
|
|
|
|
use crate::entities::traits::TruckConvertible;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
|
|
|
|
|
use crate::scene::object::{GripApply, GripDef, PropSection};
|
|
|
|
|
|
use crate::scene::wire_model::SnapHint;
|
|
|
|
|
|
|
|
|
|
|
|
const TAU: f64 = std::f64::consts::TAU;
|
|
|
|
|
|
|
|
|
|
|
|
fn to_truck(ell: &Ellipse) -> TruckEntity {
|
|
|
|
|
|
let cx = ell.center.x;
|
|
|
|
|
|
let cy = ell.center.y;
|
|
|
|
|
|
let cz = ell.center.z;
|
2026-05-02 17:20:25 +03:00
|
|
|
|
let normal = (ell.normal.x, ell.normal.y, ell.normal.z);
|
|
|
|
|
|
let (nx, ny, nz) = normal;
|
|
|
|
|
|
|
|
|
|
|
|
// Center in WCS.
|
|
|
|
|
|
let (cwx, cwy, cwz) = crate::scene::transform::ocs_point_to_wcs((cx, cy, cz), normal);
|
|
|
|
|
|
|
|
|
|
|
|
// Major axis vector rotated into WCS: maj_ocs.x*Ax + maj_ocs.y*Ay + maj_ocs.z*N
|
|
|
|
|
|
let (ax_basis, ay_basis) = crate::scene::transform::ocs_axes(normal);
|
|
|
|
|
|
let (mx, my, mz) = (ell.major_axis.x, ell.major_axis.y, ell.major_axis.z);
|
|
|
|
|
|
let wcs_maj = glam::Vec3::new(
|
|
|
|
|
|
(mx * ax_basis.0 + my * ay_basis.0 + mz * nx) as f32,
|
|
|
|
|
|
(mx * ax_basis.1 + my * ay_basis.1 + mz * ny) as f32,
|
|
|
|
|
|
(mx * ax_basis.2 + my * ay_basis.2 + mz * nz) as f32,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
);
|
2026-05-02 17:20:25 +03:00
|
|
|
|
let r_major = wcs_maj.length() as f64;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let r_minor = r_major * ell.minor_axis_ratio;
|
|
|
|
|
|
let t0 = ell.start_parameter;
|
|
|
|
|
|
let mut t1 = ell.end_parameter;
|
|
|
|
|
|
if t1 <= t0 {
|
|
|
|
|
|
t1 += TAU;
|
|
|
|
|
|
}
|
|
|
|
|
|
let u = if r_major > 1e-9 {
|
2026-05-02 17:20:25 +03:00
|
|
|
|
wcs_maj / wcs_maj.length()
|
2026-03-22 14:56:27 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
glam::Vec3::X
|
|
|
|
|
|
};
|
2026-05-02 17:20:25 +03:00
|
|
|
|
// Minor axis direction: WCS_normal × u (both unit vectors, always perpendicular).
|
|
|
|
|
|
let wcs_normal = glam::Vec3::new(nx as f32, ny as f32, nz as f32);
|
|
|
|
|
|
let v_axis = wcs_normal.cross(u);
|
|
|
|
|
|
let center_v3 = Vec3::new(cwx as f32, cwy as f32, cwz as f32);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let is_closed = (t1 - t0 - TAU).abs() < 1e-6;
|
|
|
|
|
|
|
|
|
|
|
|
if is_closed {
|
|
|
|
|
|
let n = 16usize;
|
|
|
|
|
|
let pts_upper: Vec<Point3> = (0..=n)
|
|
|
|
|
|
.map(|i| {
|
|
|
|
|
|
let t = (i as f64 / n as f64) * std::f64::consts::PI;
|
|
|
|
|
|
let lx = (r_major * t.cos()) as f32;
|
|
|
|
|
|
let lz = (r_minor * t.sin()) as f32;
|
|
|
|
|
|
Point3::new(
|
2026-05-02 17:20:25 +03:00
|
|
|
|
cwx + (lx * u.x + lz * v_axis.x) as f64,
|
|
|
|
|
|
cwy + (lx * u.y + lz * v_axis.y) as f64,
|
|
|
|
|
|
cwz + (lx * u.z + lz * v_axis.z) as f64,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let pts_lower: Vec<Point3> = (0..=n)
|
|
|
|
|
|
.map(|i| {
|
|
|
|
|
|
let t = std::f64::consts::PI + (i as f64 / n as f64) * std::f64::consts::PI;
|
|
|
|
|
|
let lx = (r_major * t.cos()) as f32;
|
|
|
|
|
|
let lz = (r_minor * t.sin()) as f32;
|
|
|
|
|
|
Point3::new(
|
2026-05-02 17:20:25 +03:00
|
|
|
|
cwx + (lx * u.x + lz * v_axis.x) as f64,
|
|
|
|
|
|
cwy + (lx * u.y + lz * v_axis.y) as f64,
|
|
|
|
|
|
cwz + (lx * u.z + lz * v_axis.z) as f64,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let v_pos = builder::vertex(*pts_upper.first().unwrap());
|
|
|
|
|
|
let v_neg = builder::vertex(*pts_upper.last().unwrap());
|
|
|
|
|
|
let kv_u = KnotVec::uniform_knot(1, n);
|
|
|
|
|
|
let kv_l = KnotVec::uniform_knot(1, n);
|
|
|
|
|
|
let spl_u = BSplineCurve::new(kv_u, pts_upper);
|
|
|
|
|
|
let spl_l = BSplineCurve::new(kv_l, pts_lower);
|
|
|
|
|
|
let edge_upper = Edge::new(&v_pos, &v_neg, Curve::BSplineCurve(spl_u));
|
|
|
|
|
|
let edge_lower = Edge::new(&v_neg, &v_pos, Curve::BSplineCurve(spl_l));
|
|
|
|
|
|
let wire: Wire = [edge_upper, edge_lower].into_iter().collect();
|
2026-05-02 17:20:25 +03:00
|
|
|
|
// Quadrant points at ±major and ±minor axis endpoints in WCS.
|
2026-04-27 02:34:51 +03:00
|
|
|
|
let q = |lx: f64, lz: f64| {
|
|
|
|
|
|
Vec3::new(
|
2026-05-02 17:20:25 +03:00
|
|
|
|
(cwx + lx * u.x as f64 + lz * v_axis.x as f64) as f32,
|
|
|
|
|
|
(cwy + lx * u.y as f64 + lz * v_axis.y as f64) as f32,
|
|
|
|
|
|
(cwz + lx * u.z as f64 + lz * v_axis.z as f64) as f32,
|
2026-04-27 02:34:51 +03:00
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let snap_pts = vec![
|
|
|
|
|
|
(center_v3, SnapHint::Center),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
(q(r_major, 0.0), SnapHint::Quadrant),
|
2026-04-27 02:34:51 +03:00
|
|
|
|
(q(-r_major, 0.0), SnapHint::Quadrant),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
(q(0.0, r_minor), SnapHint::Quadrant),
|
2026-04-27 02:34:51 +03:00
|
|
|
|
(q(0.0, -r_minor), SnapHint::Quadrant),
|
|
|
|
|
|
];
|
2026-03-22 14:56:27 +03:00
|
|
|
|
TruckEntity {
|
|
|
|
|
|
object: TruckObject::Contour(wire),
|
2026-04-27 02:34:51 +03:00
|
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris: vec![],
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let n = 32usize;
|
|
|
|
|
|
let ctrl_pts: Vec<Point3> = (0..=n)
|
|
|
|
|
|
.map(|i| {
|
|
|
|
|
|
let t = t0 + (t1 - t0) * (i as f64 / n as f64);
|
|
|
|
|
|
let lx = (r_major * t.cos()) as f32;
|
|
|
|
|
|
let lz = (r_minor * t.sin()) as f32;
|
|
|
|
|
|
Point3::new(
|
2026-05-02 17:20:25 +03:00
|
|
|
|
cwx + (lx * u.x + lz * v_axis.x) as f64,
|
|
|
|
|
|
cwy + (lx * u.y + lz * v_axis.y) as f64,
|
|
|
|
|
|
cwz + (lx * u.z + lz * v_axis.z) as f64,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let kv = KnotVec::uniform_knot(1, n);
|
|
|
|
|
|
let bspline = BSplineCurve::new(kv, ctrl_pts.clone());
|
|
|
|
|
|
let v_start = builder::vertex(*ctrl_pts.first().unwrap());
|
|
|
|
|
|
let v_end = builder::vertex(*ctrl_pts.last().unwrap());
|
|
|
|
|
|
let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline));
|
2026-04-27 02:22:51 +03:00
|
|
|
|
let pt_start = ctrl_pts.first().unwrap();
|
|
|
|
|
|
let pt_end = ctrl_pts.last().unwrap();
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let key_vertices: Vec<[f64; 3]> = vec![
|
|
|
|
|
|
[pt_start.x, pt_start.y, pt_start.z],
|
|
|
|
|
|
[pt_end.x, pt_end.y, pt_end.z],
|
2026-04-27 02:22:51 +03:00
|
|
|
|
];
|
2026-03-22 14:56:27 +03:00
|
|
|
|
TruckEntity {
|
|
|
|
|
|
object: TruckObject::Curve(edge),
|
|
|
|
|
|
snap_pts: vec![(center_v3, SnapHint::Center)],
|
|
|
|
|
|
tangent_geoms: vec![],
|
2026-04-27 02:22:51 +03:00
|
|
|
|
key_vertices,
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris: vec![],
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn grips(ell: &Ellipse) -> Vec<GripDef> {
|
|
|
|
|
|
let ctr = Vec3::new(
|
|
|
|
|
|
ell.center.x as f32,
|
|
|
|
|
|
ell.center.y as f32,
|
|
|
|
|
|
ell.center.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let maj = Vec3::new(
|
|
|
|
|
|
(ell.center.x + ell.major_axis.x) as f32,
|
|
|
|
|
|
(ell.center.y + ell.major_axis.y) as f32,
|
|
|
|
|
|
(ell.center.z + ell.major_axis.z) as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let major_xy =
|
|
|
|
|
|
((ell.major_axis.x * ell.major_axis.x + ell.major_axis.y * ell.major_axis.y) as f64).sqrt();
|
|
|
|
|
|
let (px, py) = if major_xy > 1e-10 {
|
|
|
|
|
|
let s = ell.major_axis_length() * ell.minor_axis_ratio / major_xy;
|
|
|
|
|
|
(-ell.major_axis.y * s, ell.major_axis.x * s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(0.0, ell.major_axis_length() * ell.minor_axis_ratio)
|
|
|
|
|
|
};
|
|
|
|
|
|
let min = Vec3::new(
|
|
|
|
|
|
(ell.center.x + px) as f32,
|
|
|
|
|
|
(ell.center.y + py) as f32,
|
|
|
|
|
|
ell.center.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
vec![
|
|
|
|
|
|
diamond_grip(0, ctr),
|
|
|
|
|
|
square_grip(1, maj),
|
|
|
|
|
|
square_grip(2, min),
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn properties(ell: &Ellipse) -> PropSection {
|
|
|
|
|
|
let r_major = (ell.major_axis.x * ell.major_axis.x
|
|
|
|
|
|
+ ell.major_axis.y * ell.major_axis.y
|
|
|
|
|
|
+ ell.major_axis.z * ell.major_axis.z)
|
|
|
|
|
|
.sqrt();
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
edit("Center X", "center_x", ell.center.x),
|
|
|
|
|
|
edit("Center Y", "center_y", ell.center.y),
|
|
|
|
|
|
edit("Center Z", "center_z", ell.center.z),
|
|
|
|
|
|
ro("Major Radius", "major_r", format!("{r_major:.4}")),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Minor Radius",
|
|
|
|
|
|
"minor_r",
|
|
|
|
|
|
format!("{:.4}", r_major * ell.minor_axis_ratio),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Minor/Major",
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
format!("{:.4}", ell.minor_axis_ratio),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(_ell: &mut Ellipse, _field: &str, _value: &str) {}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
|
|
|
|
|
match (grip_id, apply) {
|
|
|
|
|
|
(0, GripApply::Translate(d)) => {
|
|
|
|
|
|
ell.center.x += d.x as f64;
|
|
|
|
|
|
ell.center.y += d.y as f64;
|
|
|
|
|
|
ell.center.z += d.z as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
(0, GripApply::Absolute(p)) => {
|
|
|
|
|
|
ell.center.x = p.x as f64;
|
|
|
|
|
|
ell.center.y = p.y as f64;
|
|
|
|
|
|
ell.center.z = p.z as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
(1, GripApply::Absolute(p)) => {
|
|
|
|
|
|
ell.major_axis.x = p.x as f64 - ell.center.x;
|
|
|
|
|
|
ell.major_axis.y = p.y as f64 - ell.center.y;
|
|
|
|
|
|
ell.major_axis.z = p.z as f64 - ell.center.z;
|
|
|
|
|
|
}
|
|
|
|
|
|
(2, GripApply::Absolute(p)) => {
|
|
|
|
|
|
let major_len = ell.major_axis_length();
|
|
|
|
|
|
if major_len > 1e-10 {
|
|
|
|
|
|
let dx = p.x as f64 - ell.center.x;
|
|
|
|
|
|
let dy = p.y as f64 - ell.center.y;
|
|
|
|
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
|
|
|
|
ell.minor_axis_ratio = (dist / major_len).clamp(0.001, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) {
|
|
|
|
|
|
crate::scene::transform::apply_standard_entity_transform(ell, t, |entity, p1, p2| {
|
|
|
|
|
|
crate::scene::transform::reflect_xy_point(
|
|
|
|
|
|
&mut entity.center.x,
|
|
|
|
|
|
&mut entity.center.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
crate::scene::transform::reflect_xy_point(
|
|
|
|
|
|
&mut entity.major_axis.x,
|
|
|
|
|
|
&mut entity.major_axis.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl TruckConvertible for Ellipse {
|
|
|
|
|
|
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
|
|
|
|
|
Some(to_truck(self))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(entities): collapse trait wrapper boilerplate via macro
Every entity file ended with the same three-trait wrapper block:
impl Grippable for T {
fn grips(&self) -> Vec<GripDef> { grips(self) }
fn apply_grip(&mut self, ...) { apply_grip(self, ...) }
}
impl PropertyEditable for T { ... }
impl Transformable for T { ... }
— 23 lines of pure delegation around five same-named free functions.
Replace with `crate::impl_entity_basics!(T);` (a macro in
entities/traits.rs that expands to the three trait impls).
Applied where the file already follows the free-fn delegation
convention (Arc, Circle, Ellipse, Insert, Leader, Line, LwPolyline,
MultiLeader, Ole2Frame, Point, Spline, Viewport). Files that inlined
the trait method bodies directly (Ray, XLine, Shape, Solid, Tolerance,
MLine, Mesh, Solid3D/Region/Body, Table, RasterImage, Wipeout,
Underlay, Hatch, Polyline, Polyline2D, Polyline3D, Dimension) left
alone — they already skip the indirection. Text/MText/Attribute use
the with-text-style flavour and will move once that variant is needed
beyond the existing two manual impls.
A second macro `impl_entity_basics_with_text_styles!` is included for
the text-like flavour but no entity uses it yet.
Net: -204 lines.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:16:06 +03:00
|
|
|
|
crate::impl_entity_basics!(Ellipse);
|
refactor(commands): per-entity MassProps + TextContent traits
Two free helpers in app/commands.rs lived as big match-on-EntityType
blocks: massprop_entity (Circle/Arc/Line/LwPolyline/Ellipse area +
perimeter + centroid, ~140 lines) and entity_text_content /
replace_entity_text (Text/MText/AttributeDefinition/AttributeEntity).
Move each to its own trait in entities/traits.rs and implement
per-entity:
MassPropsCalc — Circle, Arc, Line, LwPolyline, Ellipse
TextContent — Text, MText, AttributeDefinition, AttributeEntity
EntityTypeOps gets dispatch shims that route to the trait via the
existing macro, so callsites just call entity.mass_props() /
entity.text_content() / entity.replace_text(search, rep).
The remaining free helpers in commands.rs (entity_list_details,
entity_extra_info, entity_type_name, build_data_extraction_csv) are
single-command formatting helpers tightly coupled to LIST / EXTRACT /
text-format output. Distributing them as per-entity methods would
inflate every entity file with command-side formatting concerns
without making the command-side any cleaner — left in place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:30:34 +03:00
|
|
|
|
|
|
|
|
|
|
impl crate::entities::traits::MassPropsCalc for acadrust::entities::Ellipse {
|
|
|
|
|
|
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
|
|
|
|
|
use std::f64::consts::{PI, TAU};
|
|
|
|
|
|
let e = self;
|
|
|
|
|
|
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
|
|
|
|
|
|
let b = a * e.minor_axis_ratio;
|
|
|
|
|
|
let t0 = e.start_parameter;
|
|
|
|
|
|
let t1 = {
|
|
|
|
|
|
let mut t = e.end_parameter;
|
|
|
|
|
|
if t <= t0 {
|
|
|
|
|
|
t += TAU;
|
|
|
|
|
|
}
|
|
|
|
|
|
t
|
|
|
|
|
|
};
|
|
|
|
|
|
let span = t1 - t0;
|
|
|
|
|
|
let is_full = (span - TAU).abs() < 1e-6;
|
|
|
|
|
|
let area = if is_full {
|
|
|
|
|
|
PI * a * b
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Sector area of ellipse approximated via 256-pt integration
|
|
|
|
|
|
let n = 256usize;
|
|
|
|
|
|
let mut s = 0.0f64;
|
|
|
|
|
|
for k in 0..n {
|
|
|
|
|
|
let t = t0 + span * (k as f64 / n as f64);
|
|
|
|
|
|
let tp = t0 + span * ((k + 1) as f64 / n as f64);
|
|
|
|
|
|
let nx = e.major_axis.x / a;
|
|
|
|
|
|
let ny = e.major_axis.y / a;
|
|
|
|
|
|
let x0 = a * t.cos() * nx - b * t.sin() * ny;
|
|
|
|
|
|
let y0 = a * t.cos() * ny + b * t.sin() * nx;
|
|
|
|
|
|
let x1 = a * tp.cos() * nx - b * tp.sin() * ny;
|
|
|
|
|
|
let y1 = a * tp.cos() * ny + b * tp.sin() * nx;
|
|
|
|
|
|
s += x0 * y1 - x1 * y0;
|
|
|
|
|
|
}
|
|
|
|
|
|
(s / 2.0).abs()
|
|
|
|
|
|
};
|
|
|
|
|
|
// Arc length via 256-pt numerical integration
|
|
|
|
|
|
let nx = e.major_axis.x / a.max(1e-12);
|
|
|
|
|
|
let ny = e.major_axis.y / a.max(1e-12);
|
|
|
|
|
|
let perimeter = {
|
|
|
|
|
|
let n = 256usize;
|
|
|
|
|
|
let mut len = 0.0f64;
|
|
|
|
|
|
for k in 0..n {
|
|
|
|
|
|
let t = t0 + span * (k as f64 / n as f64);
|
|
|
|
|
|
let tp = t0 + span * ((k + 1) as f64 / n as f64);
|
|
|
|
|
|
let x0 = e.center.x + a * t.cos() * nx - b * t.sin() * ny;
|
|
|
|
|
|
let y0 = e.center.y + a * t.cos() * ny + b * t.sin() * nx;
|
|
|
|
|
|
let x1 = e.center.x + a * tp.cos() * nx - b * tp.sin() * ny;
|
|
|
|
|
|
let y1 = e.center.y + a * tp.cos() * ny + b * tp.sin() * nx;
|
|
|
|
|
|
len += (x1 - x0).hypot(y1 - y0);
|
|
|
|
|
|
}
|
|
|
|
|
|
len
|
|
|
|
|
|
};
|
|
|
|
|
|
crate::entities::traits::MassProps {
|
|
|
|
|
|
area,
|
|
|
|
|
|
perimeter,
|
|
|
|
|
|
cx: e.center.x,
|
|
|
|
|
|
cy: e.center.y,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|