More Better
I’ve come up with a better way. At least I think it is. Herewith, a report. Green shoes.
I had a few free moments yesterday and tried a couple of different ideas for expressing a linkage. I think one of them is notably better than what we had.
Here is our most recent way, in actual use;
linkage = Linkage()
linkage.add_one(FixedPoint(base))
wheel = linkage.add_one(Wheel())
crank = linkage.add_one(Crank(radius=radius, lead_degrees=lead_angle))
linkage.add_one(ConRod(length=rod_length, piston_angle=tilt_angle))
linkage.add_one(Piston(length=piston_length, angle=tilt_angle))
linkage.add_one(SideRod(length=7), crank)
linkage.add_one(Crank(radius=0.5), wheel)
arc_rod = ArcRod(
length=5,
target_position=bell_pos,
target_radius=0.55,
target_angle=135)
linkage.add_one(arc_rod)
The method add_one used to be named add. It’s the method that applies a remembered parent if one is not provided, as is done in the case of the SideRod and the subsequent Crank:
def add_one(self, component, parent=None):
le = self.Element(component, parent or self._previous)
self._elements[component] = le
self._previous = component
return component
The component is returned from the add_one to facilitate saving it, if it’s going to be needed as a future parent. We see the two assignments of wheel and crank above.
I then tried a cascading method, add_l which was just a nonce name to see whether I liked the scheme, which looks like this:
def test_cascade(self):
linkage = Linkage()\
.add_l(FixedPoint(position=vector(2,1)))\
.add_l(Wheel())\
.add_l(Crank(radius=1))
assert len(linkage._elements) == 3
add_l returns the linkage, which allows us to do the somewhat strange “end the line with backslash and then send another message” trick.
I think that’s clever but too far beyond what one usually does in Python. As my brother once said about some weird notion: “It is like green shoes: interesting, but not sought after”.
Then I tried this scheme:
def test_batch(self):
linkage = Linkage()
linkage.add(
FixedPoint(position=vector(2,1)),
w := Wheel(),
c1 := Crank(radius=1)
)
linkage.parent = w
linkage.add(c2 := Crank(radius=0.5), w)
assert len(linkage._elements) == 4
assert w._angle_radians == 0
elt = linkage._elements[c2]
assert elt.parent != c1
assert elt.parent == w
In this scheme, the add method accepts any number of components, setting the parent of each one to the previous one, starting from whatever parent is currently saved. (Starting with None: “root” objects expect no parent.)
That seems decent to me. The weirdest bit is the use of the walrus operator :=, which allows one to save an intermediate result in a variable. Standard Python, rarely used but valuable when it is. I’ll allow it.
Let’s see what the demonstration linkage looks like in this form:
linkage = Linkage()
linkage.add(
FixedPoint(base),
wheel := Wheel(),
crank := Crank(radius=radius, lead_degrees=lead_angle),
ConRod(length=rod_length, piston_angle=tilt_angle),
Piston(length=piston_length, angle=tilt_angle)
)
linkage.parent = crank
linkage.add(SideRod(length=7))
linkage.parent = wheel
linkage.add(
Crank(radius=0.5),
ArcRod(
length=5,
target_position=bell_pos,
target_radius=0.55,
target_angle=135)
)
How does that compare with what we had before?
linkage = Linkage()
linkage.add_one(FixedPoint(base))
wheel = linkage.add_one(Wheel())
crank = linkage.add_one(Crank(radius=radius, lead_degrees=lead_angle))
linkage.add_one(ConRod(length=rod_length, piston_angle=tilt_angle))
linkage.add_one(Piston(length=piston_length, angle=tilt_angle))
linkage.add_one(SideRod(length=7), crank)
linkage.add_one(Crank(radius=0.5), wheel)
arc_rod = ArcRod(
length=5,
target_position=bell_pos,
target_radius=0.55,
target_angle=135)
linkage.add_one(arc_rod)
The new scheme is 19 lines rather than 14. Two of those are essentially blank. And, as presently constituted, it takes two lines to add an item that needs a parent other than the previous object, where the scheme with add_one only requires one.
We might imagine a method add that requires a parent and then the open list of objects. Or we might imagine a way of interpolating a parent into the list with a special object:
# a sketch, not implemented
linkage = Linkage()
linkage.add(
FixedPoint(base),
wheel := Wheel(),
crank := Crank(radius=radius, lead_degrees=lead_angle),
ConRod(length=rod_length, piston_angle=tilt_angle),
Piston(length=piston_length, angle=tilt_angle),
Parent(crank),
SideRod(length=7),
Parent(wheel),
Crank(radius=0.5),
ArcRod(
length=5,
target_position=bell_pos,
target_radius=0.55,
target_angle=135)
)
Still 17 lines, but arguably less cryptic than the old scheme.
Which is best?
I assure you that I do not know that. I’ll try living with the current scheme for a while, see what I think. I suspect that all these schemes are a bit moot. In use, I imagine that a user might prefer to define their linkage, not in Python but perhaps in a spreadsheet that we would stringify and import as text. They might even demand that we examine some objects in Blender and extract the information we need.
For now, our main purpose is to learn the domain, working out objects that we can use to compute animations. And so far, that’s going pretty well.
I do have that concern from the other day, where it looked like an ArcRod had gone awry. We’ll keep that on the list to work on. For now, we’ll sit with this scheme for a while, while we check our existing work and add new objects. I think the BellCrank might be next.
See you then!