feat: yay, math

This commit is contained in:
cha0s 2021-03-19 09:05:51 -05:00
parent 5eedb539da
commit a61f7c79af
9 changed files with 282 additions and 3 deletions

View File

@ -22,7 +22,8 @@
"@latus/core": "^2.0.0",
"autoprefixer": "^9.8.6",
"d3-quadtree": "^2.0.0",
"debug": "4.3.1"
"debug": "4.3.1",
"graham_scan": "^1.0.4"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.4.0",

View File

@ -0,0 +1,47 @@
export default function floodwalk2D(data, position, w, h) {
const n = data.length;
const [x, y] = position;
let i = x + w * y;
if (i < 0 || i >= n) {
return [];
}
const b = data[i];
const points = [];
const seen = [];
seen[-1] = true;
seen[i] = true;
const todo = [i];
while (todo.length > 0) {
i = todo.pop();
const v = data[i];
if (b !== v) {
// eslint-disable-next-line no-continue
continue;
}
points.push(i);
const xx = i % w;
const yy = Math.floor(i / w);
const us = yy >= 1;
const rs = xx + 1 < w;
const ds = yy + 1 < h;
const ls = xx >= 1;
const sel = [
us ? i - w : -1,
rs ? i + 1 : -1,
ds ? i + w : -1,
ls ? i - 1 : -1,
us && rs ? i - w + 1 : -1,
rs && ds ? i + w + 1 : -1,
ds && ls ? i + w - 1 : -1,
ls && us ? i - w - 1 : -1,
];
for (let i = 0; i < sel.length; ++i) {
const j = sel[i];
if (!seen[j]) {
todo.push(j);
seen[j] = true;
}
}
}
return points;
}

View File

@ -4,6 +4,7 @@ import types from './types';
import * as Vector from './vector';
export * from './math';
export {default as floodwalk2D} from './floodwalk';
export {default as QuadTree} from './quadtree';
export {noise, noiseSeed} from './noise';
export {default as Range} from './range';

View File

@ -142,6 +142,10 @@ export function round(v) {
return [Math.round(v[0]), Math.round(v[1])];
}
export function cross(l, r) {
return l[0] * r[1] - l[1] * r[0];
}
// Get the dot product of two vectors. If only one gievn, dupe it.
//
// avocado> Vector.dot [2, 3], [4, 5]

View File

@ -1,3 +1,5 @@
import GrahamScan from 'graham_scan';
import * as Vector from './vector';
// Vertice operations.
@ -5,6 +7,10 @@ import * as Vector from './vector';
// **Vertice** is a utility class to help with vertice operations. A vertice
// is implemented as a 2-element array. Element 0 is *x* and element 1 is *y*.
export function isCollinear(p1, p2, p3) {
return (p1[1] - p2[1]) * (p1[0] - p3[0]) === (p1[1] - p3[1]) * (p1[0] - p2[0]);
}
// Translate a vertice from an origin point using rotation and scale.
// eslint-disable-next-line import/prefer-default-export
export function translate(vertice, origin, rotation = 0, scale = 1) {
@ -20,3 +26,178 @@ export function translate(vertice, origin, rotation = 0, scale = 1) {
),
), scale);
}
export function convexHull(vertices) {
const scanner = new GrahamScan();
for (let i = 0; i < vertices.length; i++) {
const [x, y] = vertices[i];
scanner.addPoint(x, y);
}
return scanner.getHull()
.map(({x, y}) => [x, y]);
}
const c = [
Vector.fromDirection(0),
Vector.fromDirection(1),
Vector.fromDirection(2),
Vector.fromDirection(3),
];
export function ortho(vs) {
if (vs.length < 4) {
throw new TypeError('Vertice.ortho(): vs.length < 4');
}
const _index = {};
for (let i = 0; i < vs.length; i++) {
const [x, y] = vs[i];
if (!_index[y]) {
_index[y] = {};
}
_index[y][x] = vs[i];
}
const index = ([x, y]) => _index[y]?.[x];
const sorted = [vs[0]];
let d = 0;
let walk = 0;
// eslint-disable-next-line no-labels, no-restricted-syntax
select:
// eslint-disable-next-line no-constant-condition
while (true) {
for (let i = 0; i < 4; i++) {
// eslint-disable-next-line no-bitwise
const s = (i + d + 3) & 3;
const v = index(Vector.add(vs[walk], c[s]));
if (vs[0] === v) {
return sorted;
}
if (v && -1 === sorted.findIndex((v2) => v === v2)) {
d = s;
sorted.push(v);
walk = vs.findIndex((v2) => v === v2);
// eslint-disable-next-line no-continue, no-labels, no-extra-label
continue select;
}
}
return [];
}
}
export function removeCollinear(vs) {
const trimmed = [];
// eslint-disable-next-line no-param-reassign
vs = vs.concat();
let i = 0;
if (vs.length < 3) {
return vs;
}
vs.push(vs[0]);
vs.push(vs[1]);
while (i < vs.length - 2) {
if (isCollinear(vs[i], vs[i + 1], vs[i + 2])) {
vs.splice(i + 1, 1);
}
else {
trimmed.push(vs[i]);
i += 1;
}
}
return trimmed;
}
export function smooth(vs) {
const smoothed = [];
// eslint-disable-next-line no-use-before-define
const sorted = ortho(unique(vs));
if (0 === sorted.length) {
return [];
}
sorted.push(sorted[0]);
let i = 0;
while (i < sorted.length - 2) {
const f = Vector.sub(sorted[i + 1], sorted[i]);
const g = Vector.sub(sorted[i + 2], sorted[i + 1]);
const o = Math.atan2(Vector.cross(f, g), Vector.dot(f, g));
if (Math.abs(o) === Math.PI / 2 || Math.abs(o) === Math.PI / 4) {
const h = Vector.add(sorted[i + 1], Vector.scale(Vector.sub(g, f), 0.5));
// eslint-disable-next-line no-use-before-define
if (0 === windingNumber(h, sorted)) {
sorted.splice(i + 1, 1);
i -= 1;
}
else {
smoothed.push(sorted[i]);
i += 1;
}
}
else {
smoothed.push(sorted[i]);
i += 1;
}
}
smoothed.push(sorted[i]);
smoothed.push(sorted[i + 1]);
return smoothed;
}
export function sort(vs) {
let mx = 0;
let my = 0;
for (let i = 0; i < vs.length; ++i) {
const [x, y] = vs[i];
mx += x;
my += y;
}
const m = Vector.scale([mx, my], 1 / vs.length);
return vs.sort((l, r) => {
const ol = Vector.toRadians(Vector.sub(m, l));
const or = Vector.toRadians(Vector.sub(m, r));
if (ol < or) {
return -1;
}
if (ol > or) {
return 1;
}
return Vector.distance(vs[0], l) > Vector.distance(vs[0], r) ? -1 : 1;
});
}
export function unique(vs) {
const record = {};
const unique = [];
for (let i = 0; i < vs.length; ++i) {
const [x, y] = vs[i];
if (!record[y]) {
record[y] = {};
}
if (!record[y][x]) {
unique.push([x, y]);
record[y][x] = true;
}
}
return unique;
}
// ty http://geomalgorithms.com/a03-_inclusion.html
const isLeft = (p0, p1, p2) => (
(p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1])
);
export const windingNumber = (p, vs) => {
let wn = 0;
for (let i = 0; i < vs.length - 1; ++i) {
if (vs[i][1] <= p[1]) {
if (vs[i + 1][1] > p[1]) {
if (isLeft(vs[i], vs[i + 1], p) > 0) {
wn += 1;
}
}
}
else if (vs[i + 1][1] <= p[1]) {
if (isLeft(vs[i], vs[i + 1], p) < 0) {
wn -= 1;
}
}
}
return wn;
};

View File

@ -0,0 +1,27 @@
require('source-map-support').install();
import {expect} from 'chai';
import floodwalk from '../src/floodwalk';
describe('Floodwalk', () => {
it('can walk', () => {
const data = [
0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 1, 1, 1, 0,
1, 1, 1, 1, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
];
let correct = data.reduce((r, v, i) => r.concat(v ? [+i] : []), []);
expect(
floodwalk(data, [1, 1], 8, 4)
.sort((l, r) => l < r ? -1 : 1)
).to.deep.equal(correct);
data[12] = 0;
correct = [9, 10, 16, 17, 18, 19];
expect(
floodwalk(data, [1, 1], 8, 4)
.sort((l, r) => l < r ? -1 : 1)
).to.deep.equal(correct);
});
});

View File

@ -62,9 +62,9 @@ describe('Rectangle', function() {
expect(Rectangle.floor([3.14, 4.70, 5.32, 1.8])).to.deep.equal([3, 4, 5, 1]);
});
it('can test inside', () => {
console.log(Rectangle.isInside(
expect(Rectangle.isInside(
[-22.55753963192675, -36.22420629859356, 64, 64],
[-6.557539631926751, -20.224206298593554, 32, 32],
));
)).to.be.true;
});
});

View File

@ -0,0 +1,13 @@
require('source-map-support').install();
import {expect} from 'chai';
import * as Vertice from '../src/vertice';
describe('Vertice', () => {
it('can compute convex hull', () => {
const vertices = [[0, 0], [5, 0], [5, 5], [10, 5], [10, 10], [0, 10]];
const hull = [[0, 0], [5, 0], [10, 5], [10, 10], [0, 10]];
expect(Vertice.convexHull(vertices)).to.deep.equal(hull);
});
});

View File

@ -3363,6 +3363,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "http://npm.cha0sdev/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graham_scan@^1.0.4:
version "1.0.4"
resolved "http://npm.cha0sdev/graham_scan/-/graham_scan-1.0.4.tgz#399651dd1f8353e1880f59ea8e5f94b9df4c928a"
integrity sha1-OZZR3R+DU+GID1nqjl+Uud9Mkoo=
growl@1.10.5:
version "1.10.5"
resolved "http://npm.cha0sdev/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"