Back to Articles

2026-11-09

BrushCue 1.4.15 Release Notes

By Anthony Dito

Streams are gone, painters export to SVG, and painters and paths can now be taken apart, edited, and rebuilt in BrushCue Script.

BrushCue 1.4.15 is mostly about painters and paths. A painter is no longer something you can only add to and render. You can now take one apart, change the pieces, put it back together, and export it as an SVG. Paths get the same treatment through path commands. To make room for this, we removed streams and folded their operations into lists.

Streams Are Gone

BrushCue Script used to have two kinds of sequences of values: lists and streams. To map, filter, or fold over a list, you first had to convert it with to_stream(), and then convert it back with collect() if you needed a list again. Having two types for the same idea was confusing for people and for AI agents, and it added extra steps to every script.

Now map, filter, and fold work directly on lists, and the Stream type is removed from BrushCue Script, the bcue command line tool, and the Python API. In most scripts, the fix is to delete .to_stream():

// Before
Int::range(1, 5).to_stream().fold(1, multiply_step)

// After
Int::range(1, 5).fold(1, multiply_step)

The removed nodes are ListToStream, ObjectToStream, StreamMap, StreamFilter, StreamFold, StreamFirst, StreamCollect, and IntStreamSum. Use ListMap, ListFilter, ListFold, ListFirst, ObjectToList, and IntListSum instead.

Under the hood, list operations also do less repeated work. BrushCue now runs the part of a graph that doesn't change between elements once, and then runs only the remaining part for each element of a map, filter, or fold. The same applies to each frame of a sequence.

Export Painters to SVG

The new PainterToSvg operation (painter.to_svg()) writes everything a painter draws as an SVG document. Rectangles, ellipses, and paths become SVG shapes. Each one is defined once and drawn for each of its instance transforms, so repeated shapes stay small. Curved path segments, including Catmull-Rom and cardinal splines, are exported as exact cubic Béziers, so the SVG matches what BrushCue renders.

ink = ProfiledColor::from_srgb_hex("#1F3A5F")
paper = ProfiledColor::from_srgb_hex("#F2C14E")
style = RenderStyle::brush_and_fill(Brush::solid(ink, 4.0), Fill::solid(paper))
wave = Path::new()
    .move_to_point(Point2f::from_components(20.0, 120.0))
    .line_to_point(Point2f::from_components(80.0, 60.0))
    .line_to_point(Point2f::from_components(140.0, 120.0))
painter = Painter::new()
    .add_rectangle_with_render_style(Point2f::from_components(100.0, 100.0), Vector2f::from_components(160.0, 120.0), 0.0, style, Transform2::identity().to_list())
    .add_path_with_render_style(wave, RenderStyle::brush_only(Brush::solid(ink, 6.0)), Transform2::identity().to_list())
painter.to_svg()

Run this with bcue run and it prints the SVG, ready to save to a file or open in a vector editor. The result is a String, so you can also pass it to other operations in your script.

SVG export supports solid brushes and solid fills. Custom brushes, dashed brushes, custom fills, and erasing don't have an SVG equivalent yet, so a painter that uses them returns an error instead of a wrong SVG.

Take a Painter Apart

A painter is a list of painted illustrations, drawn from back to front. A painted illustration is one path, rectangle, or ellipse, together with its render style and the transforms it is drawn with. New operations let you work with that list directly:

Combined with list operations, this lets you edit a painter after it's built. For example, this keeps only the paths in an existing painter and measures what's left:

fn keep_paths(illustration: PaintedIllustration) => Bool {
    illustration.is_path()
}
paths_only = Painter::from_painted_illustrations(painter.to_painted_illustrations().filter(keep_paths))
paths_only.bounds()

The same pattern works with map to change every shape, such as moving the points of each path.

Edit Paths Point by Point

Paths work the same way. PathToCommands turns a path into a list of path commands, such as move to, line to, and the various curve commands. PathFromCommands builds a path from that list again. In between, you can read and change each command:

Here, every point in an existing path is pushed by a wind vector:

wind = Vector2f::from_components(12.0, -4.0)
fn push(command: PathCommand) => PathCommand {
    command.with_coordinate(command.coordinate().add_vector(wind))
}
Path::from_commands(path.to_commands().map(push))

Because pressure, time, and pen angles come along with each command, this works well on strokes recorded from a stylus. Make the wind vector depend on time inside a sequence, scale it by each point's pressure and its distance from the base of the stroke, and a hand-drawn branch sways in the breeze, with thinner parts moving more than thicker ones.

Upgrading

Update the bcue command line tool with sudo snap refresh bcue on Linux, brew upgrade bcue on Mac, or download it from GitHub releases. Update the Python API with pip install --upgrade brushcue. If your scripts use streams, remove .to_stream() and .collect() and call the same operations on the list.