Skip to content

Commit

Permalink
Bucket (#201)
Browse files Browse the repository at this point in the history
* Flood function stub

* Test filling transparent image

* Pass test

* Add test for perfect rectangle fill

* Pass rect fill test

* Red test

* Helper class

* Simple recursive implementation

* Test filling rectable with rounded corners

* Print color map

* Pass rounded rect fill

* Blob test

* Sort keys for better debug experience

* Pass all tests

* Fill apple

* Fix project save data test

* Bucket icon button

* Refactoring WIP

* Plug in flood algo

* Temporarily use duncan algo

* Convert to duncan color

* Fill at touch point

* Run fill algo in an isolate

* Refactor brush WIP

* WIP

* Fix canvas bug

* Remove unrasterized strokes once rasterized

* Easel shouldn't know which tools return strokes

* Remove unused depend

* Make abstract

* Freeze state for painting task

* Remove unused imports

* Move lasso state

* Fix lasso bug

* Fix lasso behaviour

* First task q test

* Fix test

* Test 2 tasks running consec

* Test running 5 tasks

* Use our algorithm for bucket

* Extensions folder

* Extract color method

* Doc

* Queue instead of recursion stack

* Prevent inf loop

* Time bucket

* Remove coord

* Inside helper func

* Store fillmask

* Efficient flood fill

* Use compute

* Save failed graphics gem translation

* Working scanline woohooo

* Fix scanline algo

* Refactor scanline

* Rename

* Comments

* Modified scanline

* Setup ffi for android

* Convert color

* Port scanline to cpp

* Fix scanline

* Port modified scanline to cpp

* Fix bucket test

* Remove unused dependency

* Compile library in codemagic

* Temp set to internal track

* Change names

* Add image cpp source to xcode

* Endl

* Return exit code

* Refactor flood fill

* Don't push new snapshot if bucket is cancelled

* Remove stopwatch

* Clean up bin button

* Dispose of images before removing from history stack

* Properly dispose of undo stack

* Reduce max number of undos

* Doc

* Doc

* Properly free project memory

* Dispose of undo stack when closing drawing page

* Dispose of picture

* Dispose of generated thumbnail in memory

* Remove unrasterized strokes outside the canvas

* Remove listeners in dispose

* Missing controller dispose

* Cleaner return

* Fix bucket memory leak

* Bump version
  • Loading branch information
ruskakimov authored Aug 5, 2021
1 parent f2da5b2 commit 90fa912
Show file tree
Hide file tree
Showing 74 changed files with 1,219 additions and 184 deletions.
1 change: 1 addition & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
/app/.cxx
6 changes: 6 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ android {
signingConfig signingConfigs.release
}
}

externalNativeBuild {
cmake {
path "../../image_library/CMakeLists.txt"
}
}
}

flutter {
Expand Down
93 changes: 93 additions & 0 deletions codemagic.yaml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions image_library/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.4.1)
set (CMAKE_CXX_STANDARD 11)

add_library(
image
SHARED
flood_fill.cpp
)
107 changes: 107 additions & 0 deletions image_library/flood_fill.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include <stdlib.h>
#include <queue>
#include <utility>

#define PUBLIC extern "C" __attribute__((visibility("default"))) __attribute__((used))

class Image {
private:
uint32_t* pixels_pointer;
int width;
int height;

public:
Image(uint32_t* pixels_pointer, int width, int height) {
this->pixels_pointer = pixels_pointer;
this->width = width;
this->height = height;
}

uint32_t* getPixel(int x, int y) {
return pixels_pointer + (y * width + x);
}
};

struct LineFillTask {
int x;
int y;
int parentDy; // y + parentDy is the y coordinate of parent
int parentXl; // left x of parent's filled line
int parentXr; // right x of parent's filled line

LineFillTask(int x, int y, int parentDy, int parentXl, int parentXr) {
this->x = x;
this->y = y;
this->parentDy = parentDy;
this->parentXl = parentXl;
this->parentXr = parentXr;
}
};

PUBLIC
// Floods the 4-connected color area with another color.
// Returns 0 if successful.
// Returns -1 if cancelled.
int flood_fill(uint32_t* pixels_pointer, int width, int height, int x, int y, int fillColor) {
auto image = Image(pixels_pointer, width, height);

uint32_t oldColor = *image.getPixel(x, y);

if (oldColor == fillColor) return -1;

std::queue<LineFillTask> q;
q.push(LineFillTask(x, y, 0, 0, 0));

auto scanLine = [&](int xl, int xr, int y, int parentDy) {
bool streak = false;
for (int x = xl; x <= xr; x++) {
if (!streak && *image.getPixel(x, y) == oldColor) {
q.push(LineFillTask(x, y, parentDy, xl, xr));
streak = true;
}
else if (streak && *image.getPixel(x, y) != oldColor) {
streak = false;
}
}
};

while (!q.empty()) {
auto t = q.front();
q.pop();

x = t.x;
y = t.y;

int xl = x;
int xr = x;

// Find start of the line.
while (xl - 1 >= 0 && *image.getPixel(xl - 1, y) == oldColor) xl--;

// Fill the whole line.
for (int x = xl; x < width && *image.getPixel(x, y) == oldColor; x++) {
*image.getPixel(x, y) = fillColor;
xr = x;
}

// Scan for new lines above.
if (t.parentDy == -1) {
if (xl < t.parentXl) scanLine(xl, t.parentXl, y - 1, 1);
if (xr > t.parentXr) scanLine(t.parentXr, xr, y - 1, 1);
}
else if (y > 0) {
scanLine(xl, xr, y - 1, 1);
}

// Scan for new lines below.
if (t.parentDy == 1) {
if (xl < t.parentXl) scanLine(xl, t.parentXl, y + 1, -1);
if (xr > t.parentXr) scanLine(t.parentXr, xr, y + 1, -1);
}
else if (y < height - 1) {
scanLine(xl, xr, y + 1, -1);
}
}

return 0;
}
4 changes: 4 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
E59360A625BB337C00050773 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59360A525BB337C00050773 /* AppViewController.swift */; };
E5AC82B026B42D6800E7C825 /* flood_fill.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5AC82AF26B42D6800E7C825 /* flood_fill.cpp */; };
E5C8108D25B7F02900ADAF39 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E5C8108C25B7F02900ADAF39 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -50,6 +51,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E543890E25C9950D00A61B5B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E59360A525BB337C00050773 /* AppViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; };
E5AC82AF26B42D6800E7C825 /* flood_fill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = flood_fill.cpp; path = ../image_library/flood_fill.cpp; sourceTree = "<group>"; };
E5C8108C25B7F02900ADAF39 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
EDB64C9FE88BAECF1C4220E3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -88,6 +90,7 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
E5AC82AF26B42D6800E7C825 /* flood_fill.cpp */,
E5C8108C25B7F02900ADAF39 /* GoogleService-Info.plist */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
Expand Down Expand Up @@ -288,6 +291,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E5AC82B026B42D6800E7C825 /* flood_fill.cpp in Sources */,
E59360A625BB337C00050773 /* AppViewController.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
Expand Down
21 changes: 21 additions & 0 deletions lib/common/data/extensions/color_methods.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'dart:ui';

extension ColorMethods on Color {
int toRGBA() {
return [
red,
green,
blue,
alpha,
].reduce((color, channel) => (color << 8) | channel);
}

int toABGR() {
return [
alpha,
blue,
green,
red,
].reduce((color, channel) => (color << 8) | channel);
}
}
File renamed without changes.
File renamed without changes.
77 changes: 77 additions & 0 deletions lib/common/data/flood_fill.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:mooltik/common/data/extensions/color_methods.dart';
import 'package:mooltik/common/data/io/image.dart';
import 'package:mooltik/ffi_bridge.dart';

/// Flood fills [image] with the given [color] starting at [startX], [startY].
Future<ui.Image?> floodFill(
ui.Image source,
int startX,
int startY,
ui.Color color,
) async {
final imageByteData = await source.toByteData();

// Can be refactored with `compute` after this PR (https://github.com/flutter/flutter/pull/86591) lands in stable.
final receivePort = ReceivePort();

final isolate = await Isolate.spawn(
_fillIsolate,
_FillIsolateParams(
imageByteData: imageByteData!,
width: source.width,
height: source.height,
startX: startX,
startY: startY,
fillColor: color,
sendPort: receivePort.sendPort,
),
);

final resultByteData = await receivePort.first as ByteData?;

receivePort.close();
isolate.kill();

if (resultByteData == null) return null;

return imageFromBytes(resultByteData, source.width, source.height);
}

class _FillIsolateParams {
final ByteData imageByteData;
final int width;
final int height;
final int startX;
final int startY;
final ui.Color fillColor;
final SendPort sendPort;

_FillIsolateParams({
required this.imageByteData,
required this.width,
required this.height,
required this.startX,
required this.startY,
required this.fillColor,
required this.sendPort,
});
}

void _fillIsolate(_FillIsolateParams params) {
final exitCode = FFIBridge().floodFill(
params.imageByteData.buffer.asUint32List(),
params.width,
params.height,
params.startX,
params.startY,
params.fillColor.toABGR(),
);

final result = exitCode == 0 ? params.imageByteData : null;

params.sendPort.send(result);
}
4 changes: 4 additions & 0 deletions lib/common/data/io/disk_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ class DiskImage with EquatableMixin {

@override
List<Object?> get props => [file.path];

void dispose() {
_snapshot?.dispose();
}
}
4 changes: 3 additions & 1 deletion lib/common/data/io/generate_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ Future<Image> generateImage(
final canvas = Canvas(recorder);
painter?.paint(canvas, Size(width.toDouble(), height.toDouble()));
final picture = recorder.endRecording();
return picture.toImage(width, height);
final image = await picture.toImage(width, height);
picture.dispose();
return image;
}
16 changes: 16 additions & 0 deletions lib/common/data/io/image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'dart:async';
import 'dart:typed_data';

import 'dart:ui';

Future<Image> imageFromBytes(ByteData bytes, int width, int height) {
final Completer<Image> completer = Completer<Image>();
decodeImageFromPixels(
bytes.buffer.asUint8List(),
width,
height,
PixelFormat.rgba8888,
(Image image) => completer.complete(image),
);
return completer.future;
}
2 changes: 1 addition & 1 deletion lib/common/data/io/mp4/slide.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:io';

import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';

/// Data class for video export.
class Slide {
Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/project/composite_frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:ui' as ui;

import 'package:equatable/equatable.dart';
import 'package:mooltik/common/data/io/generate_image.dart';
import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';
import 'package:mooltik/common/data/project/composite_image.dart';
import 'package:mooltik/common/data/sequence/time_span.dart';
import 'package:mooltik/common/ui/composite_image_painter.dart';
Expand Down
3 changes: 3 additions & 0 deletions lib/common/data/project/project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ class Project extends ChangeNotifier {
}

void _freeMemory() {
allFrames.forEach((frame) => frame.image.dispose());
_scenes?.dispose();
_scenes = null;
_soundClips = [];
}
Expand All @@ -199,6 +201,7 @@ class Project extends ChangeNotifier {
_frameSize.height.toInt(),
);
await pngWrite(thumbnail, image);
image.dispose();

await _deleteUnusedFiles();

Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/project/sava_data_transcoder.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';

class SaveDataTranscoder {
/// Transcodes save data in old formats to the latest format.
Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/project/scene.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';
import 'package:mooltik/common/data/project/composite_frame.dart';
import 'package:mooltik/common/data/project/composite_image.dart';
import 'package:mooltik/common/data/project/scene_layer.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/project/scene_layer.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';
import 'package:mooltik/common/data/sequence/sequence.dart';
import 'package:mooltik/drawing/data/frame/frame.dart';

Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/project/sound_clip.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:io';

import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';
import 'package:path/path.dart' as p;

class SoundClip {
Expand Down
2 changes: 1 addition & 1 deletion lib/common/data/sequence/sequence.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:mooltik/common/data/duration_methods.dart';
import 'package:mooltik/common/data/extensions/duration_methods.dart';
import 'package:mooltik/common/data/sequence/time_span.dart';

/// A sequence of `TimeSpan`s with a playhead.
Expand Down
26 changes: 26 additions & 0 deletions lib/common/data/task_queue.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'dart:collection';

typedef AsyncTask = Future<void> Function();

/// Executes async functions in order.
class TaskQueue {
final _queue = Queue<AsyncTask>();

void add(AsyncTask task) {
_queue.add(task);
if (!_isRunning) _run();
}

bool _isRunning = false;

void _run() async {
_isRunning = true;

while (_queue.isNotEmpty) {
final task = _queue.removeFirst();
await task();
}

_isRunning = false;
}
}
Loading

0 comments on commit 90fa912

Please sign in to comment.