diff --git a/assets/sprites/eraser-tool.png b/assets/sprites/eraser-tool.png new file mode 100644 index 0000000..1680502 Binary files /dev/null and b/assets/sprites/eraser-tool.png differ diff --git a/lib/render/interface.go b/lib/render/interface.go index 8c74a34..17883f9 100644 --- a/lib/render/interface.go +++ b/lib/render/interface.go @@ -254,6 +254,7 @@ func IterRect(p1, p2 Point) chan Point { X: TopLeft.X, Y: BottomRight.Y, } + dedupe = map[Point]interface{}{} ) // Trace all four edges and yield it. @@ -268,7 +269,10 @@ func IterRect(p1, p2 Point) chan Point { } for _, edge := range edges { for pt := range IterLine2(edge.A, edge.B) { - generator <- pt + if _, ok := dedupe[pt]; !ok { + generator <- pt + dedupe[pt] = nil + } } } diff --git a/lib/ui/label.go b/lib/ui/label.go index cf18910..86a9f1d 100644 --- a/lib/ui/label.go +++ b/lib/ui/label.go @@ -20,6 +20,7 @@ type Label struct { // Configurable fields for the constructor. Text string TextVariable *string + IntVariable *int Font render.Text width int32 @@ -32,6 +33,7 @@ func NewLabel(c Label) *Label { w := &Label{ Text: c.Text, TextVariable: c.TextVariable, + IntVariable: c.IntVariable, Font: DefaultFont, } if !c.Font.IsZero() { @@ -49,6 +51,9 @@ func (w *Label) text() render.Text { if w.TextVariable != nil { w.Font.Text = *w.TextVariable return w.Font + } else if w.IntVariable != nil { + w.Font.Text = fmt.Sprintf("%d", *w.IntVariable) + return w.Font } w.Font.Text = w.Text return w.Font diff --git a/lib/ui/supervisor.go b/lib/ui/supervisor.go index 8c69a73..ac78a51 100644 --- a/lib/ui/supervisor.go +++ b/lib/ui/supervisor.go @@ -173,7 +173,7 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl } ) - if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y { + if XY.X >= P.X && XY.X < P2.X && XY.Y >= P.Y && XY.Y < P2.Y { // Cursor intersects the widget. hovering = append(hovering, child) } else { diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index dbc2f8a..d99aa97 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -27,6 +27,22 @@ var ( // Size of Undo/Redo history for map editor. UndoHistory = 20 + + // Options for brush size. + BrushSizeOptions = []int{ + 0, + 1, + 2, + 4, + 8, + 16, + 24, + 32, + 48, + 64, + } + DefaultEraserBrushSize = 8 + MaxEraserBrushSize = 32 // the bigger, the slower ) // Edit Mode Values diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index a2317a5..5ffdd15 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -52,6 +52,14 @@ var ( Color: render.Black, } + // SmallMonoFont for cramped spaces like the +/- buttons on Toolbar. + SmallMonoFont = render.Text{ + Size: 14, + PadX: 3, + FontFilename: "DejaVuSansMono.ttf", + Color: render.Black, + } + // Color for draggable doodad. DragColor = render.MustHexColor("#0099FF") diff --git a/pkg/doodle.go b/pkg/doodle.go index 47ffb38..61782b2 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -113,6 +113,7 @@ func (d *Doodle) Run() error { // Poll for events. ev, err := d.Engine.Poll() + shmem.Cursor = render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) if ev.EnterKey.Now { log.Info("MainLoop sees enter key now") } diff --git a/pkg/drawtool/shapes.go b/pkg/drawtool/shapes.go index c11cc6d..52cd740 100644 --- a/pkg/drawtool/shapes.go +++ b/pkg/drawtool/shapes.go @@ -8,4 +8,5 @@ const ( Freehand Shape = iota Line Rectangle + Eraser // not really a shape but communicates the intention ) diff --git a/pkg/drawtool/stroke.go b/pkg/drawtool/stroke.go index 5d26bc1..0c577f1 100644 --- a/pkg/drawtool/stroke.go +++ b/pkg/drawtool/stroke.go @@ -18,6 +18,7 @@ type Stroke struct { ID int // Unique ID per each stroke Shape Shape Color render.Color + Thickness int // 0 = 1px; thickness creates a box N pixels away from each point ExtraData interface{} // arbitrary storage for extra data to attach // Start and end points for Lines, Rectangles, etc. @@ -27,6 +28,16 @@ type Stroke struct { // Array of points for Freehand shapes. Points []render.Point uniqPoint map[render.Point]interface{} // deduplicate points added + + // Storage space to recall the previous values of points that were replaced, + // especially for the Undo/Redo History tool. When the uix.Canvas commits a + // Stroke to the level data, any pixel that has replaced an existing color + // will cache its color here, so we can easily page forwards and backwards + // in history and not lose data. + // + // The data is implementation defined and controlled by the caller. This + // package does not modify OriginalPoints or do anything with it. + OriginalPoints map[render.Point]interface{} } var nextStrokeID int @@ -42,6 +53,8 @@ func NewStroke(shape Shape, color render.Color) *Stroke { // Initialize data structures. Points: []render.Point{}, uniqPoint: map[render.Point]interface{}{}, + + OriginalPoints: map[render.Point]interface{}{}, } } @@ -49,9 +62,11 @@ func NewStroke(shape Shape, color render.Color) *Stroke { func (s *Stroke) Copy() *Stroke { nextStrokeID++ return &Stroke{ - ID: nextStrokeID, - Shape: s.Shape, - Color: s.Color, + ID: nextStrokeID, + Shape: s.Shape, + Color: s.Color, + Thickness: s.Thickness, + ExtraData: s.ExtraData, Points: []render.Point{}, uniqPoint: map[render.Point]interface{}{}, @@ -66,6 +81,8 @@ func (s *Stroke) IterPoints() chan render.Point { ch := make(chan render.Point) go func() { switch s.Shape { + case Eraser: + fallthrough case Freehand: for _, point := range s.Points { ch <- point @@ -84,6 +101,23 @@ func (s *Stroke) IterPoints() chan render.Point { return ch } +// IterThickPoints iterates over the points and yield Rects of each one. +func (s *Stroke) IterThickPoints() chan render.Rect { + ch := make(chan render.Rect) + go func() { + for pt := range s.IterPoints() { + ch <- render.Rect{ + X: pt.X - int32(s.Thickness), + Y: pt.Y - int32(s.Thickness), + W: int32(s.Thickness) * 2, + H: int32(s.Thickness) * 2, + } + } + close(ch) + }() + return ch +} + // AddPoint adds a point to the stroke, for freehand shapes. func (s *Stroke) AddPoint(p render.Point) { if _, ok := s.uniqPoint[p]; ok { diff --git a/pkg/drawtool/tools.go b/pkg/drawtool/tools.go index a60d154..ddda5a7 100644 --- a/pkg/drawtool/tools.go +++ b/pkg/drawtool/tools.go @@ -10,6 +10,7 @@ const ( RectTool ActorTool // drag and move actors LinkTool + EraserTool ) var toolNames = []string{ @@ -18,6 +19,7 @@ var toolNames = []string{ "Rectangle", "Doodad", // readable name for ActorTool "Link", + "Eraser", } func (t Tool) String() string { diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index f30278a..b5a0f77 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -227,7 +227,6 @@ func (s *EditorScene) LoadLevel(filename string) error { s.filename = filename level, err := level.LoadFile(filename) - fmt.Printf("%+v\n", level) if err != nil { return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err) } diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index 1c2b852..9b647f0 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -216,8 +216,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) { u.doodadSkip = 0 } - log.Info("scrollDoodadFrame(%d): skip=%d", rows, u.doodadSkip) - // Calculate about how many rows we can see given our current window size. var ( maxVisibleHeight = int32(u.d.height - 86) @@ -233,8 +231,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) { maxSkip = 0 } - log.Info("maxSkip = (%d * %d) - (%d * %d) = %d", len(u.doodadRows), u.doodadButtonSize, u.doodadButtonSize, rowsEstimated, maxSkip) - // log.Info("maxSkip: estimate=%d rows=%d - visible=%d => %d", rowsEstimated, len(u.doodadRows), rowsVisible, maxSkip) if u.doodadSkip > maxSkip { u.doodadSkip = maxSkip } @@ -269,6 +265,5 @@ func (u *EditorUI) scrollDoodadFrame(rows int) { u.doodadScroller.Configure(ui.Config{ Width: int32(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number }) - log.Info("v%% = (%d + %d) / %d = %f", rowsBefore, rowsVisible, len(u.doodadRows), viewPercent) } diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 05f5f94..8e563b1 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -3,8 +3,8 @@ package doodle import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" - "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/sprites" ) @@ -27,6 +27,18 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Anchor: ui.N, }) + // Helper functions to toggle the correct palette panel. + var ( + showSwatchPalette = func() { + u.DoodadTab.Hide() + u.PaletteTab.Show() + } + showDoodadPalette = func() { + u.PaletteTab.Hide() + u.DoodadTab.Show() + } + ) + // Buttons. var buttons = []struct { Value string @@ -38,8 +50,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Icon: "assets/sprites/pencil-tool.png", Click: func() { u.Canvas.Tool = drawtool.PencilTool - u.DoodadTab.Hide() - u.PaletteTab.Show() + showSwatchPalette() d.Flash("Pencil Tool selected.") }, }, @@ -49,8 +60,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Icon: "assets/sprites/line-tool.png", Click: func() { u.Canvas.Tool = drawtool.LineTool - u.DoodadTab.Hide() - u.PaletteTab.Show() + showSwatchPalette() d.Flash("Line Tool selected.") }, }, @@ -60,8 +70,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Icon: "assets/sprites/rect-tool.png", Click: func() { u.Canvas.Tool = drawtool.RectTool - u.DoodadTab.Hide() - u.PaletteTab.Show() + showSwatchPalette() d.Flash("Rectangle Tool selected.") }, }, @@ -71,8 +80,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Icon: "assets/sprites/actor-tool.png", Click: func() { u.Canvas.Tool = drawtool.ActorTool - u.PaletteTab.Hide() - u.DoodadTab.Show() + showDoodadPalette() d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.") }, }, @@ -82,11 +90,28 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Icon: "assets/sprites/link-tool.png", Click: func() { u.Canvas.Tool = drawtool.LinkTool - u.PaletteTab.Hide() - u.DoodadTab.Show() + showDoodadPalette() d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") }, }, + + { + Value: drawtool.EraserTool.String(), + Icon: "assets/sprites/eraser-tool.png", + Click: func() { + u.Canvas.Tool = drawtool.EraserTool + + // Set the brush size within range for the eraser. + if u.Canvas.BrushSize < balance.DefaultEraserBrushSize { + u.Canvas.BrushSize = balance.DefaultEraserBrushSize + } else if u.Canvas.BrushSize > balance.MaxEraserBrushSize { + u.Canvas.BrushSize = balance.MaxEraserBrushSize + } + + showSwatchPalette() + d.Flash("Eraser Tool selected.") + }, + }, } for _, button := range buttons { button := button @@ -103,7 +128,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { ) var btnSize int32 = btn.BoxThickness(2) + toolbarSpriteSize - log.Info("BtnSize: %d", btnSize) btn.Resize(render.NewRect(btnSize, btnSize)) btn.Handle(ui.Click, func(p render.Point) { @@ -117,6 +141,102 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { }) } + // Spacer frame. + frame.Pack(ui.NewFrame("spacer"), ui.Pack{ + Anchor: ui.N, + PadY: 8, + }) + + // "Brush Size" label + bsLabel := ui.NewLabel(ui.Label{ + Text: "Size:", + Font: balance.LabelFont, + }) + frame.Pack(bsLabel, ui.Pack{ + Anchor: ui.N, + }) + + // Brush Size widget + { + sizeFrame := ui.NewFrame("Brush Size Frame") + frame.Pack(sizeFrame, ui.Pack{ + Anchor: ui.N, + PadY: 0, + }) + + sizeLabel := ui.NewLabel(ui.Label{ + IntVariable: &u.Canvas.BrushSize, + Font: balance.SmallMonoFont, + }) + sizeLabel.Configure(ui.Config{ + BorderSize: 1, + BorderStyle: ui.BorderSunken, + Background: render.Grey, + }) + sizeFrame.Pack(sizeLabel, ui.Pack{ + Anchor: ui.N, + FillX: true, + PadY: 2, + }) + + sizeBtnFrame := ui.NewFrame("Size Increment Button Frame") + sizeFrame.Pack(sizeBtnFrame, ui.Pack{ + Anchor: ui.N, + FillX: true, + }) + + var incButtons = []struct { + Label string + F func() + }{ + { + Label: "-", + F: func() { + // Select next smaller brush size. + for i := len(balance.BrushSizeOptions) - 1; i >= 0; i-- { + if balance.BrushSizeOptions[i] < u.Canvas.BrushSize { + u.Canvas.BrushSize = balance.BrushSizeOptions[i] + break + } + } + }, + }, + { + Label: "+", + F: func() { + // Select next bigger brush size. + for _, size := range balance.BrushSizeOptions { + if size > u.Canvas.BrushSize { + u.Canvas.BrushSize = size + break + } + } + + // Limit the eraser brush size, too big and it's slow because + // the eraser has to scan and remember pixels to be able to + // Undo the erase and restore them. + if u.Canvas.Tool == drawtool.EraserTool && u.Canvas.BrushSize > balance.MaxEraserBrushSize { + u.Canvas.BrushSize = balance.MaxEraserBrushSize + } + }, + }, + } + for _, button := range incButtons { + button := button + btn := ui.NewButton("BrushSize"+button.Label, ui.NewLabel(ui.Label{ + Text: button.Label, + Font: balance.SmallMonoFont, + })) + btn.Handle(ui.Click, func(p render.Point) { + button.F() + }) + u.Supervisor.Add(btn) + sizeBtnFrame.Pack(btn, ui.Pack{ + Anchor: ui.W, + }) + } + } + frame.Compute(d.Engine) return frame diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index b8ff013..23a1e7d 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -204,6 +204,23 @@ func (c *Chunker) Set(p render.Point, sw *Swatch) error { return chunk.Set(p, sw) } +// SetRect sets a rectangle of pixels to a color all at once. +func (c *Chunker) SetRect(r render.Rect, sw *Swatch) error { + var ( + xMin = r.X + yMin = r.Y + xMax = r.X + r.W + yMax = r.Y + r.H + ) + for x := xMin; x < xMax; x++ { + for y := yMin; y < yMax; y++ { + c.Set(render.NewPoint(x, y), sw) + } + } + + return nil +} + // Delete a pixel at the given coordinate. func (c *Chunker) Delete(p render.Point) error { coord := c.ChunkCoordinate(p) @@ -213,6 +230,25 @@ func (c *Chunker) Delete(p render.Point) error { return fmt.Errorf("no chunk %s exists for point %s", coord, p) } +// DeleteRect deletes a rectangle of pixels between two points. +// The rect is a relative one with a width and height, and the X,Y values are +// an absolute world coordinate. +func (c *Chunker) DeleteRect(r render.Rect) error { + var ( + xMin = r.X + yMin = r.Y + xMax = r.X + r.W + yMax = r.Y + r.H + ) + for x := xMin; x < xMax; x++ { + for y := yMin; y < yMax; y++ { + c.Delete(render.NewPoint(x, y)) + } + } + + return nil +} + // ChunkCoordinate computes a chunk coordinate from an absolute coordinate. func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point { if c.Size == 0 { diff --git a/pkg/shmem/globals.go b/pkg/shmem/globals.go index 23c0783..51da194 100644 --- a/pkg/shmem/globals.go +++ b/pkg/shmem/globals.go @@ -12,6 +12,9 @@ var ( // Tick is incremented by the main game loop each frame. Tick uint64 + // Current position of the cursor relative to the window. + Cursor render.Point + // Current render engine (i.e. SDL2 or HTML5 Canvas) // The level.Chunk.ToBitmap() uses this to cache a texture image. CurrentRenderEngine render.Engine diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index f2b6695..8220c9e 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -31,7 +31,8 @@ type Canvas struct { Scrollable bool // Cursor keys will scroll the viewport of this canvas. // Selected draw tool/mode, default Pencil, for editable canvases. - Tool drawtool.Tool + Tool drawtool.Tool + BrushSize int // thickness of selected brush // MaskColor will force every pixel to render as this color regardless of // the palette index of that pixel. Otherwise pixels behave the same and @@ -86,10 +87,7 @@ type Canvas struct { // mousedown-and-dragging event. currentStroke *drawtool.Stroke strokes map[int]*drawtool.Stroke // active stroke mapped by ID - - // Tracking pixels while editing. TODO: get rid of pixelHistory? - pixelHistory []*level.Pixel - lastPixel *level.Pixel + lastPixel *level.Pixel // We inherit the ui.Widget which manages the width and height. Scroll render.Point // Scroll offset for which parts of canvas are visible. diff --git a/pkg/uix/canvas_cursor.go b/pkg/uix/canvas_cursor.go new file mode 100644 index 0000000..30b4b8f --- /dev/null +++ b/pkg/uix/canvas_cursor.go @@ -0,0 +1,50 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/ui" + "git.kirsle.net/apps/doodle/pkg/shmem" +) + +// IsCursorOver returns true if the mouse cursor is physically over top +// of the canvas's widget space. +func (w *Canvas) IsCursorOver() bool { + var ( + P = ui.AbsolutePosition(w) + S = w.Size() + ) + return shmem.Cursor.Inside(render.Rect{ + X: P.X, + Y: P.Y, + W: S.W, + H: S.H, + }) +} + +// presentCursor draws something at the mouse cursor on the Canvas. +// +// This is currently used in Edit Mode when you're drawing a shape with a thick +// brush size, and draws a "preview rect" under the cursor of how big a click +// will be at that size. +func (w *Canvas) presentCursor(e render.Engine) { + if !w.IsCursorOver() { + return + } + + // Are we editing with a thick brush? + if w.BrushSize > 0 { + var r = int32(w.BrushSize) + rect := render.Rect{ + X: shmem.Cursor.X - r, + Y: shmem.Cursor.Y - r, + W: r * 2, + H: r * 2, + } + e.DrawRect(render.Black, rect) + rect.X++ + rect.Y++ + rect.W -= 2 + rect.H -= 2 + e.DrawRect(render.RGBA(153, 153, 153, 153), rect) + } +} diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index 62c5af5..fd0027a 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -8,6 +8,93 @@ import ( "git.kirsle.net/apps/doodle/pkg/level" ) +// commitStroke is the common function that applies a stroke the user is +// actively drawing onto the canvas. This is for Edit Mode. +func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) { + if w.currentStroke == nil { + // nothing to commit + return + } + + var ( + deleting = w.currentStroke.Shape == drawtool.Eraser + dedupe = map[render.Point]interface{}{} // don't revisit the same point twice + + // Helper functions to set pixels on the level while storing the original + // value of any pixel being replaced. + set = func(pt render.Point, sw *level.Swatch) { + // Take note of what pixel was originally here before we change it. + if swatch, err := w.chunks.Get(pt); err == nil { + if _, ok := dedupe[pt]; !ok { + w.currentStroke.OriginalPoints[pt] = swatch + dedupe[pt] = nil + } + } + + if deleting { + w.chunks.Delete(pt) + } else if sw != nil { + w.chunks.Set(pt, sw) + } else { + panic("Canvas.commitStroke.set: current stroke has no level.Swatch in ExtraData") + } + } + + // Rects: read existing pixels first, then write new pixels + readRect = func(rect render.Rect) { + for pt := range w.chunks.IterViewport(rect) { + point := pt.Point() + if _, ok := dedupe[point]; !ok { + w.currentStroke.OriginalPoints[pt.Point()] = pt.Swatch + dedupe[point] = nil + } + } + } + setRect = func(rect render.Rect, sw *level.Swatch) { + if deleting { + w.chunks.DeleteRect(rect) + } else if sw != nil { + w.chunks.SetRect(rect, sw) + } else { + panic("Canvas.commitStroke.setRect: current stroke has no level.Swatch in ExtraData") + } + } + ) + + var swatch *level.Swatch + if v, ok := w.currentStroke.ExtraData.(*level.Swatch); ok { + swatch = v + } + + if w.currentStroke.Thickness > 0 { + // Eraser Tool only: record which pixels will be blown away by this. + // This is SLOW for thick (rect-based) lines, but eraser tool must have it. + if deleting { + for rect := range w.currentStroke.IterThickPoints() { + readRect(rect) + } + } + for rect := range w.currentStroke.IterThickPoints() { + setRect(rect, swatch) + } + } else { + for pt := range w.currentStroke.IterPoints() { + // note: set already records the original pixel if changing it. + set(pt, swatch) + } + } + + // Add the stroke to level history. + if w.level != nil && addHistory { + w.level.UndoHistory.AddStroke(w.currentStroke) + } + + w.RemoveStroke(w.currentStroke) + w.currentStroke = nil + + w.lastPixel = nil +} + // loopEditable handles the Loop() part for editable canvases. func (w *Canvas) loopEditable(ev *events.State) error { // Get the absolute position of the canvas on screen to accurately match @@ -20,6 +107,14 @@ func (w *Canvas) loopEditable(ev *events.State) error { } ) + // If the actual cursor is not over the actual Canvas UI element, don't + // pay any attention to clicks. I added this when I saw you were able to + // accidentally draw (with large brush size) when clicking on the Palette + // panel and not the drawing itself. + if !w.IsCursorOver() { + return nil + } + switch w.Tool { case drawtool.PencilTool: // If no swatch is active, do nothing with mouse clicks. @@ -32,6 +127,7 @@ func (w *Canvas) loopEditable(ev *events.State) error { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color) + w.currentStroke.Thickness = w.BrushSize w.currentStroke.ExtraData = w.Palette.ActiveSwatch w.AddStroke(w.currentStroke) } @@ -53,18 +149,15 @@ func (w *Canvas) loopEditable(ev *events.State) error { } // Append unique new pixels. - if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel { - if lastPixel != nil { - // Draw the pixels in between. - if lastPixel != pixel { - for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) { - w.currentStroke.AddPoint(point) - } + if lastPixel != nil || lastPixel != pixel { + // Draw the pixels in between. + if lastPixel != nil && lastPixel != pixel { + for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) { + w.currentStroke.AddPoint(point) } } w.lastPixel = pixel - w.pixelHistory = append(w.pixelHistory, pixel) // Save the pixel in the current stroke. w.currentStroke.AddPoint(render.Point{ @@ -73,22 +166,7 @@ func (w *Canvas) loopEditable(ev *events.State) error { }) } } else { - // Mouse released, commit the points to the drawing. - if w.currentStroke != nil { - for _, pt := range w.currentStroke.Points { - w.chunks.Set(pt, w.Palette.ActiveSwatch) - } - - // Add the stroke to level history. - if w.level != nil { - w.level.UndoHistory.AddStroke(w.currentStroke) - } - - w.RemoveStroke(w.currentStroke) - w.currentStroke = nil - } - - w.lastPixel = nil + w.commitStroke(w.Tool, true) } case drawtool.LineTool: // If no swatch is active, do nothing with mouse clicks. @@ -101,6 +179,7 @@ func (w *Canvas) loopEditable(ev *events.State) error { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color) + w.currentStroke.Thickness = w.BrushSize w.currentStroke.ExtraData = w.Palette.ActiveSwatch w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y) w.AddStroke(w.currentStroke) @@ -108,20 +187,7 @@ func (w *Canvas) loopEditable(ev *events.State) error { w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y) } else { - // Mouse released, commit the points to the drawing. - if w.currentStroke != nil { - for pt := range render.IterLine2(w.currentStroke.PointA, w.currentStroke.PointB) { - w.chunks.Set(pt, w.Palette.ActiveSwatch) - } - - // Add the stroke to level history. - if w.level != nil { - w.level.UndoHistory.AddStroke(w.currentStroke) - } - - w.RemoveStroke(w.currentStroke) - w.currentStroke = nil - } + w.commitStroke(w.Tool, true) } case drawtool.RectTool: // If no swatch is active, do nothing with mouse clicks. @@ -134,6 +200,7 @@ func (w *Canvas) loopEditable(ev *events.State) error { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color) + w.currentStroke.Thickness = w.BrushSize w.currentStroke.ExtraData = w.Palette.ActiveSwatch w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y) w.AddStroke(w.currentStroke) @@ -141,20 +208,52 @@ func (w *Canvas) loopEditable(ev *events.State) error { w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y) } else { - // Mouse released, commit the points to the drawing. - if w.currentStroke != nil { - for pt := range render.IterRect(w.currentStroke.PointA, w.currentStroke.PointB) { - w.chunks.Set(pt, w.Palette.ActiveSwatch) - } - - // Add the stroke to level history. - if w.level != nil { - w.level.UndoHistory.AddStroke(w.currentStroke) - } - - w.RemoveStroke(w.currentStroke) - w.currentStroke = nil + w.commitStroke(w.Tool, true) + } + case drawtool.EraserTool: + // Clicking? Log all the pixels while doing so. + if ev.Button1.Now { + // Initialize a new Stroke for this atomic drawing operation? + if w.currentStroke == nil { + // The color is white, will look like white-out that covers the + // wallpaper during the stroke. + w.currentStroke = drawtool.NewStroke(drawtool.Eraser, render.White) + w.currentStroke.Thickness = w.BrushSize + w.AddStroke(w.currentStroke) } + + lastPixel := w.lastPixel + pixel := &level.Pixel{ + X: cursor.X, + Y: cursor.Y, + Swatch: w.Palette.ActiveSwatch, + } + + // If the user is holding the mouse down over one spot and not + // moving, don't do anything. The pixel has already been set and + // needless writes to the map cause needless cache rewrites etc. + if lastPixel != nil { + if pixel.X == lastPixel.X && pixel.Y == lastPixel.Y { + break + } + } + + // Append unique new pixels. + if lastPixel == nil || lastPixel != pixel { + if lastPixel != nil && lastPixel != pixel { + for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) { + w.currentStroke.AddPoint(point) + } + } + + w.lastPixel = pixel + w.currentStroke.AddPoint(render.Point{ + X: cursor.X, + Y: cursor.Y, + }) + } + } else { + w.commitStroke(w.Tool, true) } case drawtool.ActorTool: // See if any of the actors are below the mouse cursor. diff --git a/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go index 2904fa3..12c2c11 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -132,8 +132,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { } w.drawActors(e, p) - w.presentStrokes(e) + w.presentCursor(e) // XXX: Debug, show label in canvas corner. if balance.DebugCanvasLabel { diff --git a/pkg/uix/canvas_strokes.go b/pkg/uix/canvas_strokes.go index 1e29452..447aa4b 100644 --- a/pkg/uix/canvas_strokes.go +++ b/pkg/uix/canvas_strokes.go @@ -48,8 +48,48 @@ func (w *Canvas) UndoStroke() bool { latest := w.level.UndoHistory.Latest() if latest != nil { - for point := range latest.IterPoints() { - w.chunks.Delete(point) + // TODO: only single-thickness lines will restore the original color; + // thick lines just delete their pixels from the world due to performance. + // But the Eraser Tool is always thick, which always should restore its + // pixels. Can't do anything about that, so the inefficient thick rect + // restore is used only for Eraser at least. + if latest.Thickness > 0 { + if latest.Shape == drawtool.Eraser { + for rect := range latest.IterThickPoints() { + var ( + xMin = rect.X + xMax = rect.X + rect.W + yMin = rect.Y + yMax = rect.Y + rect.H + ) + for x := xMin; x < xMax; x++ { + for y := yMin; y < yMax; y++ { + if v, ok := latest.OriginalPoints[render.NewPoint(x, y)]; ok { + if swatch, ok := v.(*level.Swatch); ok { + w.chunks.Set(render.NewPoint(x, y), swatch) + } + } + } + } + } + } else { + for rect := range latest.IterThickPoints() { + w.chunks.DeleteRect(rect) + } + } + } else { + for point := range latest.IterPoints() { + // Was there a previous swatch at this point to restore? + if v, ok := latest.OriginalPoints[point]; ok { + if swatch, ok := v.(*level.Swatch); ok { + w.chunks.Set(point, swatch) + continue + } + } + + w.chunks.Delete(point) + } + } } return w.level.UndoHistory.Undo() @@ -72,14 +112,8 @@ func (w *Canvas) RedoStroke() bool { // We stored the ActiveSwatch on this stroke as we drew it. Recover it // and place the pixels back down. - if swatch, ok := latest.ExtraData.(*level.Swatch); ok { - for point := range latest.IterPoints() { - w.chunks.Set(point, swatch) - } - return true - } - - log.Error("Canvas.UndoStroke: undo was successful but no Swatch was stored on the Stroke.ExtraData!") + w.currentStroke = latest + w.commitStroke(w.Tool, false) return ok } @@ -153,6 +187,7 @@ func (w *Canvas) presentActorLinks(e render.Engine) { // Draw a line connecting the centers of each actor together. stroke := drawtool.NewStroke(drawtool.Line, color) + stroke.Thickness = 1 stroke.PointA = render.Point{ X: aP.X + (aS.W / 2), Y: aP.Y + (aS.H / 2), @@ -163,16 +198,6 @@ func (w *Canvas) presentActorLinks(e render.Engine) { } strokes = append(strokes, stroke) - - // Make it double thick. - double := stroke.Copy() - double.PointA = render.NewPoint(stroke.PointA.X, stroke.PointA.Y+1) - double.PointB = render.NewPoint(stroke.PointB.X, stroke.PointB.Y+1) - strokes = append(strokes, double) - double = stroke.Copy() - double.PointA = render.NewPoint(stroke.PointA.X+1, stroke.PointA.Y) - double.PointB = render.NewPoint(stroke.PointB.X+1, stroke.PointB.Y) - strokes = append(strokes, double) } } @@ -183,14 +208,14 @@ func (w *Canvas) presentActorLinks(e render.Engine) { // presentActorLinks to actually draw the lines to the canvas. func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) { var ( - P = ui.AbsolutePosition(w) // w.Point() // Canvas point in UI + P = ui.AbsolutePosition(w) // Canvas point in UI VP = w.ViewportRelative() // Canvas scroll viewport ) for _, stroke := range strokes { // If none of this stroke is in our viewport, don't waste time // looping through it. - if stroke.Shape == drawtool.Freehand { + if stroke.Shape == drawtool.Freehand || stroke.Shape == drawtool.Eraser { if len(stroke.Points) >= 2 { if !stroke.Points[0].Inside(VP) && !stroke.Points[len(stroke.Points)-1].Inside(VP) { continue @@ -206,20 +231,58 @@ func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) { } // Iter the points and draw what's visible. - for point := range stroke.IterPoints() { - if !point.Inside(VP) { - continue - } + if stroke.Thickness > 0 { + for rect := range stroke.IterThickPoints() { + if !rect.Intersects(VP) { + continue + } - dest := render.Point{ - X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X, - Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y, - } + // Destination rectangle to draw to screen, taking into account + // the position of the Canvas itself. + dest := render.Rect{ + X: rect.X + P.X + w.Scroll.X + w.BoxThickness(1), + Y: rect.Y + P.Y + w.Scroll.Y + w.BoxThickness(1), + W: rect.W, + H: rect.H, + } - if balance.DebugCanvasStrokeColor != render.Invisible { - e.DrawPoint(balance.DebugCanvasStrokeColor, dest) - } else { - e.DrawPoint(stroke.Color, dest) + // Cap the render square so it doesn't leave the Canvas and + // overlap other UI elements! + if dest.X < P.X { + // Left edge. TODO: right edge + delta := P.X - dest.X + dest.X = P.X + dest.W -= delta + } + if dest.Y < P.Y { + // Top edge. TODO: bottom edge + delta := P.Y - dest.Y + dest.Y = P.Y + dest.H -= delta + } + + if balance.DebugCanvasStrokeColor != render.Invisible { + e.DrawBox(balance.DebugCanvasStrokeColor, dest) + } else { + e.DrawBox(stroke.Color, dest) + } + } + } else { + for point := range stroke.IterPoints() { + if !point.Inside(VP) { + continue + } + + dest := render.Point{ + X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X, + Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y, + } + + if balance.DebugCanvasStrokeColor != render.Invisible { + e.DrawPoint(balance.DebugCanvasStrokeColor, dest) + } else { + e.DrawPoint(stroke.Color, dest) + } } } }