feat: implement OCS→WCS transform for Arc, Circle, and Line

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-05-02 17:13:42 +03:00
1 parent 7b7186a470
commit 674a94f756
4 files changed
+117 -33

No files matched your search

+25 -7
View File
@@ -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![],
+35 -24
View File
@@ -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![],
+9 -2
View File
@@ -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);
+48
View File
@@ -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,
)
}