
# tractrixNormals.py
# Andrew Davison, Nov 2025, ad@coe.psu.ac.th
'''
Draws a tractrix curve with normal lines at regular intervals.
Uses derivatives and the Vec class for vector calculations.
'''

import math
import matplotlib.pyplot as plt
from Vec import Vec

T_MAX = 6.0  # maximum t value for curve generation
DT = 0.002  # step size for t
X_RANGE = 4

NORMAL_LEN = 6  # length of normals
NORMAL_SPACING = 100  # spacing between normals (index increment)


def tractrix(u, sign):
  # Calculate a point on the tractrix curve
  x = sign * (u - math.tanh(u))
  y = 1 / math.cosh(u)
  return Vec(x, y, 0)


def tractrixDeriv(u, sign=+1):
  # Calculate the derivative (tangent vector) of the tractrix
  dxDt = sign * math.tanh(u)**2
  dyDt = -1/ math.cosh(u) * math.tanh(u)
  return Vec(dxDt, dyDt, 0)


def makeTractrix(ts):
  # Generate points for both branches of the tractrix
  leftPts = [tractrix(t, sign=-1) for t in ts]
  leftPts = [p for p in leftPts if -X_RANGE <= p.x <= 0]
  leftPts.reverse()
  
  rightPts = [tractrix(t, sign=+1) for t in ts]
  rightPts = [p for p in rightPts if 0 <= p.x <= X_RANGE]
  
  return leftPts + rightPts


def ptToT(pt):
  # For a given point, determine which tractrix branch 
  # and best approximate t value to use
  sign = 1 if pt.x >= 0 else -1
  
  # Find a t by generate tractrix points and saving the closest
  bestT = 0.0
  minDist = float('inf')
  dt = 0.1   # bigger that DT
  numSteps = int(T_MAX/dt) + 1
  for tTest in [i*dt for i in range(0, numSteps)]:
    testPt = tractrix(tTest, sign)
    dist = (testPt - pt).magnitude()   # distance from pt
    if dist < minDist:
      minDist = dist
      bestT = tTest
  return bestT


def getNormal(tangent, sign):
  tangent = tangent.normalize()
  # For left branch (sign=-1), rotate clockwise;
  # for right branch, counterclockwise
  if sign == -1:
    normal = Vec(tangent.y, -tangent.x, 0)
  else:
    normal = Vec(-tangent.y, tangent.x, 0)
  normal *= NORMAL_LEN      # Scale
  return normal



# --------------------------------------

numSteps = int(T_MAX/DT) + 1
ts = [j * DT for j in range(0, numSteps)]

# Generate all tractrix points
tPts = makeTractrix(ts)

# Extract coordinates for plotting
xs = [p.x for p in tPts]
ys = [p.y for p in tPts]
plt.plot(xs, ys, lw=2, color='blue')

# Draw normal lines at selected points using index increment
for i in range(0, len(tPts), NORMAL_SPACING):
  pt = tPts[i]
  sign = 1 if pt.x >= 0 else -1
  t = ptToT(pt)
  tangent = tractrixDeriv(t, sign)
  if not tangent.isZero():
    # Draw normal line from the point
    endPt = pt + getNormal(tangent, sign)
    plt.plot([pt.x, endPt.x], [pt.y, endPt.y], 
                        'r-', lw=1, alpha=0.6)

# Formatting
ax = plt.gca()
ax.axhline(0, color='black', lw=0.5)
ax.axvline(0, color='black', lw=0.5)
ax.set_aspect('equal')
plt.xlabel("x")
plt.ylabel("y")
plt.title("Tractrix with Normal Lines")
plt.grid(True, alpha=0.3)
plt.tight_layout()

plt.show()
