Skip to content

Commit

Permalink
select: also select images
Browse files Browse the repository at this point in the history
  • Loading branch information
adil192 committed Mar 8, 2023
1 parent f1aefa6 commit ad15a72
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 23 deletions.
4 changes: 2 additions & 2 deletions lib/components/canvas/_canvas_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class CanvasPainter extends CustomPainter {
canvas.restore();
canvas.saveLayer(canvasRect, highlighterLayerPaint);
}
if (currentSelection?.indices.contains(i) ?? false) {
if (currentSelection?.strokeIndices.contains(i) ?? false) {
paint.color = primaryColor;
} else {
paint.color = stroke.strokeProperties.color.withAlpha(255).withInversion(invert);
Expand All @@ -69,7 +69,7 @@ class CanvasPainter extends CustomPainter {
for (int i = 0; i < strokes.length; i++) {
final Stroke stroke = strokes[i];
if (stroke.penType == (Highlighter).toString()) continue;
if (currentSelection?.indices.contains(i) ?? false) {
if (currentSelection?.strokeIndices.contains(i) ?? false) {
paint.color = primaryColor;
} else {
paint.color = stroke.strokeProperties.color.withInversion(invert);
Expand Down
6 changes: 6 additions & 0 deletions lib/components/canvas/canvas_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CanvasImage extends StatefulWidget {
required this.setAsBackground,
this.isBackground = false,
this.readOnly = false,
this.selected = false,
}) : super(key: Key('CanvasImage$filePath/${image.id}'));

/// The path to the note that this image is in.
Expand All @@ -32,6 +33,7 @@ class CanvasImage extends StatefulWidget {
final void Function(EditorImage image)? setAsBackground;
final bool isBackground;
final bool readOnly;
final bool selected;

/// When notified, all [CanvasImages] will have their [active] property set to false.
static ChangeNotifier activeListener = ChangeNotifier();
Expand Down Expand Up @@ -254,6 +256,10 @@ class _CanvasImageState extends State<CanvasImage> {
),
),
),
if (widget.selected) // tint image if selected
ColoredBox(
color: colorScheme.primary.withOpacity(0.5),
),
if (!widget.readOnly)
for (double x = -20; x <= 20; x += 20)
for (double y = -20; y <= 20; y += 20)
Expand Down
6 changes: 4 additions & 2 deletions lib/components/canvas/inner_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,16 @@ class _InnerCanvasState extends State<InnerCanvas> {
child: quillEditor,
),
),
for (final EditorImage editorImage in page.images)
for (int i = 0; i < page.images.length; i++)
CanvasImage(
filePath: widget.coreInfo.filePath,
image: editorImage,
image: page.images[i],
pageSize: Size(widget.width, widget.height),
setAsBackground: widget.setAsBackground,
readOnly: widget.coreInfo.readOnly
|| !widget.currentToolIsSelect,
selected: widget.currentSelection?.imageIndices.contains(i)
?? false,
),
],
),
Expand Down
67 changes: 58 additions & 9 deletions lib/components/canvas/tools/select.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import 'package:flutter/material.dart';
import 'package:saber/components/canvas/_editor_image.dart';
import 'package:saber/components/canvas/_stroke.dart';

import 'package:saber/components/canvas/tools/_tool.dart';
Expand All @@ -10,7 +10,16 @@ class Select extends Tool {
static final Select _currentSelect = Select._();
static Select get currentSelect => _currentSelect;

SelectResult selectResult = SelectResult(-1, const [], Path());
/// The minimum ratio of points inside a stroke or image
/// for it to be selected.
static const double minPercentInside = 0.7;

SelectResult selectResult = SelectResult(
pageIndex: -1,
strokeIndices: const [],
imageIndices: const [],
path: Path(),
);
bool doneSelecting = false;

@override
Expand All @@ -23,7 +32,12 @@ class Select extends Tool {

void onDragStart(Offset position, int pageIndex) {
doneSelecting = false;
selectResult = SelectResult(pageIndex, [], Path());
selectResult = SelectResult(
pageIndex: pageIndex,
strokeIndices: [],
imageIndices: [],
path: Path(),
);
selectResult.path.moveTo(position.dx, position.dy);
onDragUpdate(position);
}
Expand All @@ -32,8 +46,10 @@ class Select extends Tool {
selectResult.path.lineTo(position.dx, position.dy);
}

/// Returns the indices of any [strokes] that are inside the selection area
void onDragEnd(List<Stroke> strokes) {
/// Adds the indices of any [strokes] that are inside the selection area
/// to [selectResult.indices].
void onDragEnd(List<Stroke> strokes, List<EditorImage> images) {

selectResult.path.close();
doneSelecting = true;

Expand All @@ -48,17 +64,50 @@ class Select extends Tool {
final ratio = pointsInside / stroke.polygon.length;

// if more than 70% of the points are inside the path, select the stroke
if (ratio > 0.7) {
selectResult.indices.add(i);
if (ratio > minPercentInside) {
selectResult.strokeIndices.add(i);
}
}

// test 5x5 grid of points inside each image
for (int i = 0; i < images.length; i++) {
final EditorImage image = images[i];

const int gridSize = 5;
final double gridCellWidth = image.dstRect.width / (gridSize - 1);
final double gridCellHeight = image.dstRect.height / (gridSize - 1);

int pointsInside = 0;
for (int x = 0; x < gridSize; x++) {
for (int y = 0; y < gridSize; y++) {
if (selectResult.path.contains(Offset(
image.dstRect.left + gridCellWidth * x,
image.dstRect.top + gridCellHeight * y,
))) {
pointsInside++;
}
}
}

// times 0.8 because the grid is not perfectly accurate
final int minPointsInside = (gridSize * gridSize * minPercentInside * 0.8).floor();
if (pointsInside >= minPointsInside) {
selectResult.imageIndices.add(i);
}
}
}
}

class SelectResult {
int pageIndex;
final List<int> indices;
final List<int> strokeIndices;
final List<int> imageIndices;
Path path;

SelectResult(this.pageIndex, this.indices, this.path);
SelectResult({
required this.pageIndex,
required this.strokeIndices,
required this.imageIndices,
required this.path,
});
}
17 changes: 11 additions & 6 deletions lib/pages/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,12 @@ class EditorState extends State<Editor> {
} else if (currentTool is Select) {
Select select = currentTool as Select;
if (select.doneSelecting) {
for (int i in select.selectResult.indices) {
for (int i in select.selectResult.strokeIndices) {
page.strokes[i].offset += offset;
}
for (int i in select.selectResult.imageIndices) {
page.images[i].dstRect = page.images[i].dstRect.shift(offset);
}
select.selectResult.path = select.selectResult.path.shift(offset);
} else {
select.onDragUpdate(position);
Expand Down Expand Up @@ -446,19 +449,21 @@ class EditorState extends State<Editor> {
if (select.doneSelecting) {
history.recordChange(EditorHistoryItem(
type: EditorHistoryItemType.move,
strokes: select.selectResult.indices
strokes: select.selectResult.strokeIndices
.map((i) => page.strokes[i])
.toList(),
images: [],
images: select.selectResult.imageIndices
.map((i) => page.images[i])
.toList(),
offset: Rect.fromLTRB(
moveOffset.dx,
moveOffset.dy,
0,
0,
moveOffset.dx,
moveOffset.dy,
),
));
} else {
select.onDragEnd(page.strokes);
select.onDragEnd(page.strokes, page.images);
}
}
});
Expand Down
65 changes: 61 additions & 4 deletions test/tools_select_test.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter_test/flutter_test.dart';
import 'package:saber/components/canvas/_editor_image.dart';
import 'package:saber/components/canvas/_stroke.dart';
import 'package:saber/components/canvas/tools/select.dart';
import 'package:saber/components/canvas/tools/stroke_properties.dart';

void main() {
test('Test that the select tool selects the right points', () async {
test('Test that the select tool selects the right strokes', () async {
Select select = Select.currentSelect;
StrokeProperties strokeProperties = StrokeProperties();
Size pageSize = const Size(100, 100);
Expand Down Expand Up @@ -41,9 +43,64 @@ void main() {
..offset = const Offset(10, 10),
];

select.onDragEnd(strokes);
select.onDragEnd(strokes, const []);

expect(select.selectResult.indices.length, 1, reason: 'Only one stroke should be selected');
expect(select.selectResult.indices.first, 0, reason: 'The first stroke should be selected');
expect(select.selectResult.strokeIndices.length, 1, reason: 'Only one stroke should be selected');
expect(select.selectResult.strokeIndices.first, 0, reason: 'The first stroke should be selected');
expect(select.selectResult.imageIndices.length, 0, reason: 'No images should be selected');
});

test('Test that the select tool selects the right images', () async {
Select select = Select.currentSelect;

// Drag gesture in a 10x10 square shape, on page 0
select.onDragStart(Offset.zero, 0);
select.onDragUpdate(const Offset(0, 10));
select.onDragUpdate(const Offset(10, 10));
select.onDragUpdate(const Offset(10, 0));

expect(select.selectResult.pageIndex, 0, reason: 'The page index should be 0');

List<EditorImage> images = [
// index 0 is inside (100% in the selection)
TestImage(
dstRect: const Rect.fromLTWH(0, 0, 10, 10),
),
// index 1 is inside (> 70% in the selection)
TestImage(
dstRect: const Rect.fromLTWH(0, 0, 10 / 0.75, 10 / 0.75),
),
// index 2 is outside (< 70% in the selection)
TestImage(
dstRect: const Rect.fromLTWH(0, 0, 10 / 0.6, 10 / 0.6),
),
];

select.onDragEnd(const [], images);

expect(select.selectResult.imageIndices.length, 2, reason: 'Two images should be selected');
expect(select.selectResult.imageIndices.contains(0), true, reason: 'The first image should be selected');
expect(select.selectResult.imageIndices.contains(1), true, reason: 'The second image should be selected');
expect(select.selectResult.strokeIndices.length, 0, reason: 'No strokes should be selected');
});
}

class TestImage extends EditorImage {
TestImage({
required super.dstRect,
}) : super(
id: -1,
extension: '.png',
bytes: Uint8List(0),
pageIndex: 0,
pageSize: const Size(100, 100),
onMoveImage: null,
onDeleteImage: null,
onMiscChange: null,
);

@override
Future<void> getImage({Size? pageSize}) async {
// do nothing
}
}

0 comments on commit ad15a72

Please sign in to comment.