fix: arc direction — sample midpoint from NX edge instead of cross-product

Cross product of (start-center) × (end-center) is zero for 180° arcs,
causing random clockwise assignment. Now samples actual midpoint via
UF Eval at t_mid, stores as 'mid' in JSON. Import prefers 'mid' over
computed clockwise direction.
This commit is contained in:
2026-02-17 02:24:32 +00:00
parent e3a79d4888
commit b411eaac25
2 changed files with 51 additions and 26 deletions

View File

@@ -109,6 +109,7 @@ class EdgeSegment:
# Arc-specific (None for lines)
center_3d: Point3D | None = None
radius: float | None = None
mid_3d: Point3D | None = None # midpoint sampled from NX edge (for arcs)
# ---------------------------------------------------------------------------
@@ -191,11 +192,29 @@ def _analyze_edge(edge: Any, lister: Any = None) -> EdgeSegment:
pass
if center is not None and radius is not None and radius > 0.0:
# Sample midpoint on the actual NX edge for reliable arc direction
mid_pt = None
try:
limits = eval_obj.AskLimits(evaluator)
t_mid = (float(limits[0]) + float(limits[1])) / 2.0
result = eval_obj.EvaluateUnitVectors(evaluator, t_mid)
# Parse point from result
if hasattr(result, '__len__') and len(result) >= 1:
item = result[0] if hasattr(result[0], 'X') else result
if hasattr(item, 'X'):
mid_pt = (float(item.X), float(item.Y), float(item.Z))
elif isinstance(item, (list, tuple)) and len(item) >= 3:
mid_pt = (float(item[0]), float(item[1]), float(item[2]))
if mid_pt is None and hasattr(result, 'X'):
mid_pt = (float(result.X), float(result.Y), float(result.Z))
except Exception as exc2:
_log(f"[edge] Arc midpoint sampling failed: {exc2}")
_log(f"[edge] ARC: center=({center[0]:.3f},{center[1]:.3f},{center[2]:.3f}) "
f"r={radius:.3f}")
f"r={radius:.3f} mid={'OK' if mid_pt else 'NONE'}")
return EdgeSegment(
seg_type="arc", start_3d=p1, end_3d=p2,
center_3d=center, radius=radius,
center_3d=center, radius=radius, mid_3d=mid_pt,
)
else:
_log(f"[edge] IsArc=True but could not extract center/radius. "
@@ -311,7 +330,7 @@ def _chain_edges_into_loops(
found = True
break
elif pts_match(current_end, s.end_3d):
# Reversed edge — swap start/end
# Reversed edge — swap start/end (mid stays the same)
used[i] = True
reversed_seg = EdgeSegment(
seg_type=s.seg_type,
@@ -319,6 +338,7 @@ def _chain_edges_into_loops(
end_3d=s.start_3d,
center_3d=s.center_3d,
radius=s.radius,
mid_3d=s.mid_3d,
)
chain.append(reversed_seg)
current_end = s.start_3d
@@ -630,8 +650,12 @@ def _segments_to_json(segments: List[EdgeSegment], frame: LocalFrame) -> List[Di
center_2d = _project_point_2d(seg.center_3d, frame)
entry["center"] = [round(center_2d[0], 6), round(center_2d[1], 6)]
entry["radius"] = round(seg.radius, 6)
# Determine clockwise/ccw: cross product of (start-center) × (end-center)
# projected onto the face normal
# Store the sampled midpoint (reliable arc direction, avoids cross-product
# degeneracy for 180° arcs)
if seg.mid_3d is not None:
mid_2d = _project_point_2d(seg.mid_3d, frame)
entry["mid"] = [round(mid_2d[0], 6), round(mid_2d[1], 6)]
# Also store clockwise as fallback (but mid is preferred)
r1 = _sub(seg.start_3d, seg.center_3d)
r2 = _sub(seg.end_3d, seg.center_3d)
cross = _cross(r1, r2)

View File

@@ -364,28 +364,29 @@ def _draw_segment(part, seg, transform, lister):
if seg_type == "arc" and "center" in seg:
center_3d = unproject_point_to_3d(seg["center"], transform)
radius = seg["radius"]
# Compute midpoint of arc for 3-point arc creation
cx, cy = seg["center"]
sx, sy = seg["start"]
ex, ey = seg["end"]
# Angles from center
sa = math.atan2(sy - cy, sx - cx)
ea = math.atan2(ey - cy, ex - cx)
clockwise = seg.get("clockwise", False)
# Compute mid-angle
if clockwise:
# CW: sa → ea going clockwise (decreasing angle)
da = sa - ea
if da <= 0:
da += 2 * math.pi
mid_angle = sa - da / 2.0
# Prefer the sampled midpoint from NX (avoids direction ambiguity)
if "mid" in seg:
mid_2d = seg["mid"]
else:
# CCW: sa → ea going counter-clockwise (increasing angle)
da = ea - sa
if da <= 0:
da += 2 * math.pi
mid_angle = sa + da / 2.0
mid_2d = [cx + radius * math.cos(mid_angle), cy + radius * math.sin(mid_angle)]
# Fallback: compute from clockwise flag
cx, cy = seg["center"]
sx, sy = seg["start"]
ex, ey = seg["end"]
sa = math.atan2(sy - cy, sx - cx)
ea = math.atan2(ey - cy, ex - cx)
clockwise = seg.get("clockwise", False)
if clockwise:
da = sa - ea
if da <= 0:
da += 2 * math.pi
mid_angle = sa - da / 2.0
else:
da = ea - sa
if da <= 0:
da += 2 * math.pi
mid_angle = sa + da / 2.0
mid_2d = [cx + radius * math.cos(mid_angle), cy + radius * math.sin(mid_angle)]
mid_3d = unproject_point_to_3d(mid_2d, transform)
try:
_draw_arc_3pt(part, start_3d, mid_3d, end_3d)