From 286ab5c504928cee177a185179908833f401258d Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 20 Jan 2019 16:19:17 -0500 Subject: [PATCH] A function that can fill arbitrary shapes on the braille canvas. --- draw/braille_fill.go | 160 ++++++++++++++++++++++ draw/braille_fill_test.go | 270 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+) create mode 100644 draw/braille_fill.go create mode 100644 draw/braille_fill_test.go diff --git a/draw/braille_fill.go b/draw/braille_fill.go new file mode 100644 index 0000000..4c966ad --- /dev/null +++ b/draw/braille_fill.go @@ -0,0 +1,160 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package draw + +// braille_fill.go implements the flood-fill algorithm for filling shapes on the braille canvas. + +import ( + "fmt" + "image" + + "github.com/mum4k/termdash/canvas/braille" + "github.com/mum4k/termdash/cell" +) + +// BrailleFillOption is used to provide options to BrailleFill. +type BrailleFillOption interface { + // set sets the provided option. + set(*brailleFillOptions) +} + +// brailleFillOptions stores the provided options. +type brailleFillOptions struct { + cellOpts []cell.Option + pixelChange braillePixelChange +} + +// newBrailleFillOptions returns a new brailleFillOptions instance. +func newBrailleFillOptions() *brailleFillOptions { + return &brailleFillOptions{ + pixelChange: braillePixelChangeSet, + } +} + +// brailleFillOption implements BrailleFillOption. +type brailleFillOption func(*brailleFillOptions) + +// set implements BrailleFillOption.set. +func (o brailleFillOption) set(opts *brailleFillOptions) { + o(opts) +} + +// BrailleFillCellOpts sets options on the cells that are set as part of +// filling shapes. +// Cell options on a braille canvas can only be set on the entire cell, not per +// pixel. +func BrailleFillCellOpts(cOpts ...cell.Option) BrailleFillOption { + return brailleFillOption(func(opts *brailleFillOptions) { + opts.cellOpts = cOpts + }) +} + +// BrailleFillClearPixels changes the behavior of BrailleFill, so that it +// clears the pixels instead of setting them. +// Useful in order to "erase" the filled area as opposed to drawing one. +func BrailleFillClearPixels() BrailleFillOption { + return brailleFillOption(func(opts *brailleFillOptions) { + opts.pixelChange = braillePixelChangeClear + }) +} + +// BrailleFill fills the braille canvas starting at the specified point. +// The function will not fill or cross over any points in the defined border. +// The start point must be in the canvas. +func BrailleFill(bc *braille.Canvas, start image.Point, border []image.Point, opts ...BrailleFillOption) error { + if ar := bc.Area(); !start.In(ar) { + return fmt.Errorf("unable to start filling canvas at point %v which is outside of the braille canvas area %v", start, ar) + } + + opt := newBrailleFillOptions() + for _, o := range opts { + o.set(opt) + } + + b := map[image.Point]struct{}{} + for _, p := range border { + b[p] = struct{}{} + } + + v := newVisitable(bc.Area(), b) + visitor := func(p image.Point) error { + switch opt.pixelChange { + case braillePixelChangeSet: + return bc.SetPixel(p, opt.cellOpts...) + case braillePixelChangeClear: + return bc.ClearPixel(p, opt.cellOpts...) + } + return nil + } + return brailleDFS(v, start, visitor) +} + +// visitable represents an area that can be visited. +// It tracks nodes that are already visited. +type visitable struct { + area image.Rectangle + visited map[image.Point]struct{} +} + +// newVisitable returns a new visitable object initialized for the provided +// area and already visited nodes. +func newVisitable(ar image.Rectangle, visited map[image.Point]struct{}) *visitable { + if visited == nil { + visited = map[image.Point]struct{}{} + } + return &visitable{ + area: ar, + visited: visited, + } +} + +// neighborsAt returns all valid neighbors for the specified point. +func (v *visitable) neighborsAt(p image.Point) []image.Point { + var res []image.Point + for _, neigh := range []image.Point{ + {p.X - 1, p.Y}, // left + {p.X + 1, p.Y}, // right + {p.X, p.Y - 1}, // up + {p.X, p.Y + 1}, // down + } { + if !neigh.In(v.area) { + continue + } + if _, ok := v.visited[neigh]; ok { + continue + } + v.visited[neigh] = struct{}{} + res = append(res, neigh) + } + return res +} + +// brailleDFS visits every point in the area and runs the visitor function. +func brailleDFS(v *visitable, p image.Point, visitFn func(image.Point) error) error { + neigh := v.neighborsAt(p) + if len(neigh) == 0 { + return nil + } + + for _, n := range neigh { + if err := visitFn(n); err != nil { + return err + } + if err := brailleDFS(v, n, visitFn); err != nil { + return err + } + } + return nil +} diff --git a/draw/braille_fill_test.go b/draw/braille_fill_test.go new file mode 100644 index 0000000..9a18e68 --- /dev/null +++ b/draw/braille_fill_test.go @@ -0,0 +1,270 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package draw + +import ( + "image" + "testing" + + "github.com/mum4k/termdash/area" + "github.com/mum4k/termdash/canvas/braille" + "github.com/mum4k/termdash/canvas/braille/testbraille" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/faketerm" +) + +func TestBrailleFill(t *testing.T) { + tests := []struct { + desc string + canvas image.Rectangle + start image.Point + border []image.Point + + // If not nil, called to prepare the braille canvas before running the test. + prepare func(*braille.Canvas) error + + opts []BrailleFillOption + want func(size image.Point) *faketerm.Terminal + wantErr bool + }{ + { + desc: "fails when start isn't in the canvas", + canvas: image.Rect(0, 0, 1, 1), + start: image.Point{-1, 0}, + wantErr: true, + }, + { + desc: "fills the full canvas without a border", + canvas: image.Rect(0, 0, 1, 1), + start: image.Point{1, 1}, + border: nil, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + testbraille.MustSetPixel(bc, image.Point{0, 0}) + testbraille.MustSetPixel(bc, image.Point{1, 0}) + testbraille.MustSetPixel(bc, image.Point{0, 1}) + testbraille.MustSetPixel(bc, image.Point{1, 1}) + testbraille.MustSetPixel(bc, image.Point{0, 2}) + testbraille.MustSetPixel(bc, image.Point{1, 2}) + testbraille.MustSetPixel(bc, image.Point{0, 3}) + testbraille.MustSetPixel(bc, image.Point{1, 3}) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + { + desc: "fills the full canvas and sets cell options", + canvas: image.Rect(0, 0, 1, 1), + start: image.Point{1, 1}, + border: nil, + opts: []BrailleFillOption{ + BrailleFillCellOpts(cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + opts := []cell.Option{ + cell.FgColor(cell.ColorRed), + cell.BgColor(cell.ColorBlue), + } + testbraille.MustSetPixel(bc, image.Point{0, 0}, opts...) + testbraille.MustSetPixel(bc, image.Point{1, 0}, opts...) + testbraille.MustSetPixel(bc, image.Point{0, 1}, opts...) + testbraille.MustSetPixel(bc, image.Point{1, 1}, opts...) + testbraille.MustSetPixel(bc, image.Point{0, 2}, opts...) + testbraille.MustSetPixel(bc, image.Point{1, 2}, opts...) + testbraille.MustSetPixel(bc, image.Point{0, 3}, opts...) + testbraille.MustSetPixel(bc, image.Point{1, 3}, opts...) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + { + desc: "clears pixels instead of setting them", + canvas: image.Rect(0, 0, 1, 1), + start: image.Point{1, 1}, + border: nil, + opts: []BrailleFillOption{ + BrailleFillClearPixels(), + }, + prepare: func(bc *braille.Canvas) error { + // Set some pixels, see if they get cleared. + if err := bc.SetPixel(image.Point{1, 0}); err != nil { + return err + } + return bc.SetPixel(image.Point{1, 0}) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + testbraille.MustSetPixel(bc, image.Point{0, 0}) + testbraille.MustClearPixel(bc, image.Point{0, 0}) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + { + desc: "clears pixels and sets cell options", + canvas: image.Rect(0, 0, 1, 1), + start: image.Point{1, 1}, + border: nil, + opts: []BrailleFillOption{ + BrailleFillCellOpts(cell.FgColor(cell.ColorRed)), + BrailleFillClearPixels(), + }, + prepare: func(bc *braille.Canvas) error { + // Set some pixels, see if they get cleared. + if err := bc.SetPixel(image.Point{1, 0}); err != nil { + return err + } + return bc.SetPixel(image.Point{1, 0}) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + opts := []cell.Option{ + cell.FgColor(cell.ColorRed), + } + testbraille.MustSetPixel(bc, image.Point{0, 0}, opts...) + testbraille.MustClearPixel(bc, image.Point{0, 0}, opts...) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + { + desc: "avoids the border", + canvas: image.Rect(0, 0, 3, 3), + start: image.Point{1, 1}, + border: []image.Point{ + {1, 3}, + {2, 3}, + {3, 3}, + {4, 3}, + + {1, 4}, + {1, 5}, + {1, 6}, + {1, 7}, + {1, 8}, + + {4, 4}, + {4, 5}, + {4, 6}, + {4, 7}, + {4, 8}, + + {1, 9}, + {2, 9}, + {3, 9}, + {4, 9}, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + mustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 11}) + mustBrailleLine(bc, image.Point{0, 0}, image.Point{5, 0}) + mustBrailleLine(bc, image.Point{0, 1}, image.Point{5, 1}) + mustBrailleLine(bc, image.Point{0, 2}, image.Point{5, 2}) + mustBrailleLine(bc, image.Point{0, 10}, image.Point{5, 10}) + mustBrailleLine(bc, image.Point{0, 11}, image.Point{5, 11}) + mustBrailleLine(bc, image.Point{5, 0}, image.Point{5, 11}) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + { + desc: "fills outside of a circle", + canvas: image.Rect(0, 0, 4, 2), + start: image.Point{0, 0}, + border: circlePoints(image.Point{4, 4}, 2), + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + bc := testbraille.MustNew(ft.Area()) + + mustBrailleLine(bc, image.Point{0, 0}, image.Point{7, 0}) + mustBrailleLine(bc, image.Point{0, 1}, image.Point{7, 1}) + + mustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2}) + mustBrailleLine(bc, image.Point{6, 2}, image.Point{7, 2}) + + mustBrailleLine(bc, image.Point{0, 3}, image.Point{1, 3}) + mustBrailleLine(bc, image.Point{7, 3}, image.Point{7, 3}) + mustBrailleLine(bc, image.Point{0, 4}, image.Point{1, 4}) + mustBrailleLine(bc, image.Point{7, 4}, image.Point{7, 4}) + mustBrailleLine(bc, image.Point{0, 5}, image.Point{1, 5}) + mustBrailleLine(bc, image.Point{7, 5}, image.Point{7, 5}) + + mustBrailleLine(bc, image.Point{0, 6}, image.Point{2, 6}) + mustBrailleLine(bc, image.Point{6, 6}, image.Point{7, 6}) + + mustBrailleLine(bc, image.Point{0, 7}, image.Point{7, 7}) + + testbraille.MustApply(bc, ft) + return ft + }, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + bc, err := braille.New(tc.canvas) + if err != nil { + t.Fatalf("braille.New => unexpected error: %v", err) + } + + if tc.prepare != nil { + if err := tc.prepare(bc); err != nil { + t.Fatalf("tc.prepare => unexpected error: %v", err) + } + } + + err = BrailleFill(bc, tc.start, tc.border, tc.opts...) + if (err != nil) != tc.wantErr { + t.Errorf("BrailleFill => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + + size := area.Size(tc.canvas) + want := faketerm.MustNew(size) + if tc.want != nil { + want = tc.want(size) + } + + got, err := faketerm.New(size) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + if err := bc.Apply(got); err != nil { + t.Fatalf("bc.Apply => unexpected error: %v", err) + } + if diff := faketerm.Diff(want, got); diff != "" { + t.Fatalf("BrailleFill => %v", diff) + } + }) + } +}