#!/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)