From 385d93a0785ca3fcb31698e70e6be037fa36cba1 Mon Sep 17 00:00:00 2001 From: nagieeb0 Date: Thu, 30 Jul 2026 08:41:38 +0300 Subject: [PATCH] Add fill support to rectangles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rectangles currently always emit `re` followed by `S`, so shapes can only be outlined. That rules out anything built from solid colour: table header backgrounds, status badges, bar charts, and QR codes — a QR drawn as stroked outlines is unscannable. Adds `:fill` to Path.Rectangle. With it, `rg` sets the non-stroking colour and `f` paints the interior. `stroke: true` alongside it emits `B` instead, filling and stroking in one operation so the two stay aligned. Default behaviour is unchanged: without `:fill`, rectangles stroke exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- lib/mudbrick/content_stream/b.ex | 11 ++++ lib/mudbrick/content_stream/f.ex | 11 ++++ lib/mudbrick/path.ex | 18 ++++++- lib/mudbrick/path/output.ex | 28 +++++++++- test/drawing_test.exs | 89 ++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 lib/mudbrick/content_stream/b.ex create mode 100644 lib/mudbrick/content_stream/f.ex diff --git a/lib/mudbrick/content_stream/b.ex b/lib/mudbrick/content_stream/b.ex new file mode 100644 index 0000000..3a92428 --- /dev/null +++ b/lib/mudbrick/content_stream/b.ex @@ -0,0 +1,11 @@ +defmodule Mudbrick.ContentStream.B do + @moduledoc false + + defstruct [] + + defimpl Mudbrick.Object do + def to_iodata(_op) do + ["B"] + end + end +end diff --git a/lib/mudbrick/content_stream/f.ex b/lib/mudbrick/content_stream/f.ex new file mode 100644 index 0000000..a9aeefe --- /dev/null +++ b/lib/mudbrick/content_stream/f.ex @@ -0,0 +1,11 @@ +defmodule Mudbrick.ContentStream.F do + @moduledoc false + + defstruct [] + + defimpl Mudbrick.Object do + def to_iodata(_op) do + ["f"] + end + end +end diff --git a/lib/mudbrick/path.ex b/lib/mudbrick/path.ex index 72de0b7..9ef8a0a 100644 --- a/lib/mudbrick/path.ex +++ b/lib/mudbrick/path.ex @@ -5,21 +5,35 @@ defmodule Mudbrick.Path do | {:dimensions, Mudbrick.coords()} | {:line_width, number()} | {:colour, Mudbrick.colour()} + | {:fill, Mudbrick.colour() | nil} + | {:stroke, boolean()} @type options :: [option()] + @typedoc """ + Without `:fill` a rectangle is stroked in `:colour`, as it always was. + + Given `:fill`, the interior is painted in that colour and the outline is + dropped — which is what backgrounds, bars and QR modules want. Set + `stroke: true` alongside it to get both, painted in one operation so the + two stay aligned. + """ @type t :: %__MODULE__{ lower_left: Mudbrick.coords(), dimensions: Mudbrick.coords(), line_width: number(), - colour: Mudbrick.colour() + colour: Mudbrick.colour(), + fill: Mudbrick.colour() | nil, + stroke: boolean() } @enforce_keys [:lower_left, :dimensions] defstruct lower_left: nil, dimensions: nil, line_width: 1, - colour: {0, 0, 0} + colour: {0, 0, 0}, + fill: nil, + stroke: false @doc false @spec new(options()) :: t() diff --git a/lib/mudbrick/path/output.ex b/lib/mudbrick/path/output.ex index 5e944ff..8ccb756 100644 --- a/lib/mudbrick/path/output.ex +++ b/lib/mudbrick/path/output.ex @@ -4,6 +4,8 @@ defmodule Mudbrick.Path.Output do defstruct operations: [] alias Mudbrick.ContentStream.{ + B, + F, L, M, QPop, @@ -38,7 +40,7 @@ defmodule Mudbrick.Path.Output do |> add(%L{coords: line.to}) |> add(%S{}) - %Mudbrick.Path.Rectangle{} = rect -> + %Mudbrick.Path.Rectangle{fill: nil} = rect -> {r, g, b} = rect.colour acc @@ -49,6 +51,30 @@ defmodule Mudbrick.Path.Output do dimensions: rect.dimensions }) |> add(%S{}) + + %Mudbrick.Path.Rectangle{} = rect -> + # A fill colour was given. `rg` sets the non-stroking colour and + # `f` paints the interior. With `stroke: true` as well, `B` fills + # and strokes in one operation so the two stay aligned. + {fr, fg, fb} = rect.fill + {r, g, b} = rect.colour + + acc + |> add(Rg.new(stroking: false, r: fr, g: fg, b: fb)) + |> then(fn output -> + if rect.stroke do + output + |> add(Rg.new(stroking: true, r: r, g: g, b: b)) + |> add(%W{width: rect.line_width}) + else + output + end + end) + |> add(%Re{ + lower_left: rect.lower_left, + dimensions: rect.dimensions + }) + |> add(if rect.stroke, do: %B{}, else: %F{}) end end end) diff --git a/test/drawing_test.exs b/test/drawing_test.exs index cd9cb38..0159118 100644 --- a/test/drawing_test.exs +++ b/test/drawing_test.exs @@ -46,6 +46,95 @@ defmodule Mudbrick.DrawingTest do |> Mudbrick.TestHelper.operations() end + test "a fill colour paints the rectangle's interior" do + import Mudbrick + + assert [ + "q", + "0 0.5 1 rg", + "10 10 40 20 re", + "f", + "Q" + ] = + new() + |> page() + |> path(fn path -> + Path.rectangle(path, + lower_left: {10, 10}, + dimensions: {40, 20}, + fill: {0, 0.5, 1} + ) + end) + |> Mudbrick.TestHelper.output() + |> Mudbrick.TestHelper.operations() + end + + test "a fill and an outline colour together fill and stroke in one operation" do + import Mudbrick + + assert [ + "q", + "1 1 0 rg", + "0 0 0 RG", + "2 w", + "10 10 40 20 re", + "B", + "Q" + ] = + new() + |> page() + |> path(fn path -> + Path.rectangle(path, + lower_left: {10, 10}, + dimensions: {40, 20}, + fill: {1, 1, 0}, + colour: {0, 0, 0}, + stroke: true, + line_width: 2 + ) + end) + |> Mudbrick.TestHelper.output() + |> Mudbrick.TestHelper.operations() + end + + test "without a fill colour a rectangle is still stroked only" do + import Mudbrick + + ops = + new() + |> page() + |> path(fn path -> + Path.rectangle(path, lower_left: {0, 0}, dimensions: {50, 60}) + end) + |> Mudbrick.TestHelper.output() + |> Mudbrick.TestHelper.operations() + + assert "S" in ops + refute "f" in ops + end + + test "many filled rectangles compose, as a QR code or a bar chart would" do + import Mudbrick + + ops = + new() + |> page() + |> path(fn path -> + for i <- 0..9, reduce: path do + acc -> + Path.rectangle(acc, + lower_left: {i * 4, 0}, + dimensions: {3, 3}, + fill: {0, 0, 0} + ) + end + end) + |> Mudbrick.TestHelper.output() + |> Mudbrick.TestHelper.operations() + + assert Enum.count(ops, &(&1 == "f")) == 10 + end + test "can construct a rectangle" do import Path