Merge, Multi-Wire, and the Death of Group Chains
Look at the canvas of any non-trivial Lux patch from a month ago and you’d see a particular shape: long diagonal towers of Group nodes, cascading down and to the right, each one combining two layers into one. To merge six shapes into a single output you needed five Group nodes chained together, wired in a staircase, taking up half the screen.
It worked. It was also terrible. Every time I built a real patch, the Group tower was the first thing that got in the way, and the first thing I wanted to fix.
This session fixed it. Three changes, each one the natural consequence of the last.
Merge: one node, any number of layers
The first change is a new node: Merge. It takes a single input, a Spread<Layer>, and outputs a single combined layer containing all the draw commands from every input layer in order.
- Inputs:
layers(Spread), order(Spread, optional) - Output:
out(Layer)
One node. Any number of layers. No chaining.
That should immediately raise a question: how do you feed multiple layer outputs into a single Spread<Layer> input pin? Until today, that wasn’t a thing the graph engine allowed. A pin could have one wire connecting to it. Connect a second wire and the first got replaced.
Which brings us to the second change.
Multi-wire connections to Spread inputs
The graph engine now allows multiple wires to terminate at the same input pin, but only if the target pin type is Spread<T>. During evaluation, the engine collects all connected source values and wraps them in a PinValue::Spread before calling process().
Non-spread pins keep their existing behaviour: one wire, and a new connection replaces the old one. That’s the semantics you want for scalar inputs, you don’t typically want three different nodes all writing to a single radius pin simultaneously.
But spread-typed inputs are designed to carry collections. Forcing you to build an explicit spread upstream (via Group, or a LinearSpread, or a hand-constructed merge) when you just want to gather several values into one pin is exactly the kind of friction that kills flow. The engine already knows the pin expects a spread. Just let it gather.
The pattern is now the obvious one:
Circle.layer ─┐
Rect.layer ─┼─→ Merge.layers → output
Ring.layer ─┘
Three wires into one pin. The engine builds the spread, Merge collects the layers, the output is a single composed frame. The Group tower is gone.
The implementation is smaller than it sounds. The connect() call used to call retain() to remove any existing connection to the target pin before adding the new one. Now it checks the target pin’s type first: if it’s a Spread, skip the retain and just append. If it’s anything else, keep the old replace-semantics behaviour. Four lines of change. The whole evaluator already knew how to pack multiple values into a spread, it just never had a reason to do it at this stage of the pipeline.
Layer ordering
With Merge collecting N layers and wiring order being arbitrary (the graph doesn’t guarantee wire order during evaluation), you need a way to control which layer draws on top.
Merge now has an optional order input, another Spread<Number>, that lets you explicitly set the draw order:
order: [2, 0, 1] → draws layer 2 first (bottom), then 0, then 1 (top)
An empty order uses connection order. A partial order (shorter than the number of layers) puts the explicitly ordered layers first and appends the rest afterwards in connection order. Out-of-range indices clamp to valid positions. The semantics are “say what you want, let the node figure out the rest.”
This is the right shape for a compositor. You have some layers, you want some of them on top, you shouldn’t have to care about the others. Specify what matters, default the rest.
Drag-to-reorder in the inspector
Wiring the order pin by hand, building a LinearSpread and connecting it, works, but it’s not what anyone actually wants. What you want is to grab a layer and drag it to a new position. Like every layer panel in every compositor since Photoshop.
When a Merge node is selected, the inspector now shows a Layer Order panel with one row per connected layer. Each row has:
- A grab handle (
≡, the Lucide icon, not painted shapes) - Up/down chevron buttons for single-position moves
- A colour dot showing the layer’s family colour
- The source node’s name (“Circle”, “Ellipse”, “LoadImage”)
- An index badge showing the original connection position
Click the up or down arrow and the layer moves one position. Grab the handle and drag to a new position, and a blue horizontal line shows where the layer will land when you drop it. Release to commit. The cursor changes to a grab cursor when hovering the handle, and to a grabbing cursor during the drag.
Every reorder writes the new order back to the order pin via the undo-recording setter, so Ctrl+Z puts the order back exactly where it was. Drag five layers into the right arrangement, realise you preferred the old one, hit Ctrl+Z five times. Or use undo coalescing on consecutive drags (still on the list for this specific interaction).
The small fight with text selection
One thing that tripped me up: egui’s default behaviour for labels inside an interactive list is to let the user select the text, which is correct for most UI, and completely wrong for a drag-to-reorder list, you’d start dragging a layer and instead of moving it, you’d be highlighting the node name. Release, and the drag never registered.
The fix was to mark the label widgets as non-selectable for the Merge reorder rows specifically. Everywhere else in the inspector, text stays selectable. The layer order list is the exception because it has a higher-priority interaction (drag) that has to win.
The grab-cursor detection was a separate iteration. Hover the ≡ handle specifically and the cursor changes to an open hand. Start dragging and it becomes a closed fist. Hover the rest of the row and it stays as the default arrow. The distinction matters because the click target is small, and without the cursor feedback people (me) would click anywhere on the row and be confused when nothing moved.
What the canvas looks like now
Before this session: patches with Merge were impossible. The closest approximation was a Group tower that took five or six nodes and three screen widths to compose a handful of shapes.
After this session: patches with Merge are the default. One node, any number of inputs, inspector UI for draw order, full undo support. The Group tower pattern is actually deprecated. Group still exists for cases where you specifically want to compose two layers with a transform between them, but for “I have a bunch of stuff and I want it all in one layer,” Merge is the right answer and it’s not even close.
Sometimes the best way to fix a pattern is to make the pattern unnecessary.