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.

← Back to blog