Let’s carry on with changing the API of these classes. Where were we?

Oh, right. The ConRod. It’s computing the crank angle up top, which means we should be able to begin to ask the crank for start and finish points instead of the r and theta.

I think we’ll just start doing that and make it work. Oh, I have a neat idea. Let’s see if I can make it work. First, let’s add a method with an extract. We have:

class ConRod:
    def update(self):
        lead_radians = self._crank.lead_radians()
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=self.wheel_radians() + lead_radians,
                length=self._length))

Extract variable:

    def update(self):
        lead_radians = self._crank.lead_radians()
        crank_radians = self.wheel_radians() + lead_radians
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=crank_radians,
                length=self._length))

Extract method:

    def update(self):
        crank_radians = self.crank_radians()
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=crank_radians,
                length=self._length))

    def crank_radians(self):
        lead_radians = self._crank.lead_radians()
        crank_radians = self.wheel_radians() + lead_radians
        return crank_radians

Inline:

    def update(self):
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=self.crank_radians(),
                length=self._length))

Let’s commit a save point. Then just to make it easier, let’s clean this up:

    def crank_radians(self):
        lead_radians = self._crank.lead_radians()
        crank_radians = self.wheel_radians() + lead_radians
        return crank_radians

Two inlines and we have:

    def crank_radians(self):
        return self.wheel_radians() + self._crank.lead_radians()

I should mention that I am confident that I could just type in some code to convert to using start and finish but I want to do as much as I can in tiny steps. My plan is to convert, inside this object, to using start and finish … and only then to fetch them from the crank. At that point we should be able to remove the r theta methods from Crank. This is certainly more steps than would be possible but they should all be easy ones.

Now, having compacted that method, I’ve decided to rewrite it, like this:

    def crank_radians(self):
        start, finish = self.get_start_finish()
        diff = finish - start
        return math.atan2(diff.y, diff.x)
    def get_start_finish(self):
        start =  self.crank_origin()
        radius = self._crank.radius()
        theta = self._crank.wheel_radians() + self._crank.lead_radians()
        finish = start + vector(radius,0).rotate_xy(theta)
        return start, finish

Now the ConRod is basing its work on start/finish, not r theta. So we should be able to put a start_finish method into Crank.

class Crank:
    def start_finish(self):
        start = self._parent.parent
        finish = self.constrained(0)
        return start, finish

Tests are all green. It has gone so smoothly that I suspect the worst. We already had constrained:

    def constrained(self, number):
        center = self._parent.parent
        r=self._radius
        effective_angle = self._leadRadians + self._parent.angle_radians
        x = r*math.cos(effective_angle) - 0*math.sin(effective_angle)
        y = r*math.sin(effective_angle) + 0*math.cos(effective_angle)
        return vector(x, y) + center

Let’s remove some of the unneeded code from these classes. I remove:

class ConRod:
    def get_start_finish(self):
        start =  self.crank_origin()
        radius = self._crank.radius()
        theta = self._crank.wheel_radians() + self._crank.lead_radians()
        finish = start + vector(radius,0).rotate_xy(theta)
        return start, finish

By calling the crank:

    def crank_radians(self):
        start, finish = self._crank.start_finish()
        diff = finish - start
        return math.atan2(diff.y, diff.x)

After a flurry of inlining, here’s ConRod:

class ConRod:
    def __init__(self, *, parent: Crank, length, piston_angle):
        self._crank = parent
        self._length = length
        self._piston_radians = piston_angle * math.pi / 180
        # mutable graphic support temps
        self.line = None
        self.guide = None

    def update(self):
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=self.crank_radians(),
                length=self._length))

    def crank_radians(self):
        start, finish = self._crank.start_finish()
        diff = finish - start
        return math.atan2(diff.y, diff.x)

    def crank_origin(self):
        return self._crank.origin()

    def init_draw(self, canvas):
        start = self._start
        finish = self._finish
        self.line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")
        wc = self.crank_origin()
        tg = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
        self.guide = canvas.create_line(wc.x, wc.y, wc.x+tg.x, wc.y+tg.y, fill="blue")
        one = vector(1,0).rotate_xy(self._piston_radians)
        half = wc + one + tg * 0.5
        s = 0.1
        canvas.create_oval(half.x-s, half.y-s, half.x+s, half.y+s, fill='red')

    def draw(self, canvas):
        start = self._start
        finish = self._finish
        canvas.coords(self.line,
                      start.x, start.y,
                      finish.x, finish.y)

The rest is just the standard con_rod_ends_complete function that we’ve been using right along, unchanged.

Over in Crank, I remove two or three methods and am left, so far, with this:

class Crank:
    def __init__(self, *, parent, radius, lead_degrees=0):
        self._parent = parent
        self._radius = radius
        self._leadRadians = lead_degrees * math.pi / 180.0

    def start_finish(self):
        start = self._parent.parent
        finish = self.constrained(0)
        return start, finish

    def constrained(self, number):
        center = self._parent.parent
        r=self._radius
        effective_angle = self._leadRadians + self._parent.angle_radians
        x = r*math.cos(effective_angle) - 0*math.sin(effective_angle)
        y = r*math.sin(effective_angle) + 0*math.cos(effective_angle)
        return vector(x, y) + center

    def radius(self):
        return self._radius

    def origin(self):
        return self._parent.parent

Someone still uses radius. Find them. Ah, it is in ConRod. Right there in the call to complete:

    def update(self):
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self._crank.radius(),
                crank_radians=self.crank_radians(),
                length=self._length))

Extract method:

    def update(self):
        self._start, self._finish = (
            con_rod_ends_complete(
                piston_radians=self._piston_radians,
                wheel_center=self.crank_origin(),
                radius=self.crank_radius(),
                crank_radians=self.crank_radians(),
                length=self._length))

    def crank_radius(self):
        return self._crank.radius()

Re-code crank_radius:

    def crank_radius(self):
        start, finish = self._crank.start_finish()
        return finish.distance(start)

Remove method from Crank. Test. Commit: conrod and crank converted to start/finish.

There’s more we can do. The method constrained(number) needs to be removed, I think, but that’s not for now. And we’d like to make Wheel adhere to the new start/finish scheme.

But for now, we’ve reached a logical conclusion, and there have been a dozen commits, and could have been a few more, most of the work was done with machine refactorings, and the tests stayed green throughout, although a time or two this morning I reverted when I got off track. Overall, very satisfying and a very welcome design improvement along the way to standardizing on start/finish notation. We’ll deal with additional points beyond start and finish if they ever happen.