From 674a94f756d53f6725189f77e94555161b93dfa9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 2 May 2026 17:13:42 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20OCS=E2=86=92WCS=20transform?= =?UTF-8?q?=20for=20Arc,=20Circle,=20and=20Line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entities carrying a non-standard extrusion normal (normal ≠ (0,0,1)) were rendered flat in the XY plane. The DXF arbitrary-axis algorithm is now implemented in scene::transform::{ocs_axes, ocs_point_to_wcs} and applied during tessellation of Arc, Circle, and Line so they render correctly on tilted planes in 3D DXF files. Co-Authored-By: Claude Sonnet 4.6 --- src/entities/arc.rs | 32 ++++++++++++++++++----- src/entities/circle.rs | 59 +++++++++++++++++++++++++----------------- src/entities/line.rs | 11 ++++++-- src/scene/transform.rs | 48 ++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 0fd0c9dba..6888fdafe 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -18,8 +18,12 @@ fn to_truck(arc: &Arc) -> TruckEntity { let r = arc.radius; let sa = arc.start_angle; let ea = arc.end_angle; - // normal.z < 0 means the arc sweeps clockwise in the XY plane; - // place the midpoint on the CW side so the curve goes the right way. + let normal = (arc.normal.x, arc.normal.y, arc.normal.z); + + // Compute OCS basis vectors for this entity's normal. + let (ax, ay) = crate::scene::transform::ocs_axes(normal); + + // When normal.z < 0 the arc sweeps clockwise; reverse the half-angle choice. let mid_a = if arc.normal.z < 0.0 { let cw_span = if sa >= ea { sa - ea } else { sa - ea + TAU }; sa - cw_span * 0.5 @@ -27,18 +31,32 @@ fn to_truck(arc: &Arc) -> TruckEntity { let ccw_end = if ea >= sa { ea } else { ea + TAU }; sa + (ccw_end - sa) * 0.5 }; - let p_start = Point3::new(cx + r * sa.cos(), cy + r * sa.sin(), cz); - let p_end = Point3::new(cx + r * ea.cos(), cy + r * ea.sin(), cz); - let p_mid = Point3::new(cx + r * mid_a.cos(), cy + r * mid_a.sin(), cz); + + // Arc centre in WCS. + let (cwx, cwy, cwz) = crate::scene::transform::ocs_point_to_wcs((cx, cy, cz), normal); + + // Arc points in WCS: centre_wcs + r*cos(a)*Ax + r*sin(a)*Ay + let arc_pt = |a: f64| { + let (c, s) = (a.cos(), a.sin()); + Point3::new( + cwx + r * c * ax.0 + r * s * ay.0, + cwy + r * c * ax.1 + r * s * ay.1, + cwz + r * c * ax.2 + r * s * ay.2, + ) + }; + + let p_start = arc_pt(sa); + let p_end = arc_pt(ea); + let p_mid = arc_pt(mid_a); let v_start = builder::vertex(p_start); let v_end = builder::vertex(p_end); let edge = builder::circle_arc(&v_start, &v_end, p_mid); - let cv = Vec3::new(cx as f32, cy as f32, cz as f32); + let cv = Vec3::new(cwx as f32, cwy as f32, cwz as f32); TruckEntity { object: TruckObject::Curve(edge), snap_pts: vec![(cv, SnapHint::Center)], tangent_geoms: vec![TangentGeom::Circle { - center: [cx as f32, cy as f32, cz as f32], + center: [cwx as f32, cwy as f32, cwz as f32], radius: r as f32, }], key_vertices: vec![], diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 84cb0d307..d06e74876 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -16,38 +16,49 @@ fn to_truck(circle: &Circle) -> TruckEntity { let cy = circle.center.y; let cz = circle.center.z; let r = circle.radius; - let right = builder::vertex(Point3::new(cx + r, cy, cz)); - let left = builder::vertex(Point3::new(cx - r, cy, cz)); - let top = Point3::new(cx, cy + r, cz); - let bot = Point3::new(cx, cy - r, cz); - let upper = builder::circle_arc(&right, &left, top); - let lower = builder::circle_arc(&left, &right, bot); + let normal = (circle.normal.x, circle.normal.y, circle.normal.z); + + let (ax, ay) = crate::scene::transform::ocs_axes(normal); + let (cwx, cwy, cwz) = crate::scene::transform::ocs_point_to_wcs((cx, cy, cz), normal); + + // Circle points in WCS: centre_wcs ± r * Ax and centre_wcs ± r * Ay + let pt = |da: (f64, f64, f64), db: (f64, f64, f64), s: f64| { + Point3::new( + cwx + s * da.0 + s * db.0, + cwy + s * da.1 + s * db.1, + cwz + s * da.2 + s * db.2, + ) + }; + // right: centre + r*Ax, left: centre - r*Ax, top: centre + r*Ay, bot: centre - r*Ay + let p_right = pt(ax, (0.0, 0.0, 0.0), r); + let p_left = pt((ax.0 * -1.0, ax.1 * -1.0, ax.2 * -1.0), (0.0, 0.0, 0.0), r); + let p_top = pt(ay, (0.0, 0.0, 0.0), r); + let p_bot = pt((ay.0 * -1.0, ay.1 * -1.0, ay.2 * -1.0), (0.0, 0.0, 0.0), r); + + let right = builder::vertex(p_right); + let left = builder::vertex(p_left); + let upper = builder::circle_arc(&right, &left, p_top); + let lower = builder::circle_arc(&left, &right, p_bot); let wire: Wire = [upper, lower].into_iter().collect(); - let cv = Vec3::new(cx as f32, cy as f32, cz as f32); + + let cv = Vec3::new(cwx as f32, cwy as f32, cwz as f32); let rf = r as f32; + let q = |d: (f64, f64, f64)| Vec3::new( + (cwx + r * d.0) as f32, + (cwy + r * d.1) as f32, + (cwz + r * d.2) as f32, + ); TruckEntity { object: TruckObject::Contour(wire), snap_pts: vec![ (cv, SnapHint::Center), - ( - Vec3::new(cx as f32 + rf, cy as f32, cz as f32), - SnapHint::Quadrant, - ), - ( - Vec3::new(cx as f32, cy as f32 + rf, cz as f32), - SnapHint::Quadrant, - ), - ( - Vec3::new(cx as f32 - rf, cy as f32, cz as f32), - SnapHint::Quadrant, - ), - ( - Vec3::new(cx as f32, cy as f32 - rf, cz as f32), - SnapHint::Quadrant, - ), + (q(ax), SnapHint::Quadrant), + (q(ay), SnapHint::Quadrant), + (q((-ax.0, -ax.1, -ax.2)), SnapHint::Quadrant), + (q((-ay.0, -ay.1, -ay.2)), SnapHint::Quadrant), ], tangent_geoms: vec![TangentGeom::Circle { - center: [cx as f32, cy as f32, cz as f32], + center: [cwx as f32, cwy as f32, cwz as f32], radius: rf, }], key_vertices: vec![], diff --git a/src/entities/line.rs b/src/entities/line.rs index e9878a2fe..afce88824 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -12,8 +12,15 @@ use crate::scene::object::{GripApply, GripDef, PropSection}; use crate::scene::wire_model::TangentGeom; fn to_truck(line: &Line) -> TruckEntity { - let p0 = Point3::new(line.start.x, line.start.y, line.start.z); - let p1 = Point3::new(line.end.x, line.end.y, line.end.z); + let normal = (line.normal.x, line.normal.y, line.normal.z); + let (sx, sy, sz) = crate::scene::transform::ocs_point_to_wcs( + (line.start.x, line.start.y, line.start.z), normal, + ); + let (ex, ey, ez) = crate::scene::transform::ocs_point_to_wcs( + (line.end.x, line.end.y, line.end.z), normal, + ); + let p0 = Point3::new(sx, sy, sz); + let p1 = Point3::new(ex, ey, ez); let v0 = builder::vertex(p0); let v1 = builder::vertex(p1); let edge = builder::line(&v0, &v1); diff --git a/src/scene/transform.rs b/src/scene/transform.rs index 89540339d..7eeaed741 100644 --- a/src/scene/transform.rs +++ b/src/scene/transform.rs @@ -63,3 +63,51 @@ 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 use Wy=(0,1,0)×N, else Wx=(1,0,0)×N. + 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 { + // (1,0,0) × (nx,ny,nz) = (0, -nz, ny) + let (x, y, z) = (0.0, -nz, ny); + 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, + ) +}