use acadrust::types::{Transform, Vector3}; use glam::Vec3; use crate::command::EntityTransform; #[inline] fn to_v3(v: Vec3) -> Vector3 { Vector3::new(v.x as f64, v.y as f64, v.z as f64) } pub fn apply_standard_transform(entity: &mut T, center: Vec3, angle_rad: f32) where T: acadrust::Entity, { let z_axis = Vector3::new(0.0, 0.0, 1.0); let t = Transform::from_translation(to_v3(-center)) .then(&Transform::from_rotation(z_axis, angle_rad as f64)) .then(&Transform::from_translation(to_v3(center))); entity.apply_transform(&t); } pub fn apply_standard_scale(entity: &mut T, center: Vec3, factor: f32) where T: acadrust::Entity, { let s = factor as f64; let t = Transform::from_scaling_with_origin(Vector3::new(s, s, s), to_v3(center)); entity.apply_transform(&t); } pub fn apply_standard_entity_transform(entity: &mut T, t: &EntityTransform, mirror: F) where T: acadrust::Entity, F: FnOnce(&mut T, Vec3, Vec3), { match t { EntityTransform::Translate(d) => entity.translate(to_v3(*d)), EntityTransform::Rotate { center, angle_rad } => { apply_standard_transform(entity, *center, *angle_rad) } EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor), EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2), } } /// Reflection across the world-XY line through `p1`→`p2` as an acadrust /// `Transform`, for delegating MIRROR to acadrust's entity-aware /// `apply_transform` paths (which handle direction flags, stored-angle /// conventions and bulges themselves). Degenerate line → identity. pub fn reflection_about_xy_line(p1: Vec3, p2: Vec3) -> acadrust::types::Transform { use acadrust::types::{Matrix4, Transform}; let dx = (p2.x - p1.x) as f64; let dy = (p2.y - p1.y) as f64; if dx * dx + dy * dy < 1e-12 { return Transform::identity(); } let ang = dy.atan2(dx); let m = Matrix4::translation(p1.x as f64, p1.y as f64, 0.0) * Matrix4::rotation_z(ang) * Matrix4::scaling(1.0, -1.0, 1.0) * Matrix4::rotation_z(-ang) * Matrix4::translation(-(p1.x as f64), -(p1.y as f64), 0.0); Transform::from_matrix(m) } pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: Vec3, p2: Vec3) { let ax = (p2.x - p1.x) as f64; let ay = (p2.y - p1.y) as f64; let len2 = ax * ax + ay * ay; if len2 < 1e-12 { return; } let rx = *x - p1.x as f64; let ry = *y - p1.y as f64; let dot = rx * ax + ry * ay; let mx = 2.0 * dot * ax / len2 - rx; let my = 2.0 * dot * ay / len2 - ry; *x = p1.x as f64 + mx; *y = p1.y as f64 + my; } pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: Vec3, p2: Vec3) { reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2); reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2); } /// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS /// for a given entity normal vector. /// /// Returns (Ax, Ay) each as (x, y, z) tuples. When normal ≈ (0,0,1) the /// function returns the standard basis ((1,0,0), (0,1,0)) without any /// cross-product computation. pub fn ocs_axes(normal: (f64, f64, f64)) -> ((f64, f64, f64), (f64, f64, f64)) { let (nx, ny, nz) = normal; // Fast path: normal is effectively Z-up, OCS = WCS. if nx.abs() < 1e-10 && ny.abs() < 1e-10 && (nz - 1.0).abs() < 1e-10 { return ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0)); } // Arbitrary-axis algorithm (DXF spec). // When |Nx| < 1/64 and |Ny| < 1/64 the normal is near ±Z, so cross with // Wy=(0,1,0); otherwise cross with Wz=(0,0,1). The else branch MUST use Wz, // not Wx: Wx=(1,0,0)×N collapses to zero for an X-aligned normal (e.g. // N=(-1,0,0)), zeroing the OCS axes and mapping every point to the origin — // the resulting coincident points then send the arc tessellator into an // infinite loop. (#142) let (ax_x, ax_y, ax_z) = if nx.abs() < 1.0 / 64.0 && ny.abs() < 1.0 / 64.0 { // (0,1,0) × (nx,ny,nz) = (nz, 0, -nx) let (x, y, z) = (nz, 0.0, -nx); let len = (x * x + y * y + z * z).sqrt().max(1e-12); (x / len, y / len, z / len) } else { // (0,0,1) × (nx,ny,nz) = (-ny, nx, 0) let (x, y, z) = (-ny, nx, 0.0); let len = (x * x + y * y + z * z).sqrt().max(1e-12); (x / len, y / len, z / len) }; // Ay = N × Ax let ay_x = ny * ax_z - nz * ax_y; let ay_y = nz * ax_x - nx * ax_z; let ay_z = nx * ax_y - ny * ax_x; ((ax_x, ax_y, ax_z), (ay_x, ay_y, ay_z)) } /// Transform a single OCS point to WCS using the given normal vector. #[inline] pub fn ocs_point_to_wcs(ocs: (f64, f64, f64), normal: (f64, f64, f64)) -> (f64, f64, f64) { let (ax, ay) = ocs_axes(normal); let (ox, oy, oz) = ocs; let (nx, ny, nz) = normal; ( ox * ax.0 + oy * ay.0 + oz * nx, ox * ax.1 + oy * ay.1 + oz * ny, ox * ax.2 + oy * ay.2 + oz * nz, ) } #[cfg(test)] mod mirror_delegation_tests { use super::*; use crate::command::EntityTransform; use crate::entities::traits::Transformable; use acadrust::entities::hatch::{BoundaryEdge, BoundaryPath, CircularArcEdge, LineEdge}; use acadrust::entities::Hatch; use acadrust::types::Vector2; // MIRROR on a hatch goes through reflection_about_xy_line + acadrust's // transform_hatch. Mirror a Line→Arc→Line path across a vertical line and // assert the arc stays endpoint-continuous, flips its direction flag, and // keeps its sweep magnitude (the stored-angle conventions the old // hand-rolled closure violated). #[test] fn hatch_mirror_keeps_arc_continuous_and_sweep() { let mut path = BoundaryPath::new(); path.edges.push(BoundaryEdge::Line(LineEdge { start: Vector2::new(0.0, -1.0), end: Vector2::new(2.0, 0.0), })); path.edges.push(BoundaryEdge::CircularArc(CircularArcEdge { center: Vector2::new(1.0, 0.0), radius: 1.0, start_angle: 0.0, end_angle: std::f64::consts::PI, counter_clockwise: true, })); path.edges.push(BoundaryEdge::Line(LineEdge { start: Vector2::new(0.0, 0.0), end: Vector2::new(0.0, -1.0), })); let mut h = Hatch::new(); h.paths.push(path); // Mirror across the vertical line x = 5. h.apply_transform(&EntityTransform::Mirror { p1: Vec3::new(5.0, 0.0, 0.0), p2: Vec3::new(5.0, 1.0, 0.0), }); let edges = &h.paths[0].edges; let (l1, arc) = match (&edges[0], &edges[1]) { (BoundaryEdge::Line(a), BoundaryEdge::CircularArc(b)) => (a, b), _ => panic!("edge kinds changed"), }; assert!(!arc.counter_clockwise, "mirror must flip the flag"); let sweep = arc.end_angle - arc.start_angle; assert!( (sweep - std::f64::consts::PI).abs() < 1e-9, "stored sweep must stay π, got {sweep}" ); // Stored-angle convention: true point of a CW edge is at -θ. let pt = |theta: f64| { let a = if arc.counter_clockwise { theta } else { -theta }; ( arc.center.x + arc.radius * a.cos(), arc.center.y + arc.radius * a.sin(), ) }; let (sx, sy) = pt(arc.start_angle); assert!( (sx - l1.end.x).abs() < 1e-9 && (sy - l1.end.y).abs() < 1e-9, "arc start {:?} must meet previous line end {:?}", (sx, sy), (l1.end.x, l1.end.y) ); // Mirror of (2,0) across x=5 is (8,0). assert!((sx - 8.0).abs() < 1e-9 && sy.abs() < 1e-9); } } #[cfg(test)] mod ocs_axes_142 { #[test] fn x_normal_axes_nonzero() { let (ax, ay) = super::ocs_axes((-1.0, 0.0, 0.0)); let nz = |v: (f64,f64,f64)| v.0.abs()+v.1.abs()+v.2.abs(); assert!(nz(ax) > 0.5, "ax collapsed: {:?}", ax); assert!(nz(ay) > 0.5, "ay collapsed: {:?}", ay); let p = super::ocs_point_to_wcs((-25.0, 90.0, 0.0), (-1.0,0.0,0.0)); assert!(p.0.abs()+p.1.abs()+p.2.abs() > 1.0, "point collapsed: {:?}", p); } }