Filter: The Missing Spread Node
Spreads are the backbone of Lux. Everything interesting happens when you’re working with collections of values — scattering circles, stepping through sequences, mapping data to visuals. We’ve had nodes to Take, Skip, Sort, Distinct, and Reverse spreads for a while. But there was a gap: what if you want to keep only the elements that match a condition?
Filter
The new Filter node takes two inputs:
- spread — any spread of values
- mask — a boolean spread of the same length
It returns only the elements where the mask is true.
spread: [10, 20, 30, 40, 50]
mask: [T, F, T, F, T ]
out: [10, 30, 50]
That’s it. Simple to understand, but it unlocks a whole class of patches.
Why it matters
The real power comes from what you wire into the mask input. Connect a comparison node — Greater, Less, Equal, InRange — and you’ve got conditional filtering without any branching logic.
Want only the values above a threshold? Wire your spread into both Filter’s spread input and a Greater node. Greater outputs a boolean spread. Plug that into Filter’s mask. Done.
Want to remove duplicates based on a condition? Filter. Want to select elements at specific indices? Build a boolean mask and filter. Want to keep only the points inside a radius? Compare distances, filter.
It composes with everything because it speaks the same language as every other spread node: spreads in, spreads out.
Edge cases
The implementation handles mismatched lengths sensibly:
- Mask shorter than spread — extra elements are dropped (conservative default)
- Mask longer than spread — extra mask values are ignored
- Empty spread or mask — returns an empty spread
- Non-boolean mask values — numbers are truthy if non-zero, strings if non-empty
8 tests cover all of these cases.
The pattern
Filter follows the same pattern as every other spread node in Lux: it’s spread_native, meaning it handles spreads directly rather than being auto-spread by the evaluator. It takes spread inputs and produces a spread output. No special cases in the engine.
let result: Vec<PinValue> = items
.into_iter()
.zip(mask.iter())
.filter(|(_, m)| is_truthy(m))
.map(|(item, _)| item)
.collect();
Five lines of logic. The rest is documentation and tests.