scratch-paint/src/tools/blob.js

342 lines
13 KiB
JavaScript
Raw Normal View History

2017-07-27 16:48:37 -04:00
import paper from 'paper';
import log from '../log/log';
import broadBrushHelper from './broad-brush-helper';
2017-07-20 22:48:07 -04:00
class BlobTool {
static get BROAD () {
return 'broadbrush';
}
static get SEGMENT () {
return 'segmentbrush';
}
2017-07-27 16:48:37 -04:00
// If brush size >= threshold use segment brush, else use broadbrush
2017-07-20 22:48:07 -04:00
// Segment brush has performance issues at low threshold, but broad brush has weird corners
2017-07-27 16:48:37 -04:00
// which get more obvious the bigger it is
2017-07-20 22:48:07 -04:00
static get THRESHOLD () {
return 100000;
}
setOptions (options) {
2017-07-25 15:00:35 -04:00
if (this.tool) {
this.tool.options = options;
this.tool.resizeCursorIfNeeded();
2017-07-20 22:48:07 -04:00
}
}
activateTool (isEraser, tool, options) {
this.tool = tool;
2017-07-25 15:00:35 -04:00
tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000);
tool.resizeCursorIfNeeded = function (point) {
if (typeof point === 'undefined') {
point = this.cursorPreviewLastPoint;
} else {
this.cursorPreviewLastPoint = point;
}
if (this.brushSize === this.options.brushSize) {
return;
}
const newPreview = new paper.Path.Circle({
center: point,
radius: this.options.brushSize / 2
});
if (this.cursorPreview) {
this.cursorPreview.segments = newPreview.segments;
newPreview.remove();
} else {
this.cursorPreview = newPreview;
}
this.brushSize = this.options.brushSize;
};
this.setOptions(options);
2017-07-20 22:48:07 -04:00
tool.stylePath = function (path) {
if (isEraser) {
path.fillColor = 'white';
2017-07-25 15:00:35 -04:00
if (path === this.cursorPreview) {
2017-07-20 22:48:07 -04:00
path.strokeColor = 'cornflowerblue';
path.strokeWidth = 1;
}
} else {
// TODO keep a separate active toolbar style for brush vs pen?
//path = pg.stylebar.applyActiveToolbarStyle(path);
//TODO FIX
path.fillColor = 'black';
2017-07-25 15:00:35 -04:00
if (path === this.cursorPreview) {
2017-07-20 22:48:07 -04:00
path.strokeColor = 'cornflowerblue';
path.strokeWidth = 1;
}
}
};
2017-07-25 15:00:35 -04:00
tool.stylePath(this.tool.cursorPreview);
2017-07-20 22:48:07 -04:00
tool.fixedDistance = 1;
2017-07-25 15:00:35 -04:00
broadBrushHelper(tool);
2017-07-20 22:48:07 -04:00
// TODO add
//pg.segmentbrushhelper(tool, options);
2017-07-25 15:00:35 -04:00
2017-07-20 22:48:07 -04:00
tool.onMouseMove = function (event) {
2017-07-25 15:00:35 -04:00
tool.resizeCursorIfNeeded(event.point);
tool.stylePath(this.cursorPreview);
this.cursorPreview.bringToFront();
this.cursorPreview.position = event.point;
2017-07-20 22:48:07 -04:00
};
tool.onMouseDown = function (event) {
2017-07-25 15:00:35 -04:00
tool.resizeCursorIfNeeded(event.point);
2017-07-20 22:48:07 -04:00
if (event.event.button > 0) return; // only first mouse button
2017-07-25 15:00:35 -04:00
if (this.options.brushSize < BlobTool.THRESHOLD) {
2017-07-20 22:48:07 -04:00
this.brush = BlobTool.BROAD;
this.onBroadMouseDown(event);
} else {
this.brush = BlobTool.SEGMENT;
this.onSegmentMouseDown(event);
}
2017-07-25 15:00:35 -04:00
this.cursorPreview.bringToFront();
this.cursorPreview.position = event.point;
2017-07-20 22:48:07 -04:00
paper.view.draw();
};
tool.onMouseDrag = function (event) {
2017-07-25 15:00:35 -04:00
tool.resizeCursorIfNeeded(event.point);
2017-07-20 22:48:07 -04:00
if (event.event.button > 0) return; // only first mouse button
if (this.brush === BlobTool.BROAD) {
this.onBroadMouseDrag(event);
} else if (this.brush === Blob.SEGMENT) {
this.onSegmentMouseDrag(event);
} else {
log.warning(`Brush type does not exist: ${this.brush}`);
}
2017-07-25 15:00:35 -04:00
this.cursorPreview.bringToFront();
this.cursorPreview.position = event.point;
2017-07-20 22:48:07 -04:00
paper.view.draw();
};
tool.onMouseUp = function (event) {
2017-07-25 15:00:35 -04:00
tool.resizeCursorIfNeeded(event.point);
2017-07-20 22:48:07 -04:00
if (event.event.button > 0) return; // only first mouse button
let lastPath;
if (this.brush === BlobTool.BROAD) {
lastPath = this.onBroadMouseUp(event);
} else if (this.brush === BlobTool.SEGMENT) {
lastPath = this.onSegmentMouseUp(event);
} else {
log.warning(`Brush type does not exist: ${this.brush}`);
}
if (isEraser) {
tool.mergeEraser(lastPath);
} else {
tool.mergeBrush(lastPath);
}
2017-07-25 15:00:35 -04:00
this.cursorPreview.bringToFront();
this.cursorPreview.position = event.point;
2017-07-20 22:48:07 -04:00
// Reset
this.brush = null;
tool.fixedDistance = 1;
};
tool.mergeBrush = function (lastPath) {
// Get all path items to merge with
const paths = paper.project.getItems({
match: function (item) {
return tool.isMergeable(lastPath, item);
}
});
let mergedPath = lastPath;
let i;
// Move down z order to first overlapping item
for (i = paths.length - 1; i >= 0 && !tool.touches(paths[i], lastPath); i--) {
continue;
}
let mergedPathIndex = i;
for (; i >= 0; i--) {
if (!tool.touches(paths[i], lastPath)) {
continue;
}
if (!paths[i].getFillColor()) {
// Ignore for merge. Paths without fill need to be in paths though,
// since they can visibly change if z order changes
} else if (tool.colorMatch(paths[i], lastPath)) {
// Make sure the new shape isn't overlapped by anything that would
// visibly change if we change its z order
for (let j = mergedPathIndex; j > i; j--) {
if (tool.touches(paths[j], paths[i])) {
continue;
}
}
// Merge same fill color
const tempPath = mergedPath.unite(paths[i]);
tempPath.strokeColor = paths[i].strokeColor;
tempPath.strokeWidth = paths[i].strokeWidth;
if (mergedPath === lastPath) {
tempPath.insertAbove(paths[i]); // First intersected path determines z position of the new path
} else {
tempPath.insertAbove(mergedPath); // Rest of merges join z index of merged path
mergedPathIndex--; // Removed an item, so the merged path index decreases
}
mergedPath.remove();
mergedPath = tempPath;
paths[i].remove();
paths.splice(i, 1);
}
}
// TODO FIX
//pg.undo.snapshot('broadbrush');
};
tool.mergeEraser = function (lastPath) {
// Get all path items to merge with
// If there are selected items, try to erase from amongst those.
let items = paper.project.getItems({
match: function (item) {
return item.selected && tool.isMergeable(lastPath, item) && tool.touches(lastPath, item);
}
});
// Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset
// and deselect the selection
if (items.length === 0) {
// TODO FIX
//pg.selection.clearSelection();
items = paper.project.getItems({
match: function (item) {
return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item);
}
});
}
for (let i = items.length - 1; i >= 0; i--) {
// Erase
const newPath = items[i].subtract(lastPath);
// Gather path segments
const subpaths = [];
2017-07-25 15:00:35 -04:00
// TODO handle compound path
if (items[i] instanceof paper.Path && !items[i].closed) {
2017-07-20 22:48:07 -04:00
const firstSeg = items[i].clone();
const intersections = firstSeg.getIntersections(lastPath);
// keep first and last segments
if (intersections.length === 0) {
continue;
}
for (let j = intersections.length - 1; j >= 0; j--) {
subpaths.push(firstSeg.splitAt(intersections[j]));
}
subpaths.push(firstSeg);
}
// Remove the ones that are within the eraser stroke boundary, or are already part of new path.
// This way subpaths only remain if they didn't get turned into a shape by subtract.
for (let k = subpaths.length - 1; k >= 0; k--) {
const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point;
if (lastPath.contains(segMidpoint) || newPath.contains(segMidpoint)) {
subpaths[k].remove();
subpaths.splice(k, 1);
}
}
// Divide topologically separate shapes into their own compound paths, instead of
// everything being stuck together.
// Assume that result of erase operation returns clockwise paths for positive shapes
const clockwiseChildren = [];
const ccwChildren = [];
if (newPath.children) {
for (let j = newPath.children.length - 1; j >= 0; j--) {
const child = newPath.children[j];
if (child.isClockwise()) {
clockwiseChildren.push(child);
} else {
ccwChildren.push(child);
}
}
for (let j = 0; j < clockwiseChildren.length; j++) {
const cw = clockwiseChildren[j];
cw.copyAttributes(newPath);
cw.fillColor = newPath.fillColor;
cw.strokeColor = newPath.strokeColor;
cw.strokeWidth = newPath.strokeWidth;
cw.insertAbove(items[i]);
// Go backward since we are deleting elements
let newCw = cw;
for (let k = ccwChildren.length - 1; k >= 0; k--) {
const ccw = ccwChildren[k];
if (tool.firstEnclosesSecond(ccw, cw) || tool.firstEnclosesSecond(cw, ccw)) {
const temp = newCw.subtract(ccw);
temp.insertAbove(newCw);
newCw.remove();
newCw = temp;
ccw.remove();
ccwChildren.splice(k, 1);
}
}
}
newPath.remove();
}
items[i].remove();
}
lastPath.remove();
// TODO FIX
//pg.undo.snapshot('eraser');
};
tool.colorMatch = function (existingPath, addedPath) {
// Note: transparent fill colors do notdetect as touching
return existingPath.getFillColor().equals(addedPath.getFillColor()) &&
(addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null
(addedPath.getStrokeColor() &&
addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) &&
addedPath.getStrokeWidth() === existingPath.getStrokeWidth() &&
tool.touches(existingPath, addedPath);
};
tool.touches = function (path1, path2) {
// Two shapes are touching if their paths intersect
if (path1 && path2 && path1.intersects(path2)) {
return true;
}
return tool.firstEnclosesSecond(path1, path2) || tool.firstEnclosesSecond(path2, path1);
};
tool.firstEnclosesSecond = function (path1, path2) {
// Two shapes are also touching if one is completely inside the other
if (path1 && path2 && path2.firstSegment && path2.firstSegment.point &&
path1.hitTest(path2.firstSegment.point)) {
return true;
}
// TODO clean up these no point paths
return false;
};
tool.isMergeable = function (newPath, existingPath) {
return existingPath instanceof paper.PathItem && // path or compound path
2017-07-25 15:00:35 -04:00
existingPath !== this.cursorPreview && // don't merge with the mouse preview
2017-07-20 22:48:07 -04:00
existingPath !== newPath && // don't merge with self
existingPath.parent instanceof paper.Layer; // don't merge with nested in group
};
}
deactivateTool () {
2017-07-25 15:00:35 -04:00
if (this.tool) {
this.tool.cursorPreview.remove();
this.tool.remove();
}
2017-07-20 22:48:07 -04:00
}
}
module.exports = BlobTool;