fix(ellipse): accurately transform ellipse and elliptical arcs in blocks and standard transforms

- Add transform_ellipse_geometry helper in scene::view::transform to compute exact principal semi-axes, normal, ratio, and shifted arc parameters under any affine transform (negative scales, reflections, negative rotations, non-uniform scaling, and translations).
- Use transform_ellipse_geometry in block_cache::transform_tangent for TangentGeom::PlanarEllipse so GPU analytical instances and curve endpoints match true WCS positions.
- Update entities::ellipse::apply_transform to use transform_ellipse_geometry for complete scaling, rotation, mirroring, and affine transformations.
- Clean up tangent parameters in to_render for Ellipse so start_param and end_param are consistently unwrapped.
- Add comprehensive unit tests in transform.rs, block_cache.rs, and ellipse.rs verifying negative rotations, negative scales, and distant origins.
This commit is contained in:
yztink committed 2026-09-28 16:09:41 -07:00
1 parent c8c2238d4a
commit a35fc949f3
3 files changed
+421 -27

No files matched your search

+68 -11
View File
@@ -2997,21 +2997,22 @@ fn transform_tangent(
start_param,
end_param,
} => {
let c = t.apply(Vector3::new(center[0], center[1], center[2]));
let m = t.apply_rotation(Vector3::new(major_axis[0], major_axis[1], major_axis[2]));
let n = t.apply_rotation(Vector3::new(normal[0], normal[1], normal[2]));
let n_len = n.length();
if n_len <= 1.0e-12 {
return None;
}
let n = n / n_len;
let (c, m, n, ratio, s, e) = crate::scene::view::transform::transform_ellipse_geometry(
Vector3::new(center[0], center[1], center[2]),
Vector3::new(major_axis[0], major_axis[1], major_axis[2]),
Vector3::new(normal[0], normal[1], normal[2]),
*minor_axis_ratio,
*start_param,
*end_param,
t,
)?;
Some(TangentGeom::PlanarEllipse {
center: [c.x, c.y, c.z],
major_axis: [m.x, m.y, m.z],
normal: [n.x, n.y, n.z],
minor_axis_ratio: *minor_axis_ratio,
start_param: *start_param,
end_param: *end_param,
minor_axis_ratio: ratio,
start_param: s,
end_param: e,
})
}
}
@@ -3263,4 +3264,60 @@ mod compact_nested_tests {
assert_eq!(wires.len(), 1);
assert!(wires[0].render_instance.is_none());
}
#[test]
fn test_transform_tangent_ellipse_arc_negative_scale_and_rotation() {
let ellipse_geom = TangentGeom::PlanarEllipse {
center: [10.0, 20.0, 0.0],
major_axis: [100.0, 0.0, 0.0],
normal: [0.0, 0.0, 1.0],
minor_axis_ratio: 0.5,
start_param: std::f64::consts::FRAC_PI_4,
end_param: 3.0 * std::f64::consts::FRAC_PI_4,
};
// Negative scale (-1, -1, 1), negative rotation -45 deg, distant translation (7000, 5000, 0)
let t = Transform::from_scaling(Vector3::new(-1.0, -1.0, 1.0))
.then(&Transform::from_rotation(Vector3::UNIT_Z, -std::f64::consts::FRAC_PI_4))
.then(&Transform::from_translation(Vector3::new(7000.0, 5000.0, 0.0)));
let transformed = transform_tangent(&ellipse_geom, &t).expect("transform_tangent should succeed");
let TangentGeom::PlanarEllipse {
center: c,
major_axis: m,
normal: n,
minor_axis_ratio: ratio,
start_param: s,
end_param: _e,
} = transformed else {
panic!("Expected PlanarEllipse");
};
// Center must be transformed
let expected_c = t.apply(Vector3::new(10.0, 20.0, 0.0));
assert!((c[0] - expected_c.x).abs() < 1e-9);
assert!((c[1] - expected_c.y).abs() < 1e-9);
// Major axis length must be 100, ratio 0.5
let m_vec = Vector3::new(m[0], m[1], m[2]);
assert!((m_vec.length() - 100.0).abs() < 1e-9);
assert!((ratio - 0.5).abs() < 1e-9);
assert!((n[2] - 1.0).abs() < 1e-9);
// Start and end points on the curve must match mathematical transformation of original start/end
let orig_start_pt = Vector3::new(10.0, 20.0, 0.0)
+ Vector3::new(100.0, 0.0, 0.0) * (std::f64::consts::FRAC_PI_4).cos()
+ Vector3::new(0.0, 50.0, 0.0) * (std::f64::consts::FRAC_PI_4).sin();
let expected_start_pt = t.apply(orig_start_pt);
let new_v = Vector3::new(n[0], n[1], n[2]).cross(&m_vec) * ratio;
let actual_start_pt = Vector3::new(c[0], c[1], c[2])
+ m_vec * s.cos()
+ new_v * s.sin();
assert!(
(actual_start_pt - expected_start_pt).length() < 1e-9,
"Start point mismatch: expected {expected_start_pt:?}, got {actual_start_pt:?}"
);
}
}