Skip to content

Commit fad6d6a

Browse files
committed
Fix OCS→WCS coordinate transformation for ARC and CIRCLE entities
DXF stores ARC/CIRCLE center points in Object Coordinate System (OCS) when the entity Normal differs from (0,0,1). Without applying the AutoCAD Arbitrary Axis Algorithm to convert OCS→WCS, circles and arcs with reversed or tilted normals appeared at incorrect positions (e.g. mirrored along the X axis for Normal=(0,0,-1)). Apply OcsToWcs() in CreateArc (including the degenerate arc→circle fallback) and CreateCircle. Add regression test verifying that an ARC with Normal=(0,0,-1) at OCS (-1000,-500) lands at WCS (1000,-500). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ka4ocDhmumPuQDD2fDs7ad
1 parent d79466b commit fad6d6a

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

CADability/ImportDxf.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,22 @@ private IGeoObject GeoObjectFromEntity(Entity item)
229229
private static GeoPoint GeoPoint(XYZ p) => new GeoPoint(p.X, p.Y, p.Z);
230230
private static GeoVector GeoVector(XYZ p) => new GeoVector(p.X, p.Y, p.Z);
231231

232+
// Convert a 2D entity's position from OCS (Object Coordinate System) to WCS.
233+
// DXF stores ARC/CIRCLE centers, TEXT insert points, etc. in OCS when Normal ≠ (0,0,1).
234+
// The OCS axes are derived via the AutoCAD Arbitrary Axis Algorithm.
235+
private static GeoPoint OcsToWcs(XYZ ocsPoint, XYZ normal)
236+
{
237+
GeoVector n = GeoVector(normal);
238+
GeoVector ax = (Math.Abs(normal.X) < 1.0 / 64 && Math.Abs(normal.Y) < 1.0 / 64)
239+
? CADability.GeoVector.YAxis ^ n
240+
: CADability.GeoVector.ZAxis ^ n;
241+
GeoVector ay = n ^ ax;
242+
return new GeoPoint(
243+
ocsPoint.X * ax.x + ocsPoint.Y * ay.x + ocsPoint.Z * n.x,
244+
ocsPoint.X * ax.y + ocsPoint.Y * ay.y + ocsPoint.Z * n.y,
245+
ocsPoint.X * ax.z + ocsPoint.Y * ay.z + ocsPoint.Z * n.z);
246+
}
247+
232248
internal static Plane Plane(XYZ center, XYZ normal)
233249
{
234250
// AutoCAD Arbitrary Axis Algorithm — must use this for correct plane orientation
@@ -448,17 +464,18 @@ private IGeoObject CreateArc(ACadSharp.Entities.Arc arc)
448464
GeoObject.Ellipse e = GeoObject.Ellipse.Construct();
449465
GeoVector nor = GeoVector(arc.Normal);
450466
Plane plane = Plane(arc.Center, arc.Normal);
467+
GeoPoint wcsCenter = OcsToWcs(arc.Center, arc.Normal);
451468
double start = arc.StartAngle;
452469
double end = arc.EndAngle;
453470
double sweep = end - start;
454471
if (sweep < 0.0) sweep += Math.PI * 2.0;
455472
if (start == end) sweep = 0.0;
456473
if (start == Math.PI * 2.0 && end == 0.0) sweep = 0.0;
457-
e.SetArcPlaneCenterRadiusAngles(plane, GeoPoint(arc.Center), arc.Radius, start, sweep);
474+
e.SetArcPlaneCenterRadiusAngles(plane, wcsCenter, arc.Radius, start, sweep);
458475
if (e.IsCircle && sweep == 0.0 && Precision.IsEqual(e.StartPoint, e.EndPoint))
459476
{
460477
GeoObject.Ellipse circle = GeoObject.Ellipse.Construct();
461-
circle.SetCirclePlaneCenterRadius(plane, GeoPoint(arc.Center), arc.Radius);
478+
circle.SetCirclePlaneCenterRadius(plane, wcsCenter, arc.Radius);
462479
e = circle;
463480
}
464481
double th = arc.Thickness;
@@ -471,7 +488,8 @@ private IGeoObject CreateCircle(ACadSharp.Entities.Circle circle)
471488
{
472489
GeoObject.Ellipse e = GeoObject.Ellipse.Construct();
473490
Plane plane = Plane(circle.Center, circle.Normal);
474-
e.SetCirclePlaneCenterRadius(plane, GeoPoint(circle.Center), circle.Radius);
491+
GeoPoint wcsCenter = OcsToWcs(circle.Center, circle.Normal);
492+
e.SetCirclePlaneCenterRadius(plane, wcsCenter, circle.Radius);
475493
double th = circle.Thickness;
476494
GeoVector no = GeoVector(circle.Normal);
477495
if (th != 0.0 && !no.IsNullVector())

tests/CADability.Tests/ProjectTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,30 @@ public void import_dxf_empty_block_insert_not_placed_at_origin()
980980
$"Empty block insert must be skipped; expected 1 object, got {model.AllObjects.Count}");
981981
}
982982

983+
[TestMethod]
984+
public void import_dxf_arc_with_reversed_normal_uses_wcs_center()
985+
{
986+
// Regression: ARC/CIRCLE entities with Normal=(0,0,-1) store their center in OCS.
987+
// OCS→WCS via Arbitrary Axis Algorithm flips the X component for Normal=(0,0,-1).
988+
// OCS (-1000,-500,0) with Normal=(0,0,-1) → WCS (1000,-500,0).
989+
// Without the fix the center was imported at the raw OCS coordinate (-1000,-500).
990+
const string dxf = " 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 1\r\nAC1015\r\n 0\r\nENDSEC\r\n 0\r\nSECTION\r\n 2\r\nBLOCKS\r\n 0\r\nBLOCK\r\n 8\r\n0\r\n 2\r\n*Model_Space\r\n 10\r\n0.0\r\n 20\r\n0.0\r\n 30\r\n0.0\r\n 3\r\n*Model_Space\r\n 4\r\n\r\n 0\r\nARC\r\n 8\r\n0\r\n 10\r\n-1000.0\r\n 20\r\n-500.0\r\n 30\r\n0.0\r\n 40\r\n100.0\r\n 50\r\n0.0\r\n 51\r\n90.0\r\n210\r\n0.0\r\n220\r\n0.0\r\n230\r\n-1.0\r\n 0\r\nENDBLK\r\n 8\r\n0\r\n 0\r\nENDSEC\r\n 0\r\nSECTION\r\n 2\r\nENTITIES\r\n 0\r\nENDSEC\r\n 0\r\nEOF\r\n";
991+
var file = this.TestContext.TestName + ".dxf";
992+
File.WriteAllText(file, dxf);
993+
var project = Project.ReadFromFile(file, "dxf");
994+
Assert.IsNotNull(project);
995+
var model = project.GetActiveModel();
996+
Assert.IsNotNull(model);
997+
Assert.IsTrue(model.AllObjects.Count > 0, "ARC entity must be imported");
998+
var arc = model.AllObjects[0] as GeoObject.Ellipse;
999+
Assert.IsNotNull(arc, "Imported object must be an Ellipse/Arc");
1000+
// WCS center must be (1000,-500,0), not the raw OCS value (-1000,-500,0)
1001+
Assert.IsTrue(Math.Abs(arc.Center.x - 1000.0) < 1e-6,
1002+
$"Arc WCS X must be 1000, got {arc.Center.x}");
1003+
Assert.IsTrue(Math.Abs(arc.Center.y - (-500.0)) < 1e-6,
1004+
$"Arc WCS Y must be -500, got {arc.Center.y}");
1005+
}
1006+
9831007
[TestMethod]
9841008
[DeploymentItem(@"Files/Step/issue153.stp", nameof(import_step_issue153_succeeds))]
9851009
public void import_step_issue153_succeeds()

0 commit comments

Comments
 (0)