mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
This commit is contained in:
parent
4b609251e1
commit
3a70cefda2
@ -16,6 +16,7 @@ import { sessionReducers } from 'features/controlLayers/store/sessionReducers';
|
|||||||
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
|
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
|
||||||
import { toolReducers } from 'features/controlLayers/store/toolReducers';
|
import { toolReducers } from 'features/controlLayers/store/toolReducers';
|
||||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||||
|
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
|
||||||
import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
||||||
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { isEqual, pick } from 'lodash-es';
|
import { isEqual, pick } from 'lodash-es';
|
||||||
@ -55,11 +56,12 @@ const initialState: CanvasV2State = {
|
|||||||
type: 'inpaint_mask',
|
type: 'inpaint_mask',
|
||||||
fill: {
|
fill: {
|
||||||
style: 'diagonal',
|
style: 'diagonal',
|
||||||
color: { r: 255, g: 122, b: 0, a: 1 }, // some orange color
|
color: { r: 255, g: 122, b: 0 }, // some orange color
|
||||||
},
|
},
|
||||||
rasterizationCache: [],
|
rasterizationCache: [],
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
objects: [],
|
objects: [],
|
||||||
|
opacity: 1,
|
||||||
position: {
|
position: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@ -292,37 +294,43 @@ export const canvasV2Slice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDrawableEntity(entity)) {
|
if (!isDrawableEntity(entity)) {
|
||||||
entity.objects.push(brushLine);
|
assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`);
|
||||||
// When adding a brush line, we need to invalidate the rasterization caches.
|
|
||||||
invalidateRasterizationCaches(entity, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) });
|
||||||
|
// When adding a brush line, we need to invalidate the rasterization caches.
|
||||||
|
invalidateRasterizationCaches(entity, state);
|
||||||
},
|
},
|
||||||
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
||||||
const { entityIdentifier, eraserLine } = action.payload;
|
const { entityIdentifier, eraserLine } = action.payload;
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return;
|
return;
|
||||||
} else if (isDrawableEntity(entity)) {
|
|
||||||
entity.objects.push(eraserLine);
|
|
||||||
// When adding an eraser line, we need to invalidate the rasterization caches.
|
|
||||||
invalidateRasterizationCaches(entity, state);
|
|
||||||
} else {
|
|
||||||
assert(false, 'Not implemented');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDrawableEntity(entity)) {
|
||||||
|
assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) });
|
||||||
|
// When adding an eraser line, we need to invalidate the rasterization caches.
|
||||||
|
invalidateRasterizationCaches(entity, state);
|
||||||
},
|
},
|
||||||
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
||||||
const { entityIdentifier, rect } = action.payload;
|
const { entityIdentifier, rect } = action.payload;
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return;
|
return;
|
||||||
} else if (isDrawableEntity(entity)) {
|
|
||||||
entity.objects.push(rect);
|
|
||||||
// When adding an eraser line, we need to invalidate the rasterization caches.
|
|
||||||
invalidateRasterizationCaches(entity, state);
|
|
||||||
} else {
|
|
||||||
assert(false, 'Not implemented');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDrawableEntity(entity)) {
|
||||||
|
assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.objects.push(rect);
|
||||||
|
// When adding an eraser line, we need to invalidate the rasterization caches.
|
||||||
|
invalidateRasterizationCaches(entity, state);
|
||||||
},
|
},
|
||||||
entityDeleted: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
entityDeleted: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||||
const { entityIdentifier } = action.payload;
|
const { entityIdentifier } = action.payload;
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Adapted from https://github.com/mourner/simplify-js/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017, Vladimir Agafonkin
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
* permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||||
|
* of conditions and the following disclaimer in the documentation and/or other materials
|
||||||
|
* provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||||
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||||
|
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Coordinate } from 'features/controlLayers/store/types';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
// square distance between 2 points
|
||||||
|
function getSqDist(p1: Coordinate, p2: Coordinate) {
|
||||||
|
const dx = p1.x - p2.x;
|
||||||
|
const dy = p1.y - p2.y;
|
||||||
|
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// square distance from a point to a segment
|
||||||
|
function getSqSegDist(p: Coordinate, p1: Coordinate, p2: Coordinate) {
|
||||||
|
let x = p1.x;
|
||||||
|
let y = p1.y;
|
||||||
|
let dx = p2.x - x;
|
||||||
|
let dy = p2.y - y;
|
||||||
|
|
||||||
|
if (dx !== 0 || dy !== 0) {
|
||||||
|
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (t > 1) {
|
||||||
|
x = p2.x;
|
||||||
|
y = p2.y;
|
||||||
|
} else if (t > 0) {
|
||||||
|
x += dx * t;
|
||||||
|
y += dy * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dx = p.x - x;
|
||||||
|
dy = p.y - y;
|
||||||
|
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
// rest of the code doesn't care about point format
|
||||||
|
|
||||||
|
// basic distance-based simplification
|
||||||
|
function simplifyRadialDist(points: Coordinate[], sqTolerance: number): Coordinate[] {
|
||||||
|
let prevPoint = points[0]!;
|
||||||
|
const newPoints = [prevPoint];
|
||||||
|
let point: Coordinate;
|
||||||
|
|
||||||
|
for (let i = 1, len = points.length; i < len; i++) {
|
||||||
|
point = points[i]!;
|
||||||
|
|
||||||
|
if (getSqDist(point, prevPoint!) > sqTolerance) {
|
||||||
|
newPoints.push(point);
|
||||||
|
prevPoint = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevPoint !== point!) {
|
||||||
|
newPoints.push(point!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyDPStep(
|
||||||
|
points: Coordinate[],
|
||||||
|
first: number,
|
||||||
|
last: number,
|
||||||
|
sqTolerance: number,
|
||||||
|
simplified: Coordinate[]
|
||||||
|
) {
|
||||||
|
let maxSqDist = sqTolerance;
|
||||||
|
let index;
|
||||||
|
|
||||||
|
for (let i = first + 1; i < last; i++) {
|
||||||
|
const sqDist = getSqSegDist(points[i]!, points[first]!, points[last]!);
|
||||||
|
|
||||||
|
if (sqDist > maxSqDist) {
|
||||||
|
index = i;
|
||||||
|
maxSqDist = sqDist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxSqDist > sqTolerance) {
|
||||||
|
if (index! - first > 1) {
|
||||||
|
simplifyDPStep(points, first, index!, sqTolerance, simplified);
|
||||||
|
}
|
||||||
|
simplified.push(points[index!]!);
|
||||||
|
if (last - index! > 1) {
|
||||||
|
simplifyDPStep(points, index!, last, sqTolerance, simplified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simplification using Ramer-Douglas-Peucker algorithm
|
||||||
|
function simplifyDouglasPeucker(points: Coordinate[], sqTolerance: number) {
|
||||||
|
const last = points.length - 1;
|
||||||
|
|
||||||
|
const simplified = [points[0]!];
|
||||||
|
simplifyDPStep(points, 0, last, sqTolerance, simplified);
|
||||||
|
simplified.push(points[last]!);
|
||||||
|
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimplifyOptions = {
|
||||||
|
tolerance?: number;
|
||||||
|
highestQuality?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// both algorithms combined for awesome performance
|
||||||
|
export function simplifyCoords(points: Coordinate[], options?: SimplifyOptions): Coordinate[] {
|
||||||
|
const { tolerance, highestQuality } = { ...options, tolerance: 1, highestQuality: false };
|
||||||
|
|
||||||
|
if (points.length <= 2) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqTolerance = tolerance * tolerance;
|
||||||
|
|
||||||
|
const firstPassPoints = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
|
||||||
|
const secondPassPoints = simplifyDouglasPeucker(firstPassPoints, sqTolerance);
|
||||||
|
|
||||||
|
return secondPassPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coordsToFlatNumbersArray(coords: Coordinate[]): number[] {
|
||||||
|
return coords.flatMap((coord) => [coord.x, coord.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flatNumbersArrayToCoords(array: number[]): Coordinate[] {
|
||||||
|
assert(array.length % 2 === 0, 'Array length must be even');
|
||||||
|
const coords: Coordinate[] = [];
|
||||||
|
for (let i = 0; i < array.length; i += 2) {
|
||||||
|
coords.push({ x: array[i]!, y: array[i + 1]! });
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simplifyFlatNumbersArray(array: number[], options?: SimplifyOptions): number[] {
|
||||||
|
return coordsToFlatNumbersArray(simplifyCoords(flatNumbersArrayToCoords(array), options));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user