We need lots of small steps to unwind the previous design while moving toward the scheme of doing everything without reference to parent. It goes … OK.

Back again. I have code running all the old tests, and one new test that is not running.

    def test_basic_sf_mode(self):
        radius = 1
        length=5
        tilt_angle = 0
        lead_angle = 0
        angle_degrees = 0
        center = FixedPoint(vector(0, 0))
        s,f = None, None
        s,f = center.update(s, f)
        wheel = Wheel(parent=center, angle_degrees=angle_degrees)
        s,f = wheel.update(s, f)
        crank = Crank(parent=wheel, radius=radius, lead_degrees=lead_angle)
        s,f = crank.update(s, f)
        con_rod = ConRod(parent=crank, length=length, piston_angle=tilt_angle)
        s,target = con_rod.update(s, f)
        assert target == vector(6,0)

I kind of wonder how the drawing works, as it is using only s, f and update. Oh, wow, I’m mistaken, the drawing is showing an error that the tests have not caught.

error

I think it’s just that the Guide is off from the wheel center a bit. Am I using a FixedPoint in Wheel, in the drawing? No. OK, we’ll have to deal with that as well. Let’s follow the test. Wheel is failing:

    @property
    def finish(self):
>       return self._position + vector(1, 0).rotate_xy(self._angle_radians)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: unsupported operand type(s) for +: 'FixedPoint' and 'VtVector'

Ah yes. In the test, we’re passing a FixedPoint. Everywhere else, just a vector. Making this work will break other tests. We’ll start at Wheel.update:

    def update(self, start, finish):
        self._angle_radians = self._angle_radians + 4*math.pi / 180.0
        return self.start, self.finish

    @property
    def start(self):
        return self._position
    @property
    def finish(self):
        return self._position + vector(1, 0).rotate_xy(self._angle_radians)

But _position is a FixedPoint in the new situation. And we have many calls to start and finish.

I need to untangle this in a small number of changes. A key issue is that the start and finish properties are not passed the parent’s start and finish so cannot do what they should. Let’s inline correct code inside update and then fix up the two properties. Then we can try to get rid of them.

OK, first of all, what is that 4 degrees doing in there? I remove that and my new test passes. All the old ones of course break, because update isn’t being called in the tests. If I leave start and finish alone, doing their own computation, all the tests pass. That won’t do.

Let’s roll back and think.

Oops, lost FixedPoint. Let’s do that in its own file. OK, that’s done. I’ll mark the new test to skip and make a save point, then un-mark it.

OK, let’s redo update to do its own calculations (again) but not change the start / finish properties this time.

class Wheel:
    def update(self, start, finish):
        return start, finish + vector(1,0).rotate_xy(self._angle_radians)

This breaks my new test because Crank is using start and finish on itself. Can I just fix that?

class Crank:
    def update(self, p_start, p_finish):
        dif = p_finish - p_start
        parent_radians = math.atan2(dif.y, dif.x)
        return self.start, self.finish

I think this is only half done. Let’s see if we can get it all done.

self.start is p_start. Change that. (I was experimenting here using p_start for the parent start, to see if it would help avoid confusion. So far, I remain a bit confused.)

Now finish:

    @property
    def finish(self):
        return self.constrained(0)

    def constrained(self, number):
        center = self._parent.start
        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

I think I want to inline constrained into update. It needs a bit of modification, maybe like this:

    def update(self, p_start, p_finish):
        dif = p_finish - p_start
        parent_radians = math.atan2(dif.y, dif.x)
        center = p_start
        r = self._radius
        effective_angle = self._leadRadians + parent_radians
        x = r * math.cos(effective_angle) - 0 * math.sin(effective_angle)
        y = r * math.sin(effective_angle) + 0 * math.cos(effective_angle)
        finish = vector(x, y) + center
        return p_start, finish

Test, see what happens. Tests run green. Righteous improvement. However, we really need to get rid of the start and finish property methods altogether. We’re trying to obsolete them. I’ll look for senders and see what I can do.

I think I’ll have to recast those tests into the same form as the new one to make things work. Let’s remove start and see what breaks. But wait, I think I can commit a save point. Well, not quite, the drawing has stopped working.

This is getting a bit ragged. After adding a FixedPoint to the drawing main, and adding null init_draw and draw to FixedPoint, the drawing isn’t moving. That suggests to me that the Wheel is not honoring its angle?

Oh, right! That’s what that 4 was about. It was incrementing the angle. My Bad.

OK, that makes it move. The guide is still off center. I want to see what that used to look like, because it is way off. Ah. I find that to draw that line, we need the parent’s start. Bummer. I use it to get green.

However, putting the 4 degrees back breaks my new test. update really shouldn’t be updating the angle, that should be separate.

I add this method to Wheel:

    def turn_by(self, degrees):
        self._angle_radians += degrees * math.pi / 180.0

And call it in the drawing code:

def animate_linkage(components):
    canvas.scale("all", 0,0, 1/scale, 1/scale)
    s = f = None
    wheel.turn_by(4)
    for component in components:
        s, f = component.update(s, f)
        component.draw(canvas)
    canvas.scale("all", 0,0, scale, scale)
    root.after(30, animate_linkage, components)  # Call again after 30ms

All good. Commit save point. Time for break.

Summary

The use of the parent, plus the start/finish methods, has resulted in an over-complicated situation. This is, of course, why we want to convert to passing start and finish as parameters, but there are details every where that need to be unwound. This afternoon’s session has unwound a few, and we can continue to peck away at the calls to start and finish in small steps, so long as we get to save points frequently, as we managed to do this afternoon.

I think we’re going to be OK, and I wish I had thought of this idea sooner.

However, if there is a “big” lesson here, it is that even though we have a sticky web running through our objects, we can unwind it in small steps that won’t interfere with other progress should we wish to work on other things. We move from one green to the next, and whatever we do in between will be just fine. Of course, if we were to do a new feature, we’d want to do it in the new way, not the old one. It might be necessary to preserve both ways for a while, but once we see one way of getting around the old way, we’ll not have to use the old scheme again.

Right now, however, we do have an access to parent in the drawing code, and I am not at all sure whether we can get it out. I guess we could cache the parent start in the update and use it in the draw. We’ll see.

For now, we’re green and drawing the right picture. Life is good.

See you next time!