#!/usr/bin/env python3
# hootnanny.py - simulate Hoot-Nanny / Magic Designer drawing toy
# Currently hard-coded to output figure "25KM" in HP-GL,
#  a simple but somewhat obsolete plotting language.
# Output figures as close to actual sizes as I could manage.
# All dimensions in inches, unfortunately, but HP-GL is bilingual.
# scruss, 2022-02 - code cleanup - 2022-12
# Licence: CC-BY-SA - share freely, but credit me and make your
#                     improvements freely available for all
# -*- coding: utf-8 -*-

from math import sin, cos, sqrt, radians, degrees, atan2


def arm_length(letter):
    """
    each metal arm has pivot holes marked A - R, at 1/4" spacing
    from 5.75 to 1.5". The perpendicular distance from the pivot holes
    to the pencil is 5/16". This doesn't add much to the overall arm
    length, but it's easy to take into account
    """
    if len(letter) > 1 or letter > "R" or letter < "A":
        # wrong input
        return None
    return sqrt(
        (5.75 - (ord(letter) - ord("A")) / 4.0) ** 2 + (5 / 16) ** 2
    )


def angle_setting(deg):
    """
    the hoot-nanny has two smaller gears: one fixed, the other on a movable
    track with a scale 10 to 70 degrees. The indicator isn't the same as the
    angle between the gears: when 40 degrees is indicated, the gears are
    60 degrees apart. So the actual range is 30 to 90 degrees.

    The smaller gears are 1" in diameter (32 teeth) on a 7" PCD
    Each smaller gear has a pivot point for the arms at 3/8" radius
    The large gear is 6" in diameter (192 teeth)
    """
    return 20 + deg


def distance(p1, p2):
    """
    return Euclidean distance between two points in 2d space
    Note that I'm using a two element list to represent [x, y]
    """
    return sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)


def epitrochoid(r1, r2, d, theta):
    """
    If you held the paper on the Hoot-Nanny fixed, the smaller
    gears would describe epitrochoids around the larger
    """
    return [
        (r1 + r2) * cos(theta) - d * cos(((r1 + r2) / r2) * theta),
        (r1 + r2) * sin(theta) - d * sin(((r1 + r2) / r2) * theta),
    ]


def nearest_circle_intersection(p0, p1, r0, r1):
    """
    return the intersection point of two circles nearer the origin
    if equidistant, may not return the point you expect
    """
    d = distance(p0, p1)
    a = (r0**2 - r1**2 + d**2) / (2 * d)
    h = sqrt(r0**2 - a**2)
    x2 = p0[0] + a * (p1[0] - p0[0]) / d
    y2 = p0[1] + a * (p1[1] - p0[1]) / d
    p3 = [x2 + h * (p1[1] - p0[1]) / d, y2 - h * (p1[0] - p0[0]) / d]
    p4 = [x2 - h * (p1[1] - p0[1]) / d, y2 + h * (p1[0] - p0[0]) / d]
    if distance(p3, [0, 0]) <= distance(p4, [0, 0]):
        return p3
    else:
        return p4


# this is where I hard-code the "25KM" setting
arm1 = arm_length("K")
arm2 = arm_length("M")
t = angle_setting(25)

# output the figure in HP-GL: it may be old, but it's very simple
# the path steps around a circle once in full degrees
for i in range(360):
    """
    p is the fixed smaller gear path:
     * large radius   = 3"
     * small radius   = 1/2"
     * cam arm length = 3/8"
    """
    p = epitrochoid(3, 1 / 2, 3 / 8, radians(i))
    # q is the movable smaller gear path
    q = epitrochoid(3, 1 / 2, 3 / 8, radians(i - t))
    # pencil holder is where the two arms intersect
    # coordinates of ph are [x, y] in inches (blecch)
    ph = nearest_circle_intersection(p, q, arm1, arm2)
    # scale to HP-GL units: 40 units / mm == 1016 units / inch
    # thanks, Carl Edvard Johansson!
    x = int(1016 * ph[0])
    y = int(1016 * ph[1])
    if i == 0:
        # initialize and plot first point
        print("IN;")  # HP-GL INitialize plotter
        print("SP1;")  # HP-GL Select Pen 1
        print("PU", x, ",", y, ";")  # HP-GL Pen Up (= move) to x, y
        print("PD;")  # HP-GL Pen Down (= plot)
        # save first point to close figure
        first = [x, y]
    else:
        print("PD", x, ",", y, ";")  # HP-GL Pen Down (= plot) to x, y
# close figure
print("PD", first[0], ",", first[1], ";")
# plot is now finished, so pick up pen and put it away
print("PU;")  # HP-GL Pen Up (= pick up pen, if no coordinates)
print("SP0;")  # HP-GL Select Pen 0 (= put the pen away)