diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 0cf40a5b2..d64184dd3 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -354,58 +354,14 @@ fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) { } fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) { - use codec::types::{Transform, Vector3}; - let transform = match t { - EntityTransform::Translate(d) => { - ell.center = ell.center + Vector3::new(d.x, d.y, d.z); - return; - } - 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) - }; - } + // The codec maps the whole ellipse — ratio, principal axes and arc + // parameters — under rotate, scale, affine and mirror alike. + crate::scene::view::transform::apply_standard_entity_transform(ell, t, |entity, p1, p2| { + codec::Entity::apply_mirror( + entity, + &crate::scene::view::transform::reflection_about_xy_line(p1, p2), + ); + }); } impl RenderConvertible for Ellipse { @@ -581,9 +537,12 @@ mod grip_tests { }; 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); - // Normal should flip to -Z to preserve counter-clockwise traversal - assert!(e.normal.z < 0.0); + assert!((e.normal.z - 1.0).abs() < 1e-9); + 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}"); } } diff --git a/src/scene/view/transform.rs b/src/scene/view/transform.rs index 151995ba9..52cc26321 100644 --- a/src/scene/view/transform.rs +++ b/src/scene/view/transform.rs @@ -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; } -/// Fully transforms ellipse / elliptical arc geometry under any 3D affine transform -/// (including negative scales, reflections, negative rotations, non-uniform scales, and translation). +/// An ellipse / elliptical arc's image under any affine transform, through the +/// 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)` -/// where `new_major_axis` is guaranteed to be orthogonal to `new_normal`, its length is >= semi-minor axis length, -/// and `new_minor_axis_ratio <= 1.0`. +/// Returns `(center, major_axis, normal, minor_axis_ratio, start_param, +/// end_param)`; the major axis is the longer one and a reflection flips the +/// normal. pub fn transform_ellipse_geometry( center: Vector3, major_axis: Vector3, @@ -135,76 +136,32 @@ pub fn transform_ellipse_geometry( end_param: f64, transform: &Transform, ) -> Option<(Vector3, Vector3, Vector3, f64, f64, f64)> { - let norm_len = normal.length(); - if norm_len <= 1e-12 || !norm_len.is_finite() { + use codec::Entity; + if normal.length() <= 1e-12 || major_axis.length() <= 1e-12 { return None; } - let norm = normal / norm_len; - let m = major_axis; - let m_len = m.length(); - if m_len <= 1e-12 || !m_len.is_finite() { - return None; - } - let ratio = minor_axis_ratio.clamp(1e-12, 1.0); - // V = (N x M) * ratio - 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) + let mut ellipse = codec::entities::Ellipse { + center, + major_axis, + normal, + minor_axis_ratio, + start_parameter: start_param, + end_parameter: end_param, + ..Default::default() }; - - Some((c_prime, m_new, n_new, new_ratio, new_start, new_end)) + ellipse.apply_transform(transform); + 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