Wiring and nodes

Basic wiring

The central element of wiring in manim-eng is the Wire class. It takes a start terminal and an end terminal, and automatically plans a path between the two. It actually does this continuously via an updater, so if the terminals to which the wire is attached are moved, the wire will plan a new path seamlessly.

class WireBasics(Scene):
    def construct(self):
        r1 = Resistor().shift(LEFT)
        r2 = Resistor().shift(RIGHT)
        self.add(r1, r2)
        self.add(Wire(r1.right, r2.left))
        self.wait()
        self.play(
            r1.animate.shift(UP),
            r2.animate.shift(DOWN),
        )
       self.play(
            r1.animate.shift(LEFT),
            r2.animate.shift(RIGHT),
        )
        self.play(Rotate(r2, angle=-PI))
        self.wait()

Planning wire paths

In order to determine what path a wire should follow, manim-eng follows the following principles:

  1. Wires only run horizontally or vertically;

  2. Terminals are treated as having the nearest cardinal direction (e.g. a terminal at 60° anticlockwise from the horizontal would be treated as pointing up);

  3. Wires should never go ‘backwards’ out of a terminal;

  4. Wire should, where possible, come out of the front of a terminal; and

  5. Where three line segments are required, the middle segment should be placed in the middle (meaning the other two segments have the same length).

In general, the only situation in which this will produce results that look visibly strange is if two collinear terminals pointing in the same direction are connected, as demonstrated in the example below.

class StrangeAutomaticWiring(Scene):
    def construct(self):
        r1 = Resistor().shift(LEFT)
        r2 = Resistor().shift(RIGHT)
        self.add(r1, r2)
        self.add(Wire(r1.right, r2.right))
../_images/StrangeAutomaticWiring-1.png

Note that these are both resistors (i.e. the right-hand one is not a fuse!)

Advanced wiring: using nodes

A notable deficiency of the Wire class is that it can only connect two terminals. To produce more complex layouts that include intersections, we have to turn to the Node class.

Nodes behave a little differently to normal components, namely in the way they handle terminals. Nodes can have terminals in any direction, and create them as and when they’re needed. Node terminals are only visible when wires are attached to them, and are otherwise invisible. [1]

Attaching and detaching wires

In order for a terminal to change its visibility based on whether or not a wire is attached to it, it needs to know whether or not a wire is attached. This brings us to the attach() and detach() methods.

These methods instruct a wire to tell the terminals it has in its start and end attributes that it is now attached, by calling the _increment_connection_count() or _decrement_connection_count() methods on them.

But why not attach automatically?

It comes down to animations. Consider the following example:

n1 = Node().shift(LEFT)
n2 = Node().shift(RIGHT)
w = Wire(start=n1.right, end=n2.left)

self.add(n1, n2)
self.wait()
self.add(w.attach())
self.wait()

If Wire called attach() automatically in its constructor, then the terminals of node 1 and node 2 would be updated to display before w were displayed. We would then see the following.

Important

This is not what you would see if you ran the code above! This is what you would see if the wire constructor ran attach() automatically.

We end up with a weird artefact of the node terminals being presented before the wire is. As an aside, if we actually run the code above, we see the below…

… which looks much better. Of course, this is a contrived example, but hopefully you see the reasoning.

Some things handle attaching for you

There are some ways of getting around having to worry about this. The WireBase class (the base class of all other wires) overrides the Create and Uncreate animations to call attach() or detach(), respectively. The Circuit class also handles this itself.

If you’re using a mix of these automatic solutions and manual handling, or you’re not quite sure, it’s always worth erring on the side of attaching and detaching manually. The attach()/detach() method checks whether a wire is already attached/detached before attaching/detaching, so attaching/detaching twice has no detrimental effects.

Autoblobbing

Circuit drawing best practice says that intersections of wires should be drawn with solder blobs. By default, Nodes handle this automatically, in what manim-eng calls autoblobbing. Let’s take a look at an example.

class AutoblobbingExample(Scene):
    def construct(self):
        r1 = Resistor().shift(UL)
        r2 = Resistor().shift(DL)
        r3 = Resistor().shift(RIGHT)
        n = Node()
        self.add(
            r1, r2, r3, n,
            Wire(r1.right, n.up).attach(),
            Wire(r2.right, n.down).attach(),
            Wire(r3.left, n.right).attach(),
        )
        n.update()
../_images/AutoblobbingExample-1.png

Note

Nodes use updaters to perform autoblobbing, and so in static scenes they need kicking into gear by manually calling update(). If you’re dealing with more than one node, the manim.scene.scene.Scene.update_mobjects() method on scenes can update them all at once.

If you don’t want to use this behaviour, you can pass autoblob=False to the node constructor or use the disable_autoblobbing() method and use show_blob() or hide_blob() to manually control whether or not to display a blob. (Note: these methods disable autoblobbing as well, to prevent there being two sources of truth on whether a blob should be visible.)

Note that, by default, nodes will display their blob if autoblobbing is disabled in the constructor. If we change the node construction to be

n = Node().hide_blob()

we see the below.

../_images/AutoblobbingExample-2.png

No blob!

Keeping nodes in place using updaters

Nodes provide us with a way to produce complex layouts, but currently we can’t make them responsive to component movement. That’s where Manim’s updaters come in.

Note

This does not aim to be a comprehensive guide to updaters, as they are a Manim feature and not something introduced by manim-eng. This will only touch on how they can be used to create responsive layouts within manim-eng. If you want to learn more about updaters, Manim’s deep dive page touches on them briefly, and you can also take a look at the entries in its API docs.

Let’s examine the simple example of a potential divider.

class PotentialDivider(Scene):
    def construct(self):
        r1 = Resistor().shift(2 * UP).rotate(0.5 * PI)
        r2 = Resistor().shift(2 * DOWN).rotate(0.5 * PI)
        n = Node().shift(1 * DOWN)
        t = OpenNode().shift(2 * RIGHT + 1 * DOWN)

        self.add(
            r1, r2, n, t,
            Wire(r1.left, n.up).attach(),
            Wire(r2.right, n.down).attach(),
            Wire(n.right, t.left).attach(),
        )
../_images/PotentialDivider-1.png

In a somewhat contrived manner, I’ve deliberately placed the open node off-centre. Let’s put that right with an animation. We’ll add the following lines to our scene.

self.play(t.animate.shift(UP))
self.wait()

That looks fine, but it’s a bit annoying that the node and open node are misaligned now. We could of course just move the node as well, but that’s kind of cumbersome. Instead, we’ll add an updater, which will keep the node aligned.

To do this, we’ll add the line below just above the self.add() call.

n.add_updater(lambda mob: mob.align_terminal("right", t.left))

Note that this uses the align_terminal() method, a positioning method added by manim-eng that allows you to keep terminals of different components aligned with one another. Using this updater gives us the below.

That looks much better. This example, although simple, hopefully shows the theory of combining nodes and updaters to create responsive layouts.

The open node: the other kind of node

The OpenNode acts precisely like a normal node, but with the centre unfilled and autoblobbing disabled. It is used to represent circuit terminals (note that this is not referring to the manim-eng concept of component terminals, but the external connections to a circuit in a circuit diagram. In fact, I snuck one in in the previous section. For another example, the below displays a two-port network low-pass filter.

class LowPassFilter(Scene):
    def construct(self):
        in_pwr = OpenNode().shift(UP + 2 * LEFT)
        in_gnd = OpenNode().shift(DOWN + 2 * LEFT)
        out_pwr = OpenNode().shift(UP + 2 * RIGHT)
        out_gnd = OpenNode().shift(DOWN + 2 * RIGHT)
        r = Resistor().shift(UP + 0.5 * LEFT)
        c = Capacitor().shift(RIGHT).rotate(0.5 * PI)
        node_up = Node().shift(UR)
        node_down = Node().shift(DR)

        self.add(
            in_pwr, in_gnd, out_pwr, out_gnd,
            r, c,
            node_up, node_down,
            Wire(in_pwr.right, r.left).attach(),
            Wire(in_gnd.right, node_down.left).attach(),
            Wire(r.right, node_up.left).attach(),
            Wire(node_up.right, out_pwr.left).attach(),
            Wire(node_down.right, out_gnd.left).attach(),
            Wire(node_up.down, c.right).attach(),
            Wire(node_down.up, c.left).attach(),
        )
        self.update_mobjects(0)
../_images/LowPassFilter-1.png

Advanced wiring: the ManualWire class

The automatic wire routing is great, but sometimes, you need something custom. In this case, you have two options. The first is to subclass the WireBase class — this is particularly good if you want to encode new routing behaviour to use repeatedly.

The other, simpler, method is the ManualWire class. This requires a list of points at which the wire should have corners, which can be updated later using the set_corner_points() method if desired. This can be combined with an updater to make a responsive wire, but if using anything more than trivial logic for this you probably want to be using the subclassing option above.

Let’s take a look at ManualWire in action. For this we’ll use a simple ‘switchboard’, which aptly demonstrates the main use case of the ManualWire: it facilitates diagonal wires.

class Switchboard(Scene):
    def construct(self):
        in1 = OpenNode().shift(UP + 2 * LEFT)
        in2 = OpenNode().shift(DOWN + 2 * LEFT)
        out1 = OpenNode().shift(UP + 2 * RIGHT)
        out2 = OpenNode().shift(DOWN + 2 * RIGHT)
        self.add(in1, in2, out1, out2)
        self.add(
            ManualWire(
                in1.right, out2.left, [UP + 0.5 * LEFT, DOWN + 0.5 * RIGHT]
            ).attach(),
            ManualWire(
                in2.right, out1.left, [DOWN + 0.5 * LEFT, UP + 0.5 * RIGHT]
            ).attach(),
        )
../_images/Switchboard-1.png

In fact, if you just want to connect two terminals directly, you don’t need to specify the list of corner points at all. Let’s look at what this would look like in our switchboard example.

class Switchboard(Scene):
    def construct(self):
        in1 = OpenNode().shift(UP + 2 * LEFT)
        in2 = OpenNode().shift(DOWN + 2 * LEFT)
        out1 = OpenNode().shift(UP + 2 * RIGHT)
        out2 = OpenNode().shift(DOWN + 2 * RIGHT)
        self.add(in1, in2, out1, out2)
        self.add(
            ManualWire(in1.right, out2.left).attach(),
            ManualWire(in2.right, out1.left).attach(),
        )
../_images/Switchboard-2.png

As can be seen above, the wires now go directly from one terminal to another.

Circuits: a teaser

With that, you should now have a decent idea of the ways you can use and customise manim-eng’s various wire types. Those of you who have gone through the tutorial will note that we are yet to touch on one other class that can perform wiring: the Circuit. This deserved to be handled separately, and so is outlined in the next guide.

Footnotes