import Rhino.Geometry as rg
# x: list[rg.Point3d] — your triangle vertices (3 pts)
# returns: dict with metrics + aligned equilateral vertices in 3D
def triangle_equilateral_fit_metrics(x):
raise ValueError("Need exactly 3 points.")
# --- 1) Fit plane and get 2D coordinates (UV) ---
success, plane = rg.Plane.FitPlaneToPoints(x)
raise ValueError("Could not fit a plane to points.")
to_plane = rg.Transform.PlaneToPlane(plane, rg.Plane.WorldXY)
from_plane = rg.Transform.PlaneToPlane(rg.Plane.WorldXY, plane)
pts2d.append(rg.Point2d(q.X, q.Y))
# Convert to tuples for math
def p2t(p): return (p.X, p.Y)
P = [p2t(p) for p in pts2d] # target triangle (2D)
# --- 2) Make a centered, unit equilateral template in 2D ---
# vertices at angles 0, 120, 240; then center to centroid
T = [(math.cos(a), math.sin(a)) for a in (0.0, 2*math.pi/3, 4*math.pi/3)]
cx = sum(t[0] for t in T)/3.0
cy = sum(t[1] for t in T)/3.0
T = [(t[0]-cx, t[1]-cy) for t in T]
return (sum(a[0] for a in A)/3.0, sum(a[1] for a in A)/3.0)
return [(a[0]-c[0], a[1]-c[1]) for a in A]
def dot(u, v): return u[0]*v[0] + u[1]*v[1]
def procrustes_similarity(X, Y):
Find s,R,t to map X -> Y with uniform scale.
X,Y: lists of 2D tuples, len=3, already corresponded.
Returns (s, cosθ, sinθ, t=(tx,ty)), and residuals.
cx = centroid(X); cy = centroid(Y)
X0 = center(X, cx); Y0 = center(Y, cy)
a = sum(dot(x, y) for x, y in zip(X0, Y0))
b = sum(x[0]*y[1] - x[1]*y[0] for x, y in zip(X0, Y0)) # cross term
normX2 = sum(dot(x, x) for x in X0)
# rotation (cosθ, sinθ), scale s
# translation t: Y ≈ s R X + t
Rc = (cos_t*cx[0] - sin_t*cx[1], sin_t*cx[0] + cos_t*cx[1])
t = (cy[0] - s*Rc[0], cy[1] - s*Rc[1])
# Map X -> Yhat and compute residuals
Rx = (cos_t*x[0] - sin_t*x[1], sin_t*x[0] + cos_t*x[1])
yhat = (s*Rx[0] + t[0], s*Rx[1] + t[1])
errs = [math.hypot(y[0]-yh[0], y[1]-yh[1]) for y, yh in zip(Y, Yhat)]
rms = math.sqrt(sum(e*e for e in errs)/3.0)
return (s, cos_t, sin_t, t, Yhat, errs, rms)
# Try all 3 cyclic matchings between template T and target P
Tk = [T[(i+k)%3] for i in range(3)]
fit = procrustes_similarity(Tk, P)
if (best is None) or (fit[-1] < best[-1]):
raise ValueError("Alignment failed.")
k, s, c, s_, t, Yhat, errs, rms = best
# --- 3) Side/angle diagnostics on the input triangle ---
math.hypot(A[1][0]-A[0][0], A[1][1]-A[0][1]),
math.hypot(A[2][0]-A[1][0], A[2][1]-A[1][1]),
math.hypot(A[0][0]-A[2][0], A[0][1]-A[2][1])
# return internal angles in radians
# angle at b of triangle abc
ux, uy = a[0]-b[0], a[1]-b[1]
vx, vy = c[0]-b[0], c[1]-b[1]
du = math.hypot(ux, uy); dv = math.hypot(vx, vy)
if du*dv == 0: return 0.0
cosv = max(-1.0, min(1.0, (ux*vx + uy*vy)/(du*dv)))
return [ang(P[2],P[0],P[1]), ang(P[0],P[1],P[2]), ang(P[1],P[2],P[0])]
side_rel_std = (math.sqrt(sum((s-meanS)**2 for s in S)/3.0) / meanS) if meanS > 0 else 0.0
ang_dev = math.sqrt(sum((a - (math.pi/3))**2 for a in Ang)/3.0) # rms deviation from 60°
# --- 4) Build the aligned equilateral in 3D for visualization ---
# Inverse transform from 2D back to plane, using the Yhat positions:
q2d = rg.Point3d(u, v, 0)
q3d.Transform(from_plane)
"rms_err": rms, # distance units
"max_err": max_err, # distance units
"side_rel_std": side_rel_std, # 0 for equilateral
"angle_rms_dev_rad": ang_dev, # 0 for equilateral (in radians)
"equilateral_3d": eq3d # fitted equilateral vertices (match order of x)
metrics = triangle_equilateral_fit_metrics(x)
a = [metrics["rms_err"], metrics["max_err"], metrics["side_rel_std"], metrics["angle_rms_dev_rad"]]
eq = metrics["equilateral_3d"] # optional: output for preview