forked from 1b-agent/opencadstudio-upstream
fix(ellipse): #1532 follow-up — one transform, in the codec
The PR's principal-axis math now lives in the codec's transform_ellipse (8464e0a), which explode, INSERT and every other codec path share. transform_ellipse_geometry delegates to it for the block-cache tangent, and the entity goes back through apply_standard_entity_transform, with MIRROR delegated to the codec's apply_mirror: the old closure reflected only the centre and major axis, leaving mirrored arcs on the wrong side, and the PR's version turned the normal to -Z. The mirror test now checks the kept +Z normal and the negated parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df8ea089d7
commit
69a2e7c73a
2 files changed
+43
-127
No files matched your search
+14
-55
@@ -354,58 +354,14 @@ fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) {
|
fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) {
|
||||||
use codec::types::{Transform, Vector3};
|
// The codec maps the whole ellipse — ratio, principal axes and arc
|
||||||
let transform = match t {
|
// parameters — under rotate, scale, affine and mirror alike.
|
||||||
EntityTransform::Translate(d) => {
|
crate::scene::view::transform::apply_standard_entity_transform(ell, t, |entity, p1, p2| {
|
||||||
ell.center = ell.center + Vector3::new(d.x, d.y, d.z);
|
codec::Entity::apply_mirror(
|
||||||
return;
|
entity,
|
||||||
}
|
&crate::scene::view::transform::reflection_about_xy_line(p1, p2),
|
||||||
EntityTransform::Rotate { center, axis, angle_rad } => {
|
);
|
||||||
let axis_norm = Vector3::new(axis.x, axis.y, axis.z);
|
});
|
||||||
let axis_len = axis_norm.length();
|
|
||||||
let axis_v = if axis_len > 1e-12 { axis_norm / axis_len } else { Vector3::UNIT_Z };
|
|
||||||
Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z))
|
|
||||||
.then(&Transform::from_rotation(axis_v, *angle_rad))
|
|
||||||
.then(&Transform::from_translation(Vector3::new(center.x, center.y, center.z)))
|
|
||||||
}
|
|
||||||
EntityTransform::Scale { center, factor } => {
|
|
||||||
let s = *factor;
|
|
||||||
Transform::from_scaling_with_origin(
|
|
||||||
Vector3::new(s, s, s),
|
|
||||||
Vector3::new(center.x, center.y, center.z),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
EntityTransform::Mirror { p1, p2, working_normal } => {
|
|
||||||
crate::scene::view::transform::reflection_about_working_line(
|
|
||||||
*p1,
|
|
||||||
*p2,
|
|
||||||
*working_normal,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
EntityTransform::Affine(tr) => tr.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((c, m, n, ratio, s, e)) = crate::scene::view::transform::transform_ellipse_geometry(
|
|
||||||
ell.center,
|
|
||||||
ell.major_axis,
|
|
||||||
ell.normal,
|
|
||||||
ell.minor_axis_ratio,
|
|
||||||
ell.start_parameter,
|
|
||||||
ell.end_parameter,
|
|
||||||
&transform,
|
|
||||||
) {
|
|
||||||
ell.center = c;
|
|
||||||
ell.major_axis = m;
|
|
||||||
ell.normal = n;
|
|
||||||
ell.minor_axis_ratio = ratio;
|
|
||||||
ell.start_parameter = s;
|
|
||||||
let is_full = (e - s - std::f64::consts::TAU).abs() < 1e-9;
|
|
||||||
ell.end_parameter = if is_full {
|
|
||||||
std::f64::consts::TAU
|
|
||||||
} else {
|
|
||||||
e.rem_euclid(std::f64::consts::TAU)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderConvertible for Ellipse {
|
impl RenderConvertible for Ellipse {
|
||||||
@@ -581,9 +537,12 @@ mod grip_tests {
|
|||||||
};
|
};
|
||||||
apply_transform(&mut e, &mirror);
|
apply_transform(&mut e, &mirror);
|
||||||
|
|
||||||
// Major axis should be reflected to (-10, 0, 0)
|
// Major axis reflected to (-10, 0, 0); MIRROR keeps the normal facing
|
||||||
|
// the mirrored plane and negates the parameters instead.
|
||||||
assert!((e.major_axis.x - (-10.0)).abs() < 1e-9);
|
assert!((e.major_axis.x - (-10.0)).abs() < 1e-9);
|
||||||
// Normal should flip to -Z to preserve counter-clockwise traversal
|
assert!((e.normal.z - 1.0).abs() < 1e-9);
|
||||||
assert!(e.normal.z < 0.0);
|
let (s, end) = (e.start_parameter, e.end_parameter);
|
||||||
|
assert!((s - (std::f64::consts::TAU - 1.5)).abs() < 1e-9, "{s}");
|
||||||
|
assert!((end - s - 1.4).abs() < 1e-9, "{end}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+29
-72
@@ -120,12 +120,13 @@ pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
|
|||||||
*y = p1.y as f64 + my;
|
*y = p1.y as f64 + my;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fully transforms ellipse / elliptical arc geometry under any 3D affine transform
|
/// An ellipse / elliptical arc's image under any affine transform, through the
|
||||||
/// (including negative scales, reflections, negative rotations, non-uniform scales, and translation).
|
/// codec's `transform_ellipse` so the block-cache fast path and the entity
|
||||||
|
/// agree with every other path (explode, insert) by construction.
|
||||||
///
|
///
|
||||||
/// Returns `(new_center, new_major_axis, new_normal, new_minor_axis_ratio, new_start_param, new_end_param)`
|
/// Returns `(center, major_axis, normal, minor_axis_ratio, start_param,
|
||||||
/// where `new_major_axis` is guaranteed to be orthogonal to `new_normal`, its length is >= semi-minor axis length,
|
/// end_param)`; the major axis is the longer one and a reflection flips the
|
||||||
/// and `new_minor_axis_ratio <= 1.0`.
|
/// normal.
|
||||||
pub fn transform_ellipse_geometry(
|
pub fn transform_ellipse_geometry(
|
||||||
center: Vector3,
|
center: Vector3,
|
||||||
major_axis: Vector3,
|
major_axis: Vector3,
|
||||||
@@ -135,76 +136,32 @@ pub fn transform_ellipse_geometry(
|
|||||||
end_param: f64,
|
end_param: f64,
|
||||||
transform: &Transform,
|
transform: &Transform,
|
||||||
) -> Option<(Vector3, Vector3, Vector3, f64, f64, f64)> {
|
) -> Option<(Vector3, Vector3, Vector3, f64, f64, f64)> {
|
||||||
let norm_len = normal.length();
|
use codec::Entity;
|
||||||
if norm_len <= 1e-12 || !norm_len.is_finite() {
|
if normal.length() <= 1e-12 || major_axis.length() <= 1e-12 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let norm = normal / norm_len;
|
let mut ellipse = codec::entities::Ellipse {
|
||||||
let m = major_axis;
|
center,
|
||||||
let m_len = m.length();
|
major_axis,
|
||||||
if m_len <= 1e-12 || !m_len.is_finite() {
|
normal,
|
||||||
return None;
|
minor_axis_ratio,
|
||||||
}
|
start_parameter: start_param,
|
||||||
let ratio = minor_axis_ratio.clamp(1e-12, 1.0);
|
end_parameter: end_param,
|
||||||
// V = (N x M) * ratio
|
..Default::default()
|
||||||
let v = norm.cross(&m) * ratio;
|
|
||||||
|
|
||||||
let c_prime = transform.apply(center);
|
|
||||||
let m0 = transform.apply_rotation(m);
|
|
||||||
let v0 = transform.apply_rotation(v);
|
|
||||||
|
|
||||||
let m0_len_sq = m0.length_squared();
|
|
||||||
let v0_len_sq = v0.length_squared();
|
|
||||||
if m0_len_sq <= 1e-24 || v0_len_sq <= 1e-24 || !m0_len_sq.is_finite() || !v0_len_sq.is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extremum angle theta to find the principal axes of the transformed ellipse
|
|
||||||
let dot = m0.dot(&v0);
|
|
||||||
let diff = m0_len_sq - v0_len_sq;
|
|
||||||
let theta = 0.5 * (2.0 * dot).atan2(diff);
|
|
||||||
|
|
||||||
let (sin_t, cos_t) = theta.sin_cos();
|
|
||||||
let m_new = m0 * cos_t + v0 * sin_t;
|
|
||||||
let v_new = m0 * (-sin_t) + v0 * cos_t;
|
|
||||||
|
|
||||||
let a = m_new.length();
|
|
||||||
let b = v_new.length();
|
|
||||||
if a <= 1e-12 || !a.is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cross = m_new.cross(&v_new);
|
|
||||||
let cross_len = cross.length();
|
|
||||||
if cross_len <= 1e-12 || !cross_len.is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let n_new = cross / cross_len;
|
|
||||||
let new_ratio = (b / a).clamp(1e-12, 1.0);
|
|
||||||
|
|
||||||
// Sweep and parameters
|
|
||||||
use std::f64::consts::TAU;
|
|
||||||
let raw = end_param - start_param;
|
|
||||||
let is_full = (raw - TAU).abs() < 1e-9
|
|
||||||
|| (raw.abs() >= TAU - 1e-9)
|
|
||||||
|| (raw.rem_euclid(TAU).abs() < 1e-9 && raw.abs() > 1e-9);
|
|
||||||
|
|
||||||
let (new_start, new_end) = if is_full {
|
|
||||||
(0.0, TAU)
|
|
||||||
} else {
|
|
||||||
let sweep = if raw <= 0.0 {
|
|
||||||
let s = raw.rem_euclid(TAU);
|
|
||||||
if s <= 1e-9 { TAU } else { s }
|
|
||||||
} else if raw > TAU {
|
|
||||||
TAU
|
|
||||||
} else {
|
|
||||||
raw
|
|
||||||
};
|
|
||||||
let s = (start_param - theta).rem_euclid(TAU);
|
|
||||||
(s, s + sweep)
|
|
||||||
};
|
};
|
||||||
|
ellipse.apply_transform(transform);
|
||||||
Some((c_prime, m_new, n_new, new_ratio, new_start, new_end))
|
let values = [ellipse.center, ellipse.major_axis, ellipse.normal];
|
||||||
|
if values.iter().any(|v| !(v.x.is_finite() && v.y.is_finite() && v.z.is_finite())) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
ellipse.center,
|
||||||
|
ellipse.major_axis,
|
||||||
|
ellipse.normal,
|
||||||
|
ellipse.minor_axis_ratio,
|
||||||
|
ellipse.start_parameter,
|
||||||
|
ellipse.end_parameter,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS
|
/// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS
|
||||||
|
|||||||
Reference in new issue
Block a user