Files
InvokeAI/invokeai/frontend/web/src/features/controlLayers/util/simplify.ts
2024-09-06 22:56:24 +10:00

165 lines
4.9 KiB
TypeScript

/**
* 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
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;
}
function coordsToFlatNumbersArray(coords: Coordinate[]): number[] {
return coords.flatMap((coord) => [coord.x, coord.y]);
}
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));
}