fix(ellipse,mirror): WCS ellipse rendering + delegate hatch MIRROR upstream

Two follow-ups from the boundary-arc convention audit:

- ELLIPSE is a WCS entity in DXF (codes 10/11 are world coordinates,
  unlike ARC/CIRCLE). The renderer ran center and major_axis through the
  arbitrary-axis OCS, which misplaced any ellipse whose normal isn't Z-up
  — e.g. the (0,0,-1) result of a mirrored-block explode. Read them as
  WCS; pairs with acadrust 569bd4d, which stores WCS on its explode
  paths. Z-up ellipses are unaffected (the conversion was an identity).

- The MIRROR command's hand-rolled hatch closure swapped boundary-arc
  angles itself — only valid for ccw arcs and ignorant of the stored
  mirrored-angle / wrap-encoded-sweep conventions. It now builds a
  reflection Transform (new reflection_about_xy_line helper) and
  delegates to acadrust's transform_hatch, the single source of truth
  for those conventions. Covered by a Line→Arc→Line continuity + sweep
  regression test.

Note: full effect of the ellipse change lands once the acadrust pin is
bumped to 569bd4d; until then only the (previously broken) non-Z-up
corner differs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-11 09:03:18 +03:00
commit d09614be39
3 changed files with 110 additions and 62 deletions

View file

@ -12,22 +12,19 @@ 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;
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);
// ELLIPSE is one of the few WCS entities in DXF: `center` (code 10) and
// `major_axis` (code 11) are world coordinates already — unlike ARC /
// CIRCLE, whose centers are OCS. (This used to run both through the
// arbitrary-axis OCS, which misplaced any ellipse whose normal isn't
// Z-up — e.g. the (0,0,-1) result of a mirrored-block explode.)
let (cwx, cwy, cwz) = (ell.center.x, ell.center.y, ell.center.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,
ell.major_axis.x as f32,
ell.major_axis.y as f32,
ell.major_axis.z as f32,
);
let r_major = wcs_maj.length() as f64;
let r_minor = r_major * ell.minor_axis_ratio;